7.2 KiB
7.2 KiB
WebSocket 服务端分析
日期: 2026-06-12
基于源码: src/protocol/libmongoose/src/mongoose.c(ws.c 部分约 300 行)
1. 协议概述
WebSocket 是 RFC 6455 定义的全双工通信协议,通过 HTTP Upgrade 从 HTTP 升级到持久 TCP 连接。RTU 使用 Mongoose 7.x 内置的 WebSocket 实现提供实时数据推送。
1.1 帧操作码
| 操作码 | 值 | 说明 |
|---|---|---|
| CONTINUE | 0x0 | 分片帧的后续帧 |
| TEXT | 0x1 | UTF-8 文本帧 |
| BINARY | 0x2 | 二进制帧 |
| CLOSE | 0x8 | 关闭连接 |
| PING | 0x9 | 心跳请求 |
| PONG | 0xA | 心跳响应 |
2. Mongoose WebSocket 实现
2.1 核心结构
// 消息结构(零拷贝,引用 c->recv 缓冲区)
struct mg_ws_message {
struct mg_str data; // 消息数据
uint8_t flags; // 高4位=FIN标志,低4位=操作码
};
// 内部帧解析结构
struct ws_msg {
uint8_t flags;
size_t header_len; // 帧头长度(含掩码key)
size_t data_len; // 数据长度
};
2.2 帧格式(RFC 6455)
字节0: [FIN(1)] [RSV(3)] [OPCODE(4)]
字节1: [MASK(1)] [PAYLOAD_LEN(7)]
字节2-3: [扩展长度16位] (如果 PAYLOAD_LEN == 126)
字节2-9: [扩展长度64位] (如果 PAYLOAD_LEN == 127)
字节n: [掩码Key 4字节] (如果 MASK == 1)
字节n+4: [Payload Data]
2.3 帧头构建
static size_t mkhdr(size_t len, int op, bool is_client, uint8_t *buf):
buf[0] = op | 128; // FIN=1, Opcode=op
if (len < 126):
buf[1] = len, n = 2 // 7位直接编码
else if (len < 65536):
buf[1] = 126, n = 4 // 16位扩展长度(大端)
else:
buf[1] = 127, n = 10 // 64位扩展长度(大端)
if (is_client):
buf[1] |= 0x80 // 设置MASK位
mg_random(&buf[n], 4) // 随机掩码key
n += 4
return n
关键: 服务端发送的帧不设 MASK 位。只有客户端发往服务端的帧才需要掩码(防缓存投毒攻击)。
2.4 帧解析
static size_t ws_process(uint8_t *buf, size_t len, struct ws_msg *msg):
→ 解析字节0: msg->flags
→ 解析字节1: payload_len (低7位) + mask_len (MASK位→4或0)
→ 根据 payload_len 读取扩展长度
→ 安全检查: data_len 不能超过 1GB
→ 如果有掩码: payload[i] ^= masking_key[i % 4] // XOR解码
→ 返回 header_len + data_len
3. 握手流程
3.1 HTTP Upgrade
客户端 → 服务端:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务端 → 客户端:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
3.2 Accept 值计算
1. SHA1( Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" )
2. Base64 编码 SHA1 结果
3. 魔数是 RFC 6455 §4.2.2 固定值,防跨协议攻击
3.3 Mongoose API
// 服务端升级
void mg_ws_upgrade(struct mg_connection *c, struct mg_http_message *hm, const char *fmt, ...);
// 内部: 计算Accept → 发送101 → 切换c->pfn = mg_ws_cb → c->is_websocket = 1
// 客户端连接
struct mg_connection *mg_ws_connect(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data, const char *fmt, ...);
4. 协议处理器 mg_ws_cb
WebSocket 连接的核心处理器,在握手完成后注册为 c->pfn:
mg_ws_cb (MG_EV_READ 处理):
解析帧 → opcode 分类:
TEXT/BINARY + FIN=1 (完整帧):
→ 触发 MG_EV_WS_MSG(用户在此接收消息)
TEXT/BINARY + FIN=0 (分片开始):
→ 保留flags,累积数据,等待后续帧
CONTINUE:
→ 剥离帧头追加到累积缓冲区
→ FIN=1: 触发 MG_EV_WS_MSG,从缓冲区首字节恢复原始opcode
CLOSE (0x8):
→ 触发 MG_EV_WS_CTL
→ 回显CLOSE帧给对端
→ c->is_draining = 1(优雅关闭)
PING (0x9):
→ 自动回复PONG
→ 触发 MG_EV_WS_CTL
PONG (0xA):
→ 触发 MG_EV_WS_CTL(用户可检测心跳响应)
4.1 分片帧处理
客户端发送 3 条分片:
Frame1: FIN=0, op=TEXT, data="Hello "
Frame2: FIN=0, op=CONTINUE, data="World"
Frame3: FIN=1, op=CONTINUE, data="!"
内部处理:
Frame1: 在c->recv中保存flags(0x01) + 数据 → "\x01Hello "
Frame2: 剥离帧头追加 → "\x01Hello World"
Frame3: 剥离帧头追加 → "\x01Hello World!" → FIN=1触发
→ m.flags = c->recv.buf[0] (TEXT)
→ m.data = "Hello World!" (跳过标记字节)
→ 删除已处理数据
5. 发送函数
// 发送一条完整消息(构建帧头+掩码+发送)
size_t mg_ws_send(struct mg_connection *c, const void *buf, size_t len, int op);
// 格式化发送(mg_vxprintf → mg_ws_wrap)
size_t mg_ws_printf(struct mg_connection *c, int op, const char *fmt, ...);
// 为已在c->send中的数据添加WS帧头(内部)
size_t mg_ws_wrap(struct mg_connection *c, size_t len, int op);
6. 服务端最佳实践
6.1 标准事件处理模板
static void fn(struct mg_connection *c, int ev, void *ev_data) {
// WebSocket 握手
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
if (mg_http_get_header(hm, "Sec-WebSocket-Key")) {
mg_ws_upgrade(c, hm, NULL);
} else {
mg_http_reply(c, 200, "", "Use WebSocket\n");
}
}
// 连接建立
if (ev == MG_EV_WS_OPEN) {
// 注入 per-connection 资源
}
// 接收消息
if (ev == MG_EV_WS_MSG) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
// 处理文本/二进制消息
}
// 连接关闭
if (ev == MG_EV_CLOSE) {
// 释放 per-connection 资源
}
}
6.2 广播
void broadcast(struct mg_mgr *mgr, const char *msg, size_t len):
for each c in mgr->conns:
if c->is_websocket:
mg_ws_send(c, msg, len, WEBSOCKET_OP_TEXT)
6.3 心跳检测
// 定时发送 PING(通过 mg_timer_add)
mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING);
// 在 MG_EV_WS_CTL 中检测 PONG 确认存活
7. 服务端 vs 客户端差异
| 特性 | 服务端 | 客户端 |
|---|---|---|
| 帧掩码 | 不掩码 | 必须掩码(4字节随机key + XOR) |
| 握手 | 接收Key,计算Accept | 生成随机Key,验证Accept |
| API | mg_ws_upgrade() |
mg_ws_connect() |
is_client |
false |
true |
8. RTU 中的应用
RTU 的 libweb_server 模块通过 Mongoose WebSocket 实现:
- 实时数据推送: 客户端订阅信号 →
ws_task每 100ms 推送变更的 JSON 数据 - 前端交互: 内嵌 SPA 通过 WebSocket 接收遥测/遥信实时更新
- 命令下发: 前端通过 WebSocket 发送控制命令(遥控/定值修改)
- 多浏览器: per-connection session 隔离,各浏览器独立订阅
浏览器 ──WebSocket──→ Mongoose (mg_mgr_poll, 10ms)
↓ MG_EV_WS_MSG
ws_recv() → 命令分发
↓
datacenter API
↓
ws_task() → 增量 JSON 推送 (100ms)
↓ mg_ws_send
浏览器更新