226 lines
8.2 KiB
Markdown
226 lines
8.2 KiB
Markdown
# libcomm 模块分析
|
||
|
||
**日期**: 2026-06-12
|
||
**基于源码**: `src/public/libcomm/`(8个文件,约2200行)
|
||
|
||
---
|
||
|
||
## 1. 模块定位
|
||
|
||
`libcomm` 是 RTU 的**统一通信抽象层**,位于公共库层。它将 TCP/UART/UDP 三种通信方式包装为统一的 C 接口,上层模块只需通过 `comm_id` 句柄操作连接、收发数据,不感知底层传输差异。
|
||
|
||
```
|
||
上层(com_channel / icp67)
|
||
↓ comm_create / comm_send / comm_recv_register
|
||
libcomm(本模块)
|
||
↓ dispatch by CommType
|
||
TCP client / TCP server / UART / UDP client / UDP server
|
||
```
|
||
|
||
## 2. 架构设计:工厂模式 + 虚函数表
|
||
|
||
### 2.1 统一基类 `stru_comm`
|
||
|
||
```cpp
|
||
typedef struct {
|
||
CommType type;
|
||
CommInit init; // no / ok
|
||
CommDebugShow debug_show; // on / off
|
||
void *p_comm; // 指向具体子类(stru_comm_tcp/uart/udp)
|
||
int (*comm_connect)(int id);
|
||
int (*comm_disconnect)(int id);
|
||
int (*comm_run)(int id);
|
||
int (*comm_state_register)(int id, comm_state_cb cb);
|
||
int (*comm_recv_register)(int id, comm_recv_cb cb);
|
||
int (*comm_send)(int id, const char *data, uint16_t len);
|
||
} stru_comm;
|
||
```
|
||
|
||
### 2.2 子类:TCP / UART / UDP
|
||
|
||
每种通信方式有自己的结构体,包含:
|
||
|
||
| 结构体 | 特有字段 | 函数指针表 |
|
||
|--------|---------|-----------|
|
||
| `stru_comm_tcp` | sockfd, sock_listen_fd, client_fd[16], p_para | tcp_client_connect / tcp_server_connect / tcp_send / tcp_close |
|
||
| `stru_comm_uart` | uart_fd, p_para | uart_connect / uart_send / uart_close |
|
||
| `stru_comm_udp` | sockfd, p_para | udp_client_connect / udp_server_connect / udp_send / udp_close |
|
||
|
||
### 2.3 注册与管理
|
||
|
||
创建时分配 `*_create` 填充函数指针表 → `comm_create` 按 `CommType` 分派 → 全局 `g_comm_map`(map<int, stru_comm>):
|
||
|
||
```cpp
|
||
std::map<int, stru_comm> g_comm_map;
|
||
static int g_comm_id = 0;
|
||
```
|
||
|
||
所有后续操作通过 `comm_id` 查找 `g_comm_map`,再按 `type` 向下转型调用具体子类的函数指针。
|
||
|
||
## 3. TCP 实现(comm_tcp.cpp,640行)
|
||
|
||
### 3.1 客户端模式(`tcp_client_connect`)
|
||
|
||
```
|
||
while(1):
|
||
socket() → connect(非阻塞) → select(3秒超时) → SO_ERROR 检查
|
||
→ tcp_open(SO_REUSEADDR + TCP keepalive)
|
||
→ 通知 state_cb(connected)
|
||
|
||
while(sockfd 有效):
|
||
select(sockfd, 100ms超时)
|
||
→ recv → recv_cb(id, fd, data, len)
|
||
→ len <= 0 → close → 通知 state_cb(disconnected) → 外层循环重新连接
|
||
```
|
||
|
||
**特点**: 自动断线重连(5秒等待),单连接模型,非阻塞 connect 避免永久阻塞。
|
||
|
||
### 3.2 服务端模式(`tcp_server_connect`)
|
||
|
||
```
|
||
tcp_listen → socket + bind + listen
|
||
while(1):
|
||
select(listen_fd + client_fd[])
|
||
→ listen_fd 可读 → accept → 分配 client_fd[16] 空闲槽位
|
||
→ client_fd 可读 → recv → recv_cb
|
||
→ recv <= 0 → close + 通知 disconnected + 释放槽位
|
||
```
|
||
|
||
**特点**: 最多 16 个并发客户端,select 阻塞等待(无超时),每个客户端 fd 分配独立槽位。`send` 失败时自动关闭对应连接。
|
||
|
||
### 3.3 Keepalive 配置
|
||
|
||
```cpp
|
||
SO_KEEPALIVE + TCP_KEEPIDLE(30s) + TCP_KEEPINTVL(5s) + TCP_KEEPCNT(3)
|
||
// 30秒无活动 → 开始探测 → 每5秒一次 → 3次失败断开 → 最长75秒检测断线
|
||
```
|
||
|
||
### 3.4 send 实现
|
||
|
||
```cpp
|
||
send(fd, data, len, MSG_NOSIGNAL)
|
||
// 失败时: 关闭 fd → state_cb(disconnected) → 自动触发重连
|
||
```
|
||
|
||
## 4. UART 实现(comm_uart.cpp,692行)
|
||
|
||
### 4.1 初始化流程
|
||
|
||
```cpp
|
||
uart_connect:
|
||
→ open(device, O_RDWR | O_NOCTTY)
|
||
→ tcgetattr + 配置波特率/数据位/停止位/校验位
|
||
→ 原始模式: ~ICANON, ~ECHO
|
||
→ 超时: VTIME=1(100ms), VMIN=0
|
||
```
|
||
|
||
### 4.2 波特率支持
|
||
|
||
600 / 1200 / 2400 / 4800 / 9600 / 19200 / 38400 / 57600 / 115200 / 500000 / 1M / 2.5M
|
||
|
||
### 4.3 运行时循环
|
||
|
||
```cpp
|
||
while(1):
|
||
read(uart_fd, buf, 1024)
|
||
→ len > 0 → recv_cb(id, fd, buf, len)
|
||
→ uart_sleep(10ms)
|
||
```
|
||
|
||
**当前实现**: 逐次 read → 逐次回调,不做帧拼装。注释掉的旧版本有超时分帧逻辑(20ms 空闲检测 + 动态帧缓冲区),但被废弃。
|
||
|
||
### 4.4 uart_send
|
||
|
||
```cpp
|
||
write(uart_fd, tx, tx_len)
|
||
```
|
||
|
||
## 5. UDP 实现(comm_udp.cpp,298行)
|
||
|
||
### 5.1 客户端
|
||
|
||
```cpp
|
||
socket(SOCK_DGRAM) → connect 到 remote:port → state_cb(connected) → udp_run
|
||
```
|
||
|
||
### 5.2 服务端
|
||
|
||
```cpp
|
||
socket → bind local:port → state_cb(connected) → udp_run
|
||
```
|
||
|
||
### 5.3 `udp_run` 接收循环
|
||
|
||
```cpp
|
||
while(1):
|
||
recvfrom(sockfd, buf, 2048)
|
||
→ recv_cb(id, sockfd, buf, len)
|
||
→ 出错时仅 continue,不触发重连
|
||
```
|
||
|
||
### 5.4 send
|
||
|
||
```cpp
|
||
sendto(fd, data, len, remote_ip:remote_port)
|
||
// 目标和 recv 使用相同参数,服务端回包无需知道对端地址
|
||
```
|
||
|
||
## 6. fd 诊断工具(comm.cpp 尾部)
|
||
|
||
### `get_fd_type(fd)`
|
||
|
||
通过 `fstat` + `TIOCGSERIAL` 判断 fd 类型:socket / serial / pipe / file。
|
||
|
||
### `get_fd_info(fd, info)`
|
||
|
||
根据类型填充详细信息:
|
||
- socket: `getsockname` + `getpeername` → local/remote IP + port
|
||
- serial: `readlink /proc/self/fd/N` → 设备路径 + `tcgetattr` → 波特率/数据位/停止位/校验
|
||
- file: `fstat` → 文件大小 + `readlink` → 路径
|
||
|
||
## 7. 调试支持
|
||
|
||
```cpp
|
||
enum CommDebugShow { off, on };
|
||
|
||
static void comm_debug_show(str, dir, data, len):
|
||
→ 方向前缀(rx/tx) + 颜色(绿色)
|
||
→ 每 64 字节换行的 hex dump
|
||
```
|
||
|
||
## 8. 与 icp67 / com_channel 的集成
|
||
|
||
`com_channel.cpp` 通过 `myComm.h` 定义 `stru_tcp_para`、`stru_uart_para` 等参数结构体,调用 `comm_create` 创建通道实例,注册 `comm_recv_register` 和 `comm_state_register` 回调。收到的数据进入 icp67 帧解码。
|
||
|
||
---
|
||
|
||
## 9. 优点
|
||
|
||
| 优点 | 说明 |
|
||
|------|------|
|
||
| **统一接口** | 三种传输方式一视同仁,上层看不到 socket fd 和串口 fd 的区别 |
|
||
| **自动重连** | TCP 客户端断线后带 5 秒退避自动重连,无需上层管理 |
|
||
| **调试开关** | 配置级 hex dump + 彩色方向标识,定位通信问题极快 |
|
||
| **fd 诊断** | `get_fd_info` 可以运行时查看任何 fd 的完整信息(本地/远端 IP、串口参数等) |
|
||
| **TCP keepalive** | 配置了完整的心跳参数(30s idle + 3 次探测),能在 75 秒内检测 TCP 半开连接 |
|
||
| **虚函数表模式** | 通过函数指针表实现多态,避免 switch/if-else 类型检查 |
|
||
| **服务端多连接** | TCP server 支持最多 16 个并发客户端,每个客户端独立 fd |
|
||
| **UART 参数完整** | 波特率覆盖 600~2.5M,支持奇偶校验配置 |
|
||
|
||
## 10. 缺点
|
||
|
||
| 缺点 | 严重度 | 说明 |
|
||
|------|--------|------|
|
||
| **TCP 客户端空轮询** | 中 | ✅ **已修复 (2026-06-15)** — select 去掉 100ms 超时,改为永久阻塞,零 CPU 空转 |
|
||
| **stru_comm 冗余函数指针** | 中 | ✅ **已修复 (2026-06-15)** — 删除 `stru_comm` 中从未使用的 6 个函数指针字段(comm_connect/comm_disconnect/comm_run/comm_state_register/comm_recv_register/comm_send) |
|
||
| **UART send 未完整实现** | 高 | ✅ **已修复 (2026-06-15)** — 重写 comm_uart.cpp:恢复原始模式(~ICANON/~ECHO/~OPOST)、VTIME=1 读超时、tcflush+tcdrain 完整发送 |
|
||
| **无内存释放** | 中 | ✅ **已修复 (2026-06-15)** — 新增 `comm_destroy(id)` API,释放 `p_comm` 内存并从 `g_comm_map` 移除 |
|
||
| **TCP 服务端阻塞 select** | 低 | select 无超时,服务端空闲时线程永久阻塞,若需同时处理定时任务则无法在同一线程 |
|
||
| **UDP 接收无超时** | 中 | ✅ **已修复 (2026-06-15)** — `udp_run` 中 `recvfrom` 前加 `select` 1s 超时,超时时 `continue` 继续循环 |
|
||
| **UDP close 回调 fd 错误** | 低 | ✅ **已修复 (2026-06-15)** — `udp_close` 先保存 `fd` 再 `close`,回调传入正确的旧 fd |
|
||
| **void* 类型擦除** | 中 | `p_comm` 是 `void*` 再强转为具体类型,编译器无法检测类型错误 |
|
||
| **uart_connect 直接阻塞** | 低 | 串口打开和配置在工作线程的 `while(1)` 循环中,没有与上层消息分发解耦 |
|
||
| **TCP client_fd 槽位管理粗糙** | 低 | 最多 16 个客户端,超出直接拒绝,没有等待队列或优雅降级 |
|
||
| **无流量控制** | 低 | send 没有检查 socket 缓冲区余量,高负载下可能 EAGAIN |
|
||
| **日志宏不统一** | 低 | 混用 `LOG_E`(C++ 风格)和 `MY_LOG_E`,部分日志无模块前缀 |
|