跳至主要內容
Skip to content

幀 (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: Ping
    • 0xA: 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 長度時有三種情況:

  1. < 126:長度直接寫在 7 bits 中。
  2. 126:後續 2 bytes 代表真正的長度(最大 65,535)。
  3. 127:後續 8 bytes 代表長度(支援極大數據,甚至 GB 級)。

總結

WebSocket 的幀設計展現了二進制協議的精巧編排。理解這些底層細節,能讓你更有效地優化數據傳輸並理解許多疑難雜症的成因。

下一章,我們將解開安全性防禦:TLS 加密與 CSWSH 攻擊防止。


️ 進階挑戰

  1. 實作挑戰:修改上面的 unmask 函數,嘗試親手寫出一個能夠自動判斷長度並提取 Payload 的簡易 Parser。
  2. 故障排除:如果客戶端發送的訊息 MASK 位元為 0,根據 RFC 6455,伺服器應該做出什麼反應?(提示:查看規範中關於 Protocol Error 的部分)。

延伸閱讀與資源