一、什么是 WebSocket?
定義:WebSocket 是一種在單個 TCP 連接上進行全雙工通信的協議。
作用:實現客戶端(瀏覽器)和服務器之間的實時、雙向通信。
優勢:
連接保持,通信實時性強(不像 HTTP 需要每次請求都建立連接)。
節省帶寬,減少延遲。
適合聊天室、實時推送、游戲、股票行情、物聯網等場景。
二、WebSocket 工作原理簡述
客戶端發起 HTTP 請求(帶有 Upgrade: websocket 頭)請求升級協議。
服務器響應協議升級,建立 WebSocket 連接。
建立連接后,客戶端和服務器可以隨時互相發送消息,連接保持,直到關閉。
連接關閉后,雙方不能再通信。
三、Java 原生 WebSocket 使用示例
Java 標準庫中
javax.websocket
提供了 WebSocket 支持。下面示例是一個簡單的服務端:1. 添加依賴(以 Maven 為例)
<!-- 只在Java EE容器或者支持Java WebSocket API的容器中需要 --> <dependency><groupId>javax.websocket</groupId><artifactId>javax.websocket-api</artifactId><version>1.1</version> </dependency>
2. 編寫WebSocketConfig配置類
Spring Boot 默認不支持直接掃描 @ServerEndpoint,需要做個配置類注冊它:
@Configuration public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();} }
這個 ServerEndpointExporter 負責注冊所有帶 @ServerEndpoint 注解的 WebSocket 類。
(1)?
@ServerEndpoint
注解的類和 Spring 容器
使用標準 Java EE
javax.websocket
API 的@ServerEndpoint
注解時,WebSocket 服務端點是由 Java EE 容器(如 Tomcat、Jetty)掃描并管理的,不依賴 Spring 容器。但是,Spring Boot 本身不會自動掃描和管理 用
@ServerEndpoint
注解的類。
(2) 為什么需要
ServerEndpointExporter
ServerEndpointExporter
是 Spring Boot WebSocket 模塊提供的一個 bean,作用是:
讓 Spring Boot 容器掃描并注冊所有用
@ServerEndpoint
注解的類。把這些端點交給底層的 WebSocket 容器(Tomcat 等)管理。
(3) 什么時候需要
ServerEndpointExporter
?
如果你用的是 Spring Boot 嵌入式 Tomcat 或其他容器,且你的 WebSocket 端點是用標準的
@ServerEndpoint
注解實現的,通常需要配置這個 bean3. 編寫 WebSocket 服務器端
@ServerEndpoint("/myWs") @Component @Slf4j public class WsServerEndpont {static Map<String,Session> sessionMap = new ConcurrentHashMap<>();//連接建立時執行的操作@OnOpenpublic void onOpen(Session session){sessionMap.put(session.getId(),session);log.info("websocket is open");}//收到了客戶端消息執行的操作@OnMessagepublic String onMessage(String text){log.info("收到了一條消息:"+text);return "已收到你的消息";}//連接關閉的時候執行的操作@OnClosepublic void onClose(Session session){sessionMap.remove(session.getId());log.info("websocket is close");}//每2s發送給客戶端心跳消息@Scheduled(fixedRate = 2000)public void sendMsg() throws IOException {for(String key:sessionMap.keySet()){sessionMap.get(key).getBasicRemote().sendText("心跳");}} }
3. 部署運行
需要在支持 Java EE WebSocket 的容器中(如 Tomcat 8+、Jetty、Glassfish)部署。
客戶端可用瀏覽器或工具連接:
ws://localhost:8080/your-app/websocket
四、Spring Boot 中使用 WebSocket(實現多人聊天)
????????Spring Boot 提供了非常方便的 WebSocket 支持,通常結合 STOMP 協議和 SockJS 來實現消息的訂閱和廣播。
1. 添加依賴(Maven)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency>
Spring Boot 的依賴管理機制(依賴版本繼承)
Spring Boot 使用了 “依賴管理(Dependency Management)”,這是它的核心特性之一。
你只需在你的項目中聲明
spring-boot-starter-parent
或通過spring-boot-dependencies
管理依賴版本,Spring Boot 會自動幫你管理和統一版本號。2. 配置 MyWsConfig
// WebSocket配置類,注冊WebSocket處理器和攔截器 @Configuration @EnableWebSocket // 啟用WebSocket支持 public class MyWsConfig implements WebSocketConfigurer {@ResourceMyWsHandler myWsHandler; // 自定義的WebSocket處理器@ResourceMyWsInterceptor myWsInterceptor; // 自定義的握手攔截器@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {// 注冊WebSocket處理器,路徑為/myWs1// 添加握手攔截器,允許所有源跨域請求(*)registry.addHandler(myWsHandler, "/myWs1") // 注冊WebSocket處理器及路徑.addInterceptors(myWsInterceptor) // 添加握手階段的攔截器,做一些額外處理.setAllowedOrigins("*"); // 允許所有域發起跨域連接請求} }
2.1 代碼講解:
(1)?
addHandler(myWsHandler, "/myWs1")
作用:給 WebSocket 服務器注冊一個處理器(Handler),并指定客戶端連接時使用的 URL 路徑。
myWsHandler 是你自定義的 WebSocket 業務處理類(繼承自
WebSocketHandler
或AbstractWebSocketHandler
)。"/myWs1" 是 WebSocket 連接的訪問路徑,客戶端通過
ws://服務器地址/myWs1
來建立 WebSocket 連接。總結:這句相當于告訴服務器,“當客戶端請求
/myWs1
路徑時,交給myWsHandler
來處理這次 WebSocket 連接和消息。”
(2)
addInterceptors(myWsInterceptor)
作用:給這個 WebSocket 處理器綁定一個或多個攔截器(
HandshakeInterceptor
),用來攔截 WebSocket 握手階段的請求。握手階段是 WebSocket 建立連接的第一步,類似 HTTP 請求升級。
你可以在攔截器里做一些額外操作,比如:
記錄日志
權限校驗
傳遞用戶信息到 WebSocket Session
修改握手請求和響應
你的
myWsInterceptor
繼承自HttpSessionHandshakeInterceptor
,默認支持把 HTTP Session 關聯到 WebSocket Session。總結:這句是告訴服務器,“在建立 WebSocket 連接握手時,執行
myWsInterceptor
中的邏輯。”
(3)
setAllowedOrigins("*")
作用:設置允許連接的客戶端來源(Origin),即支持跨域連接的源。
這里
"*"
表示允許所有源都能連接你的 WebSocket 服務。Origin 是瀏覽器在 WebSocket 握手請求頭里自動帶上的,服務器根據它判斷是否允許該請求。
注意:生產環境一般不要用
"*"
,建議指定可信的域名,如setAllowedOrigins("https://example.com")
。總結:這句是告訴服務器,“允許哪些來源的網頁可以發起 WebSocket 連接請求”。
3. 配置 MyWsHandler
@Slf4j @Component public class MyWsHandler extends AbstractWebSocketHandler {// 存放所有連接的客戶端會話,線程安全的Mapprivate static Map<String, SessionBean> sessionBeanMap;// 用于生成客戶端唯一ID的原子計數器private static AtomicInteger clientIdMaker;// 用于存放群聊消息的緩沖區private static StringBuffer stringBuffer;static {sessionBeanMap = new ConcurrentHashMap<>();clientIdMaker = new AtomicInteger(0);stringBuffer = new StringBuffer();}/*** 連接建立成功時調用//相當于上面原生的onOpe******************************/@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {super.afterConnectionEstablished(session);// 新建SessionBean,給該連接分配唯一clientIdSessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());// 放入sessionMap管理sessionBeanMap.put(session.getId(), sessionBean);log.info(sessionBean.getClientId() + "建立了連接");// 群聊消息中記錄進入群聊通知stringBuffer.append(sessionBean.getClientId() + "進入了群聊<br/>");// 給所有客戶端發送當前群聊消息sendMessage(sessionBeanMap);}/*** 收到客戶端消息時調用//相當于上面原生的onMessage*********************************/@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {super.handleTextMessage(session, message);// 記錄日志,打印客戶端發送的消息log.info(sessionBeanMap.get(session.getId()).getClientId() + ":" + message.getPayload());// 將消息追加到群聊緩沖區stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId() + ":" + message.getPayload() + "<br/>");// 廣播給所有客戶端sendMessage(sessionBeanMap);}/*** 傳輸發生異常時調用*/@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {super.handleTransportError(session, exception);// 如果會話還開著,先關閉if (session.isOpen()) {session.close();}// 移除該會話sessionBeanMap.remove(session.getId());}/*** 連接關閉時調用 //相當于上面原生的onClose*****************************/@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {super.afterConnectionClosed(session, status);// 獲取即將關閉連接的clientIdint clientId = sessionBeanMap.get(session.getId()).getClientId();// 移除關閉的會話sessionBeanMap.remove(session.getId());log.info(clientId + "關閉了連接");// 群聊消息中記錄退出通知stringBuffer.append(clientId + "退出了群聊<br/>");// 廣播給所有客戶端sendMessage(sessionBeanMap);}/*** 給所有客戶端發送消息*/public void sendMessage(Map<String, SessionBean> sessionBeanMap) {for (String key : sessionBeanMap.keySet()) {try {// 發送群聊緩沖區里的消息給每個客戶端sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage(stringBuffer.toString()));} catch (IOException e) {// 日志記錄異常log.error(e.getMessage());}}} }
4. 配置 MyWsInterceptor
@Slf4j @Component public class MyWsInterceptor extends HttpSessionHandshakeInterceptor {/*** 握手之前調用,可用于記錄日志或做權限校驗*/@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {log.info(request.getRemoteAddress().toString() + "開始握手");return super.beforeHandshake(request, response, wsHandler, attributes);}/*** 握手完成調用*/@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,WebSocketHandler wsHandler, Exception ex) {log.info(request.getRemoteAddress().toString() + "完成握手");super.afterHandshake(request, response, wsHandler, ex);} }
5. 配置 SessionBean
// 簡單的會話封裝類,保存WebSocketSession和客戶端唯一ID @AllArgsConstructor @Data public class SessionBean {private WebSocketSession webSocketSession;private Integer clientId; }
6. 啟動類WsDemoApplication?
// 啟動類,開啟定時任務支持 @EnableScheduling @SpringBootApplication public class WsDemoApplication {public static void main(String[] args) {SpringApplication.run(WsDemoApplication.class, args);} }
7. 前端示例(HTML + JavaScript)
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>ws client</title> </head> <body> <p style="border:1px solid black;width: 600px;height: 500px" id="talkMsg"></p> <input id="message" /><button id="sendBtn" onclick="sendMsg()">發送</button> </body> <script>let ws = new WebSocket("ws://localhost:8080/myWs1")// ws.onopen=function () {// }ws.onmessage=function (message) {document.getElementById("talkMsg").innerHTML = message.data}function sendMsg() {ws.send(document.getElementById("message").value)document.getElementById("message").value=""} </script> </html>
五、小結
特點 Java 原生 WebSocket Spring Boot WebSocket + STOMP 適用場景 簡單直接的 WebSocket 應用 需要消息訂閱/廣播、復雜消息路由的應用 配置復雜度 低(容器支持即可) 需要配置消息代理,依賴更多 功能支持 基礎雙向通信 支持訂閱、廣播、分組、消息轉換 前端開發支持 需自行實現協議 使用 STOMP.js 等庫簡化開發