WebSocket 协议、帧结构与 MTU 详解

106 阅读7分钟

WebSocket 协议、帧结构与 MTU 详解

目录

  1. WebSocket 协议概述
  2. WebSocket 帧结构
  3. MTU 与网络分层
  4. 帧拆分与重组机制
  5. libwebsockets 实现细节
  6. 实际传输流程示例

WebSocket 协议概述

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许客户端和服务器之间进行实时、双向的数据传输。

协议特点

  • 全双工通信:客户端和服务器可以同时发送和接收数据
  • 基于 TCP:建立在可靠的 TCP 连接之上
  • 帧格式传输:数据以帧(Frame)为单位进行传输
  • 支持消息分片:大消息可以被拆分成多个帧

WebSocket 帧结构

帧格式

WebSocket 帧由以下部分组成:

 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 ...                |
+---------------------------------------------------------------+

帧头字段说明

字段位数说明
FIN1 bit标识是否为消息的最后一个片段。1 = 最后片段,0 = 还有后续片段
RSV1-33 bits保留字段,必须为 0
Opcode4 bits帧类型:
0x0 = 延续帧
0x1 = 文本帧
0x2 = 二进制帧
0x8 = 连接关闭
0x9 = ping
0xA = pong
MASK1 bit是否使用掩码。客户端到服务器必须为 1,服务器到客户端为 0
Payload Length7 bits载荷长度:
0-125 = 实际长度
126 = 后续 2 字节表示长度
127 = 后续 8 字节表示长度
Masking-key0/4 bytes如果 MASK=1,则包含 4 字节掩码键
Payload Data可变实际数据载荷

帧类型示例

单帧消息(小消息)
FIN=1, Opcode=0x1, Payload="Hello"
  • 一条完整的文本消息,在一个帧中传输
多帧消息(大消息)
帧1: FIN=0, Opcode=0x1, Payload="前3KB数据"
帧2: FIN=0, Opcode=0x0, Payload="中间4KB数据"  (延续帧)
帧3: FIN=1, Opcode=0x0, Payload="后3KB数据"   (延续帧)
  • 第一条帧的 Opcode 表示消息类型(文本/二进制)
  • 后续帧的 Opcode 必须为 0x0(延续帧)
  • 只有最后一个帧的 FIN=1

MTU 与网络分层

MTU 概念

MTU (Maximum Transmission Unit) 是网络层能够传输的最大数据包大小。

  • 以太网 MTU:通常为 1500 字节
  • 实际 TCP 载荷:MTU - IP 头(20字节) - TCP 头(20字节) = 1460 字节

网络协议栈分层

┌─────────────────────────────────────┐
│  应用层: WebSocket 消息 (10KB)      │
├─────────────────────────────────────┤
│  WebSocket 层: WebSocket 帧        │  ← 帧大小:几KB到几十KB
│  (受 libwebsockets 缓冲区限制)     │
├─────────────────────────────────────┤
│  TCP 层: TCP 段                    │  ← 段大小:受 TCP 发送缓冲区限制
│  (受 TCP 发送缓冲区限制)            │
├─────────────────────────────────────┤
│  IP 层: IP 数据包                  │  ← 包大小:受 MTU 限制 (1500字节)
│  (根据 MTU 自动分片)                │
├─────────────────────────────────────┤
│  以太网层: 以太网帧                 │
└─────────────────────────────────────┘

各层的作用

  1. WebSocket 层:将消息拆分成帧,处理协议逻辑
  2. TCP 层:提供可靠传输,流量控制,拥塞控制
  3. IP 层:根据 MTU 自动分片,路由转发
  4. 以太网层:物理传输

帧拆分与重组机制

发送端:自动拆分

当发送大消息时,libwebsockets 会自动将消息拆分成多个帧:

拆分依据

  • 主要因素:libwebsockets 的内部缓冲区大小(通常 4KB-64KB)
  • 次要因素:TCP 发送缓冲区大小
  • 不是直接按 MTU 拆分:MTU 是 IP 层的事情,WebSocket 层不关心

拆分过程

完整消息 (10KB)
    ↓
libwebsockets 按缓冲区大小拆分
    ↓
帧1: FIN=0, Opcode=0x1, Payload=4KB
帧2: FIN=0, Opcode=0x0, Payload=4KB  (延续帧)
帧3: FIN=1, Opcode=0x0, Payload=2KB  (延续帧)

接收端:需要手动重组

接收端需要根据 FIN 标志判断消息是否完整:

重组逻辑

  1. 收到帧时,检查 FIN 标志
  2. 如果 FIN=0:将 payload 累积到缓冲区
  3. 如果 FIN=1:合并所有片段,得到完整消息

代码实现(参考 CppWebSocket.cpp):

case LWS_CALLBACK_CLIENT_RECEIVE:
    bool is_final = lws_is_final_fragment(wsi);
    
    // 累积当前片段
    receiveBuffer.append((const char *)in, len);
    
    // 如果是最后一个片段,处理完整消息
    if (is_final) {
        std::string completeMsg = std::move(receiveBuffer);
        receiveBuffer.clear();
        callOnMessage(completeMsg);  // 传递给上层
    }

libwebsockets 实现细节

协议配置

CppWebSocket.cpp 中的配置:

static struct lws_protocols protocols[] = {
    {
        "cpp-websocket-protocol",           // 协议名称
        &CppWebSocket::Impl::lwsCallback,   // 回调函数
        sizeof(void *),                     // 每个连接的用户数据大小
        4096,                               // rx_buffer_size: 接收缓冲区大小
        0,                                  // tx_packet_size: 发送包大小(0=自动)
        nullptr,                            // 协议特定数据
        0                                   // 协议索引
    }
};

关键参数说明

参数说明
rx_buffer_size4096接收缓冲区大小,影响每次 LWS_CALLBACK_CLIENT_RECEIVE 回调的最大数据量
tx_packet_size0发送包大小,0 表示由库自动决定(通常也是几KB)

发送流程

// 1. 用户调用 send()
send("10KB完整消息");

// 2. 消息加入队列
sendQueue.push(message);

// 3. 触发写事件
lws_callback_on_writable(wsi);

// 4. 在 WRITEABLE 回调中发送
lws_write(wsi, buf.data() + LWS_PRE, msg.size(), LWS_WRITE_TEXT);
// libwebsockets 会自动拆分帧并发送

接收流程

// 1. libwebsockets 解析帧头
// 2. 提取 payload 数据
// 3. 调用 LWS_CALLBACK_CLIENT_RECEIVE
// 4. 用户代码检查 FIN 标志并重组消息

实际传输流程示例

场景:发送一条 10KB 的消息

第 1 层:WebSocket 层(应用层)
原始消息: 10KB 文本数据
    ↓ libwebsockets 拆分(按内部缓冲区,假设 4KB)
WebSocket 帧1: FIN=0, Opcode=0x1, Payload=4KB
WebSocket 帧2: FIN=0, Opcode=0x0, Payload=4KB
WebSocket 帧3: FIN=1, Opcode=0x0, Payload=2KB
第 2 层:TCP 层
每个 WebSocket 帧 → TCP 段
TCP 段1: ~4KB (包含 WebSocket 帧1)
TCP 段2: ~4KB (包含 WebSocket 帧2)
TCP 段3: ~2KB (包含 WebSocket 帧3)
第 3 层:IP 层(根据 MTU=1500 字节分片)
TCP 段1 (4KB)  IP 数据包
IP 包1: 1460 字节 payload (1500 - 20 IP头 - 20 TCP头)
IP 包2: 1460 字节 payload
IP 包3: 1460 字节 payload
IP 包4: 220 字节 payload

TCP 段2 (4KB)  IP 数据包
IP 包5: 1460 字节 payload
IP 包6: 1460 字节 payload
IP 包7: 1460 字节 payload
IP 包8: 220 字节 payload

TCP 段3 (2KB)  IP 数据包
IP 包9: 1460 字节 payload
IP 包10: 540 字节 payload
接收端重组过程
IP 层: 重组 IP 分片 → TCP 段
TCP 层: 重组 TCP 段 → WebSocket 帧
WebSocket 层: 根据 FIN 标志重组帧 → 完整消息

接收回调1: payload=4KB, FIN=0 → 累积到缓冲区
接收回调2: payload=4KB, FIN=0 → 累积到缓冲区
接收回调3: payload=2KB, FIN=1 → 合并完成,得到 10KB 完整消息 ✓

关键要点总结

1. WebSocket 帧大小 ≠ MTU

  • WebSocket 帧:通常几 KB 到几十 KB(由 libwebsockets 缓冲区决定)
  • MTU:通常 1500 字节(IP 层限制)
  • WebSocket 帧可以大于 MTU,TCP/IP 层会自动分片

2. 为什么不是直接按 MTU 拆分?

  • 减少帧头开销:WebSocket 帧头约 2-14 字节,按 MTU 拆分会产生更多帧头
  • 提高效率:较大的帧减少协议处理次数
  • 简化实现:由 TCP/IP 层处理 MTU 分片,应用层无需关心

3. 发送端 vs 接收端

处理方式说明
发送端自动处理libwebsockets 自动拆分帧,用户只需调用 lws_write()
接收端手动重组需要根据 FIN 标志累积片段,直到收到完整消息

4. 实际配置建议

  • rx_buffer_size: 根据应用场景调整(4KB-64KB)
    • 太小:增加帧数量,增加处理开销
    • 太大:增加内存占用,延迟增加
  • 保持默认值:对于大多数应用,4KB 是合理的默认值

参考资料


文档版本: 1.0
最后更新: 2025