<修改> 1、调整webserver模块,增加多连接,各连接单资源,连接断开释放资源

This commit is contained in:
ypc 2026-06-10 13:33:53 +08:00
parent e18f239d77
commit a3ae78e5e8
13 changed files with 1832 additions and 168 deletions

View File

@ -0,0 +1,273 @@
# libiec61850s 模块分析
**日期**2026-06-10
---
## 1. 模块概览
`libiec61850s` 是 IEC 61850 MMS 服务端的应用线程模块(`app_iec61850s`),属于系统层的第 9 号线程。它作为桥接层,连接三个子系统:
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ DataCenter │ ←→ │ libiec61850s │ ←→ │ libmms_s │
│ (信号数据中心) │ │ (应用线程/桥接层) │ │ (MMS 服务端封装) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
┌──────────────────┐
│ mms_s.xml 配置 │
└──────────────────┘
```
### 目录结构
```
src/system/libiec61850s/
├── inc/
│ ├── iec61850s.h # 头文件(包含 mySystem/myMms_s 等)
│ └── parse_xml.h # XML 配置解析类型 + 接口
└── src/
├── iec61850s.cpp # 应用线程主逻辑(~487 行)
└── parse_xml.cpp # mms_s.xml 解析(~129 行)
```
---
## 2. 应用线程模型
与所有 app 线程一致libiec61850s 遵循 `init1 → init2 → fun_cb` 三段式:
| 阶段 | 函数 | 主要工作 |
|------|------|---------|
| init1 | `app_iec61850s_init1()` | 解析配置、注册回调 |
| init2 | `app_iec61850s_init2()` | 初始化信号、启动 MMS 服务器 |
| fun_cb | `app_iec61850s()` | 主循环3 定时器,当前空闲) |
### 2.1 init1 流程([iec61850s.cpp:392](src/system/libiec61850s/src/iec61850s.cpp#L392)
```
1. get_base_path() → 获取进程目录
2. parse_mms_xml("config/MMS/mms_s.xml") → 解析 XML 配置
3. mms_s_dbg_switch(false) → 关闭调试
4. mms_s_file_path_set(base_path) → 设置文件根目录
5. mms_s_value_update_register(&cb) → 注册值更新回调
```
### 2.2 init2 流程([iec61850s.cpp:421](src/system/libiec61850s/src/iec61850s.cpp#L421)
```
1. iec61850s_signals_init() → 初始化五类信号
2. mms_s_init("config/MMS/PCS.icd", 102) → 启动 MMS 服务器
```
### 2.3 主循环([iec61850s.cpp:446](src/system/libiec61850s/src/iec61850s.cpp#L446)
三个定时器事件(`EV_TIMER1/2/3`)当前均为空闲,仅 `EV_TIMER3``run_cnt++`。信号驱动的工作全部通过 libmms_s 的内部线程和 DataCenter 回调完成——本线程主要作为容器,维护 MMS 服务器的生命周期。
---
## 3. 配置解析子系统([parse_xml.cpp](src/system/libiec61850s/src/parse_xml.cpp)
### 3.1 XML 结构mms_s.xml
```xml
<Config>
<St> <!-- 遥信信号 -->
<Signal link="st.0" />
</St>
<Mx> <!-- 遥测信号 -->
<Signal link="mx.0" />
</Mx>
<Co> <!-- 遥控信号 -->
<Signal link="co.0" />
</Co>
<Ao> <!-- 定值信号 -->
<Signal link="ao.0" />
</Ao>
<Param> <!-- 参数信号 -->
<Signal link="param.0" />
</Param>
</Config>
```
### 3.2 数据结构
```cpp
typedef struct {
std::vector<stru_mms_s_signal_base> vec_st; // 遥信
std::vector<stru_mms_s_signal_base> vec_mx; // 遥测
std::vector<stru_mms_s_signal_base> vec_co; // 遥控
std::vector<stru_mms_s_signal_base> vec_ao; // 定值
std::vector<stru_mms_s_signal_base> vec_param; // 参数
} stru_mms_cfg;
```
每个 Signal 只需要 `link`sAddr属性`type` 和 `ctrl_model` 在后续从 DataCenter 获取。
---
## 4. 五类信号初始化
`iec61850s_signals_init()` 按顺序初始化五类信号([iec61850s.cpp:347](src/system/libiec61850s/src/iec61850s.cpp#L347)
### 4.1 遥信ST初始化
```
for each st signal:
dc_signal_out_link_with_callback(saddr, &p_data, iec61850s_st_mx_change_callback)
```
将 DataCenter 的 `out` 信号与本地指针绑定,注册变化回调 `iec61850s_st_mx_change_callback`
### 4.2 遥测MX初始化
逻辑与 ST 完全一致,共用同一个回调函数。
### 4.3 ST/MX 变化回调
```cpp
iec61850s_st_mx_change_callback(saddr, type, p_data, p_last_data)
dc_get_signal_val(p_data, type) → 获取当前值字符串
g_mms_s_value_update_cb(saddr, val) → 更新 MMS 模型中的 DA 值
```
数据流向:**DataCenter 信号变化 → 回调 → libmms_s::mms_s_value_update() → IedServer 模型更新**
### 4.4 遥控CO初始化
```
for each co signal:
dc_get_yk_signal_info(saddr, desc, type, ctrl_model, &p_data)
mms_s_control_register(&g_vec_control, iec61850s_control_callback)
```
控制执行时,`mms_s_control.cpp` 的 `control_handler()` 会触发 `iec61850s_control_callback()`,根据 `ctrl_model` 调用 DataCenter 的 `dc_signal_yk_set_status()`
**ctrl_model 映射**
| MMS Control Model | DataCenter 动作 |
|-------------------|----------------|
| DIRECT_NORMAL / DIRECT_ENHANCED | `SIGNAL_CTRL_TYPE::DIRECT_NORMAL``dc_signal_yk_set_status(DIRECT)` |
| SBO_NORMAL / SBO_ENHANCED | `SIGNAL_CTRL_TYPE::SBO_NORMAL` → select + direct 两步 |
| STATUS_ONLY | 仅日志,不操作 |
### 4.5 定值AO初始化
```
for each ao signal:
dc_get_ao_signal_info(saddr, desc, type, null, ctrl_model, &p_data, null)
mms_s_setting_register(&g_vec_setting, iec61850s_setting_callback)
```
客户端写定值时,`mms_s_setting.cpp` 的 `writeAccessHandler()` 校验通过后触发 `iec61850s_setting_callback()`,调用 `dc_signal_ao_set_val()`
### 4.6 参数Param初始化
```
for each param signal:
dc_get_param_signal_info(saddr, desc, type, null, ctrl_model, &p_data_vec, null)
mms_s_param_register(&g_vec_param, iec61850s_param_callback)
```
参数支持多定值区(`p_data[MMS_S_PARAM_MAX]`,最大 16 组)。客户端确认编辑后,`edit_sg_confirmation_handler()` 触发 `iec61850s_param_callback()`,调用 `dc_signal_param_set_val()`,传入 `setting_zone` 索引。
---
## 5. 完整数据流向
### 5.1 上行数据RTU → 客户端)
```
DataCenter 信号变化
→ iec61850s_st_mx_change_callback()
→ g_mms_s_value_update_cb(saddr, val)
→ mms_s_value_update() [mms_s_value.cpp]
→ IedServer_update*AttributeValue()
→ libiec61850 内部触发报告(根据 TrgOps 配置)
→ MMS 报告上送到客户端
```
### 5.2 下行数据(客户端 → RTU
**控制**
```
客户端 Select/Operate
→ libiec61850 → control_handler() [mms_s_control.cpp]
→ iec61850s_control_callback()
→ dc_signal_yk_set_status()
```
**定值**
```
客户端 Write SP
→ libiec61850 → writeAccessHandler() [mms_s_setting.cpp]
→ 值校验(范围/步长)
→ iec61850s_setting_callback()
→ dc_signal_ao_set_val()
```
**参数(定值组)**
```
客户端切换定值组
→ libiec61850 → param_active_sg_changed_handler() [mms_s_param.cpp]
→ param_load_active_sg_values() → 加载 SG DA
客户端编辑确认
→ edit_sg_confirmation_handler() [mms_s_param.cpp]
→ iec61850s_param_callback()
→ dc_signal_param_set_val(setting_zone)
```
---
## 6. 数据结构
### 6.1 本地信号存储
| 类型 | 容器 | 数据结构 |
|------|------|---------|
| 遥信 | `g_vec_st` | `vector<stru_local_st_mx>` — {base, p_data} |
| 遥测 | `g_vec_mx` | `vector<stru_local_st_mx>` — {base, p_data} |
| 遥控 | `g_vec_control` | `vector<stru_mms_s_control>` — {base, p_data} |
| 定值 | `g_vec_setting` | `vector<stru_mms_s_setting>` — {base, p_data} |
| 参数 | `g_vec_param` | `vector<stru_mms_s_param>` — {base, param_num, p_data[]} |
### 6.2 公共信号基类([myMms_s.h](release/inc/myMms_s.h)
```cpp
typedef struct {
char saddr[MMS_S_STR_LEN]; // 短地址(如 "st.0", "mx.5"
char desc[MMS_S_STR_LEN]; // 描述
uint8_t type; // 数据类型DATA_TYPE_*
uint8_t ctrl_model; // 控制模型
} stru_mms_s_signal_base;
```
---
## 7. 与 libmms_m / libiec61850m 的对比
| 维度 | MMS 客户端 (m) | MMS 服务端 (s) |
|------|---------------|---------------|
| **角色** | 主动连接 IED订阅报告 | 被动等待客户端连接 |
| **模型来源** | 从 IED 发现 | ICD 文件预定义 |
| **数据方向** | 先订阅 RCB再接收报告 | 收到控制/写请求后更新 DataCenter |
| **线程模型** | libmms_m 启动 pthreadlibiec61850m 做桥接 | libmms_s 启动 pthreadlibiec61850s 做桥接 |
| **桥接层复杂度** | 复杂RCB 发现/匹配/订阅/GI 触发/重连 | 相对简单:注册信号+回调libmms_s 处理细节 |
| **配置方式** | mms_m.xmlIED 连接参数) | mms_s.xml信号映射+ PCS.icd模型定义 |
---
## 8. 对外接口
| 函数 | 说明 |
|------|------|
| `app_iec61850s_init1(void *arg)` | 第一段初始化:解析 XML、注册值更新回调 |
| `app_iec61850s_init2(void *arg)` | 第二段初始化:初始化信号、启动 MMS 服务器 |
| `app_iec61850s(void *arg)` | 主线程循环 |
| `parse_mms_xml(path)` | 解析 mms_s.xml 配置 |
| `mms_cfg_ptr_get()` | 获取配置数据指针 |
| `show_mms_xml(cfg)` | 打印配置内容(调试) |

View File

@ -0,0 +1,379 @@
# libmms_s 模块分析
**日期**2026-06-10
---
## 1. 模块概览
`libmms_s` 是 IEC 61850 MMS 服务端的封装库,基于 `libiec61850` v1.5.3 的服务端 API提供 ICD 文件解析、动态数据模型创建、IedServer 生命周期管理,以及控制/定值/参数/文件等子系统的初始化。
### 目录结构
```
src/protocol/libmms_s/
├── inc/
│ ├── mms_s.h # 核心头文件:日志宏、全局访问接口、类型转换
│ ├── mms_s_icd.h # ICD 解析数据结构(完整 SCL 类型体系)
│ ├── mms_s_model.h # 动态模型创建接口
│ ├── mms_s_value.h # DA 初始值设定
│ ├── mms_s_control.h # 控制功能接口
│ ├── mms_s_param.h # 定值组参数接口
│ ├── mms_s_setting.h # 定值SP接口
│ └── mms_s_file.h # 文件传输服务接口
└── src/
├── mms_s.cpp # 核心init/run/task/类型转换
├── mms_s_icd.cpp # ICD XML 解析器tinyxml2
├── mms_s_model.cpp # 动态 IedModel 创建
├── mms_s_control.cpp # 控制SBO/直控)处理
├── mms_s_param.cpp # 定值组SG/SE管理
├── mms_s_setting.cpp # 定值SP管理
├── mms_s_value.cpp # 数据属性初始值设定
└── mms_s_file.cpp # MMS 文件传输服务
```
---
## 2. 核心架构
### 2.1 单例全局状态
```cpp
LOCAL IedServer gp_iedServer = NULL; // 全局 IED 服务器(单例)
LOCAL stru_icd *gp_icd = NULL; // 全局 ICD 数据(单例)
LOCAL int g_running = 0; // 线程运行标志
LOCAL bool g_dbg_switch = false; // 调试输出开关
```
整个库采用单例模式,全局只有一个 IedServer 和一个 ICD 数据结构。
### 2.2 模块分层
```
┌──────────────────────────────────────────┐
│ myMms_s.h │ ← 公共 API + 类型定义
├──────────────────────────────────────────┤
│ mms_s.cpp (init / run / 类型转换) │ ← 库入口
├──────────────────────────────────────────┤
│ mms_s_icd.cpp │ mms_s_model.cpp │ ← ICD 解析 → 模型构建
├──────────────────────────────────────────┤
│ mms_s_value.cpp (DA 初始值) │ ← 模型回调
├──────────────────────────────────────────┤
│ mms_s_control.cpp │ mms_s_param.cpp │ ← 功能模块
│ mms_s_setting.cpp │ mms_s_file.cpp │
├──────────────────────────────────────────┤
│ libiec61850 (libiec61850.a) │ ← 底层协议栈
└──────────────────────────────────────────┘
```
---
## 3. 完整初始化流程
`mms_s_init()` 的执行顺序([mms_s.cpp:261](src/protocol/libmms_s/src/mms_s.cpp#L261)
```
1. icd_parse(icd_path) → 解析 ICD 文件 → stru_icd
2. model_init(*gp_icd) → 创建动态数据模型 → IedModel
3. IedServerConfig_create() → 创建服务器配置
4. IedServer_createWithConfig() → 创建 IedServer 实例
5. control_init() → 安装控制回调
6. param_init() → 安装定值组回调
7. file_init() → 安装文件服务
8. IedServer_setRCBEventHandler() → 注册 RCB 事件监听
9. IedServer_start(port) → 启动服务器(默认端口 102
10. setting_init() → 初始化定值
11. IedServer_isRunning() → 检查运行状态
12. Thread_create(mms_s_run_task) → 启动后台线程
```
---
## 4. ICD 解析子系统
### 4.1 数据结构体系([mms_s_icd.h](src/protocol/libmms_s/inc/mms_s_icd.h)
对应 IEC 61850-6 SCL 标准的完整类型体系:
**数据类型模板层DataTypeTemplates**
| 结构 | 说明 | 关键字段 |
|------|------|---------|
| `stru_LNodeType` | 逻辑节点类型 | lnClass, vec_do, map_do(key:name) |
| `stru_DO` | 数据对象定义 | desc, type→ DOType id |
| `stru_DOType` | 数据对象类型 | cdc, vec_da, map_da, vec_sdo, map_sdo |
| `stru_DA` | 数据属性定义 | bType, type, dchg, qchg, fc, text |
| `stru_DAType` | 数据属性类型 | vec_bda, map_bda(key:name) |
| `stru_BDA` | 基本数据属性 | bType, type, fc, dchg, qchg |
| `stru_EnumType` | 枚举类型 | vec_ord, map_enumVal(key:ord) |
**实例化模型层IED/Server**
| 结构 | 说明 |
|------|------|
| `stru_ied` | IED 顶层模型(含 model, name, 各逻辑设备) |
| `stru_LDevice` | 逻辑设备(含 Ldevice 指针, sgcb, ln0, vec_ln |
| `stru_LN0` | LLN0含 数据集, 报告控制, 定值控制) |
| `stru_LN` | 普通逻辑节点 |
| `stru_DOI` | 数据对象实例(含 SDI, DAI |
| `stru_SDI` | 子数据对象实例(可递归嵌套) |
| `stru_DAI` | 数据属性实例(含 sAddr, val |
| `stru_DataSet` | 数据集(含 FCDA 列表) |
| `stru_FCDA` | 功能约束数据属性ldInst/lnClass/doName/daName/fc |
| `stru_ReportControl` | 报告控制块(含 TrgOps, OptFields, RptEnabled_max |
| `stru_SettingControl` | 定值控制块actSG, numOfSGs |
**运行时辅助结构**
| 结构 | 说明 |
|------|------|
| `stru_all_DO` | DO 运行时树(含 libiec61850 DataObject 指针, map_all_sdo, map_all_da |
| `stru_all_DA` | DA 运行时树(含 libiec61850 DataAttribute 指针, map_all_da 子节点) |
| `stru_contorl_DO` | 控制 DO 定位信息ldevice_inst/ln_class/ln_inst/do_name/p_all_do |
| `stru_saddr_point` | sAddr 到模型节点的映射node, da_t, da_q |
| `stru_setting_info` | 定值信息DA 指针, 类型, 值) |
### 4.2 解析流程([mms_s_icd.cpp](src/protocol/libmms_s/src/mms_s_icd.cpp)
```
icd_parse(icd_file)
├── XMLDocument.LoadFile()
├── parse_ied() → 解析 IED 实例
│ ├── parse_LDevice() → 每个逻辑设备
│ │ ├── parse_LN0() → LLN0数据集/报告控制/定值控制/DOI
│ │ │ ├── parse_DataSet()
│ │ │ ├── parse_ReportControl()
│ │ │ │ ├── parse_ReportControl_TrgOps()
│ │ │ │ ├── parse_ReportControl_OptFields()
│ │ │ │ └── parse_ReportControl_RptEnable() → RptEnabled_max
│ │ │ ├── parse_DOI() → parse_SDI() / parse_DAI()
│ │ │ └── parse_SettingControl()
│ │ └── parse_LN() → 普通 LN + DOI
│ └── (沿 SCL > IED > AccessPoint > Server > LDevice 路径)
├── parse_dataTypeTemplates() → 解析模板
│ ├── parse_LNodeType()
│ ├── parse_DOType()
│ ├── parse_DAType()
│ ├── parse_EnumType()
│ └── parse_BDA_fc() ×2 → 传播 FC/dchg/qchg 到嵌套 BDA
└── check_ied_dataTypeTemplates() → 一致性校验
└── 递归验证每个实例节点都能在模板中找到定义
```
### 4.3 BDA 属性传播机制
关键设计:当 DA 的 type 指向 DATypeStruct 类型)时,`parse_BDA_fc()` 将父级 DA 的 fc/dchg/qchg 属性向下传播到 DAType 中所有 BDA确保嵌套 Struct 的底层 BDA 也能继承正确的 FC 约束。
---
## 5. 动态模型创建子系统
### 5.1 model_init() 主流程([mms_s_model.cpp:1319](src/protocol/libmms_s/src/mms_s_model.cpp#L1319)
```
1. IedModel_create(name)
2. model_ldevice_init()
├── LogicalDevice_create()
├── model_ln0_init()
│ ├── LogicalNode_create("LLN0")
│ ├── model_dataobject_init() → 创建所有 DO/SDO/DA
│ ├── model_DataSet_init() → 创建数据集+条目
│ ├── model_ReportControlBlock_init() → 创建 RCB
│ └── SettingGroupControlBlock_create()
└── model_LNodes_init() → 创建普通 LN
3. model_sync_ied_default_values() → 同步 ICD DAI 中的 sAddr/val
4. model_search_control_DataObjects() → 搜索 ctlModel→vec_control_do
5. icd.ied.model->initializer = mms_s_values_init → 注册回调
```
### 5.2 类型映射表
**bType → DataAttributeType**[mms_s_model.cpp:43](src/protocol/libmms_s/src/mms_s_model.cpp#L43)
`BOOLEAN → IEC61850_BOOLEAN`, `INT32 → IEC61850_INT32`, `FLOAT32 → IEC61850_FLOAT32`, `Struct → IEC61850_CONSTRUCTED`, `Quality → IEC61850_QUALITY`, `Timestamp → IEC61850_TIMESTAMP`, `Dbpos → IEC61850_GENERIC_BITSTRING` 等,共约 25 种映射。
**FC → FunctionalConstraint**[mms_s_model.cpp:86](src/protocol/libmms_s/src/mms_s_model.cpp#L86)
`ST/MX/SP/SV/CF/DC/SG/SE/SR/OR/BL/EX/CO/US/MS/RP/BR/LG/GO` → 对应 IEC61850 枚举。
### 5.3 SG/SE 双数据属性设计
当 DA 的 FC=SG 时,除了创建 FC=SG 的 DA 外,还会额外创建一个 FC=SE 的同名 DAkey 加 `_SE` 后缀)。反之 FC=SE 同理。sAddr 映射时也会对应添加 `_SG` / `_SE` 后缀区分。
这是一个独创设计——在同一个 DO 下同时暴露 SG当前激活值和 SE编辑缓冲区值使得外部系统可以同时访问定值的"当前生效值"和"正在编辑中的值"。
### 5.4 sAddr 映射机制
ICD 文件中 DAI 元素的 `sAddr` 属性定义了该数据点与外部数据源的绑定关系。`model_sync_ied_default_values()` 将 sAddr 写入模型节点的 `sAddr` 字段,同时建立:
- `icd.ied.vec_saddr` — sAddr 列表
- `icd.ied.map_saddr_point` — sAddr → ModelNode 映射
对于 FC=SG 的点sAddr 后缀 `_SG`FC=SE 的点后缀 `_SE`
---
## 6. 控制子系统([mms_s_control.cpp](src/protocol/libmms_s/src/mms_s_control.cpp)
### 6.1 双回调模式
| 回调 | 阶段 | 职能 |
|------|------|------|
| `check_handler()` | Select / Interlock | 权限校验,匹配合法 DO 则返回 CONTROL_ACCEPTED |
| `control_handler()` | Operate | 执行控制命令,更新 t时间戳+ stVal触发外部回调 |
### 6.2 控制执行流程
```
客户端 → Select Request → check_handler() → CONTROL_ACCEPTED
客户端 → Operate Request → check_handler() (interlock check) → control_handler()
├── IedServer_updateUTCTimeAttributeValue(t)
├── 匹配 sAddr → cb_control() → 通知应用层
└── 根据 stVal 类型:
├── BOOLEAN → IedServer_updateAttributeValue()
└── Dbpos → IedServer_updateDbposValue()
```
### 6.3 控制 DO 发现
`model_search_control_DataObjects()` 遍历模型树,找到所有包含 `ctlModel` DA 的 DO存入 `icd.ied.vec_control_do`。`control_init()` 遍历该列表,为每个安装 `control_handler``check_handler`
---
## 7. 定值组子系统([mms_s_param.cpp](src/protocol/libmms_s/src/mms_s_param.cpp)
### 7.1 SG vs SE
| FC | 含义 | mmsValue 加载时机 |
|----|------|------------------|
| SG | Setting Group — 当前激活定值区的实际值 | 激活定值组切换时加载 |
| SE | Setting Editable — 编辑缓冲区值 | 编辑定值组切换时加载 |
### 7.2 回调链
```
激活定值组切换 → param_active_sg_changed_handler() → param_load_active_sg_values()
编辑定值组切换 → param_edit_sg_changed_handler() → param_load_edit_sg_values()
编辑确认 → edit_sg_confirmation_handler() → 回读 SE 值 → cb_param()
```
### 7.3 值的加载与校验
- 加载:根据 sAddr+"_SG"/"_SE" 在 `map_saddr_point` 中查找节点,按 MMS 类型分发更新函数
- 回读:`edit_sg_confirmation_handler()` 读取 SE DA 当前值,做 min/max/step 校验,通过后触发外部回调 `g_param_cb`
- 类型分发表:`g_param_update_funcs[]`(写)和 `g_param_get_funcs[]`(读),覆盖 BOOLEAN/INT/UINT/FLOAT/STRING
---
## 8. 定值子系统([mms_s_setting.cpp](src/protocol/libmms_s/src/mms_s_setting.cpp)
### 8.1 与参数的差异
定值FC=SP不参与定值组切换是单一设置值。
### 8.2 写保护机制
1. 全局访问策略:`IedServer_setWriteAccessPolicy(IEC61850_FC_SP, ACCESS_POLICY_DENY)`
2. 精确放行:通过 `IedServer_handleWriteAccess()` 为已注册 sAddr 安装 `writeAccessHandler`
3. `writeAccessHandler` 做值校验(范围+步长),通过后触发外部回调 `cb_setting`
### 8.3 校验流程
```
客户端写 SP 值 → writeAccessHandler()
├── 匹配 sAddr
├── 类型分发 (setting_get_BOOLEAN/INT/UINT/FLOAT/STRING)
│ └── setting_min_max_step_get() → 读取同级 minVal/maxVal/stepSize
│ └── 范围校验 + 步长校验
├── cb_setting() → 通知应用层
└── return DATA_ACCESS_ERROR_SUCCESS → libiec61850 更新 mmsValue
```
---
## 9. 数据属性值初始化([mms_s_value.cpp](src/protocol/libmms_s/src/mms_s_value.cpp)
`mms_s_values_init()` 作为 `IedModel.initializer` 回调,在 IedServer 创建过程中由 libiec61850 自动调用。
**初始化类型覆盖**BOOLEAN, INT8U/32, FLOAT32, VisString*, Unicode*, Timestamp, Dbpos, Enum特殊按文本值或序号查找枚举值
**Dbpos 特殊处理**`mms_s_da_value_init_Dbpos()` 按 val 值映射到 DBPOS_INTERMEDIATE_STATE(0)/OFF(1)/ON(2)/BAD_STATE(>=3)。
### 9.1 值更新路径
`mms_s_value_update()` 是预留的值更新函数,通过 `mms_s_value_update_register()` 注册给外部。当数据中心信号变化时,上层调用此函数更新模型中的 DA 值,同时自动更新父 DO 的 `t`(时间戳)和 `q`(品质)。
---
## 10. 文件传输服务([mms_s_file.cpp](src/protocol/libmms_s/src/mms_s_file.cpp)
提供 MMS 文件传输基础能力:
- 设置文件存储根目录 `IedServer_setFilestoreBasepath()`
- 访问控制:禁止重命名、禁止删除 `IEDSERVER.BIN`
- 连接事件日志
---
## 11. 后台运行线程
```cpp
LOCAL void *mms_s_run_task(void *parameter)
{
while(g_running) {
IedServer_lockDataModel(gp_iedServer);
IedServer_unlockDataModel(gp_iedServer);
Thread_sleep(100); // 100ms 周期
}
IedServer_stop(gp_iedServer);
IedServer_destroy(gp_iedServer);
IedModel_destroy(iedModel);
}
```
线程负责保护 IedServer 生命周期,通过 lock/unlock 持有模型锁保持服务活跃。收到 SIGINT 后 `g_running=0`,线程退出并销毁资源。
---
## 12. RCB 事件监听
`rcbEventHandler()` 监听客户端的报告控制块操作([mms_s.cpp:48](src/protocol/libmms_s/src/mms_s.cpp#L48)
| 事件 | 含义 |
|------|------|
| `RCB_EVENT_ENABLE` | 客户端使能报告 |
| `RCB_EVENT_DISABLE` | 客户端关闭报告 |
| `RCB_EVENT_RESERVED` | 客户端预订 RCB |
| `RCB_EVENT_UNRESERVED` | 客户端释放预订 |
| `RCB_EVENT_GI` | 总召触发 |
| `RCB_EVENT_SET_PARAMETER` | 客户端设置 RCB 参数(如 TrgOps 等) |
| `RCB_EVENT_GET_PARAMETER` | 客户端获取 RCB 参数 |
**特殊处理**:当客户端设置 `TrgOps` 参数时,服务器强制追加 `dchg``rcb->trgOps |= 0x01`),确保数据变化一定能触发报告上送。
---
## 13. 对外 API 汇总
| API | 文件 | 说明 |
|-----|------|------|
| `mms_s_init(icd_path, port)` | mms_s.cpp | 完整初始化 MMS 服务器 |
| `mms_s_dbg_switch(on)` | mms_s.cpp | 调试开关 |
| `mms_s_get_icd_ptr()` | mms_s.cpp | 获取 ICD 数据 |
| `mms_s_get_ied_server_ptr()` | mms_s.cpp | 获取 IedServer |
| `mms_s_control_register(...)` | mms_s_control.cpp | 注册控制点 |
| `mms_s_setting_register(...)` | mms_s_setting.cpp | 注册定值 |
| `mms_s_param_register(...)` | mms_s_param.cpp | 注册参数(定值组) |
| `mms_s_file_path_set(path)` | mms_s_file.cpp | 设置文件根路径 |
| `mms_s_value_update_register(cb)` | mms_s_value.cpp | 注册值更新回调 |
| `mms_s_get_string_by_mms_type(...)` | mms_s.cpp | MMS 类型→字符串 |
| `mms_s_get_string_by_type(...)` | mms_s.cpp | 自定义类型→字符串 |
---
## 14. 已知问题
### 14.1 check_handler 未调用 cb_interlock
`mms_s_control.h` 中声明了 `mms_s_interlock_cb``mms_s_set_interlock_cb()`,但 `check_handler()` 中仅检查 ICD 中是否存在对应 DO`cb_interlock` 未被实际调用。外部注册的联锁检查回调不会生效。
### 14.2 param_min_max_step_get 实现不一致
`mms_s_param.cpp``param_min_max_step_get()` 通过遍历 ModelNode 父子关系查找 minVal/maxVal/stepSize向上找到 DataObject 再遍历 firstChild`mms_s_setting.cpp``setting_min_max_step_get()` 是通过 parent->firstChild 直接遍历。两者逻辑不同,前者更复杂(向上一级再到 DO可能是修正后的版本但后者仍保留了旧逻辑。
### 14.3 mms_s_value_update 未使用 g_iec61850s_value_init_cb_map
`mms_s_value_update()` 使用 `g_iec61850s_value_update_cb_map`(按 DataAttributeType 索引),但表中大部分类型回调为 NULL仅 BOOLEAN/INT32/INT32U/FLOAT32/Enum/VisString32/Unicode255/Timestamp 有实际实现。其他类型(如 INT8/INT16/INT64/FLOAT64 等)更新时会被跳过。

View File

@ -0,0 +1,217 @@
# 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 | 释放连接对应的所有信号资源 |

View File

@ -0,0 +1,598 @@
# WebSocket 服务端深度分析
## 一、概述
Mongoose 的 WebSocket 实现位于 `src/ws.c`~302 行),基于 RFC 6455 规范,同时支持服务端和客户端。本文档聚焦**服务端**的使用和内部实现。
**核心流程**
```
HTTP 请求到达 (Upgrade: websocket)
→ 用户处理器检测到 WebSocket 升级请求
→ 调用 mg_ws_upgrade() 完成握手
→ pfn 切换为 mg_ws_cbWebSocket 协议处理器)
→ 后续消息通过 MG_EV_WS_MSG 事件传递
→ mg_ws_send() 发送帧
```
## 二、数据结构
### 帧操作码Opcode
```c
// 定义在 ws.h
#define WEBSOCKET_OP_CONTINUE 0 // 分片帧的后续帧
#define WEBSOCKET_OP_TEXT 1 // 文本帧UTF-8
#define WEBSOCKET_OP_BINARY 2 // 二进制帧
#define WEBSOCKET_OP_CLOSE 8 // 关闭连接
#define WEBSOCKET_OP_PING 9 // 心跳请求
#define WEBSOCKET_OP_PONG 10 // 心跳响应
```
### WebSocket 消息结构
```c
// 定义在 ws.h 第 12-15 行
struct mg_ws_message {
struct mg_str data; // 消息数据(引用 c->recv 缓冲区,零拷贝)
uint8_t flags; // 帧标志字节FIN + Opcode
};
```
`flags` 字节的高位是 FIN 标志bit 7低 4 位是操作码:
```
flags = 0b1xxx_xxxx → FIN=1最后一帧
flags = 0b0xxx_xxxx → FIN=0还有后续帧
flags & 15 → 操作码0-15
```
### 内部帧解析结构ws.c 第 12-16 行)
```c
struct ws_msg {
uint8_t flags; // 帧标志
size_t header_len; // 帧头长度(含掩码 key
size_t data_len; // 数据长度
};
```
## 三、WebSocket 服务端完整使用流程
### 3.1 最小示例
```c
#include "mongoose.h"
// 统一的 HTTP + WebSocket 事件处理函数
static void fn(struct mg_connection *c, int ev, void *ev_data) {
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
// 判断是否是 WebSocket 升级请求
if (mg_http_get_header(hm, "Sec-WebSocket-Key")) {
mg_ws_upgrade(c, hm, NULL); // 执行握手,切换到 WS 模式
} else {
// 普通 HTTP 请求处理
mg_http_reply(c, 200, "", "hello\n");
}
} else if (ev == MG_EV_WS_MSG) {
// WebSocket 消息到达(握手完成后)
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
// 判断消息类型
if (wm->flags & WEBSOCKET_OP_TEXT) {
// 文本消息:回显
mg_ws_send(c, wm->data.buf, wm->data.len, WEBSOCKET_OP_TEXT);
} else if (wm->flags & WEBSOCKET_OP_BINARY) {
// 二进制消息处理
}
} else if (ev == MG_EV_CLOSE) {
// 连接关闭(包括 WebSocket 关闭)
}
}
int main() {
struct mg_mgr mgr;
mg_mgr_init(&mgr);
mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL);
for (;;) mg_mgr_poll(&mgr, 1000);
}
```
### 3.2 完整事件流
```
连接建立:
MG_EV_OPEN → 连接创建
HTTP 阶段:
MG_EV_ACCEPT → 连接被接受(可在此时初始化 TLS
MG_EV_READ → 数据到达
MG_EV_HTTP_MSG → HTTP 请求完整接收
→ 用户检测 Sec-WebSocket-Key 头部
→ 调用 mg_ws_upgrade(c, hm, NULL)
WebSocket 握手:
mg_ws_upgrade() 内部:
→ 计算 Sec-WebSocket-Accept
→ 发送 HTTP 101 响应
→ c->pfn = mg_ws_cb
→ c->is_websocket = 1
→ 触发 MG_EV_WS_OPEN
WebSocket 通信阶段:
MG_EV_WS_MSG → 文本/二进制消息到达
MG_EV_WS_CTL → 控制帧到达Ping/Pong/Close
MG_EV_READ → 原始数据到达ws_cb 内部处理)
MG_EV_WRITE → 数据写入完成
连接关闭:
MG_EV_CLOSE → 连接关闭
```
## 四、握手过程详解 (`mg_ws_upgrade`)
### 4.1 函数签名ws.c 第 269 行)
```c
void mg_ws_upgrade(struct mg_connection *c, struct mg_http_message *hm,
const char *fmt, ...);
```
### 4.2 内部实现
```
mg_ws_upgrade():
1. 从 HTTP 头部提取 "Sec-WebSocket-Key"
→ 如果不存在,返回 426 Upgrade Required 错误
2. 可选:提取 "Sec-WebSocket-Protocol"(子协议协商)
3. 调用 ws_handshake() 生成握手响应
4. 设置 c->pfn = mg_ws_cb切换协议处理器
5. 设置 c->is_websocket = 1
6. 设置 c->is_resp = 0标记响应完成
7. 触发 MG_EV_WS_OPEN 事件
```
### 4.3 握手计算 (`ws_handshake`, 第 35 行)
```c
static void ws_handshake(struct mg_connection *c, const struct mg_str *wskey,
const struct mg_str *wsproto, const char *fmt,
va_list *ap) {
const char *magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // RFC 6455 魔数
unsigned char sha[20], b64_sha[30];
// 1. SHA1(client_key + magic)
mg_sha1_ctx sha_ctx;
mg_sha1_init(&sha_ctx);
mg_sha1_update(&sha_ctx, (unsigned char *) wskey->buf, wskey->len);
mg_sha1_update(&sha_ctx, (unsigned char *) magic, 36);
mg_sha1_final(sha, &sha_ctx);
// 2. Base64 编码 SHA1 结果
mg_base64_encode(sha, sizeof(sha), (char *) b64_sha, sizeof(b64_sha));
// 3. 构建 HTTP 101 响应
mg_xprintf(mg_pfn_iobuf, &c->send,
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: %s\r\n",
b64_sha);
// 4. 添加用户自定义响应头(通过 fmt 参数)
if (fmt != NULL) mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, &ap);
// 5. 可选的子协议响应头
if (wsproto != NULL) {
mg_printf(c, "Sec-WebSocket-Protocol: %.*s\r\n", ...);
}
// 6. 结束响应头
mg_send(c, "\r\n", 2);
}
```
**关键常量**:魔数 `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` 是 RFC 6455 第 4.2.2 节规定的固定值,用于防止跨协议攻击。
### 4.4 握手时的进阶用法
**添加自定义响应头**(如 Cookie、Token 等):
```c
mg_ws_upgrade(c, hm, "Set-Cookie: token=%s\r\nX-User: %s\r\n", token, username);
```
**子协议协商**
```c
struct mg_str *proto = mg_http_get_header(hm, "Sec-WebSocket-Protocol");
// Mongoose 会自动回显匹配的协议,也可手动处理
mg_ws_upgrade(c, hm, NULL);
```
## 五、帧格式详解
### 5.1 RFC 6455 帧结构
```
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
```
### 5.2 Mongoose 的帧头构建 (`mkhdr`, 第 96 行)
```c
static size_t mkhdr(size_t len, int op, bool is_client, uint8_t *buf) {
size_t n = 0;
buf[0] = (uint8_t) (op | 128); // FIN=1, Opcode=op
if (len < 126) { // 7位长度足够
buf[1] = (unsigned char) len;
n = 2;
} else if (len < 65536) { // 16位扩展长度
uint16_t tmp = mg_htons((uint16_t) len);
buf[1] = 126;
memcpy(&buf[2], &tmp, sizeof(tmp));
n = 4;
} else { // 64位扩展长度
buf[1] = 127;
// 先写高32位再写低32位
tmp = mg_htonl((uint32_t) (((uint64_t) len) >> 32));
memcpy(&buf[2], &tmp, sizeof(tmp));
tmp = mg_htonl((uint32_t) (len & 0xffffffffU));
memcpy(&buf[6], &tmp, sizeof(tmp));
n = 10;
}
// 客户端帧需要掩码
if (is_client) {
buf[1] |= 1 << 7; // 设置 MASK
mg_random(&buf[n], 4); // 生成随机掩码 key
n += 4;
}
return n;
}
```
**注意****服务端发送的帧不需要掩码**RFC 6455 第 5.1 节。只有客户端发送到服务端的帧才需要掩码。Mongoose 通过 `is_client` 标志来控制。
### 5.3 帧解析 (`ws_process`, 第 66 行)
```c
static size_t ws_process(uint8_t *buf, size_t len, struct ws_msg *msg) {
memset(msg, 0, sizeof(*msg));
if (len >= 2) {
n = buf[1] & 0x7f; // 7位载荷长度
mask_len = buf[1] & 128 ? 4 : 0; // MASK 位 → 掩码 key 长度
msg->flags = buf[0];
if (n < 126 && len >= mask_len) {
msg->data_len = n;
msg->header_len = 2 + mask_len;
} else if (n == 126 && len >= 4 + mask_len) {
msg->header_len = 4 + mask_len;
msg->data_len = (((size_t) buf[2]) << 8) | buf[3]; // 16位长度
} else if (len >= 10 + mask_len) {
msg->header_len = 10 + mask_len;
msg->data_len = be32(buf+2)<<32 | be32(buf+6); // 64位长度
}
}
// 安全检查:数据长度不能超过 1GB
if (msg->data_len > 1024 * 1024 * 1024) return 0;
if (msg->header_len + msg->data_len > len) return 0; // 数据不完整
// 如果有掩码,解码
if (mask_len > 0) {
uint8_t *p = buf + msg->header_len, *m = p - mask_len;
for (i = 0; i < msg->data_len; i++) p[i] ^= m[i & 3]; // XOR 解码
}
return msg->header_len + msg->data_len;
}
```
### 5.4 掩码处理
**为什么需要掩码**RFC 6455 要求客户端发往服务端的所有帧必须掩码。这是为了防止"缓存投毒攻击"——恶意脚本通过浏览器发送精心构造的 WebSocket 帧,可能被中间代理缓存误解为 HTTP 请求。
**解码算法**(异或):
```
for (i = 0; i < data_len; i++)
payload[i] = payload[i] ^ masking_key[i % 4]
```
**发送时的掩码**`mg_ws_mask`, ws.c 第 124 行):
```c
static void mg_ws_mask(struct mg_connection *c, size_t len) {
if (c->is_client && c->send.buf != NULL) {
uint8_t *p = c->send.buf + c->send.len - len, *mask = p - 4;
for (i = 0; i < len; i++) p[i] ^= mask[i & 3];
}
}
```
只在客户端模式(`c->is_client == true`)时对数据执行掩码。服务端不需要。
## 六、协议处理器 `mg_ws_cb` 详解ws.c 第 166 行)
这是 WebSocket 连接的核心处理器,注册为 `c->pfn`,处理所有接收到的帧:
```
mg_ws_cb 处理流程MG_EV_READ 事件时):
1. 客户端模式:检查握手是否完成
→ 未完成:调用 mg_ws_client_handshake() 验证 HTTP 101 响应
→ 完成:继续解析帧
2. 循环解析帧:
while (ws_process() 成功解析一帧) {
3. 根据操作码分类处理:
┌─ WEBSOCKET_OP_CONTINUE (0):
│ 分片帧 → 触发 MG_EV_WS_CTL
├─ WEBSOCKET_OP_TEXT (1) / WEBSOCKET_OP_BINARY (2):
│ 如果 FIN=1 (完整帧):
│ → 触发 MG_EV_WS_MSG用户在此接收消息
│ 如果 FIN=0 (分片开始):
│ → 不触发事件(等待后续帧组装)
├─ WEBSOCKET_OP_CLOSE (8):
│ → 触发 MG_EV_WS_CTL
│ → 回显 CLOSE 帧给对端
│ → 设置 c->is_draining = 1优雅关闭
├─ WEBSOCKET_OP_PING (9):
│ → 自动回复 PONG
│ → 触发 MG_EV_WS_CTL通知用户
├─ WEBSOCKET_OP_PONG (10):
│ → 触发 MG_EV_WS_CTL用户可检测心跳响应
└─ 未知操作码:
→ mg_error() 关闭连接
4. 分片帧处理ws.c 第 215-233 行):
如果 FIN=0 或 op=CONTINUE:
→ 第一条分片帧:保留 1 字节 op 标记
→ 后续帧:剥离帧头,保留数据
→ 所有分片数据累积在 c->recv 中
如果 FIN=1 且 op=CONTINUE:
→ 分片结束,触发 MG_EV_WS_MSG
→ 从 c->recv 中删除完整消息
}
```
### 分片帧的处理细节
Mongoose 支持分片帧的自动组装:
```
客户端发送三条分片帧:
Frame 1: FIN=0, op=TEXT, data="Hello "
Frame 2: FIN=0, op=CONTINUE, data="World"
Frame 3: FIN=1, op=CONTINUE, data="!"
Mongoose 内部处理:
1. 收到 Frame 1:
→ 保留 flags 在 c->recv 中 (buf[0]=0x01 TEXT)
→ 剥离帧头,数据变为 "\x01Hello "
→ ofs 追踪到数据末尾
2. 收到 Frame 2:
→ 剥离帧头,数据追加 → "\x01Hello World"
→ ofs 更新
3. 收到 Frame 3:
→ 剥离帧头,数据追加 → "\x01Hello World!"
→ FIN=1, op=CONTINUE: 触发 MG_EV_WS_MSG
→ m.flags = c->recv.buf[0] (TEXT)
→ m.data = "Hello World!" (跳过第1字节的标记)
→ 删除已处理数据
```
## 七、发送函数详解
### 7.1 `mg_ws_send()` — 发送一条完整消息(第 132 行)
```c
size_t mg_ws_send(struct mg_connection *c, const void *buf, size_t len, int op);
```
流程:
1. 调用 `mkhdr()` 构建帧头
2. 发送帧头(通过 `mg_send` 写入 `c->send` 缓冲区)
3. 发送数据
4. 如果是客户端模式 → 对数据执行掩码
```c
// 使用示例
mg_ws_send(c, "hello", 5, WEBSOCKET_OP_TEXT); // 发送文本
mg_ws_send(c, data, len, WEBSOCKET_OP_BINARY); // 发送二进制
mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING); // 发送 Ping
```
### 7.2 `mg_ws_printf()` — 格式化发送(第 26 行)
```c
size_t mg_ws_printf(struct mg_connection *c, int op, const char *fmt, ...);
// 使用示例
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{\"temp\":%.2f,\"unit\":\"%s\"}", 23.5, "C");
```
内部调用 `mg_vxprintf()` 格式化到 `c->send`,然后调用 `mg_ws_wrap()` 添加帧头。
### 7.3 `mg_ws_wrap()` — 为已有数据添加帧头(第 289 行)
```c
size_t mg_ws_wrap(struct mg_connection *c, size_t len, int op);
```
这个是内部函数,用于为已经在 `c->send` 中的数据添加 WebSocket 帧头。适用场景:先写入 JSON 数据到 send 缓冲区,再包装成 WS 帧。
```c
// mg_ws_printf 的内部流程:
c->send.len → mg_vxprintf 写入 JSON 数据
→ mg_ws_wrap 在前面插入帧头
→ mg_ws_mask 如果是客户端则掩码
```
## 八、服务端完整事件处理最佳实践
### 8.1 标准事件处理模板
```c
static void fn(struct mg_connection *c, int ev, void *ev_data) {
// 1. TLS 初始化(如果需要 WSS
if (ev == MG_EV_ACCEPT) {
struct mg_tls_opts opts = {
.cert = mg_str(s_cert_pem),
.key = mg_str(s_key_pem),
};
mg_tls_init(c, &opts);
}
// 2. WebSocket 握手
if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
struct mg_str *ws_key = mg_http_get_header(hm, "Sec-WebSocket-Key");
if (ws_key != NULL) {
// 可以在此做认证
// struct mg_str *token = mg_http_get_header(hm, "Authorization");
mg_ws_upgrade(c, hm, NULL); // 升级到 WebSocket
} else {
mg_http_reply(c, 200, "", "Use WebSocket\n");
}
}
// 3. WebSocket 连接建立
if (ev == MG_EV_WS_OPEN) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
// hm 是发起升级的原始 HTTP 请求,可以获取 URI、Cookie 等
MG_INFO(("WebSocket connected, URI: %.*s", hm->uri.len, hm->uri.buf));
}
// 4. 接收 WebSocket 消息
if (ev == MG_EV_WS_MSG) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
uint8_t op = wm->flags & 15;
if (op == WEBSOCKET_OP_TEXT) {
// 文本消息
MG_INFO(("TEXT: %.*s", wm->data.len, wm->data.buf));
// 回显
mg_ws_send(c, wm->data.buf, wm->data.len, WEBSOCKET_OP_TEXT);
} else if (op == WEBSOCKET_OP_BINARY) {
// 二进制消息
MG_INFO(("BINARY: %zu bytes", wm->data.len));
// 处理二进制数据
}
}
// 5. 控制帧通知
if (ev == MG_EV_WS_CTL) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
uint8_t op = wm->flags & 15;
if (op == WEBSOCKET_OP_PING) {
MG_DEBUG(("Ping received"));
// Ping 已被 Mongoose 自动回复 Pong
} else if (op == WEBSOCKET_OP_PONG) {
MG_DEBUG(("Pong received"));
// 可用于检测客户端存活
} else if (op == WEBSOCKET_OP_CLOSE) {
MG_INFO(("Client sent close"));
}
}
// 6. 连接关闭
if (ev == MG_EV_CLOSE) {
MG_INFO(("WebSocket disconnected"));
}
}
```
### 8.2 广播消息给多个客户端
```c
static void broadcast(struct mg_mgr *mgr, const char *msg, size_t len) {
struct mg_connection *c;
for (c = mgr->conns; c != NULL; c = c->next) {
if (c->is_websocket && !c->is_listening) { // 只发给 WS 客户端
mg_ws_send(c, msg, len, WEBSOCKET_OP_TEXT);
}
}
}
```
### 8.3 心跳检测
```c
// 定时发送 Ping 帧检测客户端是否存活
static void heartbeat_timer(void *arg) {
struct mg_mgr *mgr = (struct mg_mgr *) arg;
struct mg_connection *c;
for (c = mgr->conns; c != NULL; c = c->next) {
if (c->is_websocket && !c->is_listening) {
mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING);
}
}
}
// 在 main 中添加定时器
mg_timer_add(&mgr, 30000, MG_TIMER_REPEAT, heartbeat_timer, &mgr);
// 在事件处理器中检测 Pong
if (ev == MG_EV_WS_CTL) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
if ((wm->flags & 15) == WEBSOCKET_OP_PONG) {
// 客户端存活确认
}
}
```
## 九、服务端 vs 客户端差异总结
| 特性 | 服务端 | 客户端 |
|------|--------|--------|
| 帧掩码 | **不掩码** | **必须掩码**4 字节随机 key + XOR |
| 握手方式 | `mg_ws_upgrade()` | `mg_ws_connect()` |
| 握手角色 | 接收 `Sec-WebSocket-Key`,计算 `Accept` | 生成随机 `Key`,验证 `Accept` |
| Ping/Pong | 可主动发送 | 可主动发送 |
| 关闭帧 | 收到后回显 | 收到后回显 |
| `is_client` | `false` | `true`(由 `mg_connect` 设置) |
## 十、调试与诊断
### 启用 hex dump
```c
// 在 MG_EV_ACCEPT 或 MG_EV_WS_OPEN 中设置
c->is_hexdumping = 1;
```
这会将 WebSocket 帧的原始字节打印到日志中(通过 `mg_hexdump()`)。
### 常见问题排查
1. **客户端收到 "not http" 错误**:握手 URL 没有使用 `http://``https://` 前缀
2. **握手被拒绝**:检查 `Sec-WebSocket-Key` 是否存在URL 路径是否正确
3. **消息收到乱码**检查客户端是否正确掩码操作码是否正确TEXT=1, BINARY=2
4. **内存泄漏**:确保 `mg_mgr_poll()` 被循环调用,否则连接不会释放
## 十一、MQTT over WebSocket
Mongoose 支持 MQTT over WebSocket。当 MQTT 连接 URL 使用 `mqtt://` 方案且底层升级为 WebSocket 时MQTT 模块会自动使用 WebSocket 帧封装 MQTT 包。这是服务端常用的场景——通过 WebSocket 传输 MQTT 协议。

View File

@ -20,3 +20,37 @@
**涉及文件**`myMms_m.h`, `mms_m.h`, `mms_m.cpp`, `iec61850m.cpp`
---
### #2 libweb_server 模块缺陷修复
**问题**WebSocket 服务端模块存在 10 个缺陷含严重级别单客户端、多线程竞态、悬空指针、non-null-terminated UB和高危/中等级别(调试 printf、SBO 阻塞、LOG 格式化缺失等)。
**需求**
1. 兼容多个 WebSocket 客户端同时连接
2. 完善断连处理
3. 多线程安全保护
4. 修复 UB 和格式化问题
5. 增量推送优化(仅变化时发送)
**处理计划**[libweb_server模块分析](../工程/libweb_server模块分析.md)
**状态**:✅ 已完成
**涉及文件**`web_server.cpp`, `ws_method.cpp`
---
### #3 libweb_server 多连接资源共享冲突
**问题**:支持多客户端后,所有连接共享同一套全局信号资源(`g_ws_out_signals` 等),一个客户端的 add/del 操作会影响其他客户端的数据推送。
**需求**
1. 每个连接独立的信号资源per-connection session
2. 连接建立时自动开辟资源,断开时自动释放
3. `ws_task()` 按 session 独立构建 JSON 推送到对应连接
**处理计划**[libweb_server模块分析](../工程/libweb_server模块分析.md)
**状态**:✅ 已完成
**涉及文件**`ws_method.h`, `ws_method.cpp`, `web_server.cpp`
---

View File

@ -1,7 +1,7 @@
# 包含外部Makefile
include ./../../../linux.mk
ProjectName := libmoongoose
ProjectName := libmongoose
DIR := $(realpath $(CURDIR)/..)

View File

@ -6,7 +6,7 @@ SUBDIRS += ./libicp67
SUBDIRS += ./libmms_m
SUBDIRS += ./libmms_s
SUBDIRS += ./libmoongoose
SUBDIRS += ./libmongoose

View File

@ -16,7 +16,7 @@ MakeDirCommand := mkdir -p
# Libs := -lcom_channel -lcom_scan -liec -lself_ptl -lfunc -ltask -lcomm -lshell -lpthread
Libs := -lcom_channel -lcom_scan -liec -liec61850m -liec61850s -lself_ptl -lweb_server -ldatacenter
Libs += -l60870 -licp67 -lmms_m -lmms_s -lmoongoose
Libs += -l60870 -licp67 -lmms_m -lmms_s -lmongoose
Libs += -liec61850
Libs += -lmy_xxhash -lcmd -lmd5 -lxml -lcJSON -lcomm -ltask -lfunc
Libs += -lpthread
@ -64,12 +64,15 @@ $(OutputFile): $(ALL_OBJECTS)
$(CPP) -o $@ $^ $(LDFLAGS)
@if [ -f "$@" ]; then \
echo "Build successful: $@"; \
# 显示文件信息 \
file $@; \
# 显示链接的库 \
echo "Linked libraries:"; \
ldd $@ 2>/dev/null || echo "(static binary)"; \
fi
@if [ "$(ARCH)" = "x86" ]; then \
$(MakeDirCommand) $(TEST_DIR); \
cp $(OutputFile) $(TEST_DIR)/$(ProjectName); \
echo "Copied to $(TEST_DIR)/$(ProjectName)"; \
fi
# 重新构建
rebuild: veryclean all

View File

@ -2,6 +2,10 @@
#include "myBase.h"
void ws_recv(const char *p_rx, uint16_t rx_len);
void ws_send(const char *p_tx, uint16_t tx_len);
void ws_task(void);
struct mg_connection;
void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len);
void ws_send_all(const char *p_tx, uint16_t tx_len);
void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len);
void ws_session_destroy(struct mg_connection *c);
void ws_task(void);

View File

@ -11,25 +11,55 @@ static const char *s_web_root = "web_root";
std::string g_web_root = "";
static struct mg_mgr mgr;
static struct mg_connection *p_conn = NULL;
LOCAL std::vector<struct mg_connection *> g_ws_conns;
LOCAL pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t thread_id;
void ws_send(const char *p_tx, uint16_t tx_len)
void ws_send_all(const char *p_tx, uint16_t tx_len)
{
if(NULL == p_conn || NULL == p_tx || 0 == tx_len)
if(NULL == p_tx || 0 == tx_len)
{
LOG_E("ws_send arg not valid, p_conn:%p, p_tx:%p, tx_len:%d", p_conn, p_tx, tx_len);
LOG_E("ws_send_all arg not valid, p_tx:%p, tx_len:%d", p_tx, tx_len);
return;
}
mg_ws_send(p_conn, p_tx, tx_len, WEBSOCKET_OP_TEXT);
// LOG_I("ws_send %d bytes: %s", tx_len, p_tx);
pthread_mutex_lock(&g_ws_conns_mutex);
for(uint32_t i = 0; i < g_ws_conns.size(); i++)
{
struct mg_connection *c = g_ws_conns[i];
if(c->is_websocket && !c->is_draining)
{
mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
}
LOCAL void web_server_recv(const char *p_rx, uint16_t rx_len)
void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len)
{
if(NULL == p_tx || 0 == tx_len)
{
LOG_E("ws_send_one arg not valid, p_tx:%p, tx_len:%d", p_tx, tx_len);
return;
}
pthread_mutex_lock(&g_ws_conns_mutex);
for(uint32_t i = 0; i < g_ws_conns.size(); i++)
{
struct mg_connection *c = g_ws_conns[i];
if(c->id == conn_id && c->is_websocket && !c->is_draining)
{
mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
}
LOCAL void web_server_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len)
{
if(NULL == p_rx || 0 == rx_len)
{
@ -37,9 +67,7 @@ LOCAL void web_server_recv(const char *p_rx, uint16_t rx_len)
return;
}
// LOG_I("web_server_recv %d bytes: %s", rx_len, p_rx);
ws_recv(p_rx, rx_len);
ws_recv(c, p_rx, rx_len);
}
LOCAL void web_server_task(struct mg_connection *c, int ev, void *p_data)
@ -60,12 +88,63 @@ LOCAL void web_server_task(struct mg_connection *c, int ev, void *p_data)
}
else if(ev == MG_EV_WS_MSG)
{
p_conn = c;
struct mg_ws_message *wm = (struct mg_ws_message *) p_data;
printf("Got %d bytes: %s\n", (int) wm->data.len, wm->data.buf);
web_server_recv(wm->data.buf, (int)wm->data.len);
bool found = false;
pthread_mutex_lock(&g_ws_conns_mutex);
for(uint32_t i = 0; i < g_ws_conns.size(); i++)
{
if(g_ws_conns[i] == c)
{
found = true;
break;
}
}
if(!found)
{
g_ws_conns.push_back(c);
}
pthread_mutex_unlock(&g_ws_conns_mutex);
std::string msg(wm->data.buf, wm->data.len);
LOG_I("WS recv %zu bytes: %s", msg.size(), msg.c_str());
web_server_recv(c, msg.c_str(), (uint16_t)msg.size());
}
else if(ev == MG_EV_WS_CTL)
{
struct mg_ws_message *wm = (struct mg_ws_message *) p_data;
uint8_t op = wm->flags & 15;
if(op == WEBSOCKET_OP_CLOSE)
{
ws_session_destroy(c);
pthread_mutex_lock(&g_ws_conns_mutex);
for(uint32_t i = 0; i < g_ws_conns.size(); i++)
{
if(g_ws_conns[i] == c)
{
g_ws_conns.erase(g_ws_conns.begin() + i);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
}
}
else if(ev == MG_EV_CLOSE)
{
ws_session_destroy(c);
pthread_mutex_lock(&g_ws_conns_mutex);
for(uint32_t i = 0; i < g_ws_conns.size(); i++)
{
if(g_ws_conns[i] == c)
{
g_ws_conns.erase(g_ws_conns.begin() + i);
break;
}
}
pthread_mutex_unlock(&g_ws_conns_mutex);
}
}
@ -137,8 +216,8 @@ void *app_web_server(void *arg)
while (1)
{
task_event_recv(p_app->p_event,
EV_TIMER1 | EV_TIMER2 | EV_TIMER3,
task_event_recv(p_app->p_event,
EV_TIMER1 | EV_TIMER2 | EV_TIMER3,
TASK_EVENT_FLAG_OR | TASK_EVENT_FLAG_CLEAR,
TASK_EVENT_WAIT_FOREVER,
&event);
@ -160,7 +239,7 @@ void *app_web_server(void *arg)
ws_task();
}
}
}
}

View File

@ -3,6 +3,7 @@
#include "cJSON.h"
#include "mySystem.h"
#include "myDatacenter.h"
#include "mongoose.h"
typedef struct
{
@ -10,56 +11,62 @@ typedef struct
std::string desc;
uint8_t data_type;
uint8_t ctrl_type;
// void *p_data;
std::vector<void *> vec_p_data;
std::vector<void *> vec_p_default_data;
stru_signal_ctrl *p_ctrl;
stru_signal_param *p_param;
std::string last_val;
}stru_ws_signal;
LOCAL std::vector<stru_ws_signal> g_ws_out_signals;
LOCAL std::vector<stru_ws_signal> g_ws_in_signals;
LOCAL std::vector<stru_ws_signal> g_ws_yk_signals;
LOCAL std::vector<stru_ws_signal> g_ws_ao_signals;
LOCAL std::vector<stru_ws_signal> g_ws_param_signals;
typedef struct
{
std::vector<stru_ws_signal> out_signals;
std::vector<stru_ws_signal> in_signals;
std::vector<stru_ws_signal> yk_signals;
std::vector<stru_ws_signal> ao_signals;
std::vector<stru_ws_signal> param_signals;
}stru_ws_session;
LOCAL std::map<unsigned long, stru_ws_session> g_ws_sessions;
LOCAL pthread_mutex_t g_ws_session_mutex = PTHREAD_MUTEX_INITIALIZER;
LOCAL void add_out_signal(const std::string& saddr);
LOCAL void add_in_signal(const std::string& saddr);
LOCAL void add_yk_signal(const std::string& saddr);
LOCAL void add_ao_signal(const std::string& saddr);
LOCAL void add_param_signal(const std::string& saddr);
LOCAL void add_out_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void add_in_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void add_yk_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void add_ao_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void add_param_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void del_out_signal(const std::string& saddr);
LOCAL void del_in_signal(const std::string& saddr);
LOCAL void del_yk_signal(const std::string& saddr);
LOCAL void del_ao_signal(const std::string& saddr);
LOCAL void del_param_signal(const std::string& saddr);
LOCAL void del_out_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void del_in_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void del_yk_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void del_ao_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void del_param_signal(stru_ws_session &s, const std::string& saddr);
LOCAL void set_out_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_in_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_ao_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_out_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_in_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_yk_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_ao_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL void set_param_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
LOCAL int make_out_signal_json(cJSON *root);
LOCAL int make_in_signal_json(cJSON *root);
LOCAL int make_yk_signal_json(cJSON *root);
LOCAL int make_ao_signal_json(cJSON *root);
LOCAL int make_param_signal_json(cJSON *root);
LOCAL int make_out_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
LOCAL int make_in_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
LOCAL int make_yk_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
LOCAL int make_ao_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
LOCAL int make_param_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
typedef struct
{
void (*add_signal)(const std::string& saddr);
void (*del_signal)(const std::string& saddr);
void (*set_signal_data)(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
int (*make_signal_json)(cJSON *root);
void (*add_signal)(stru_ws_session &s, const std::string& saddr);
void (*del_signal)(stru_ws_session &s, const std::string& saddr);
void (*set_signal_data)(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
int (*make_signal_json)(stru_ws_session &s, cJSON *root, bool &has_change);
}stru_ws_signal_method;
LOCAL std::map<std::string, stru_ws_signal_method> g_ws_signal_methods_map =
LOCAL std::map<std::string, stru_ws_signal_method> g_ws_signal_methods_map =
{
{"out", {add_out_signal, del_out_signal, set_out_signal_data, make_out_signal_json}},
{"in", {add_in_signal, del_in_signal, set_in_signal_data, make_in_signal_json}},
@ -83,22 +90,21 @@ LOCAL int signal_is_exist(const std::string& saddr, const std::vector<stru_ws_si
return -1;
}
LOCAL void add_out_signal(const std::string& saddr)
LOCAL void add_out_signal(stru_ws_session &s, const std::string& saddr)
{
std::vector<stru_ws_signal> *p_signals = &g_ws_out_signals;
stru_ws_signal *p = nullptr;
std::vector<stru_ws_signal> *p_signals = &s.out_signals;
stru_ws_signal *p = nullptr;
if(0 == signal_is_exist(saddr, g_ws_out_signals))
if(0 == signal_is_exist(saddr, s.out_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
}
p_signals->reserve(p_signals->size() + 1);
p_signals->push_back({saddr, "", 0, 0});
p = &p_signals->back();
void *p_data = nullptr;
if(0 != dc_get_out_signal_info(saddr, p->desc, p->data_type, &p_data))
{
LOG_E("add_signals: dc_get_out_signal_info failed, saddr = %s", saddr.c_str());
@ -111,12 +117,12 @@ LOCAL void add_out_signal(const std::string& saddr)
p->vec_p_data.push_back(p_data);
}
LOCAL void add_in_signal(const std::string& saddr)
LOCAL void add_in_signal(stru_ws_session &s, const std::string& saddr)
{
std::vector<stru_ws_signal> *p_signals = &g_ws_in_signals;
std::vector<stru_ws_signal> *p_signals = &s.in_signals;
stru_ws_signal *p = nullptr;
if(0 == signal_is_exist(saddr, g_ws_in_signals))
if(0 == signal_is_exist(saddr, s.in_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
@ -137,16 +143,16 @@ LOCAL void add_in_signal(const std::string& saddr)
p->vec_p_data.push_back(p_data);
}
LOCAL void add_yk_signal(const std::string& saddr)
LOCAL void add_yk_signal(stru_ws_session &s, const std::string& saddr)
{
std::vector<stru_ws_signal> *p_signals = &g_ws_yk_signals;
std::vector<stru_ws_signal> *p_signals = &s.yk_signals;
stru_ws_signal *p = nullptr;
if(0 == signal_is_exist(saddr, g_ws_yk_signals))
if(0 == signal_is_exist(saddr, s.yk_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
}
}
p_signals->push_back({saddr, "", 0, 0});
p_signals->back().p_ctrl = new stru_signal_ctrl;
@ -177,15 +183,15 @@ LOCAL void add_yk_signal(const std::string& saddr)
p->vec_p_default_data.clear();
p_signals->pop_back();
return;
}
}
}
LOCAL void add_ao_signal(const std::string& saddr)
LOCAL void add_ao_signal(stru_ws_session &s, const std::string& saddr)
{
std::vector<stru_ws_signal> *p_signals = &g_ws_ao_signals;
std::vector<stru_ws_signal> *p_signals = &s.ao_signals;
stru_ws_signal *p = nullptr;
if(0 == signal_is_exist(saddr, g_ws_ao_signals))
if(0 == signal_is_exist(saddr, s.ao_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
@ -212,7 +218,7 @@ LOCAL void add_ao_signal(const std::string& saddr)
}
p->vec_p_data.push_back(p_data);
p->vec_p_default_data.push_back(p_default_data);
p->p_ctrl->type = p->ctrl_type;
p->p_ctrl->step = SIGNAL_CTRL_STEP::READY;
p->p_ctrl->data_type = p->data_type;
@ -227,15 +233,15 @@ LOCAL void add_ao_signal(const std::string& saddr)
p->vec_p_default_data.clear();
p_signals->pop_back();
return;
}
}
}
LOCAL void add_param_signal(const std::string& saddr)
LOCAL void add_param_signal(stru_ws_session &s, const std::string& saddr)
{
std::vector<stru_ws_signal> *p_signals = &g_ws_param_signals;
stru_ws_signal *p = nullptr;
std::vector<stru_ws_signal> *p_signals = &s.param_signals;
stru_ws_signal *p = nullptr;
if(0 == signal_is_exist(saddr, g_ws_param_signals))
if(0 == signal_is_exist(saddr, s.param_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
@ -244,7 +250,7 @@ LOCAL void add_param_signal(const std::string& saddr)
p_signals->back().p_ctrl = new stru_signal_ctrl;
p_signals->back().p_param = new stru_signal_param;
p = &p_signals->back();
if(0 != dc_get_param_signal_info(saddr, p->desc, p->data_type, p->p_param, p->ctrl_type, &p->vec_p_data, &p->vec_p_default_data))
{
LOG_E("add_signals: dc_get_param_signal_info failed, saddr = %s", saddr.c_str());
@ -293,48 +299,48 @@ LOCAL void del_signal(std::vector<stru_ws_signal> &signals, const std::string& s
p_signal->vec_p_data.clear();
p_signal->vec_p_default_data.clear();
signals.erase(signals.begin() + i);
return;
}
}
}
LOG_E("del_signal: not found saddr = %s", saddr.c_str());
LOG_E("del_signal: not found saddr = %s", saddr.c_str());
}
LOCAL void del_out_signal(const std::string& saddr)
LOCAL void del_out_signal(stru_ws_session &s, const std::string& saddr)
{
del_signal(g_ws_out_signals, saddr);
del_signal(s.out_signals, saddr);
}
LOCAL void del_in_signal(const std::string& saddr)
LOCAL void del_in_signal(stru_ws_session &s, const std::string& saddr)
{
del_signal(g_ws_in_signals, saddr);
del_signal(s.in_signals, saddr);
}
LOCAL void del_yk_signal(const std::string& saddr)
LOCAL void del_yk_signal(stru_ws_session &s, const std::string& saddr)
{
del_signal(g_ws_yk_signals, saddr);
del_signal(s.yk_signals, saddr);
}
LOCAL void del_ao_signal(const std::string& saddr)
LOCAL void del_ao_signal(stru_ws_session &s, const std::string& saddr)
{
del_signal(g_ws_ao_signals, saddr);
del_signal(s.ao_signals, saddr);
}
LOCAL void del_param_signal(const std::string& saddr)
LOCAL void del_param_signal(stru_ws_session &s, const std::string& saddr)
{
del_signal(g_ws_param_signals, saddr);
del_signal(s.param_signals, saddr);
}
LOCAL void set_out_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
LOCAL void set_out_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
uint8_t data[128] = {0};
for(uint32_t i = 0; i < g_ws_out_signals.size(); i++)
for(uint32_t i = 0; i < s.out_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_out_signals.at(i);
stru_ws_signal *p_signal = &s.out_signals.at(i);
if(p_signal->saddr == saddr)
{
dc_set_signal_val_from_str(data, p_signal->data_type, val);
@ -348,18 +354,18 @@ LOCAL void set_out_signal_data(const std::string& saddr, const uint8_t setting_z
}
}
LOCAL void set_in_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
LOCAL void set_in_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
LOG_E("signal_type is in, not support set_signal_data, saddr = %s", saddr.c_str());
}
LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
LOCAL void set_yk_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
uint8_t data[128] = {0};
for(uint32_t i = 0; i < g_ws_yk_signals.size(); i++)
for(uint32_t i = 0; i < s.yk_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_yk_signals.at(i);
stru_ws_signal *p_signal = &s.yk_signals.at(i);
if(p_signal->saddr == saddr)
{
dc_set_signal_val_from_str(data, p_signal->data_type, val);
@ -383,8 +389,7 @@ LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zo
return;
}
task_sleep_ms(1000);
MY_LOG_I("saddr %s, select value %s success !!!", saddr.c_str(), val.c_str());
LOG_I("saddr %s, select value %s success !!!", saddr.c_str(), val.c_str());
if(0 != dc_signal_yk_set_status(saddr, SIGNAL_CTRL_STEP::DIRECT, *p_signal->p_ctrl, (void *)data))
{
@ -392,12 +397,12 @@ LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zo
return;
}
MY_LOG_I("saddr %s, direct value %s success !!!", saddr.c_str(), val.c_str());
LOG_I("saddr %s, direct value %s success !!!", saddr.c_str(), val.c_str());
return;
}
else
{
LOG_E("p_signal->ctrl_type is invalid, p_signal->ctrl_type = %d");
LOG_E("p_signal->ctrl_type is invalid, p_signal->ctrl_type = %d", p_signal->ctrl_type);
return;
}
}
@ -405,13 +410,13 @@ LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zo
}
LOCAL void set_ao_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
LOCAL void set_ao_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
uint8_t data[128] = {0};
for(uint32_t i = 0; i < g_ws_ao_signals.size(); i++)
for(uint32_t i = 0; i < s.ao_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_ao_signals.at(i);
stru_ws_signal *p_signal = &s.ao_signals.at(i);
if(p_signal->saddr == saddr)
{
dc_set_signal_val_from_str(data, p_signal->data_type, val);
@ -437,7 +442,7 @@ LOCAL void set_ao_signal_data(const std::string& saddr, const uint8_t setting_zo
}
LOG_I("saddr %s, select, value %s success !!!", saddr.c_str(), val.c_str());
if(0 != dc_signal_ao_set_val(saddr, SIGNAL_CTRL_STEP::DIRECT, *p_signal->p_ctrl, (void *)data))
{
LOG_E("set_signal_data: dc_signal_ao_set_val direct failed, saddr = %s, val = %s", saddr.c_str(), val.c_str());
@ -456,17 +461,17 @@ LOCAL void set_ao_signal_data(const std::string& saddr, const uint8_t setting_zo
}
}
LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
LOCAL void set_param_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
uint8_t data[128] = {0};
for(uint32_t i = 0; i < g_ws_param_signals.size(); i++)
for(uint32_t i = 0; i < s.param_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_param_signals.at(i);
stru_ws_signal *p_signal = &s.param_signals.at(i);
if(p_signal->saddr == saddr)
{
dc_set_signal_val_from_str(data, p_signal->data_type, val);
if(p_signal->ctrl_type == SIGNAL_CTRL_TYPE::DIRECT_NORMAL)
{
if(0 != dc_signal_param_set_val(saddr, SIGNAL_CTRL_STEP::DIRECT, *p_signal->p_ctrl, setting_zone, (void *)data))
@ -499,7 +504,7 @@ LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting
}
else
{
LOG_E("p_signal->ctrl_type is invalid, p_signal->ctrl_type = %d");
LOG_E("p_signal->ctrl_type is invalid, p_signal->ctrl_type = %d", p_signal->ctrl_type);
return;
}
}
@ -508,7 +513,7 @@ LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting
void ws_recv(const char* p_rx, uint16_t rx_len)
void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
{
if(nullptr == p_rx || 0 == rx_len)
{
@ -522,14 +527,14 @@ void ws_recv(const char* p_rx, uint16_t rx_len)
LOG_E("ws_recv: cJSON_Parse failed, p_rx = %s", p_rx);
return;
}
if(!cJSON_IsObject(root))
{
LOG_E("ws_recv: root is not an object, p_rx = %s", p_rx);
cJSON_Delete(root);
return;
}
cJSON *saddr = cJSON_GetObjectItem(root, "saddr");
cJSON *signal_type = cJSON_GetObjectItem(root, "signal_type");
cJSON *signal_data = cJSON_GetObjectItem(root, "signal_data");
@ -570,7 +575,7 @@ void ws_recv(const char* p_rx, uint16_t rx_len)
LOG_E("ws_recv: saddr is empty, p_rx = %s", p_rx);
return;
}
if(signal_type_str.empty())
{
LOG_E("ws_recv: signal_type is empty, p_rx = %s", p_rx);
@ -589,32 +594,37 @@ void ws_recv(const char* p_rx, uint16_t rx_len)
return;
}
pthread_mutex_lock(&g_ws_session_mutex);
stru_ws_session &s = g_ws_sessions[c->id];
stru_ws_signal_method method = g_ws_signal_methods_map[signal_type_str];
if(0 == curd_str.compare("add"))
{
method.add_signal(saddr_str);
method.add_signal(s, saddr_str);
}
else if(0 == curd_str.compare("del"))
{
method.del_signal(saddr_str);
method.del_signal(s, saddr_str);
}
else if(0 == curd_str.compare("set"))
{
method.set_signal_data(saddr_str, atoi(setting_zone_str.c_str()), signal_data_str);
method.set_signal_data(s, saddr_str, atoi(setting_zone_str.c_str()), signal_data_str);
}
pthread_mutex_unlock(&g_ws_session_mutex);
}
LOCAL int make_out_signal_json(cJSON *root)
LOCAL int make_out_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *out_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "out", out_arr);
for(uint32_t i = 0; i < g_ws_out_signals.size(); i++)
for(uint32_t i = 0; i < s.out_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_out_signals.at(i);
stru_ws_signal *p_signal = &s.out_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@ -629,19 +639,25 @@ LOCAL int make_out_signal_json(cJSON *root)
std::string val = dc_get_signal_val(p_signal->vec_p_data[0], p_signal->data_type);
cJSON_AddItemToObject(item, "val", cJSON_CreateString(val.c_str()));
cJSON_AddItemToArray(out_arr, item);
if(val != p_signal->last_val)
{
p_signal->last_val = val;
has_change = true;
}
}
return 0;
}
LOCAL int make_in_signal_json(cJSON *root)
LOCAL int make_in_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *in_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "in", in_arr);
for(uint32_t i = 0; i < g_ws_in_signals.size(); i++)
for(uint32_t i = 0; i < s.in_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_in_signals.at(i);
stru_ws_signal *p_signal = &s.in_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@ -655,18 +671,24 @@ LOCAL int make_in_signal_json(cJSON *root)
std::string val = dc_get_signal_val(p_signal->vec_p_data[0], p_signal->data_type);
cJSON_AddItemToObject(item, "val", cJSON_CreateString(val.c_str()));
cJSON_AddItemToArray(in_arr, item);
if(val != p_signal->last_val)
{
p_signal->last_val = val;
has_change = true;
}
}
return 0;
}
LOCAL int make_yk_signal_json(cJSON *root)
LOCAL int make_yk_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *yk_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "yk", yk_arr);
for(uint32_t i = 0; i < g_ws_yk_signals.size(); i++)
for(uint32_t i = 0; i < s.yk_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_yk_signals.at(i);
stru_ws_signal *p_signal = &s.yk_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@ -681,19 +703,25 @@ LOCAL int make_yk_signal_json(cJSON *root)
cJSON_AddItemToObject(item, "val", cJSON_CreateString(val.c_str()));
cJSON_AddItemToObject(item, "ctrl_type", cJSON_CreateNumber(p_signal->ctrl_type));
cJSON_AddItemToArray(yk_arr, item);
if(val != p_signal->last_val)
{
p_signal->last_val = val;
has_change = true;
}
}
return 0;
}
LOCAL int make_ao_signal_json(cJSON *root)
LOCAL int make_ao_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *ao_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "ao", ao_arr);
for(uint32_t i = 0; i < g_ws_ao_signals.size(); i++)
for(uint32_t i = 0; i < s.ao_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_ao_signals.at(i);
stru_ws_signal *p_signal = &s.ao_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@ -719,19 +747,25 @@ LOCAL int make_ao_signal_json(cJSON *root)
cJSON_AddItemToObject(item, "default_val", cJSON_CreateString(default_val.c_str()));
cJSON_AddItemToArray(ao_arr, item);
if(val != p_signal->last_val)
{
p_signal->last_val = val;
has_change = true;
}
}
return 0;
}
LOCAL int make_param_signal_json(cJSON *root)
LOCAL int make_param_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *param_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "param", param_arr);
for(uint32_t i = 0; i < g_ws_param_signals.size(); i++)
for(uint32_t i = 0; i < s.param_signals.size(); i++)
{
stru_ws_signal *p_signal = &g_ws_param_signals.at(i);
stru_ws_signal *p_signal = &s.param_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@ -782,44 +816,87 @@ LOCAL int make_param_signal_json(cJSON *root)
void ws_task()
{
if(g_ws_out_signals.empty() && g_ws_in_signals.empty() && g_ws_yk_signals.empty() && g_ws_ao_signals.empty() && g_ws_param_signals.empty())
{
return;
}
std::vector<std::string> signal_type_list = {"out", "in", "yk", "ao", "param"};
cJSON *root = cJSON_CreateObject();
if(nullptr == root)
{
LOG_E("ws_task: cJSON_CreateObject failed");
return;
}
pthread_mutex_lock(&g_ws_session_mutex);
for(uint32_t i = 0; i < signal_type_list.size(); i++)
for(auto &pair : g_ws_sessions)
{
if(g_ws_signal_methods_map.find(signal_type_list[i]) != g_ws_signal_methods_map.end())
unsigned long conn_id = pair.first;
stru_ws_session &s = pair.second;
if(s.out_signals.empty() && s.in_signals.empty() && s.yk_signals.empty() && s.ao_signals.empty() && s.param_signals.empty())
{
if(0 != g_ws_signal_methods_map[signal_type_list[i]].make_signal_json(root))
{
cJSON_Delete(root);
continue;
}
LOG_E("ws_task: make_signal_json %s failed", signal_type_list[i].c_str());
return;
cJSON *root = cJSON_CreateObject();
if(nullptr == root)
{
LOG_E("ws_task: cJSON_CreateObject failed");
continue;
}
bool has_change = false;
for(uint32_t i = 0; i < signal_type_list.size(); i++)
{
if(g_ws_signal_methods_map.find(signal_type_list[i]) != g_ws_signal_methods_map.end())
{
if(0 != g_ws_signal_methods_map[signal_type_list[i]].make_signal_json(s, root, has_change))
{
cJSON_Delete(root);
LOG_E("ws_task: make_signal_json %s failed", signal_type_list[i].c_str());
root = nullptr;
break;
}
}
}
if(nullptr == root)
{
continue;
}
if(!has_change)
{
cJSON_Delete(root);
continue;
}
char *p_tx = cJSON_Print(root);
if(nullptr == p_tx)
{
LOG_E("ws_task: cJSON_Print failed");
cJSON_Delete(root);
continue;
}
ws_send_one(conn_id, p_tx, strlen(p_tx));
cJSON_Delete(root);
free(p_tx);
}
char *p_tx = cJSON_Print(root);
if(nullptr == p_tx)
pthread_mutex_unlock(&g_ws_session_mutex);
}
void ws_session_destroy(struct mg_connection *c)
{
if(nullptr == c)
{
LOG_E("ws_task: cJSON_Print failed");
cJSON_Delete(root);
return;
}
ws_send(p_tx, strlen(p_tx));
pthread_mutex_lock(&g_ws_session_mutex);
cJSON_Delete(root);
free(p_tx);
}
auto it = g_ws_sessions.find(c->id);
if(it != g_ws_sessions.end())
{
g_ws_sessions.erase(it);
}
pthread_mutex_unlock(&g_ws_session_mutex);
}