# WebSocket簡介及於SpringBoot下的範例 ![](https://i.imgur.com/81loDPO.png) ## WebSocket簡介 WebSocket 是 HTML5 開始提供,獨立於單一 TCP 連線並進行全雙工通訊的有狀態協定(不同於無狀態的 HTTP)。 它支援二進位frame、擴展協定、部分自訂的子協定、壓縮等特性。W ebSocket 的 RFC6455 標準中制定了 2 個高級組件,一個是用於協商連接參數的開放性 HTTP handshake,另一個是二進位消息分幀機制,用於支援低開銷、基於訊息的文本和二進位資料傳輸。 關於 HTTP handshake,可參考以下 header 來理解。 Request: ```header GET /chat Host: (你的domain) Origin: https://(你的domain) Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap, wamp ``` Response: ```header 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap ``` 其中請求的方法必須是GET,HTTP版本必須至少是1.1 Sec-WebSocket-Key 欄位用於握手階段。伺服器用來證明它收到的資訊能有效的完成 WebSocket 握手。這有助於確保伺服器不會接受來自非 WebSocket 用戶端(例如 HTTP 用戶端)的連線,以免被濫用發送資料到毫無防備的 WebSocket 伺服器。 Sec-WebSocket-Key/Sec-WebSocket-Accept 欄位都只是在握手的時候保證握手成功及確認用途但並無資安保證,用 wss:// 會稍微安全一點。 從 Sec-WebSocket-Extensions 欄位可了解,WebSocket 通訊本質是由「frames」訊框組成,可從任何一方發送,並且有以下幾種類型: “text frames” 包含各方發送給彼此的文本資料。 “binary data frames” 包含各方發送給彼此的二進位資料。 “ping/pong frames” 被用於檢查伺服器發送連結,流覽器會自動回應它們。 其他尚有 “connection close frame”,以及其他服務 frames。 附註一點,我們不能使用 XMLHttpRequest 或 fetch 來進行申請 websocket 的 HTTP 請求,原因是這些方法不允許 JavaScript 設置上述 header。 ## WebSocket基本性質 **WebSocket 屬性ws.readyState:** 0 - CONNECTING - 表示連接尚未建立。 1 - OPEN - 表示連接已建立,可以進行通信。 2 - CLOSING - 表示連接正在進行關閉。 3 - CLOSED - 表示連接已經關閉或者連接不能打開。 **WebSocket事件:** **onopen** 連接建立時觸發 **onmessage** 用戶端接收服務端資料時觸發 **onerror** 通信發生錯誤時觸發 **onclose** 連接關閉時觸發 **WebSocket方法:** Socket.send() 使用連接發送資料 Socket.close() 關閉連接 WebSocket API 極其簡潔,JavaScript 可以呼叫的函數只有這些: ```JavaScript var ws = new WebSocket('wss://example.com/socket'); ws.onerror = function (error) { ... } ws.onclose = function () { ... } ws.onopen = function () { ws.send("Connection established. Hello server!"); } ws.onmessage = function(msg) { if(msg.data instanceof Blob) { processBlob(msg.data); } else { processText(msg.data); } } ``` ## 試作 WebSocket 程式 ### 傳統@ServerEndpoint方式 #### 後端:1個@ServerEndpoint@Conponent + 1個@Configuration@Bean 由於 Springboot 自行管理容器,故需要用 config 註冊以@ServerEndpoint 註解的物件(否則報404) *若移到另外做容器掃描的專案環境,例如 Spring+Tomcat,則需移除 ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } ``` ### SpringBoot推薦作法 #### 後端:1個 WebSocketConfigurer攔截器 + 1個 WebSocketHandler ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyWebSocketHandler(), "/ws/serverTwo")//設定連接路徑和處理 .setAllowedOrigins("*") .addInterceptors(new MyWebSocketInterceptor());//設定攔截器 } /** * 自訂攔截器攔截WebSocket請求 */ class MyWebSocketInterceptor implements HandshakeInterceptor { //前置攔截一般用來註冊使用者資訊,綁定 WebSocketSession @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { System.out.println("前置攔截"); if (!(request instanceof ServletServerHttpRequest)) return true; // HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); // String userName = (String) servletRequest.getSession().getAttribute("userName"); String userName = "willLin"; attributes.put("userName", userName); return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { System.out.println("後置攔截"); } } } ``` ```java public class MyWebSocketHandler implements WebSocketHandler { private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String userName = session.getAttributes().get("userName").toString(); SESSIONS.put(userName, session); System.out.println(String.format("成功建立連接 userName: %s", userName)); } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { String msg = message.getPayload().toString(); System.out.println(msg); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { System.out.println("連線出錯"); if (session.isOpen()) { session.close(); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { System.out.println("連線已關閉,status:" + closeStatus); } @Override public boolean supportsPartialMessages() { return false; } /** * 指定發消息 * * @param message */ public static void sendMessage(String userName, String message) { WebSocketSession webSocketSession = SESSIONS.get(userName); if (webSocketSession == null || !webSocketSession.isOpen()) return; try { webSocketSession.sendMessage(new TextMessage(message)); } catch (IOException e) { e.printStackTrace(); } } /** * 群發消息 * * @param message */ public static void fanoutMessage(String message) { SESSIONS.keySet().forEach(us -> sendMessage(us, message)); } } ``` WebSocket開發的核心: 需要實現WebSocketHandler 介面,該介面提供了五個方法。 1、 afterConnectionEstablished():建立新的socket連線後回檔的方法。 2、handleMessage():接收用戶端發送的Socket。 3、handleTransportError():連線出錯時,回檔的方法。 4、afterConnectionClosed():連線關閉時,回檔的方法。 5、supportsPartialMessages():這個是WebSocketHandler是否處理部分消息,或者返回false就完成。 ### 參考資料: https://halfrost.com/websocket/ https://zh.javascript.info/websocket https://juejin.cn/post/7080171898176274463#comment