5. Data Framing (数据帧)
WebSocket协议使用帧来传输数据。本章定义了WebSocket帧的格式和处理规则。
5.1 Overview (概述)
一旦WebSocket连接建立,客户端和服务器可以双向传输数据。数据以一系列帧的形式传输。
关键概念:
- 帧 (Frame): 传输的基本单位,包含头部和有效载荷
- 消息 (Message): 应用程序级别的数据,可能由一个或多个帧组成
- 分片 (Fragmentation): 大消息可以分成多个帧发送
5.2 Base Framing Protocol (基础帧协议)
帧结构
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 ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
字段说明
FIN (1 bit)
0: 这不是消息的最后一帧(还有后续帧)1: 这是消息的最后一帧(或唯一帧)
消息分片示例:
帧1: FIN=0, Opcode=0x1 (Text), Data="Hello "
帧2: FIN=0, Opcode=0x0 (Continuation), Data="World"
帧3: FIN=1, Opcode=0x0 (Continuation), Data="!"
完整消息: "Hello World!"
RSV1, RSV2, RSV3 (各1 bit)
- 保留给扩展使用
- 如果未协商扩展,必须 (MUST) 为0
- 如果收到非零值且未定义扩展,必须 (MUST) 关闭连接
Opcode (4 bits)
定义帧的类型:
| Opcode | 类型 | 说明 |
|---|---|---|
0x0 | Continuation | 延续帧(分片消息的后续帧) |
0x1 | Text | 文本帧(UTF-8编码) |
0x2 | Binary | 二进制帧 |
0x3-0x7 | - | 保留(数据帧) |
0x8 | Close | 关闭帧 |
0x9 | Ping | Ping帧 |
0xA | Pong | Pong帧 |
0xB-0xF | - | 保留(控制帧) |
MASK (1 bit)
- 客户端发送到服务器: 必须 (MUST) 为1
- 服务器发送到客户端: 必须 (MUST) 为0
如果MASK=1,Payload Data必须使用Masking-key进行掩码。
Payload Length (7 bits, 7+16 bits, 或 7+64 bits)
有效载荷长度的编码:
- 0-125: 这就是实际长度
- 126: 后续16位(2字节)为实际长度(网络字节序)
- 127: 后续64位(8字节)为实际长度(网络字节序)
示例:
Payload长度 = 100字节
→ Payload len = 100 (直接编码)
Payload长度 = 1000字节
→ Payload len = 126
→ Extended payload length = 1000 (16位)
Payload长度 = 100000字节
→ Payload len = 127
→ Extended payload length = 100000 (64位)
Masking-key (0 或 4 bytes)
如果MASK=1,包含32位(4字节)的掩码密钥。
Payload Data (x+y bytes)
有效载荷数据 = Extension Data + Application Data
- Extension Data: 长度x,由扩展协商决定,默认为0
- Application Data: 长度y,实际的应用数据
5.3 Client-to-Server Masking (客户端到服务器的掩码)
为什么需要掩码?
安全原因: 防止缓存投毒攻击 (Cache Poisoning Attack)。某些中间代理可能错误地缓存WebSocket帧,掩码确保数据不可预测。
掩码算法
客户端必须 (MUST) 使用以下算法掩码所有发送到服务器的帧:
1. 生成32位随机掩码密钥
2. 将掩码密钥放入帧头的Masking-key字段
3. 对Payload Data的每个字节应用掩码:
transformed-octet-i = original-octet-i XOR masking-key[i MOD 4]
算法实现 (JavaScript):
function maskData(data, maskingKey) {
const masked = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
masked[i] = data[i] ^ maskingKey[i % 4];
}
return masked;
}
// 示例
const data = Buffer.from('Hello');
const maskingKey = Buffer.from([0x37, 0xfa, 0x21, 0x3d]);
const masked = maskData(data, maskingKey);
// 解码(使用相同算法)
const unmasked = maskData(masked, maskingKey); // 'Hello'
关键点:
- XOR是自逆运算:
(A XOR B) XOR B = A - 服务器使用相同算法解码
- 每次发送帧必须使用新的随机密钥
5.4 Fragmentation (分片)
大消息可以分成多个帧发送。
分片规则
- 第一帧: FIN=0, Opcode=数据类型 (0x1或0x2)
- 中间帧: FIN=0, Opcode=0x0 (Continuation)
- 最后帧: FIN=1, Opcode=0x0 (Continuation)
分片示例
发送消息 "Hello World!" 分三帧:
帧1:
FIN = 0
Opcode = 0x1 (Text)
Payload = "Hello "
帧2:
FIN = 0
Opcode = 0x0 (Continuation)
Payload = "World"
帧3:
FIN = 1
Opcode = 0x0 (Continuation)
Payload = "!"
分片限制
- 控制帧 (Close, Ping, Pong) 禁止 (MUST NOT) 分片
- 控制帧可以插入在分片的数据帧之间
- 必须按顺序发送和接收分片
5.5 Control Frames (控制帧)
控制帧用于通信连接状态。Opcode范围: 0x8-0xF。
5.5.1 Close (关闭帧)
- Opcode:
0x8 - 可以包含关闭代码和原因
- 详见第7章
5.5.2 Ping (Ping帧)
- Opcode:
0x9 - 用途: 心跳检测,检查连接是否活跃
- 可以携带应用数据(最多125字节)
- 接收方必须 (MUST) 用Pong帧响应
客户端 → 服务器: Ping (Opcode=0x9)
服务器 → 客户端: Pong (Opcode=0xA, 相同Payload)
5.5.3 Pong (Pong帧)
- Opcode:
0xA - 用途: 响应Ping帧
- 必须 (MUST) 包含Ping帧中的相同Payload
- 也可以主动发送(单向心跳)
控制帧规则
- 最大Payload长度: 125字节
- 不能 (MUST NOT) 分片
- 可以插入在分片的数据帧之间
5.6 Data Frames (数据帧)
数据帧用于传输应用或扩展数据。Opcode范围: 0x0-0x2, 0x3-0x7保留。
Text Frame (文本帧)
- Opcode:
0x1 - Payload必须是有效的UTF-8编码文本
- 如果收到无效UTF-8,必须 (MUST) 关闭连接
Binary Frame (二进制帧)
- Opcode:
0x2 - Payload可以是任意二进制数据
- 应用层负责解释
5.7 Examples (示例)
单帧未掩码文本消息
0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f
│ │ └─────────┬────────────┘
│ │ └─ "Hello" (5字节)
│ └─ Payload len = 5
└─ FIN=1, Opcode=0x1 (Text)
单帧掩码文本消息
0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58
│ │ └─────┬────────┘ └──────┬───────────┘
│ │ │ └─ Masked "Hello"
│ │ └─ Masking key
│ └─ MASK=1, Payload len = 5
└─ FIN=1, Opcode=0x1 (Text)
分片消息
帧1: 0x01 0x03 0x48 0x65 0x6c // FIN=0, Text, "Hel"
帧2: 0x80 0x02 0x6c 0x6f // FIN=1, Continuation, "lo"
完整消息: "Hello"
Ping帧
0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f
│ │ └─────────┬────────────┘
│ │ └─ "Hello" (可选数据)
│ └─ Payload len = 5
└─ FIN=1, Opcode=0x9 (Ping)
5.8 Extensibility (可扩展性)
协议可通过以下方式扩展:
- Opcode: 0x3-0x7和0xB-0xF保留给未来使用
- RSV bits: 保留给扩展使用
- Extension Data: 扩展可以在Payload前添加数据
扩展必须通过握手协商(Sec-WebSocket-Extensions)。
参考链接
- 上一章: 4. Opening Handshake
- 下一章: 6. Sending and Receiving Data
- 详细说明: WebSocket帧结构详解