尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

多协议断线重连与断点续传机制在以太网温湿度采集终端中的设计与实现

多协议断线重连与断点续传机制在以太网温湿度采集终端中的设计与实现 做以太网温湿度采集这种项目大家往往都觉得传感器选型、精度标定才是重头戏等真正把一个设备丢到现场跑起来才发现通讯链路的稳定性比采样本身难搞十倍。我去年做了一套基于以太网的温湿度采集终端要同时对接 PLC 的 Modbus TCP、云平台的 MQTT还有本地监控用的私有 TCP 协议中间还要对付现场随时可能出现的交换机重启、网线被叉车压断、上位机程序升级导致的服务端短暂离线。项目标题里提到的多协议断线重连与断点续传机制说白了就是要解决两件事链路断了之后怎么又快又稳地恢复以及断线期间采集到的数据怎么既不丢也不乱地补发给上位机。这两个问题看着不大但把协议的差异、嵌入式资源限制、现场网络环境的不确定性全叠在一起坑是一串接一串。这篇文章就把这套机制从框架设计到具体实现完整拆开讲一遍适合正在做类似采集终端或者工控上位机通讯的朋友参考。1. 项目整体设计与方案选型1.1 通讯架构为什么选定以太网做温湿度采集可选的路有很多串口 RS485、CAN、LoRa、Wi-Fi 都有各自的适用场景。这套设备最终敲定以太网是因为现场本身已经有一套工业交换机组成的网络PLC、上位机、操作员站都在同一个局域网里新增采集设备不需要额外布线和频率审批插上交换机就能用。从技术指标上看以太网的优势也很具体。RS485 在 9600 波特率下实测吞吐也就 1KB/s 左右而哪怕是很普通的 100M 工业以太网收发器实际跑数据时链路利用率百分之一都用不到富余量极大。这意味着温湿度采样周期可以压到 5 秒一次每次上报还能把历史数据做批量上传通讯瓶颈几乎不存在。选以太网还有一个容易被忽视的点供电和传输距离。设备部署点是分散的标准机柜很多机柜位置距离交换机超过 100 米常规网线的理论极限也就是 100 米。后来部分点位用了光纤收发器转接或者通过二级交换机逐级下联拓扑上留足了灵活性。相比 RS485 的总线拓扑对偏置电阻和节点数量非常敏感以太网的星型级联结构在排查故障时也省心很多。1.2 多协议并存的取舍与职责划分所谓的多协议不是把三种协议平行堆在一起就行必须明确每种协议在系统中的角色和适用场景否则代码会写成一张到处打补丁的蜘蛛网。Modbus TCP主要对接 PLC 和组态软件走标准的 502 端口。PLC 作为主站轮询终端作为从站应答数据。这个协议的特点是简单、无状态、每个请求响应成对出现非常适合工业现场的实时读取需求。MQTT负责把数据推送到云端平台走物联网常用的发布订阅模式。终端作为客户端连接云端 Broker温湿度数据封装成 JSON 报文定期发布。MQTT 协议本身具备遗嘱消息和会话保持机制天然适合弱网环境。私有 TCP用于本地监控软件的高频数据流。监控端主动连上终端的 9100 端口终端周期推送原始采样帧监控端负责实时绘制曲线和报警。三个协同一开始就必须把职责切清楚Modbus TCP 解决有人要快读最近一帧的需求MQTT 解决全局汇聚和远程监控的需求而私有 TCP 则解决局域网内多看几组数据、看趋势的需求。三者在数据格式、推送频率、连接维护机制上都不一样所以要分开封装成独立的通讯任务各自维护自己的会话状态。1.3 模块划分与总体技术架构整个终端的软件架构采用 FreeRTOS 作为底层操作系统应用层按功能拆成四个任务采样任务定时读取 SHT30 传感器数据完成温度、湿度、露点计算把结果写入共享缓冲区。协议服务任务监听和响应 Modbus TCP、私有 TCP 连接请求。云端任务维护 MQTT 连接和发布数据。存储任务管理 SpiFlash 上的断点续传缓存区。任务之间通过消息队列和信号量解耦。采样任务不关心数据怎么发出去通讯任务也不关心数据是从哪个传感器来的。这个分层结构在后期的协议扩展中帮了大忙后来客户要求增加 SNMP 接口做网管系统对接我只需要新增一个任务其他部分完全不动。2. 核心链路通信协议与数据帧设计2.1 温湿度采样的任务拆分温湿度采样本身的实现不算复杂但和通讯任务对接时有很多细节。我选的是 SHT30 传感器I2C 接口测量精度温度正负 0.3 摄氏度、湿度正负 2% RH。采样任务每 5 秒唤醒一次读取传感器的温度值、湿度值同时根据这两项计算露点温度。换算得到的原始值乘以 100 取整作为整数保存避免浮点数在传输和存储中引入不必要的精度问题。采样任务执行完毕之后生成一条 32 字节的采样记录结构体定义为typedef struct { uint32_t seq; // 单调递增序号 uint32_t timestamp; // 绝对时间戳(秒) int16_t temperature; // 温度单位0.01℃ int16_t humidity; // 湿度单位0.01%RH uint16_t crc; // 校验 } sample_record_t;这里序号和时间戳都是后面的断线重连和断点续传机制的关键依赖。如果不记录序号断线期间缓存的数据重传时上位机就没有办法判断数据是否重复。如果不记录时间戳缓存的数据在上电重启后也无法正确排布时间轴。2.2 三种协议的通话格式拆解Modbus TCP的数据格式相对标准化功能码选择 03读保持寄存器即可。设备地址映射方面我把温度放在寄存器起始地址 0x0000湿度放在 0x0001露点放在 0x0002附加设备状态字放在 0x0003。每个寄存器都是 16 位温度值例如 23.45 摄氏度的整数形式是 2345上位机拿到后除以 100 即可还原。Modbus TCP 的 MBAP 头是 7 个字节包含事务处理标识符、协议标识符、长度、单元标识符。这里有一个坑MBAP 头的长度字段只包含后续字节数不同厂家实现存在差异有的会把这 7 字节也算进长度。调试时如果发现上位机读出来全是乱码先检查这个。MQTT的报文结构则完全不同每条发布都是完整的主题载荷。我定义了三个主题devices/{id}/report 用于周期上报数据devices/{id}/status 用于在线状态devices/{id}/history 用于断点续传补发的历史数据。载荷走 JSON例如{seq: 10023, ts: 1739093812, temp: 23.45, hum: 51.20}MQTT 的心跳机制是 PINGREQ/PINGRESP 报文这个协议本身自带了不需要额外定义但要特别注意 Broker 端的 session 和 clean session 标志位。如果设成 clean session1断线期间缓存在 Broker 端的离线消息会被清掉断点续传就失去了协议层面的保障。最终我选择 clean session0并且 QoS 设为 1确保至少交付一次。私有 TCP协议则是自定义的二进制分帧格式。帧头固定为 0xAA55命令字然后是长度字段和数据帧尾加 CRC16 校验。这个格式设计得跟 Modbus 完全不同因为私有协议不需要兼容太多通用工具链简单高效是首要目标。2.3 数据校验与字节序的细节处理以太网通讯中字节序问题是最容易踩的低级陷阱。Modbus TCP 明确规定使用 Big-endian大端字节序而 MQTT 的 JSON 明文不涉及字节序但嵌入式设备上的 C 语言结构体在对齐后直接发送是不可取的。我在构造报文时全部通过宏或函数逐字节填充比如:#define PUT16(buf, val) do { \ (buf)[0] (uint8_t)((val) 8); \ (buf)[1] (uint8_t)(val); \ } while(0)如果直接用结构体指针发报文不同编译器的字段对齐会导致报文长度不一致接收端解析必挂。校验方面TCP 协议本身有 CRC32 校验但那是链路层的覆盖不了应用层数据被篡改的情况。我在私有 TCP 的每帧加 CRC16采样的记录结构体也内置 CRC16 字段确保存储和传输两个环节都能自校验。CRC16 的算法直接用常见的 Modbus CRC16 多项式 0x8005一张表查完 32 字节也不到 1ms。3. 断线重连机制从检测到恢复3.1 断线检测心跳与 TCP 超时的取舍断线重连的前提是准确、快速地发现断线。TCP 本身有 KeepAlive 机制默认超时通常要两个小时这个时间在工业通讯中完全不可用。因此我在应用层每 15 秒发送一次应用层心跳报文对端必须在 3 个心跳周期内回复否则判定连接失效。以私有 TCP 协议为例心跳帧只有 6 字节帧头加命令字加时间戳对端收到后回一个同样短小的确认帧。有一点要说明这个心跳机制在 Modbus TCP 上不需要额外实现。因为 PLC 作为 Modbus 主站本身就以固定周期读取寄存器终端的响应就是最好的活性证明。如果超过两个轮询周期没有收到主站的新请求我才认为通讯链路失效。云端的 MQTT 则依赖协议自带的 PINGREQ这个间隔可以在连接参数里配置我设成 10 秒超过 60 秒没有收到 PINGRESP 就会触发重连流程。三种通道的判断逻辑虽然相似但触发条件和恢复流程完全独立避免一个通道抖动导致其他通道跟着重踩。3.2 重连策略指数退避与随机抖动断线重连最忌讳的就是固定重试间隔。之前见过很多设备断网后每秒重连一次把交换机搞成广播风暴现场其他设备全部误码。我的方案是指数退避加随机抖动首次重连等待 1 秒第二次 2 秒第三次 4 秒依次翻倍最大值封顶 32 秒。每次退避间隔上再叠加 0 到 500 毫秒的随机时间避免多台设备同时断线恢复后在同一瞬间发起连接造成连接风暴。有人可能会疑问为什么最大间隔要限制在 32 秒不能无限退避如果退避时间太长断线恢复后系统要等很久才能重新上报对温湿度这种慢变量影响不大但对报警类场景是致命的。32 秒是多次测试后的折中既不会在断线期间频繁打扰网络又能保证网络恢复后一分钟内完成自动重连。重连时还有一个状态清理的细节。旧的 socket 必须彻底关闭重复调用 close 之后还要将套接字描述符置为无效。有几次调试时发现重连后收发数据错乱追查下来就是旧 socket 没有完全回收两次连接的文件描述符发生了串线。3.3 重连状态机与业务暂停断线重连不能只是简单地把 socket 重新建立业务处理也必须与之联动。我设计了一个连接状态机包含主动连接、链路正常、链路断开、退避重试、重连成功五个状态。连接断开时立即停止向对应通道发送数据同时把发送缓冲区里的数据全部投递到断点续传存储区。这一步非常关键如果断线了还继续往 socket 里 write数据会积压在协议栈的发送缓冲区中应用层几乎没有任何阻塞感看起来写成功了但实际上对方根本没收到。这些数据随后会被底层静默丢弃是完全白费的。重连成功之后先进行协议层的握手。MQTT 发起 CONNECT等待 CONNACK私有 TCP 服务端等待客户端发来的 Hello 帧。握手完成才把状态切到链路正常之后才开始正常的周期上报。而在链路正常状态一旦心跳超时马上回到链路断开状态如此循环。状态机还额外引入了半开连接的判断。现场经常出现网线被物理拔掉但设备侧 socket 仍然显示 ESTABLISHED因为网线断开时没有 FIN 包送达。如果一个连接超过 45 秒没有可用的应用层收发活动我就主动关闭这条连接并触发重连不会傻等系统内核的 TCP 超时。4. 断点续传机制缓存、索引与重传4.1 断线期间的数据缓存组织断线重连解决的是连接恢复的问题而断点续传解决的是数据不丢的问题。断线发生后终端不是停下来不采了而是继续以 5 秒周期采样把记录写入片外 SpiFlash 缓存区。缓存区设计为环形覆盖结构大小 4MB按每条 32 字节计算能存 12 万条记录以 5 秒一条估算大约能覆盖约 7 天的数据。缓存区分成两个逻辑块状态区和数据区。状态区固定 4KB存当前写指针、当前读指针、缓存区初始化标识。数据区按扇区 4096 字节对齐方便擦写操作。需要注意 SpiFlash 有擦写次数寿命限制所以读取和写入尽量带 flash 磨损均衡频繁写入的指针参数不要固定放同一个地址我用索引表做轮换。每条记录写入前先计算 CRC16存入记录结构体的 crc 字段。补发数据时先从缓存区读出记录校验 CRC 通过后才会发送如果 CRC 不对则该条标记为无效跳过。这样有效避免了 NAND Flash 类存储可能出现的位翻转问题。4.2 时间戳与序号双索引断点续传最容易被忽略的是乱序问题。断线期间缓存的数据除了最后一条之外其余都是有序写入的但重连后补发的数据要和实时数据交错发送上位机必须能够识别哪些是补发的历史数据哪些是当前实时数据。我的方案是采用序号和时间戳双索引。每一条采样记录生成时seq 在全局单调递增时间戳则从 NTP 对时时钟获取。实时推送的帧和补发帧都携带完整的 seq 和 timestamp。上位机收到数据后可以通过 seq 判断是否有空洞通过 timestamp 判断数据的真实采样时刻不会出现补发数据和实时数据时间线错乱的问题。补发数据时按照 seq 从小到大的顺序批量发送每次最多发送 128 条记录。发送完一批后等待对端确认确认帧包含最后一条 seq例如确认帧: seq10050这代表 10050 及之前的数据都收到发送端将读指针前移继续发下一批。如果确认帧超时则重发当前批次。4.3 重传去重与缓存清理断点续传涉及两个方向的去重一是发送侧避免重复发送同一批数据二是接收侧避免重复写入同一条记录。发送侧通过确认机制解决接收侧则要求上位机根据 seq 做幂等判断。我再三强调 seq 的重要性具体到实现层面接收端只需保存最近收到的 seq 范围新数据到达时先查询范围再决定是否落库。缓存清理也有讲究。按常规思路数据补发成功之后把对应缓存区擦除释放空间即可。但如果上位机收到数据后在线库里 CRUD 失败它回一条业务层失败的确认帧终端又把数据保留。只回收发送成功的缓存是保证端到端可靠性的关键。断点续传的状态机与重连机制协同工作链路正常时读指针持续推进链路断开时读指针暂停只有写指针前进。重连成功后的首个动作不是立刻恢复实时发送而是先把缓存区里积压的数据补完补完再切到实时推送。整个过程对应用层是透明的上位机无需关心终端经历了多少次断线重连。5. 常见问题与排查技巧实录5.1 现场环境的干扰与接线问题项目调试阶段最诡异的一个现象是设备在实验室一切正常到了现场温度数值偶发跳变到 80 度、100 度。后来排查发现不是传感器的问题而是设备侧做 Modbus TCP 通信时网线在机柜里和 220V 动力线同槽走线干扰耦合进收发器。把网线换到独立线槽并在设备和交换机之间加了工业级隔离变压器跳变消失。网线压接也是一个高频翻车点。实测发现不少电工给 RJ45 水晶头压线时把线对顺序做错或者没有完全压到底导致千兆协商不成功降级到百兆。温湿度采集对带宽无感但后来扩展高速数据时才发现问题。所以现场施工验收时必须用网线测试仪每根线都测一遍不能只看指示灯亮不亮。另外一个容易忽略的是接地问题。工业现场的交换机、监控主机、采集设备如果接地电位不一致会通过网线的屏蔽层形成环路电流严重时直接烧坏网口。设备电源部分做了隔离外壳单独接地网口变压器隔离三样缺一不可。曾经遇到过一次网口反复失效换设备后恢复正常查下来就是现场地电压偏高导致的。5.2 重连风暴与缓冲区溢出最早版本的重连机制没有加抖动纯固定 3 秒重试。有一次现场断电恢复约 30 台设备同时上线交换机短时间内收到大量 TCP 连接请求MAC 地址表频繁刷新部分设备竟然出现数分钟无法正常通信。上限 32 秒的随机抖动加入之后这种问题再也没出现过。缓冲区溢出则发生在缓存区写满的场景。环形缓存的设计是写指针追上读指针时直接覆盖最老的数据。这在工程上是合理的因为超过缓存深度的历史数据本身就不应该承诺不丢。但采样线程和存储线程之间的锁竞争会导致偶发的时序错位也就是读指针和写指针短时间重叠需要加临界区保护。最终使用 FreeRTOS 互斥信号量写操作期间读操作挂起等待保证指针更新原子性。在 MQTT 通道上断线期间如果持续发布消息本来应该在 QoS1 的持久会话中排队但 Broker 端对离线消息数量也有限制。我把设备的离线消息数上限和缓存区深度做了匹配超过缓存区承载能力直接丢弃最旧的 MQTT 消息避免 Broker 内存被撑爆。5.3 容易被忽视的坑时钟同步、存储磨损与调试工具断点续传如果完全依赖本地 RTC长时间断电后时间会漂移。设备上电时先从 DHCP 获取 IP再从 NTP 服务器同步系统时间同步成功后重写最近一段缓存记录里缺少时间戳的记录。这样即使遇到 24 小时断电重启补发的数据在时间线上依然准确。存储磨损方面如果缓存区用的是 W25Q64 这类 NOR Flash擦写寿命通常标称十万次。数据以 5 秒一采、7 天一循环来算单个扇区的写入次数不算多但前提是磨损均匀算法要做对。我的做法是每次写入前先用状态区的索引表确定下一个可写扇区绕过最近擦除过的扇区。调试工具方面Wireshark 抓包是必须掌握的。Modbus TCP 报文很容易识别MBAP 头和功能码一目了然。私有 TCP 协议就要自己结合协议帧格式解析常见做法是先用 Wireshark 的 Follow TCP Stream 功能看一眼原始字节流再对照自己的帧格式做逐字节检查。在排查断线重连问题的时候重点看 TCP 的 SYN、FIN、RST 标志和重传次数这些信息能直接定位连接是被对端关闭还是被中间链路丢弃。还有一点小经验现场排查断线重连问题时不要一上来就改代码。先打开终端调试串口执行几个内部命令查看当前网络状态、缓存区读写指针、重连计数把现场数据抓回来再决定下一步动作。这样既节省时间也避免在已经混乱的现场反复烧录固件。这个项目的机制设计最终落地为一套可以粗粒度配置的通用模板后续接到其他传感器采集项目时只需要改采样任务和协议解析部分断线重连和断点续传的代码基本能直接复用。踩过一轮现场调试的坑之后我最大的体会是嵌入式通讯开发里失败路径往往比成功路径更值得花时间去设计断线重连、断点续传这类机制看起来不起眼却是设备长期稳定运行的底线保障值得每个开发者认真对待。
返回列表