8.2 KiB
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
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>):
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 配置
SO_KEEPALIVE + TCP_KEEPIDLE(30s) + TCP_KEEPINTVL(5s) + TCP_KEEPCNT(3)
// 30秒无活动 → 开始探测 → 每5秒一次 → 3次失败断开 → 最长75秒检测断线
3.4 send 实现
send(fd, data, len, MSG_NOSIGNAL)
// 失败时: 关闭 fd → state_cb(disconnected) → 自动触发重连
4. UART 实现(comm_uart.cpp,692行)
4.1 初始化流程
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 运行时循环
while(1):
read(uart_fd, buf, 1024)
→ len > 0 → recv_cb(id, fd, buf, len)
→ uart_sleep(10ms)
当前实现: 逐次 read → 逐次回调,不做帧拼装。注释掉的旧版本有超时分帧逻辑(20ms 空闲检测 + 动态帧缓冲区),但被废弃。
4.4 uart_send
write(uart_fd, tx, tx_len)
5. UDP 实现(comm_udp.cpp,298行)
5.1 客户端
socket(SOCK_DGRAM) → connect 到 remote:port → state_cb(connected) → udp_run
5.2 服务端
socket → bind local:port → state_cb(connected) → udp_run
5.3 udp_run 接收循环
while(1):
recvfrom(sockfd, buf, 2048)
→ recv_cb(id, sockfd, buf, len)
→ 出错时仅 continue,不触发重连
5.4 send
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. 调试支持
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,部分日志无模块前缀 |