RTU/mimo/工程/websocket-server.md

268 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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位40)
根据 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
浏览器更新
```