202 lines
6.2 KiB
Markdown
202 lines
6.2 KiB
Markdown
# 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)连接数通常较少
|