參考 RFC 6455: The WebSocket Protocol
WebSocket 協議基礎
- 協議本質:在單個 TCP 連接上提供全雙工通信通道的協議
- 核心優勢:
- 雙向實時通信(服務器主動推送)
- 低延遲(相比 HTTP 輪詢)
- 高效數據傳輸(減少 HTTP 頭部開銷)
- 協議握手:
# 來自客戶端的握手數據
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
- Connection:設置 Upgrade,表示客戶端希望連接升級
- Upgrade:設置 websocket,表示希望升級到 Websocket 協議
- Sec-WebSocket-Key:客戶端發送的一個 base64 編碼的密文,用于簡單的認證秘鑰。要求服務端必須返回一個對應加密的 Sec-WebSocket-Accept 應答,否則客戶端會拋出錯誤,并關閉連接
- Sec-WebSocket-Protocol:子協議選擇, 標識客戶端支持的協議
- Sec-WebSocket-Version :表示支持的 Websocket 版本
- Sec-WebSocket-Extensions:戶端期望使用的協議級別的擴展
# 服務端的握手響應
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
- HTTP/1.1 101 Switching Protocols:表示服務端接受 WebSocket 協議的客戶端連接
- Sec-WebSocket-Accept:驗證客戶端請求報文,同樣也是為了防止誤連接。具體做法是把請求頭里 Sec-WebSocket-Key 的值,加上一個專用的 UUID,再計算摘要
- 結束握手:任何一端都可以發送一個包含特定關閉握手的控制幀數據。收到此幀后,另一端在不發送任何數據后會發送一個結束幀作為響應。收到另一端的結束幀后,最開始發送控制幀的端在沒有數據需要發送時,就會安全的關閉此連接。在發送了一個表明連接需要被關閉的控制幀后,這個客戶端不會再發送任何的數據;在收到一個表明連接需要被關閉的控制幀后,這個客戶端會丟棄此后的所有數據。
WebSocket 幀結構
在 WebSocket 協議中,數據是通過一系列數據幀來進行傳輸的。為了避免安全問題,客戶端必須在它發送到服務器的所有幀中添加掩碼(Mask),服務端收到沒有添加掩碼的數據幀以后,必須立即關閉連接。另外服務端禁止在發送數據幀給客戶端時添加掩碼,客戶端如果收到了一個添加了掩碼的幀,必須立即關閉連接。
0 1 2 30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-------+-+-------------+-------------------------------+|F|R|R|R| opcode|M| Payload len | Extended payload length ||I|S|S|S| (4) |A| (7) | (16/64) ||N|V|V|V| |S| | (if payload len==126/127) || |1|2|3| |K| | |+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +| Extended payload length continued, if payload len == 127 |+ - - - - - - - - - - - - - - - +-------------------------------+| |Masking-key, if MASK set to 1 |+-------------------------------+-------------------------------+| Masking-key (continued) | Payload Data |+-------------------------------- - - - - - - - - - - - - - - - +: Payload Data continued ... :+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +| Payload Data continued ... |+---------------------------------------------------------------+
基礎幀頭
位偏移 | 字段名稱 | 長度 | 詳細說明 |
---|---|---|---|
0 | FIN | 1 bit | 標識是否為消息的最后一幀(1=最終幀,0=還有后續幀) |
1-3 | RSV1, RSV2, RSV3 | 各1 bit | 保留位,必須為0(除非擴展協議定義特殊用途) |
4-7 | Opcode | 4 bits | 幀類型標識: 0x0=連續幀;0x1=文本幀;0x2=二進制幀;0x8=關閉幀; 0x9=PING;0xA=PONG |
8 | Mask | 1 bit | 是否使用掩碼(客戶端到服務器必須設為1) |
9-15 | Payload Length | 7 bits | 數據長度(實際值分三種情況): 0-125:實際長度;126:后續2字節表示長度;127:后續8字節表示長度 |
Opcode類型表
Hex | 類型 | 描述 |
---|---|---|
0x0 | Continuation | 連續幀(分片消息) |
0x1 | Text | UTF-8文本數據 |
0x2 | Binary | 二進制數據 |
0x8 | Close | 連接關閉指令 |
0x9 | Ping | 心跳檢測請求 |
0xA | Pong | 心跳檢測響應 |
長度解碼規則
Payload Length值 | 后續字節數 | 實際長度范圍 |
---|---|---|
0-125 | 0 | 0-125字節 |
126 | 2 | 126-65,535字節 |
127 | 8 | 65,536-2^64-1字節 |
控制幀特殊說明
所有的控制幀必須有一個 126 字節或者更小的負載長度,并且不能被分片
-
Close幀(0x8):
- 前2字節:狀態碼(如1000表示正常關閉)
- 可選 UTF-8 原因短語
-
Ping/Pong 幀(0x9/0xA):
- 必須實現心跳應答機制
- Pong 的 Payload 需與對應 Ping 一致
示例幀解析
Hello 文本幀原始字節(Hex):
81 85 37 FA 21 3D 7F 9F 4D 51 58
解析結果:
- 81 → FIN=1, Opcode=0x1(文本幀)
- 85 → Mask=1, Payload Length=5
- 37 FA 21 3D → 掩碼密鑰
- 7F 9F 4D 51 58 → 加密后的 “Hello”
前端 WebSocket 高可用框架設計
暫時沒有設計日志和統計系統,項目地址 websocket-pro-client
組件關系圖
- WebSocketManager:入口類,管理所有連接實例和共享資源
- WebSocketClient:單個連接實例,處理連接生命周期和消息收發
- Heartbeat:心跳檢測管理,維護連接活性
- TaskScheduler:任務調度器,控制并發消息發送
- PriorityQueue:優先級隊列,確保高優先級消息優先處理
- EventEmitter:事件中心,統一處理所有連接事件
分層架構設計
架構說明
-
用戶層
- UI Components:業務組件,通過標準API與核心層交互
- 事件流:通過EventEmitter實現松耦合通信
-
核心層
- WebSocketManager:單例入口
- Connection Pool:連接池維護策略:
- 最大連接數限制(默認5個)
- LRU(最近最少使用)淘汰機制
- 相同URL自動復用連接
-
網絡層
- 封裝原生WebSocket API,增加:
- 自動重連裝飾器
- 二進制數據分片處理
- CORS安全校驗
- 封裝原生WebSocket API,增加:
數據流轉過程
詳細設計說明
連接管理
連接狀態機:
連接池實現:
class WebSocketManager {private connectionPool: Map<string, WebSocketClient>;// 獲取或創建連接public connect(url: string): WebSocketClient {if (this.connectionPool.has(url)) {return this.connectionPool.get(url)!; // 復用現有連接}const client = new WebSocketClient(url, this.config);this.connectionPool.set(url, client);return client;}// 關閉所有連接public closeAll(): void {this.connectionPool.forEach(client => client.close());}
}
連接池 vs 單連接
- 優點:避免重復握手開銷,支持多租戶隔離
- 缺點:增加內存占用,需要維護狀態一致性
錯誤處理體系
錯誤類型 | 處理方式 |
---|---|
連接錯誤 | 自動觸發重連機制,累計重試次數 |
心跳超時 | 主動關閉連接并標記為異常斷開,觸發快速重連 |
消息發送失敗 | 根據優先級存入隊列,連接恢復后自動重發 |
協議錯誤 | 關閉連接并觸發error事件,不自動重連 |
錯誤捕獲示例
// 網絡層錯誤捕獲
socket.addEventListener('error', (event) => {this.status = 'disconnected';this.emit('error', {type: 'network',error: event,willReconnect: this.reconnectAttempts < this.config.maxReconnectAttempts});this.scheduleReconnect();
});// 應用層錯誤處理
public send(data: any): Promise<void> {return new Promise((resolve, reject) => {if (this.status !== 'connected') {reject(new Error('Connection not ready'));return;}try {this.socket.send(data);resolve();} catch (error) {this.emit('error', {type: 'send',error,data});reject(error);}});
}
智能重連機制
重連算法流程:
核心代碼:
private scheduleReconnect(): void {// 1. 檢查重試上限if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {this.emit('reconnect_failed', {attempts: this.reconnectAttempts,maxAttempts: this.config.maxReconnectAttempts});return;}// 2. 指數退避計算const baseDelay = this.config.reconnectDelay;const exponent = Math.pow(this.config.reconnectExponent, this.reconnectAttempts);const cappedDelay = Math.min(baseDelay * exponent,this.config.maxReconnectDelay);// 3. 添加隨機抖動(避免集群同時重連的"驚群效應")const jitterRatio = 0.2; // ±20%的隨機波動const jitter = cappedDelay * jitterRatio * (Math.random() * 2 - 1); // [-0.2,0.2]范圍const actualDelay = Math.max(1000, cappedDelay + jitter); // 保證至少1秒// 4. 設置定時器this.reconnectTimer = setTimeout(() => {this.reconnectAttempts++;this.emit('reconnect', {attempt: this.reconnectAttempts,nextDelay: actualDelay});// 5. 實際重連操作this.connect()}, actualDelay);
}
指數退避公式
每次重試間隔 = min(初始延遲 * (退避系數^重試次數), 最大延遲)
# 初始值
initialDelay = 1000ms,
exponent = 1.5,
maxDelay = 30000ms# 重試間隔增長示例:
第1次: 1000ms
第2次: 1500ms (1000*1.5^1)
第3次: 2250ms (1000*1.5^2)
...
第10次: 30000ms (達到上限)
另外可添加服務端過載保護:
if (this.reconnectAttempts > 3) {// 隨機跳過1次重試if (Math.random() < 0.3) {this.reconnectAttempts++;this.scheduleReconnect();return;}
}
心跳檢測系統
核心代碼:
class Heartbeat {private lastPong: number = 0;public start(): void {this.intervalId = setInterval(() => {if (this.socket.readyState === WebSocket.OPEN) {this.socket.send('ping');this.timeoutId = setTimeout(() => {this.onTimeout(); // 心跳超時處理}, this.timeout);}}, this.interval);}public recordPong(): void {this.lastPong = Date.now();clearTimeout(this.timeoutId);this.emit('latency', Date.now() - this.lastPong);}
}
消息調度系統
優先級隊列設計:
優先級 | 消息類型 | 默認權重 |
---|---|---|
0 | 系統控制消息(如心跳) | 最高 |
1 | 用戶關鍵操作 | 高 |
2 | 普通數據更新 | 中 |
3 | 批量日志/非實時數據 | 低 |
優先級調度策略:
- 嚴格優先級,適用于金融交易系統等
- 加權輪詢,適用于物聯網數據采集等
- 動態調整,適用于視頻流傳輸等
調度器核心代碼:
interface Task {task: () => Promise<void>;priority: number;
}class PriorityQueue {private items: Task[] = [];public enqueue(task: Task): void {let added = false;for (let i = 0; i < this.items.length; i++) {if (task.priority > this.items[i].priority) {this.items.splice(i, 0, task);added = true;break;}}if (!added) {this.items.push(task);}}public dequeue(): Task | undefined {return this.items.shift();}public get length(): number {return this.items.length;}
}
class TaskScheduler {public addTask(task: () => Promise<void>, priority: number): Promise<void> {return new Promise((resolve, reject) => {this.queue.enqueue({task: async () => {try {await task();resolve();} catch (error) {reject(error);}},priority});this.run();});}private run(): void {while (this.runningTasks < this.maxConcurrent && this.queue.length > 0) {const { task } = this.queue.dequeue()!;this.runningTasks++;task().finally(() => {this.runningTasks--;this.run(); // 遞歸執行下一個任務});}}
}
性能優化策略
- 使用ArrayBuffer傳輸圖像數據
- 消息壓縮
- 帶寬自適應(自動適應從2G到5G的網絡環境,基于網絡類型調整策略,如心跳間隔,最大連接數等)
總結
WebSocket 是一種網絡傳輸協議,位于 OSI
模型的應用層。可在單個 TCP
連接上進行全雙工通信,能更好的節省服務器資源和帶寬并達到實時通迅。客戶端和服務器只需要完成一次握手,兩者之間就可以創建持久性的連接,并進行雙向數據傳輸。
特點
- 全雙工,通信允許數據在兩個方向上同時傳輸,它在能力上相當于兩個單工通信方式的結合
- 二進制幀,采用了二進制幀結構,語法、語義與 HTTP 完全不兼容,相比
http/2
,WebSocket 更側重于“實時通信”,而http/2
更側重于提高傳輸效率,所以兩者的幀結構也有很大的區別。不像http/2
那樣定義流,也就不存在多路復用、優先級等特性,自身就是全雙工,也不需要服務器推送 - 協議名,引入
ws
和wss
分別代表明文和密文的 WebSocket 協議,且默認端口使用 80 或 443,幾乎與http
一致 - 握手,WebSocket 也要有一個握手過程,然后才能正式收發數據。
優點
- 較少的控制開銷:數據包頭部協議較小,不同于
http
每次請求需要攜帶完整的頭部 - 更強的實時性:相對于
http
請求需要等待客戶端發起請求服務端才能響應,延遲明顯更少 - 保持創連接狀態:創建通信后,可省略狀態信息,不同于
http
每次請求需要攜帶身份驗證 - 更好的二進制支持:定義了二進制幀,更好處理二進制內容
- 支持擴展:用戶可以擴展 WebSocket 協議、實現部分自定義的子協議
- 更好的壓縮效果:WebSocket 在適當的擴展支持下,可以沿用之前內容的上下文,在傳遞類似的數據時,可以顯著地提高壓縮率