SpringBoot整合WebSocket實現線上聊天室
前言
WebSocket也是一種應用層協議,也是建立在TCP協議之上,類似HTTP,並且相容HTTP。相比HTTP,它可以實現雙向通訊,如聊天室場景,使用HTTP就必須客戶端輪訓查詢伺服器有沒有新的訊息,而使用WebSocket就可以伺服器直接通知客戶端。
Tomcat支援
Tomcat自7.0.5版本開始支援WebSocket,並實現了WebSocket規範(JSR356)。JSR356規定WebSokcet應用由一系列Endpoint組成,類似於Servlet,Tomcat支援兩種定義Endpoint的方式。
註解方式
import java.io.IOException; import javax.websocket.OnClose; import javax.websocket.OnError; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; @ServerEndpoint("/chat") public class ChatEndpoint { @OnOpen public void onOpen(Session session) { System.out.println(session.getId()); } @OnClose public void onClose(Session session) { System.out.println(session.getId()); } @OnError public void onError(Session session) { System.out.println(session.getId()); } @OnMessage public void onMessage(Session session, String msg) throws IOException { System.out.println(msg); //向客戶端傳送訊息 session.getBasicRemote().sendText("this is " + session.getId()); } }
使用註解@ServerEndpoint宣告Endpoint類,並配置請求路徑。
程式設計方式
import javax.websocket.CloseReason; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; import javax.websocket.MessageHandler.Whole; import javax.websocket.Session; public class ChatEndpoint2 extends Endpoint { @Override public void onOpen(Session session, EndpointConfig config) { System.out.println(session.getId()); //新增訊息處理器 session.addMessageHandler(new Whole<>() { @Override public void onMessage(Object message) { System.out.println(message); } }); } @Override public void onClose(Session session, CloseReason closeReason) { System.out.println(closeReason.getReasonPhrase()); } @Override public void onError(Session session, Throwable throwable) { System.out.println(throwable.getMessage()); } }
定義一個類繼承Endpoint。
import java.util.HashSet; import java.util.Set; import javax.websocket.Endpoint; import javax.websocket.server.ServerApplicationConfig; import javax.websocket.server.ServerEndpointConfig; public class MyServerApplicationConfig implements ServerApplicationConfig { @Override public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> scanned) { //根據查詢到的Endpoint實現類建立ServerEndpointConfig Set<ServerEndpointConfig> result = new HashSet<>(); if (scanned.contains(ChatEndpoint2.class)) { result.add(ServerEndpointConfig.Builder.create(ChatEndpoint2.class, "/chat").build()); } return result; } @Override public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) { //用來過濾使用註解ServerEndpoint定義的Endpoint return scanned; } }
還需要定義一個ServerApplicationConfig的實現類,用來配置Endpoint的請求路徑。
Endpoint生命週期
Endpoint的生命週期方法如下:
- onOpen:當開啟一個新的會話時呼叫,這是客戶端與伺服器握手成功後呼叫的方法,等同於註解@OnOpen。
- onClose:會話關閉時呼叫,等同於註解@OnClose。
- onError:傳輸過程異常時呼叫,等同於註解@OnError。
原理
Tomcat會在啟動時查詢所有使用註解@ServerEndpoint宣告的類和Endpoint的子類,建立WebSocketContainer(類似ServletContext)並將所有Endpoint注入其中。
但如果使用嵌入式的Tomcat(如SpringBoot內嵌的Tomcat)就不會進行此查詢,具體原因可以看Embedded Tomcat does not honor ServletContainerInitializers。根本原因是因為SpringBoot建立Tomcat的Context時沒有新增ContextConfig這個LifecycleListener(不清楚是出於什麼考慮),ContextConfig會使用java的SPI技術查詢所有ServletContainerInitializer的實現類。
SpringBoot支援
新增依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
SpringBoot自動注入了一個TomcatWebSocketServletWebServerCustomizer,向Context中添加了WsContextListener監聽器
它也會建立WebSocketContainer並初始化。
配置處理器
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
public class ChatHandler2 extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println(session.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println(session.getId());
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println(message.getPayload());
}
}
類似於繼承Endpoint的類實現。
新增配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatHandler2(), "/chat");
}
}
一定要新增@EnableWebSocket註解,它會處理所有WebSocketConfigurer的實現類。並且會建立WebSocketHandlerMapping的Bean類,之前我們處理HTTP請求時使用的是RequestMappingHandlerMapping,WebSocket請求是在HTTP請求的基礎上升級的,在握手階段還是HTTP請求,所以還是會交給SpringMVC的DispatcherServlet處理。
實現原理
使用的HandlerAdapter實現為HttpRequestHandlerAdapter,它會持有一個WebSocketHttpRequestHandler物件(其中封裝了HandshakeHandler握手處理器和我們自己定義的ChatHandler2)。實際的握手處理器實現為DefaultHandshakeHandler,核心的處理邏輯就是握手的過程,其中會建立StandardWebSocketHandlerAdapter物件。
可以看到StandardWebSocketHandlerAdapter就是一個Endpoint,最終會將這個Endpoint新增到WebSocketContainer中,後續的WebSocket請求就交給Endpoint來處理了。
聊天室實現效果
專案地址springboot_chatroom,頁面部分參考ChatRoom專案。
參考
WebSocket 教程
學習WebSocket協議—從頂層到底層的實現原理(修訂版)
Embedded Tomcat does not honor ServletContainerInitializers
Tomcat實現Web Socket
websocket之三:Tomcat的WebSocket實現