268 lines
7.2 KiB
Markdown
268 lines
7.2 KiB
Markdown
# 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
|
||
// 消息结构(零拷贝,引用 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 帧头构建
|
||
|
||
```c
|
||
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 帧解析
|
||
|
||
```c
|
||
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
|
||
|
||
```c
|
||
// 服务端升级
|
||
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. 发送函数
|
||
|
||
```c
|
||
// 发送一条完整消息(构建帧头+掩码+发送)
|
||
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 标准事件处理模板
|
||
|
||
```c
|
||
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 广播
|
||
|
||
```c
|
||
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 心跳检测
|
||
|
||
```c
|
||
// 定时发送 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
|
||
浏览器更新
|
||
```
|