218 lines
9.1 KiB
Markdown
218 lines
9.1 KiB
Markdown
# libweb_server 模块分析
|
||
|
||
**日期**:2026-06-10
|
||
|
||
---
|
||
|
||
## 1. 模块概览
|
||
|
||
`libweb_server` 是 RTU 的嵌入式 Web 服务器模块(`app_web_server` 线程),属于系统层的第 7 号线程。基于 Mongoose v7.21,提供 HTTP 静态文件服务 + WebSocket 实时数据通道。
|
||
|
||
### 目录结构
|
||
|
||
```
|
||
src/system/libweb_server/
|
||
├── inc/
|
||
│ ├── web_server.h # 对外接口(app_web_server_init1/2, app_web_server)
|
||
│ └── ws_method.h # WebSocket 消息处理方法声明
|
||
└── src/
|
||
├── web_server.cpp # HTTP/WS 服务器 + mongoose 事件循环
|
||
└── ws_method.cpp # WebSocket 命令解析 + JSON 数据推送
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 架构
|
||
|
||
### 2.1 线程模型
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ app_web_server 线程 (RTU 9号线程) │
|
||
│ │
|
||
│ init1: web_server_init() │
|
||
│ → mg_mgr_init + mg_http_listen(8000) │
|
||
│ → pthread_create → web_server_run (独立线程) │
|
||
│ │
|
||
│ init2: (空) │
|
||
│ │
|
||
│ fun_cb: 事件循环 (3个定时器) │
|
||
│ EV_TIMER3 → ws_task() (每秒推送数据) │
|
||
└─────────────────────────────────────────────────┘
|
||
│
|
||
│ g_ws_conns (连接列表) + g_ws_sessions (会话资源)
|
||
│
|
||
┌─────────────────────────────────────────────────┐
|
||
│ web_server_run 线程 (mongoose 事件线程) │
|
||
│ │
|
||
│ while(1) mg_mgr_poll(500ms) │
|
||
│ → web_server_task() 回调 │
|
||
│ MG_EV_HTTP_MSG → WS升级 / 静态文件 │
|
||
│ MG_EV_WS_MSG → 接收命令 │
|
||
│ MG_EV_CLOSE → 断连清理 │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 会话隔离模型
|
||
|
||
每个 WebSocket 连接拥有独立的 `stru_ws_session`,包含自己的五类信号集。连接建立时开辟、断开时释放。
|
||
|
||
```
|
||
客户端A (WebSocket) ─→ session_A: {out_signals_A, in_signals_A, yk_signals_A, ...}
|
||
客户端B (WebSocket) ─→ session_B: {out_signals_B, in_signals_B, yk_signals_B, ...}
|
||
```
|
||
|
||
`ws_task()` 遍历所有 session,为每个 session 构建独立的 JSON 并发送到对应连接。
|
||
|
||
### 2.3 数据流
|
||
|
||
```
|
||
浏览器 ←── HTTP ──→ mongoose ←── 静态文件 (web_root/)
|
||
浏览器 ←── WS ────→ mongoose ←── JSON 命令/数据 ←── ws_method.cpp
|
||
↕
|
||
DataCenter
|
||
```
|
||
|
||
---
|
||
|
||
## 3. web_server.cpp 核心逻辑
|
||
|
||
### 3.1 全局状态
|
||
|
||
```cpp
|
||
g_web_root → 静态文件根目录(进程目录 + "web_root")
|
||
mgr → mongoose 事件管理器
|
||
g_ws_conns → vector<mg_connection*> 活跃 WebSocket 连接列表
|
||
g_ws_conns_mutex → pthread 互斥锁,保护 g_ws_conns
|
||
g_ws_sessions → map<conn_id, stru_ws_session> 每连接独立信号资源(ws_method.cpp)
|
||
g_ws_session_mutex → pthread 互斥锁,保护 g_ws_sessions
|
||
```
|
||
|
||
### 3.2 事件处理 (web_server_task)
|
||
|
||
| 事件 | 处理 |
|
||
|------|------|
|
||
| `MG_EV_HTTP_MSG` | URI=`/ws` → `mg_ws_upgrade()` 升级为 WebSocket;其他 → `mg_http_serve_dir()` 静态文件 |
|
||
| `MG_EV_WS_MSG` | 将连接加入 `g_ws_conns`,消息经 `std::string` 安全复制后传给 `ws_recv()` |
|
||
| `MG_EV_WS_CTL` | CLOSE 帧 → 调用 `ws_session_destroy()` + 从 `g_ws_conns` 移除 |
|
||
| `MG_EV_CLOSE` | 调用 `ws_session_destroy()` + 从 `g_ws_conns` 移除 |
|
||
|
||
### 3.3 ws_send_all / ws_send_one
|
||
|
||
- `ws_send_all()`:遍历 `g_ws_conns` 广播,跳过死连接
|
||
- `ws_send_one(conn_id, ...)`:按 `c->id` 精确定位单连接发送
|
||
|
||
均加 `g_ws_conns_mutex` 锁保护。
|
||
|
||
---
|
||
|
||
## 4. ws_method.cpp 命令处理
|
||
|
||
### 4.1 会话结构
|
||
|
||
```cpp
|
||
struct stru_ws_session {
|
||
vector<stru_ws_signal> out_signals; // 本连接订阅的遥信
|
||
vector<stru_ws_signal> in_signals; // 本连接订阅的遥测
|
||
vector<stru_ws_signal> yk_signals; // 本连接订阅的遥控
|
||
vector<stru_ws_signal> ao_signals; // 本连接订阅的定值
|
||
vector<stru_ws_signal> param_signals; // 本连接订阅的参数
|
||
};
|
||
```
|
||
|
||
全局 `g_ws_sessions: map<conn_id, stru_ws_session>` 管理所有连接的独立资源。`g_ws_session_mutex` 保护。
|
||
|
||
### 4.2 信号模型
|
||
|
||
每个连接 `add` 信号时仅影响自己的 session,不同客户端之间完全隔离。
|
||
|
||
| 类型 | session 字段 | 支持操作 |
|
||
|------|-------------|---------|
|
||
| out (遥信) | `session.out_signals` | add / del / set |
|
||
| in (遥测输入) | `session.in_signals` | add / del |
|
||
| yk (遥控) | `session.yk_signals` | add / del / set (含 SBO) |
|
||
| ao (定值) | `session.ao_signals` | add / del / set (含 SBO) |
|
||
| param (参数) | `session.param_signals` | add / del / set (含 SBO, 多定值区) |
|
||
|
||
### 4.2 WebSocket 上行命令格式 (JSON)
|
||
|
||
```json
|
||
{
|
||
"curd": "add|del|set",
|
||
"signal_type": "out|in|yk|ao|param",
|
||
"saddr": "st.0",
|
||
"signal_data": "1.5",
|
||
"setting_zone": "0"
|
||
}
|
||
```
|
||
|
||
### 4.3 下行数据格式 (JSON)
|
||
|
||
每秒 (EV_TIMER3) 推送完整快照,五类信号分数组。修复后仅在值变化时推送。
|
||
|
||
### 4.4 SBO 控制流程
|
||
|
||
遥控/定值/参数支持 `DIRECT_NORMAL`(直接执行)和 `SBO_NORMAL`(选择-执行两步),通过 DataCenter 的 `dc_signal_yk_set_status` / `dc_signal_ao_set_val` / `dc_signal_param_set_val` 执行。
|
||
|
||
---
|
||
|
||
## 5. 缺陷与修复
|
||
|
||
### 5.1 缺陷清单(修复前)
|
||
|
||
| # | 严重度 | 位置 | 问题 |
|
||
|---|--------|------|------|
|
||
| 1 | 严重 | web_server.cpp L14 | `p_conn` 单指针,仅支持一个 WS 客户端 |
|
||
| 2 | 严重 | web_server.cpp | 无 `MG_EV_CLOSE` 处理,断连后悬空指针 |
|
||
| 3 | 严重 | web_server.cpp | `p_conn` 无锁保护,多线程竞态 |
|
||
| 4 | 严重 | web_server.cpp L66, ws_method.cpp L519 | `mg_str` 非 null-terminated 传给 `printf`/`cJSON_Parse`(UB) |
|
||
| 5 | 高危 | web_server.cpp L66 | 调试 `printf` 遗留 |
|
||
| 6 | 高危 | ws_method.cpp L386 | SBO `task_sleep_ms(1000)` 阻塞事件循环 |
|
||
| 7 | 中危 | — | 无心跳保活机制 |
|
||
| 8 | 中危 | ws_method.cpp | 每秒全量推送,数据不变也发送 |
|
||
| 9 | 中危 | ws_method.cpp L400,L502 | `LOG_E("%d")` 缺少对应参数 |
|
||
| 10 | 中危 | web_server.cpp L22 | `ws_send` 未校验 `is_websocket`/`is_draining` |
|
||
| 11 | 严重 | ws_method.cpp L21-25 | 多客户端共享同一套全局信号资源,客户端之间信号干扰 |
|
||
|
||
### 5.2 修复措施
|
||
|
||
**第一轮(2026-06-10)**:
|
||
|
||
| # | 修复 |
|
||
|---|------|
|
||
| 1 | `p_conn` → `g_ws_conns: vector<mg_connection*>`,遍历广播 |
|
||
| 2 | 新增 `MG_EV_CLOSE` + `MG_EV_WS_CTL(CLOSE)` 从列表移除 |
|
||
| 3 | 新增 `pthread_mutex_t g_ws_mutex`,列表读写前加锁 |
|
||
| 4 | `std::string(wm->data.buf, wm->data.len)` 安全复制后再用 |
|
||
| 5 | `printf` → `LOG_I` |
|
||
| 6 | 删除 `task_sleep_ms(1000)` |
|
||
| 7 | 当前 500ms poll 周期可替代心跳,mg_timer 需配合 wakeup_init 略复杂暂不引入 |
|
||
| 8 | `stru_ws_signal` 新增 `last_val`,`ws_task()` 仅在值变化时发送 |
|
||
| 9 | 补全 `p_signal->ctrl_type` 参数 |
|
||
| 10 | `ws_send()` 循环中增加 `c->is_websocket && !c->is_draining` 检查 |
|
||
|
||
**第二轮(2026-06-10):会话隔离重构**:
|
||
|
||
| # | 修复 |
|
||
|---|------|
|
||
| 11 | 引入 `stru_ws_session` 每连接独立信号集,全局 `g_ws_sessions: map<conn_id, session>` 管理 |
|
||
| — | `ws_recv(c, ...)` 增加连接参数,操作仅影响对应 session |
|
||
| — | `ws_task()` 遍历 sessions,为每个连接构建独立 JSON → `ws_send_one(conn_id, ...)` |
|
||
| — | `ws_session_destroy(c)` 在 `MG_EV_CLOSE`/`WS_CTL(CLOSE)` 时释放该连接所有信号资源 |
|
||
| — | 所有 `add/del/set/make` 函数改为接受 `stru_ws_session&` 参数,无状态纯函数 |
|
||
|
||
---
|
||
|
||
## 6. 对外接口
|
||
|
||
| 函数 | 文件 | 说明 |
|
||
|------|------|------|
|
||
| `app_web_server_init1(arg)` | web_server.cpp | 第一段初始化,启动 HTTP/WS 服务器 |
|
||
| `app_web_server_init2(arg)` | web_server.cpp | 第二段初始化(空) |
|
||
| `app_web_server(arg)` | web_server.cpp | 主线程循环,定时器驱动 `ws_task()` |
|
||
| `ws_send_all(p_tx, tx_len)` | web_server.cpp | 向所有 WS 客户端广播相同数据 |
|
||
| `ws_send_one(conn_id, p_tx, tx_len)` | web_server.cpp | 向单个 WS 连接发送数据 |
|
||
| `ws_recv(c, p_rx, rx_len)` | ws_method.cpp | 解析 WS 命令 JSON,操作仅在 c 对应 session 内生效 |
|
||
| `ws_task()` | ws_method.cpp | 遍历所有 session,各自构建增量 JSON 并推送 |
|
||
| `ws_session_destroy(c)` | ws_method.cpp | 释放连接对应的所有信号资源 |
|