把设备从串口换成 TCP,并不会自动获得消息边界、请求关联、心跳或重连语义。TCP 提供可靠、有序的双向字节流;设备应用协议仍要定义一条消息如何编码,以及连接中断后业务处于什么状态。
1. TCP 保证什么,不保证什么
RFC 9293 定义的 TCP 关键性质包括可靠、有序、字节流传输。它不保证:
- 一次
Send对应对端一次Receive; - 应用命令执行且持久化;
- 连接空闲时设备一定仍在线;
- 自动区分请求、响应和设备事件;
- 断线重连后延续旧会话状态。
“没有丢字节”与“业务操作成功”是两个层次。
2. 在字节流上建立帧
TCP 接收端必须复用本系列《设备通信与协议编程(三):二进制协议帧、消息边界与版本兼容》建立的增量解析模型。长度前缀是常见选择:先读固定头、验证长度上限,再累积载荷。不要用 NetworkStream.DataAvailable 判断消息结束;它只表示此刻是否有可立即读取的数据。
同样不要假设 ReadAsync 会填满缓冲区。需要固定长度时应循环读取:
| |
.NET 的 Stream.ReadExactlyAsync 已提供类似能力;手写版本用于说明循环读取的不变量。
3. 连接生命周期
| |
TcpClient.Connected 只反映最近一次 I/O 已知的状态,不能作为当前在线证明。真正发现失效通常依赖读写结果、协议心跳或业务超时。
4. 请求/响应协调器
若协议支持并发请求,每条请求应携带事务 ID(例如第 3 篇帧结构中的 Sequence 字段)。客户端维护:
| |
接收循环解析完整响应后完成对应任务;设备事件则进入独立事件通道。实现时要保证:
- 注册等待项发生在发送前,避免响应过快造成竞态;
- 超时、取消、断线都会从表中移除等待项;
- 事务 ID 有界且可回绕,不与在途请求重复;
- 未知或重复响应会记录并丢弃,不完成错误请求;
- 断线时一次性失败当前连接世代的全部等待项。
不支持事务 ID 的协议通常只能串行执行请求。
5. 发送也需要单一所有者
多个任务同时向同一 NetworkStream 写入时,即使每次写都成功,两个应用帧仍可能在调用层交错。用发送队列或锁保证“一帧编码完成后整体写入”。大帧要考虑背压,不要让无限队列吞掉内存。
高频小消息是否启用 Nagle 算法要通过协议时延和吞吐测试决定。NoDelay = true 不是设备通信的通用必选项。Nagle 与 TCP_NODELAY 的机制与参数详见《TCP/IP(九):TCP 进阶——SACK、Nagle、keepalive 与端口复用》。
6. 超时分层
至少区分:
| 超时 | 含义 |
|---|---|
| 连接超时 | TCP 建连在预算内未完成 |
| 写入超时 | 数据未能在预算内交给传输栈 |
| 首字节/完整帧超时 | 对端未及时开始或完成响应 |
| 业务超时 | 设备操作本身未在预期时间完成 |
| 空闲心跳超时 | 长期无业务时检测会话可用性 |
一个总的 Timeout = 3000 无法告诉运维究竟慢在哪里。超时预算还应由上层截止时间向下传递,避免每层各等三秒。
7. TCP keepalive 与应用心跳
TCP keepalive 是实现可选的连接探测机制,RFC 9293 要求默认关闭且默认探测间隔不得短于两小时。操作系统允许调整时,它仍主要判断传输连接,不了解设备业务线程是否卡死。keepalive 机制与参数调优同样见《TCP/IP(九)》。
应用心跳可以携带协议版本、设备状态和会话世代,但会增加负载。若正常业务本就持续往返,可直接以业务响应作为活性证据,避免重复心跳。
8. 安全边界
原始 TCP 不提供机密性和对端身份。跨越不可信网络时应使用 TLS、VPN 或协议规定的安全层,并验证证书和设备身份。不要自创“异或加密”。
即使在隔离网络,解析器仍应限制帧长、并发请求数、发送队列和日志载荷,防止故障设备拖垮上位机。
9. 小结
TCP 解决可靠有序字节流,设备协议负责消息、事务和业务结果。稳定客户端需要唯一接收循环、明确帧边界、可关联的请求响应、有界发送队列和分层超时。下一篇将在此基础上处理断线、迟到响应和“操作到底执行了没有”的灰色状态。