# 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