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