6.2 KiB
6.2 KiB
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 事件处理:
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 多连接管理
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 数组嵌入:
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 命令:
{
"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:
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 数据广播
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 单连接发送
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. 已知问题
- mg_mgr_poll 非阻塞模式:
mg_mgr_poll(&mgr, 0)零超时,高频率轮询可能消耗 CPU。Mongoose 连接数少时不明显 - packed_fs 更新: 修改前端后需重新生成
packed_fs.c并重新编译 - 广播效率:
ws_send_all逐连接发送,大量连接时效率低。当前场景(嵌入式 RTU)连接数通常较少