WebSocket 协议 #

协议概述 #

WebSocket 协议(RFC 6455)定义了一种在单个 TCP 连接上进行全双工通信的机制。它从 HTTP 协议升级而来,但独立于 HTTP 运行。

text
┌─────────────────────────────────────────────────────────────┐
│                    WebSocket 协议栈                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              WebSocket 协议                          │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │   │
│  │  │   握手阶段   │  │   数据传输   │  │   关闭阶段   │ │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘ │   │
│  └─────────────────────────────────────────────────────┘   │
│                          │                                  │
│                          ▼                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              HTTP 协议(仅握手阶段)                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                          │                                  │
│                          ▼                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              TCP 协议                                │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

握手过程 #

协议升级请求 #

WebSocket 连接始于一个 HTTP 请求,通过 Upgrade 头部请求协议升级。

text
┌─────────────────────────────────────────────────────────────┐
│                    客户端握手请求                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  GET /chat HTTP/1.1                                         │
│  Host: example.com:8080                                     │
│  Upgrade: websocket                                         │
│  Connection: Upgrade                                        │
│  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==               │
│  Sec-WebSocket-Version: 13                                  │
│  Origin: https://example.com                               │
│  Sec-WebSocket-Protocol: chat, json                        │
│  Sec-WebSocket-Extensions: permessage-deflate              │
│                                                             │
│  关键头部说明:                                              │
│  ─────────────────────────────────────────────────────────  │
│  Upgrade: websocket                                        │
│    请求升级到 WebSocket 协议                                │
│                                                             │
│  Connection: Upgrade                                       │
│    表示这是一个升级请求                                     │
│                                                             │
│  Sec-WebSocket-Key                                        │
│    Base64 编码的 16 字节随机值                              │
│    用于生成握手验证                                         │
│                                                             │
│  Sec-WebSocket-Version: 13                                 │
│    协议版本号                                               │
│                                                             │
│  Origin                                                    │
│    请求来源(用于安全验证)                                 │
│                                                             │
│  Sec-WebSocket-Protocol(可选)                            │
│    客户端支持的子协议列表                                   │
│                                                             │
│  Sec-WebSocket-Extensions(可选)                          │
│    客户端支持的扩展                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

服务端握手响应 #

text
┌─────────────────────────────────────────────────────────────┐
│                    服务端握手响应                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  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                         │
│    状态码 101 表示协议切换成功                              │
│                                                             │
│  Upgrade: websocket                                        │
│    确认升级到 WebSocket                                    │
│                                                             │
│  Connection: Upgrade                                       │
│    确认这是一个升级响应                                     │
│                                                             │
│  Sec-WebSocket-Accept                                      │
│    握手验证值                                               │
│    = Base64(SHA1(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))│
│                                                             │
│  Sec-WebSocket-Protocol(可选)                            │
│    服务端选择的子协议                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

握手验证计算 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Accept 值计算过程                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 获取客户端 Sec-WebSocket-Key                           │
│     dGhlIHNhbXBsZSBub25jZQ==                               │
│                                                             │
│  2. 拼接 GUID                                               │
│     dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11│
│                                                             │
│  3. 计算 SHA-1 哈希                                         │
│     SHA1(拼接后的字符串)                                    │
│                                                             │
│  4. Base64 编码                                             │
│     s3pPLMBiTxaQ9kYGzzhZRbK+xOo=                           │
│                                                             │
│  代码示例:                                                  │
│  ─────────────────────────────────────────────────────────  │
│  const crypto = require('crypto');                         │
│                                                             │
│  function calculateAccept(key) {                           │
│    const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';    │
│    return crypto                                            │
│      .createHash('sha1')                                   │
│      .update(key + GUID)                                   │
│      .digest('base64');                                    │
│  }                                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

握手流程图 #

text
┌─────────┐                                      ┌─────────┐
│  客户端  │                                      │  服务端  │
└────┬────┘                                      └────┬────┘
     │                                                │
     │  1. 发送握手请求                               │
     │  GET /chat HTTP/1.1                           │
     │  Upgrade: websocket                           │
     │  Sec-WebSocket-Key: xxx                       │
     │───────────────────────────────────────────────>│
     │                                                │
     │                                                │ 2. 验证请求
     │                                                │    - 检查 Upgrade 头部
     │                                                │    - 验证 Sec-WebSocket-Key
     │                                                │    - 检查 Origin
     │                                                │    - 选择子协议
     │                                                │
     │  3. 返回握手响应                               │
     │  HTTP/1.1 101 Switching Protocols             │
     │  Sec-WebSocket-Accept: yyy                    │
     │<───────────────────────────────────────────────│
     │                                                │
     │  4. 连接建立成功                               │
     │                                                │
     │  ════════════════════════════════════════════  │
     │           WebSocket 数据传输阶段               │
     │  ════════════════════════════════════════════  │
     │                                                │

数据帧格式 #

基本帧结构 #

text
┌─────────────────────────────────────────────────────────────┐
│                    WebSocket 数据帧格式                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  0                   1                   2                   │
│  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 │
│  +-+-+-+-+-------+-+-------------+---------------------------+
│  |F|R|R|R| opcode|M| Payload len |    Extended payload len   │
│  |I|S|S|S|  (4)  |A|     (7)     |          (16/64)          │
│  |N|V|V|V|       |S|             |   (if payload len==127)   │
│  | |1|2|3|       |K|             |                           │
│  +-+-+-+-+-------+-+-------------+---------------------------+
│  |     Extended payload len continued (if payload len==127)  │
│  +-----------------------------------------------------------+
│  |                          Masking-key                      │
│  +-----------------------------------------------------------+
│  |                          Payload Data                     │
│  +-----------------------------------------------------------+
│                                                             │
└─────────────────────────────────────────────────────────────┘

帧字段详解 #

text
┌─────────────────────────────────────────────────────────────┐
│                    帧字段说明                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  FIN (1 bit)                                                │
│  ─────────────────────────────────────────────────────────  │
│  表示是否为消息的最后一帧                                    │
│  0: 还有后续帧                                              │
│  1: 这是最后一帧                                            │
│                                                             │
│  RSV1, RSV2, RSV3 (各 1 bit)                               │
│  ─────────────────────────────────────────────────────────  │
│  保留位,用于扩展协议                                        │
│  除非协商了扩展,否则必须为 0                                │
│                                                             │
│  Opcode (4 bits)                                            │
│  ─────────────────────────────────────────────────────────  │
│  定义载荷类型:                                              │
│  0x0: 继续帧(Continuation)                               │
│  0x1: 文本帧(Text)                                        │
│  0x2: 二进制帧(Binary)                                    │
│  0x3-7: 保留用于非控制帧                                    │
│  0x8: 关闭帧(Close)                                       │
│  0x9: Ping 帧                                              │
│  0xA: Pong 帧                                              │
│  0xB-F: 保留用于控制帧                                      │
│                                                             │
│  MASK (1 bit)                                               │
│  ─────────────────────────────────────────────────────────  │
│  表示是否对载荷进行掩码处理                                  │
│  客户端发送必须为 1                                         │
│  服务端发送必须为 0                                         │
│                                                             │
│  Payload length (7 bits / 7+16 bits / 7+64 bits)           │
│  ─────────────────────────────────────────────────────────  │
│  载荷长度:                                                  │
│  0-125: 直接表示载荷长度                                    │
│  126: 后续 2 字节表示长度                                   │
│  127: 后续 8 字节表示长度                                   │
│                                                             │
│  Masking-key (0 或 32 bits)                                │
│  ─────────────────────────────────────────────────────────  │
│  掩码密钥,当 MASK=1 时存在                                 │
│                                                             │
│  Payload Data                                               │
│  ─────────────────────────────────────────────────────────  │
│  实际传输的数据                                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

载荷长度编码 #

text
┌─────────────────────────────────────────────────────────────┐
│                    载荷长度编码规则                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  长度范围          编码方式                                 │
│  ─────────────────────────────────────────────────────────  │
│  0-125 字节        直接用 7 位表示                          │
│  126-65535 字节    7 位 = 126,后跟 16 位长度               │
│  > 65535 字节      7 位 = 127,后跟 64 位长度               │
│                                                             │
│  示例:                                                     │
│  ─────────────────────────────────────────────────────────  │
│                                                             │
│  载荷 100 字节:                                            │
│  ┌─────────────┐                                           │
│  │ 01100100    │  = 100                                    │
│  └─────────────┘                                           │
│                                                             │
│  载荷 1000 字节:                                           │
│  ┌─────────────┬─────────────────┐                         │
│  │ 01111110    │ 0000001111101000│  = 126, 1000            │
│  └─────────────┴─────────────────┘                         │
│                                                             │
│  载荷 100000 字节:                                         │
│  ┌─────────────┬─────────────────────────────────────┐     │
│  │ 01111111    │ 00000000 00000000 00000001 10000110│     │
│  └─────────────┴─────────────────────────────────────┘     │
│                10100000 00000000                           │
│                = 127, 100000                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

掩码机制 #

为什么需要掩码? #

text
┌─────────────────────────────────────────────────────────────┐
│                    掩码的安全目的                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  掩码不是为了加密,而是为了安全:                            │
│                                                             │
│  1. 防止缓存投毒攻击                                        │
│     ─────────────────────────────────────────────────────   │
│     攻击者可能构造特殊数据包                                 │
│     伪装成有效的 HTTP 请求                                  │
│     污染代理服务器缓存                                      │
│                                                             │
│  2. 协议混淆攻击                                            │
│     ─────────────────────────────────────────────────────   │
│     防止 WebSocket 数据被误解为其他协议                     │
│                                                             │
│  规则:                                                     │
│  ─────────────────────────────────────────────────────────  │
│  ✅ 客户端发送的帧必须掩码                                  │
│  ✅ 服务端发送的帧不能掩码                                  │
│  ❌ 如果违反,必须关闭连接                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

掩码计算过程 #

text
┌─────────────────────────────────────────────────────────────┐
│                    掩码算法                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  原始数据:  D0 D1 D2 D3 D4 D5 D6 D7 ...                     │
│  掩码密钥:  M0 M1 M2 M3                                     │
│                                                             │
│  掩码后数据:                                                │
│  S0 = D0 XOR M0                                            │
│  S1 = D1 XOR M1                                            │
│  S2 = D2 XOR M2                                            │
│  S3 = D3 XOR M3                                            │
│  S4 = D4 XOR M0  (循环使用掩码)                            │
│  S5 = D5 XOR M1                                            │
│  ...                                                        │
│                                                             │
│  解码:                                                     │
│  D0 = S0 XOR M0                                            │
│  D1 = S1 XOR M1                                            │
│  ...                                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

掩码实现代码 #

javascript
function maskData(data, maskKey) {
  const masked = Buffer.alloc(data.length);
  
  for (let i = 0; i < data.length; i++) {
    masked[i] = data[i] ^ maskKey[i % 4];
  }
  
  return masked;
}

function unmaskData(maskedData, maskKey) {
  return maskData(maskedData, maskKey);
}

const data = Buffer.from('Hello');
const maskKey = Buffer.from([0x12, 0x34, 0x56, 0x78]);

const masked = maskData(data, maskKey);
const unmasked = unmaskData(masked, maskKey);

console.log('原始数据:', data.toString());
console.log('掩码后:', masked);
console.log('解掩码:', unmasked.toString());

数据帧类型 #

文本帧 #

text
┌─────────────────────────────────────────────────────────────┐
│                    文本帧示例                                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  发送文本 "Hello":                                         │
│                                                             │
│  客户端发送:                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1000 0001 1000 0101  [Mask Key]  [Masked Payload]   │   │
│  │   │   │     │                                          │
│  │   │   │     └── 载荷长度 = 5                          │   │
│  │   │   └── MASK = 1 (客户端必须掩码)                   │   │
│  │   └── Opcode = 1 (文本帧)                            │   │
│  │   └── FIN = 1 (最后一帧)                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  服务端发送:                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1000 0001 0000 0101  [Payload "Hello"]              │   │
│  │              └── MASK = 0 (服务端不掩码)             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二进制帧 #

text
┌─────────────────────────────────────────────────────────────┐
│                    二进制帧示例                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  发送二进制数据 [0x01, 0x02, 0x03, 0x04]:                  │
│                                                             │
│  客户端发送:                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1000 0010 1000 0100  [Mask Key]  [Masked Payload]   │   │
│  │       │                                              │   │
│  │       └── Opcode = 2 (二进制帧)                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  服务端发送:                                                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1000 0010 0000 0100  [0x01 0x02 0x03 0x04]         │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

消息分片 #

text
┌─────────────────────────────────────────────────────────────┐
│                    消息分片示例                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  发送长消息 "Hello World" 分成两片:                        │
│                                                             │
│  第一帧(FIN=0):                                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 0000 0001 1000 0101  [Mask]  [Masked "Hello"]       │   │
│  │   │                                                  │   │
│  │   └── FIN = 0 (还有后续帧)                          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  第二帧(FIN=1):                                          │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 1000 0000 1000 0110  [Mask]  [Masked " World"]      │   │
│  │   │       │                                          │   │
│  │   │       └── Opcode = 0 (继续帧)                   │   │
│  │   └── FIN = 1 (最后一帧)                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  分片规则:                                                  │
│  ─────────────────────────────────────────────────────────  │
│  - 第一帧 Opcode 为实际类型(文本/二进制)                  │
│  - 后续帧 Opcode 为 0(继续帧)                             │
│  - 只有最后一帧 FIN = 1                                    │
│  - 控制帧可以插入在分片消息中间                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

控制帧 #

Ping/Pong 帧 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Ping/Pong 心跳机制                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Ping 帧:                                                  │
│  ─────────────────────────────────────────────────────────  │
│  Opcode = 0x9                                              │
│  可以携带载荷(可选)                                       │
│  用于检测连接是否存活                                       │
│                                                             │
│  Pong 帧:                                                  │
│  ─────────────────────────────────────────────────────────  │
│  Opcode = 0xA                                              │
│  必须携带与 Ping 相同的载荷                                 │
│  作为 Ping 的响应                                           │
│                                                             │
│  流程:                                                     │
│                                                             │
│   ┌─────────┐                    ┌─────────┐               │
│   │  客户端  │                    │  服务端  │               │
│   └────┬────┘                    └────┬────┘               │
│        │                              │                      │
│        │  Ping (payload: "hi")       │                      │
│        │─────────────────────────────>│                      │
│        │                              │                      │
│        │  Pong (payload: "hi")       │                      │
│        │<─────────────────────────────│                      │
│        │                              │                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Close 帧 #

text
┌─────────────────────────────────────────────────────────────┐
│                    Close 关闭帧                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Close 帧格式:                                              │
│  ─────────────────────────────────────────────────────────  │
│  Opcode = 0x8                                              │
│  载荷格式:                                                  │
│  ┌─────────────┬─────────────────────────────┐             │
│  │ 状态码(2B)  │ 关闭原因(UTF-8)             │             │
│  └─────────────┴─────────────────────────────┘             │
│                                                             │
│  关闭流程:                                                  │
│                                                             │
│   ┌─────────┐                    ┌─────────┐               │
│   │  端点A  │                    │  端点B  │               │
│   └────┬────┘                    └────┬────┘               │
│        │                              │                      │
│        │  Close (1000, "Bye")        │                      │
│        │─────────────────────────────>│                      │
│        │                              │                      │
│        │  Close (1000, "Bye")        │                      │
│        │<─────────────────────────────│                      │
│        │                              │                      │
│        │  TCP 连接关闭               │                      │
│        │<────────────────────────────>│                      │
│                                                             │
│  注意:                                                     │
│  ─────────────────────────────────────────────────────────  │
│  - 收到 Close 帧后必须响应 Close 帧                        │
│  - 响应后应尽快关闭 TCP 连接                                │
│  - 控制帧载荷最大 125 字节                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

协议扩展 #

permessage-deflate 扩展 #

text
┌─────────────────────────────────────────────────────────────┐
│                    消息压缩扩展                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  握手协商:                                                  │
│  ─────────────────────────────────────────────────────────  │
│  客户端请求:                                                │
│  Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits│
│                                                             │
│  服务端响应:                                                │
│  Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=15│
│                                                             │
│  参数说明:                                                  │
│  ─────────────────────────────────────────────────────────  │
│  server_max_window_bits  服务端压缩窗口大小                 │
│  client_max_window_bits  客户端压缩窗口大小                 │
│  server_no_context_takeover 服务端不保留压缩上下文          │
│  client_no_context_takeover 客户端不保留压缩上下文          │
│                                                             │
│  使用场景:                                                  │
│  ─────────────────────────────────────────────────────────  │
│  - 大量文本消息传输                                         │
│  - JSON 数据频繁传输                                        │
│  - 带宽受限环境                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

协议解析示例 #

解析数据帧 #

javascript
function parseFrame(buffer) {
  let offset = 0;
  
  const firstByte = buffer[offset++];
  const secondByte = buffer[offset++];
  
  const fin = (firstByte >> 7) & 0x01;
  const rsv1 = (firstByte >> 6) & 0x01;
  const rsv2 = (firstByte >> 5) & 0x01;
  const rsv3 = (firstByte >> 4) & 0x01;
  const opcode = firstByte & 0x0F;
  
  const masked = (secondByte >> 7) & 0x01;
  let payloadLength = secondByte & 0x7F;
  
  if (payloadLength === 126) {
    payloadLength = buffer.readUInt16BE(offset);
    offset += 2;
  } else if (payloadLength === 127) {
    payloadLength = Number(buffer.readBigUInt64BE(offset));
    offset += 8;
  }
  
  let maskKey = null;
  if (masked) {
    maskKey = buffer.slice(offset, offset + 4);
    offset += 4;
  }
  
  let payload = buffer.slice(offset, offset + payloadLength);
  
  if (maskKey) {
    payload = unmaskData(payload, maskKey);
  }
  
  return {
    fin,
    rsv1,
    rsv2,
    rsv3,
    opcode,
    masked,
    payloadLength,
    maskKey,
    payload
  };
}

function getOpcodeName(opcode) {
  const names = {
    0x0: 'Continuation',
    0x1: 'Text',
    0x2: 'Binary',
    0x8: 'Close',
    0x9: 'Ping',
    0xA: 'Pong'
  };
  return names[opcode] || 'Unknown';
}

构建数据帧 #

javascript
function buildFrame(payload, opcode, masked = false, fin = true) {
  const payloadBuffer = Buffer.isBuffer(payload) 
    ? payload 
    : Buffer.from(payload);
  
  let headerLength = 2;
  let payloadLength = payloadBuffer.length;
  
  if (payloadLength > 65535) {
    headerLength += 8;
  } else if (payloadLength > 125) {
    headerLength += 2;
  }
  
  let maskKey = null;
  if (masked) {
    headerLength += 4;
    maskKey = Buffer.alloc(4);
    for (let i = 0; i < 4; i++) {
      maskKey[i] = Math.floor(Math.random() * 256);
    }
  }
  
  const frame = Buffer.alloc(headerLength + payloadLength);
  let offset = 0;
  
  frame[offset++] = (fin ? 0x80 : 0x00) | opcode;
  
  if (payloadLength > 65535) {
    frame[offset++] = (masked ? 0x80 : 0x00) | 127;
    frame.writeBigUInt64BE(BigInt(payloadLength), offset);
    offset += 8;
  } else if (payloadLength > 125) {
    frame[offset++] = (masked ? 0x80 : 0x00) | 126;
    frame.writeUInt16BE(payloadLength, offset);
    offset += 2;
  } else {
    frame[offset++] = (masked ? 0x80 : 0x00) | payloadLength;
  }
  
  if (maskKey) {
    maskKey.copy(frame, offset);
    offset += 4;
    
    for (let i = 0; i < payloadLength; i++) {
      frame[offset + i] = payloadBuffer[i] ^ maskKey[i % 4];
    }
  } else {
    payloadBuffer.copy(frame, offset);
  }
  
  return frame;
}

下一步 #

现在你已经深入理解了 WebSocket 协议,接下来学习 服务端实现,动手搭建 WebSocket 服务器!

最后更新:2026-03-29