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

7.2 KiB
Raw Blame History

WebSocket 服务端分析

日期: 2026-06-12
基于源码: src/protocol/libmongoose/src/mongoose.cws.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位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

// 服务端升级
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
                      浏览器更新