# 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 浏览器更新 ```