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

相关推荐
white-persist2 小时前
【vulhub spring CVE-2018-1270】CVE-2018-1270 Spring Messaging 远程命令执行漏洞 完整复现详细分析解释
java·服务器·网络·数据库·后端·python·spring
神奇小汤圆2 小时前
Spring-Boot-泛型封装-这8个坑让我调了3天
后端
深挖派3 小时前
GoLand 2026.1 安装配置与环境搭建 (保姆级图文教程)
后端·golang·编辑器·go·goland
IT枫斗者3 小时前
构建具有执行功能的 AI Agent:基于工作记忆的任务规划与元认知监控架构
android·前端·vue.js·spring boot·后端·架构
神奇小汤圆3 小时前
一文吃透 MySQL 性能优化:从执行计划到架构设计
后端
开心就好20253 小时前
苹果iOS应用开发上架与推广完整教程
后端·ios
四千岁3 小时前
Ollama+OpenWebUI 最佳组合:本地大模型可视化交互方案
前端·javascript·后端
Carsene3 小时前
AutoScan Spring Boot Starter v1.3.0 发布:高级过滤与环境配置新特性
spring boot·后端
程序员柒叔3 小时前
OpenCode 一周动态-2026-W15
后端·github