RTU/mimo/工程/libweb_server模块分析.md

202 lines
6.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.

# libweb_server 模块分析
**日期**: 2026-06-12
**基于源码**: `src/system/libweb_server/`5个文件约1500行
---
## 1. 模块定位
`libweb_server` 是 RTU 的嵌入式 Web 服务器,作为 `app_web_server` 线程9个应用线程之第7号运行。基于 Mongoose v7.x提供
- HTTP 静态文件服务(内嵌前端工程)
- WebSocket 实时数据通道JSON 格式)
- 多客户端并发支持per-connection session
- 信号增量推送(仅变更时发送)
### 目录结构
```
src/system/libweb_server/
├── inc/
│ ├── web_server.h # 模块头文件
│ └── ws_method.h # WebSocket 消息处理接口
└── src/
├── web_server.cpp # HTTP/WS 服务器 + Mongoose 事件循环
├── ws_method.cpp # WebSocket 命令解析 + JSON 推送
└── packed_fs.c # 前端文件嵌入(自动生成)
```
## 2. 架构设计
### 2.1 Mongoose 集成
Mongoose 是单线程事件驱动的网络库。RTU 的 Web 服务器以独立线程运行,集成方式:
```
app_web_server_init1:
→ mg_mgr_init(&mgr) // 初始化事件管理器
→ 加载 packed_fs前端文件 // 如果定义了 USE_PACKED_FS
app_web_server_init2:
→ mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL)
→ 启动事件循环
app_web_server 线程:
while(1):
task_event_recv(EV_TIMER1 | EV_TIMER2 | EV_TIMER3)
EV_TIMER1 (10ms): mg_mgr_poll(&mgr, 0) // 非阻塞轮询
EV_TIMER2 (100ms): ws_task() // WebSocket 推送
EV_TIMER3 (1000ms): 空闲
```
### 2.2 事件处理函数 `fn`
统一的 HTTP + WebSocket 事件处理:
```cpp
static void fn(struct mg_connection *c, int ev, void *ev_data):
switch(ev):
MG_EV_HTTP_MSG:
mg_http_get_header(hm, "Sec-WebSocket-Key") 存在?
YES mg_ws_upgrade(c, hm, NULL) // WebSocket 握手
NO mg_http_serve_dir / mg_http_serve_packed // 静态文件
MG_EV_WS_MSG:
ws_recv(c, wm->data.buf, wm->data.len) // WebSocket 消息
MG_EV_CLOSE:
ws_session_destroy(c) // 释放 per-connection 资源
```
### 2.3 多连接管理
```cpp
LOCAL std::vector<struct mg_connection *> g_ws_conns; // 所有 WS 连接
LOCAL pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
```
- 连接建立时将 `mg_connection*` 加入 `g_ws_conns`
- 断开时从 `g_ws_conns` 移除并调用 `ws_session_destroy`
- 所有对 `g_ws_conns` 的访问受互斥锁保护
## 3. 前端嵌入式部署packed_fs.c
`packed_fs.c` 由构建工具自动生成将前端文件HTML/CSS/JS`unsigned char` 数组嵌入:
```c
const struct mg_mem_file mg_packed_files[] = {
{"/css/style.css", v3, sizeof(v3) - 1, 1781072856},
{"/index.html", v4, sizeof(v4) - 1, 1781073518},
{"/js/app.js", v5, sizeof(v5) - 1, 1781073909},
{"/js/monitor.js", v6, sizeof(v6) - 1, 1781072788},
{"/js/pages.js", v7, sizeof(v7) - 1, 1781073958},
{"/js/ws-client.js", v8, sizeof(v8) - 1, 1781071573},
{NULL, NULL, 0, 0}
};
```
Mongoose 通过 `mg_http_serve_packed` 直接返回内嵌文件,无需外部 `web_root` 目录。
## 4. WebSocket 消息处理ws_method.cpp
### 4.1 消息格式
客户端发送 JSON 命令:
```json
{
"cmd": "subscribe", // 命令类型
"type": "out,ao,param", // 订阅的信号类型
"signals": ["saddr1", "saddr2"], // 指定信号(空=全部)
"conn_id": 12345 // 连接标识
}
```
### 4.2 命令分发
`ws_recv` 解析 JSON → 提取 `cmd` 字段 → 路由到处理函数:
| cmd | 功能 | 处理函数 |
|-----|------|---------|
| `subscribe` | 订阅信号推送 | `ws_handle_subscribe` |
| `unsubscribe` | 取消订阅 | `ws_handle_unsubscribe` |
| `read_all` | 读取全部当前值 | `ws_handle_read_all` |
| `set_value` | 设置 AO/Param 值 | `ws_handle_set_value` |
| `yk_control` | 遥控操作 | `ws_handle_yk_control` |
### 4.3 信号订阅与增量推送
每个连接维护独立的 per-connection session
```cpp
struct ws_session {
unsigned long conn_id;
std::set<std::string> subscribed_signals; // 已订阅的信号 saddr 集合
std::set<std::string> subscribed_types; // 已订阅的信号类型out/ao/param
// ...
};
```
订阅流程:
```
客户端发送 {cmd: "subscribe", type: "out,ao", signals: ["sig1","sig2"]}
→ ws_handle_subscribe(c, json):
获取或创建 session
将信号 saddr 加入 subscribed_signals
将类型加入 subscribed_types
回复 {status: "ok"}
```
推送流程 (`ws_task`, 每 100ms 执行):
```
ws_task():
遍历 g_ws_conns:
对每个连接:
收集该连接订阅的信号中发生了变更的
构建 JSON: {signals: [{saddr, value, time, quality}, ...]}
通过 mg_ws_send 发送
```
### 4.4 数据广播
```cpp
void ws_send_all(const char *p_tx, uint16_t tx_len):
pthread_mutex_lock(&g_ws_conns_mutex)
for each c in g_ws_conns:
mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT)
pthread_mutex_unlock(&g_ws_conns_mutex)
```
### 4.5 单连接发送
```cpp
void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len):
g_ws_conns 中查找 mg_connection.conn_id == conn_id
找到 mg_ws_send
```
## 5. HTTP API
除了 WebSocket还提供 RESTful HTTP 接口(通过 `MG_EV_HTTP_MSG` 处理):
| 方法 | 路径 | 功能 |
|------|------|------|
| GET | `/` | 静态首页 |
| GET | `/api/status` | 设备状态 |
| GET | `/api/signals` | 信号列表快照 |
| POST | `/api/control` | 控制命令 |
## 6. 线程安全
- Mongoose 事件循环在 `app_web_server` 线程中执行
- `ws_task()``EV_TIMER2`100ms中执行与事件循环同线程
- `g_ws_conns` 访问受 `g_ws_conns_mutex` 保护
- 通过任务事件机制与 datacenter 交互(信号值读取通过 `dc_get_signal_val` 等线程安全 API
## 7. 已知问题
1. **mg_mgr_poll 非阻塞模式**: `mg_mgr_poll(&mgr, 0)` 零超时,高频率轮询可能消耗 CPU。Mongoose 连接数少时不明显
2. **packed_fs 更新**: 修改前端后需重新生成 `packed_fs.c` 并重新编译
3. **广播效率**: `ws_send_all` 逐连接发送,大量连接时效率低。当前场景(嵌入式 RTU连接数通常较少