一、引言
在了解 WebSocket 的基本概念与工作原理后,深入探究其数据帧与控制帧结构,对于理解 WebSocket 如何高效传输数据、维持连接稳定至关重要。本文将详细剖析 WebSocket 的数据帧与控制帧,并结合代码示例,让你对其内部机制有更清晰的认识。
二、WebSocket 数据帧结构
2.1 帧头剖析
- fin 标志位:
- fin 占 1 位,用于标识当前帧是否为一个完整消息的最后一帧。当 fin = 1 时,表示这是消息的最后一部分;fin = 0 则意味着后续还有更多帧构成完整消息。例如,在传输大文件时,文件数据可能被分割成多个帧,只有最后一帧的 fin 为 1。
- 保留位 rsv1、rsv2、rsv3:
- 这三个标志位各占 1 位,目前主要为未来协议扩展保留,正常情况下设为 0。若在特定场景下使用扩展功能,可通过设置这些位来启用。
- 操作码 opcode:
- opcode 占 4 位,决定了帧的类型和用途。常见的 opcode 值及其含义如下:
- 0x0:连续帧(Continuation Frame),用于当一个消息被拆分成多个帧时,除第一帧外的后续帧。例如,一个长文本消息的第二帧及之后的帧可能是连续帧,它们与第一帧共同构成完整消息。
- 0x1:文本帧(Text Frame),用于传输文本数据,数据以 UTF - 8 编码。如即时通讯中的文本消息,通常以文本帧形式传输。
- 0x2:二进制帧(Binary Frame),用于传输二进制数据,如图片、音频、视频等。在实时视频流传输中,视频数据可能以二进制帧发送。
- 0x8:关闭帧(Close Frame),用于关闭 WebSocket 连接,包含关闭状态码和可选的关闭原因。
- 0x9:Ping 帧,用于发送心跳检测请求,接收方需回复 Pong 帧。
- 0xA:Pong 帧,作为对 Ping 帧的响应,用于确认连接正常。
- opcode 占 4 位,决定了帧的类型和用途。常见的 opcode 值及其含义如下:
- 掩码位 masked:
- masked 占 1 位,在客户端向服务器发送的帧中,此位必须为 1。掩码是一个 32 位的随机数,用于对有效载荷数据进行混淆处理,以增强安全性。服务器接收数据时,需根据掩码位和掩码数据还原真实数据。
- 有效载荷长度 payload_length:
- payload_length 用于表示有效载荷数据的长度,其长度编码较为灵活:
- 若值小于 126,该 7 位直接表示有效载荷的长度。
- 若值为 126,后续 2 个字节表示有效载荷长度。
- 若值为 127,后续 8 个字节表示有效载荷长度。这种设计可适应不同大小的数据传输需求,大文件传输时能准确表示数据长度。
- payload_length 用于表示有效载荷数据的长度,其长度编码较为灵活:
2.2 有效载荷
有效载荷是帧中实际传输的数据部分。根据 opcode 类型不同,有效载荷的数据格式和含义各异。例如,文本帧的有效载荷是 UTF - 8 编码的文本;二进制帧的有效载荷是二进制数据。
三、WebSocket 控制帧结构
3.1 Ping 帧
- 作用:
Ping 帧主要用于检测客户端和服务器之间的连接状态,确保连接处于活跃状态。发送方定期发送 Ping 帧,接收方必须及时回复 Pong 帧。 - 结构:
Ping 帧的 opcode 为 0x9,通常只包含帧头,有效载荷部分可包含任意数据,一般用于携带一些诊断或调试信息。例如,可在 Ping 帧有效载荷中包含当前时间戳,接收方回复 Pong 帧时带回该时间戳,发送方据此计算往返时间,检测网络延迟。
3.2 Pong 帧
- 作用:
Pong 帧作为对 Ping 帧的响应,确认接收方已收到 Ping 帧,表明连接正常。若发送方在一定时间内未收到 Pong 帧,可认为连接出现问题,进行相应处理,如重新建立连接。 - 结构:
Pong 帧的 opcode 为 0xA,同样可包含帧头和有效载荷。有效载荷数据通常与对应的 Ping 帧有效载荷数据相同,用于反馈和确认。
3.3 Close 帧
- 作用:
Close 帧用于有序关闭客户端和服务器之间的 WebSocket 连接。它允许双方在关闭连接时传递状态码和关闭原因,使对方了解关闭的原因。 - 结构:
Close 帧的 opcode 为 0x8,有效载荷部分可包含 2 字节的状态码和可选的 UTF - 8 编码的关闭原因字符串。常见状态码如 1000 表示正常关闭,1001 表示终端正在离开,1008 表示违反协议等。
四、代码示例:自定义构建与解析 WebSocket 帧
4.1 构建发送帧(JavaScript)
以下代码展示如何在 JavaScript 中构建一个简单的文本帧:
function createTextFrame(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const length = data.length;
let frame = new Uint8Array(length + 2);
// 设置 fin 为 1,opcode 为 0x1(文本帧)
frame[0] = 0x81;
if (length < 126) {
frame[1] = length;
} else if (length < 65536) {
frame[1] = 126;
frame.set(new Uint16Array([length]), 2);
} else {
frame[1] = 127;
frame.set(new BigUint64Array([length]), 2);
}
frame.set(data, length < 126? 2 : length < 65536? 4 : 10);
return frame;
}
// 使用示例
const message = "Hello, WebSocket!";
const frame = createTextFrame(message);
// 这里假设已有 WebSocket 连接对象 socket
// socket.send(frame);
4.2 解析接收帧(JavaScript)
function parseFrame(buffer) {
const view = new DataView(buffer);
const fin = (view.getUint8(0) & 0x80) === 0x80;
const opcode = view.getUint8(0) & 0x0F;
const masked = (view.getUint8(1) & 0x80) === 0x80;
let payloadLength = view.getUint8(1) & 0x7F;
let offset = 2;
if (payloadLength === 126) {
payloadLength = view.getUint16(offset, true);
offset += 2;
} else if (payloadLength === 127) {
payloadLength = view.getBigUint64(offset, true);
offset += 8;
}
let mask;
if (masked) {
mask = view.getUint32(offset, true);
offset += 4;
}
const payload = new Uint8Array(payloadLength);
for (let i = 0; i < payloadLength; i++) {
let byte = view.getUint8(offset + i);
if (masked) {
byte ^= (mask >> ((i % 4) * 8)) & 0xFF;
}
payload[i] = byte;
}
return { fin, opcode, payload };
}
// 使用示例,假设已接收到数据帧 buffer
const parsedFrame = parseFrame(buffer);
console.log('解析结果:', parsedFrame);
五、总结
深入理解 WebSocket 的数据帧与控制帧结构,能帮助开发者更好地优化 WebSocket 通信,处理复杂场景,提升应用性能与稳定性。通过本文的代码示例,希望你能在实际项目中灵活运用这些知识,构建出更强大的实时通信应用。