WebSocket 协议、帧结构与 MTU 详解

WebSocket 协议、帧结构与 MTU 详解

目录

  1. [WebSocket 协议概述](#WebSocket 协议概述 "#websocket-%E5%8D%8F%E8%AE%AE%E6%A6%82%E8%BF%B0")
  2. [WebSocket 帧结构](#WebSocket 帧结构 "#websocket-%E5%B8%A7%E7%BB%93%E6%9E%84")
  3. [MTU 与网络分层](#MTU 与网络分层 "#mtu-%E4%B8%8E%E7%BD%91%E7%BB%9C%E5%88%86%E5%B1%82")
  4. 帧拆分与重组机制
  5. [libwebsockets 实现细节](#libwebsockets 实现细节 "#libwebsockets-%E5%AE%9E%E7%8E%B0%E7%BB%86%E8%8A%82")
  6. 实际传输流程示例

WebSocket 协议概述

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

协议特点

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

WebSocket 帧结构

帧格式

WebSocket 帧由以下部分组成:

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

帧类型示例

单帧消息(小消息)
ini 复制代码
FIN=1, Opcode=0x1, Payload="Hello"
  • 一条完整的文本消息,在一个帧中传输
多帧消息(大消息)
ini 复制代码
帧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 层不关心

拆分过程

ini 复制代码
完整消息 (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):

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 中的配置:

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_size 4096 接收缓冲区大小,影响每次 LWS_CALLBACK_CLIENT_RECEIVE 回调的最大数据量
tx_packet_size 0 发送包大小,0 表示由库自动决定(通常也是几KB)

发送流程

cpp 复制代码
// 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 会自动拆分帧并发送

接收流程

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

实际传输流程示例

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

第 1 层:WebSocket 层(应用层)
ini 复制代码
原始消息: 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 字节分片)
yaml 复制代码
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
接收端重组过程
ini 复制代码
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

相关推荐
CaffeinePro20 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax21 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH21 小时前
Koa和Express的区别
后端
MariaH21 小时前
Koa框架的使用
后端
luckdewei1 天前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某1 天前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy1 天前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom1 天前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
用户1474853079741 天前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody1231 天前
用 abort 中断 AI 流式请求,我之前做错了
后端