---
# System prepended metadata

title: WebSocket簡介及於SpringBoot下的範例

---

# 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
