幀 (Frame) 的奧秘:拆解 WebSocket 的二進制細節
在第一章,我們成功完成了握手(Handshake)。但當連線建立後,數據並不是像 HTTP 那樣直接傳送純文字,而是被包裹在一個個 「幀 (Frame)」 之中。
為什麼客戶端發送的消息必須經過「加密掩碼 (Masking)」?為什麼數據可以被分段傳輸?本章我們將透視二進制流的底層結構。
一、 幀結構圖解 (RFC 6455 Section 5)
一個 WebSocket 幀由標頭(Header)與負載(Payload)組成。其位元結構如下:
text
0 1 2 3
0 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 ... :
+---------------------------------------------------------------+核心欄位解析:
- FIN (1 bit):標記這是否為訊息的最後一幀。
- Opcode (4 bits):定義數據類型。
0x1: 純文字 (Text)0x2: 二進制 (Binary)0x8: 關閉連線 (Close)0x9: Ping0xA: Pong
- MASK (1 bit):關鍵! RFC 規定客戶端發送給伺服器的數據必須進行掩碼處理(1),而伺服器發給客戶端則不需(0)。這是為了防止中繼代理伺服器的快取污染。
二、 手寫實作:解開掩碼 (Unmasking)
當伺服器收到客戶端的二進制流時,我們需要先提取 4 bytes 的 Masking-key,然後對 Payload 進行 XOR 運算:
javascript
/**
* 解開 WebSocket 幀的掩碼數據
* @param {Buffer} encodedPayload - 原始的 Payload Data
* @param {Buffer} maskKey - 4 bytes 的 Masking Key
* @returns {Buffer} - 解密後的原始數據
*/
function unmask(encodedPayload, maskKey) {
const result = Buffer.alloc(encodedPayload.length);
for (let i = 0; i < encodedPayload.length; i++) {
// 算法:encoded_byte XOR mask_key[i % 4]
result[i] = encodedPayload[i] ^ maskKey[i % 4];
}
return result;
}
// 實驗室範例:假設收到掩碼後的數據與 Key
const maskKey = Buffer.from([0x37, 0xfa, 0x21, 0x3d]);
const maskedData = Buffer.from([0x7f, 0x9f, 0x4d, 0x51, 0x58]); // 這是 "Hello" 被遮罩後的部分
const original = unmask(maskedData, maskKey);
console.log("解密後的內容:", original.toString()); // 印出 "Hello"三、 長度挑戰 (Payload Length)
處理 Payload 長度時有三種情況:
- < 126:長度直接寫在 7 bits 中。
- 126:後續 2 bytes 代表真正的長度(最大 65,535)。
- 127:後續 8 bytes 代表長度(支援極大數據,甚至 GB 級)。
總結
WebSocket 的幀設計展現了二進制協議的精巧編排。理解這些底層細節,能讓你更有效地優化數據傳輸並理解許多疑難雜症的成因。
下一章,我們將解開安全性防禦:TLS 加密與 CSWSH 攻擊防止。
️ 進階挑戰
- 實作挑戰:修改上面的
unmask函數,嘗試親手寫出一個能夠自動判斷長度並提取 Payload 的簡易 Parser。 - 故障排除:如果客戶端發送的訊息 MASK 位元為 0,根據 RFC 6455,伺服器應該做出什麼反應?(提示:查看規範中關於 Protocol Error 的部分)。
延伸閱讀與資源
- RFC 6455 Section 5:Data Framing
- MDN:Writing WebSocket servers