以太网温湿度采集的断线重连与断点续传:多协议设计与实践
提到以太网温湿度采集很多人第一反应是“接根网线把温湿度数据发到平台就行”。真做过多协议设备的人会明白重头戏从来不在一开始的一两个数据帧而在断线重连和断点续传。仓库、机房、冷链这类场景里网络不可能永远稳定交换机可能半夜自动重启光纤收发器偶发丢包物业维修还会拔错网线。你的采集终端这时候如果不会自动恢复或者恢复之后把数据从零开始重传平台上的温度曲线就会缺失、乱序甚至整台设备要人工断电重启。这篇文章就围绕一套典型的以太网温湿度采集通讯系统讲讲为什么需要多协议支持断线重连怎么做断点续传的落点在哪里以及我在实际调试中踩过的坑。1. 整体设计思路为什么多协议、断线与续传会成为核心1.1 温湿度采集设备的真实工作场景很多朋友把温湿度采集想成办公室环境网线一插就稳定了。实际上工业、医疗、冷链现场的以太网接口物理条件要恶劣得多设备往往部署在机柜、桥架、冷库夹层里网线可能和动力电缆走同一个竖井网络侧是百兆工业交换机长时间高负载运行后偶尔死机光纤收发器供电不稳时丢包更常见的是施工改造时直接把某一段网线剪断。设备采集温度、湿度之后既可能被本地组态软件通过 Modbus TCP 轮询又要把数据上报到云平台。可以说断线在这里不是概率问题而是时间问题。在这种工况下用户真正关心的是三件事第一断网一段时间后设备能不能自动回来不需要现场人员按复位键第二断网期间的数据能不能补回来如果不能补这个监测系统就失去了意义第三现场以后可能换平台设备能不能通过配置切换协议而不是重新烧一版固件。这三个需求直接引出多协议、断线重连、断点续传三个关键词。它们不是三个独立功能而是一套数据链路完整性的组合设计。1.2 多协议选型Modbus TCP、MQTT、HTTP 怎么选多协议不是把能用的协议全部堆上去而是要给设备留一个协议抽象层让同一套采集逻辑、缓存逻辑和断点续传逻辑可以通过配置切换不同的“外发通道”。我在实际项目中通常只考虑三种Modbus TCP、MQTT、HTTP/HTTPS。它们的典型对象和侧重点完全不同。协议典型对接对象适合场景重连侧重点Modbus TCPPLC、SCADA、组态软件本地局域网轮询超时、异常连接清理MQTT云平台、IoT 网关广域网长连接keepalive、cleanSession、QoSHTTP/HTTPS自建平台、REST API低频上报、批量补传token 过期、幂等键、批量游标选择的时候有一个原则如果平台方明确支持 Modbus TCP优先用 Modbus因为工业现场最看重兼容性调试工具遍地都是如果平台是云端物联网平台MQTT 是标准做法心跳和会话机制比你自己造的长连接可靠得多如果只是对接一个内部管理系统HTTP 最省事服务端甚至不用维护长连接。设备固件里把这三种协议注册成三个驱动上层统一使用“连接、发送、接收、确认”四类接口切换协议时只需要改配置不需要动缓存和断点续传逻辑。1.3 断线重连与断点续传在整体架构中的位置如果把整个温湿度采集终端分成两层来看断线重连管的是链路断点续传管的是数据。链路是水管数据是水。断线了要重新接水管接好后不能只把新水送过去还要把断管期间存在水罐里的水按顺序补送。所以架构上从上到下分为采集模块、缓存模块、协议模块、连接管理层。采集模块负责定时读取温湿度并打点缓存模块负责把没有确认的数据落盘协议模块决定这一条数据走 Modbus、MQTT 还是 HTTP连接管理层负责探测链路、实施重连策略。这里还要说清楚一个容易混淆的概念断点续传和完整上传是两回事。完整上传是断网恢复后把缓存里的数据全部重新发给平台平台自己想办法去重断点续传是设备记录一个“已确认收到的位置”只发送这个位置之后的数据。打个比方一批货要从 A 仓运到 B 仓快递车半路抛锚重新派车后从哪一单继续送只送还没确认签收的而不是从第一单重新装车。这个“确认签收”的机制就是断点续传的核心。2. 核心机制拆解断线重连与断点续传的实现原理2.1 怎么判断链路还活着以及重连间隔怎么设判断链路是否存活不能只靠一个信号。我一般分三层来看物理层看以太网 PHY 的 link 状态网线断开、交换机断电时 link 会 down但网线完好、核心交换机重启时物理层可能仍然是 up 的所以不能只依赖 link 状态。TCP 层看 socket读返回 0、写返回 EPIPE 或者 ECONNRESET都能说明连接断了但这属于事后通知太被动。真正可靠的是应用层心跳MQTT 有 PINGREQModbus TCP 可以周期性读一个保持寄存器当探活HTTP 则靠定时上报来隐式探活。心跳间隔建议设在 15 到 30 秒连续 3 次无响应就判定链路故障主动 close 当前连接进入重连流程。重连不能固定间隔否则多台设备同时检测到故障后会在同一时刻发起重连把交换机和服务器的连接数瞬间打满。我常用的策略是指数退避加随机抖动第一次 1 秒第二次 2 秒第三次 4 秒到最大 30 秒封顶每次再乘一个 0.8 到 1.2 的随机系数。公式可以写成delay min(30s, 1s * 2^n) * random(0.8, 1.2)n 是连续失败次数。抖动的作用是把几十台设备的重连请求在时间轴上散开避免“故障恢复瞬间大家都来抢连接”。另一个容易被忽略的点是重连成功不等于可以立刻补传。我习惯在 TCP 建连成功之后先做鉴权或者发送一条探测消息确认这 1 到 2 秒内链路稳定再开始批量补传。否则网络处于“刚恢复又断掉”的临界状态时数据发到一半又丢反而加剧了后续的乱序和重复。2.2 断点续传的本质记录“已发到哪里”断点续传一句话就能说清楚记录平台已经确认收到的数据序号只发送这个序号之后的数据。为了做到这一点设备里每一条温湿度数据都要有独立的序号和采样时间。我在 MCU 上定义的数据结构大致是下面这样typedef struct { uint32_t seq; // 序号单调递增 uint32_t timestamp; // 采样时间Unix 时间戳 int16_t temperature; // 温度单位 0.1℃ uint16_t humidity; // 湿度单位 0.1% uint8_t sensor_id; // 传感器通道号 } sensor_data_t;设备发送线程维护一个last_acked_seq每次从last_acked_seq 1开始取数据发送服务端每收到一批就返回一个ack_seq表示“这个序号之前的数据我都收到了”。设备收到ack_seq后更新水位线并把小于等于ack_seq的缓存释放掉。如果断网期间缓存了 1000 条数据网络恢复后只需要从第一条未确认数据开始补而不需要把所有缓存重新推一遍。这样做的好处很直观节省带宽、降低平台压力、让数据按原始序号有序到达。这个设计还要考虑掉电重启的情况。MCU 内部 SRAM 里的水位线一掉电就没了所以我要求水位线必须持久化到 Flash 或者外部存储里。每次成功收到ack后不需要立刻擦写 Flash可以先记在内存里等积累到一定数量或者掉电前统一提交。但如果想要“任何时候掉电都不重不漏”就得把last_acked_seq和写指针一起保存并且带 CRC 校验。2.3 时序对齐、幂等上报与“完整上传”的差异断点续传做得再好如果设备时间不准补回来的数据曲线也会是乱的。设备在断网期间只能靠 RTC 走时RTC 本身有精度误差长时间离线后误差可能累积到分钟级。我的做法是每条数据都使用设备产生的采样时间戳但落库最终以服务端收到时校验后的时间戳为准设备一旦联网首先做 NTP 校时或者通过协议同步时间再开始补传这样能最大限度降低时间偏差。平台侧如果发现某条数据的seq和采样时间发生冲突以seq为准来判断先后因为seq是设备内单调递增的时序不会乱。至于幂等这是解决重复问题的关键。网络超时重发、ack 丢失、协议切换任何一个环节都可能让平台收到同一条数据两次。所以设备上报的每条数据必须有一个全局唯一标识我用(sensor_id, seq)作为联合主键服务端数据库直接针对这两个字段建唯一索引。重复发送时后到的那一条会被数据库忽略或者服务端主动查重后丢弃。这正是断点续传和完整上传在协议层面的根本区别完整上传只告诉平台“我有一堆数据你看着收”断点续传则把“我已经确认到哪里、接下来从哪开始”作为协议的一部分显式传递出去。3. 实操三种协议下的重连与续传配置3.1 Modbus TCP 轮询超时与重试参数如果你的采集终端是作为 Modbus TCP 服务端对上位机组态软件提供数据那寄存器规划很重要。我一般把温度放在保持寄存器地址 0x0100湿度放 0x0101状态字放 0x0102寄存器数值用 int16温度值乘以 10 表示避免浮点位数的歧义。设备端要关注的并不是传统意义上的“重连外网”而是及时清理已经死掉的客户端连接上位机主动断开后设备必须 close 对应的 socket否则连接数会被慢慢占满新上位机连不上来。如果设备是作为 Modbus TCP 客户端去轮询下面的温湿度探头或者远程 IO那重点就是超时和重试。我常用的参数是功能码 03 读取保持寄存器响应超时 500ms 到 1s单次失败重试 2 到 3 次连续 5 次轮询失败才判定链路故障进入断线重连状态。不能一次超时就立刻重连现场网络抖动很常见误重连反而会让采集器在多个网络节点之间反复横跳。链路恢复后不要急着补业务数据先读一次设备型号或者状态寄存器确认链路正常再恢复轮询。3.2 MQTT 连接参数与会话保持的坑MQTT 本身自带断线重连机制但参数配不好重连回来照样出问题。最关键的几个参数是 clientID、keepalive、cleanSession 和 QoS。clientID 必须唯一我直接用设备 MAC 地址或者出厂序列号如果有两台设备共用 clientIDbroker 会把后连接的那台踢掉现场排查起来非常莫名其妙。keepalive 我设 30 秒broker 如果在 1.5 倍时间也就是 90 秒内收不到 PINGREQ就会判定连接断掉。QoS 我选 1既能保证消息不丢又不像 QoS2 那样带来两倍以上的交互开销。这里要说一个我踩过的坑cleanSession 设 false 时broker 会保存客户端离线期间订阅主题的消息等设备重连上线后一股脑推过来。如果设备本地又有断点续传同一批数据就会出现两条路同时补平台收到两份。我后来做了取舍后端平台本身有按(device_id, seq)去重的机制所以直接用 cleanSession true离线期间的消息全部不要完全靠设备本地缓存补传。这样逻辑更单一也更可控。另外 LWT 遗嘱消息要配上断线时 broker 会在离线状态 topic 发一条“设备离线”平台侧可以马上感知不用干等心跳超时。3.3 HTTP 增量补传接口怎么设计HTTP 本身无状态断线重连的逻辑最简单用定时上报作为探活上报失败就进入退避重试。真正需要动脑子的是补传接口。我不建议用“上传全部缓存”这种接口而是设计成批量增量补传核心在请求里带start_seq和一批数据响应里带ack_seq。一个典型的请求长这样POST /api/v1/device/TH200/telemetry/batch { start_seq: 1520, items: [ {seq: 1520, ts: 1710000000, temp: 25.1, hum: 60.2}, {seq: 1521, ts: 1710000010, temp: 25.2, hum: 60.1} ] }服务端处理完后返回{ ack_seq: 1521 }设备收到ack_seq 1521就知道 1521 及之前的数据都确认了下一次从 1522 开始。如果一批数据太大网络传输容易失败我一般控制在 100 条以内或者限制请求体 64KB超了就拆包。重试时要注意 HTTP 状态码5xx 和网络超时可以重试4xx 说明请求本身有问题比如鉴权过期、数据格式错这种重试多少次都没用要进入故障状态上报日志不能死循环。4. 工程落地状态机、存储保护与采样策略4.1 通信状态机与多协议抽象层多协议、断线重连、断点续传一起做最怕业务逻辑散落在各个协议回调里。我会在固件里定义一个统一的状态机任何协议都跑同一套状态流转。核心状态就 5 个IDLE、CONNECTING、ONLINE、RETRYING、DRAIN。IDLE 是上电初始化CONNECTING 正在建连ONLINE 是正常收发RETRYING 是等待下一次重连DRAIN 是链路刚恢复、正在补传缓存。状态之间的关系大概是下面这个伪代码void comm_poll(comm_state_t *s) { switch (*s) { case IDLE: load_config(); *s CONNECTING; break; case CONNECTING: if (protocol_connect() OK) *s DRAIN; else *s RETRYING; break; case DRAIN: if (flush_cache() CACHE_EMPTY) *s ONLINE; else *s RETRYING; break; case ONLINE: if (!protocol_heartbeat_ok()) *s RETRYING; break; case RETRYING: delay(next_backoff()); *s CONNECTING; break; } }这样做的最大好处是更换协议时只需要实现connect()、send()、recv()、ack_handle()四个接口状态机、缓存机制、重连策略完全不用动。我在项目中从 MQTT 切到 HTTP只改配置和注册表一版固件同时支持三种协议现场的维护成本低了很多。4.2 掉线缓存的数据结构与掉电保护断点续传的缓存不能只放内存必须考虑掉电。最稳妥的方案是在 Nor Flash 里划分两个数据区交替写入。数据区的尾部放一个结构体记录写位置和确认位置typedef struct { uint32_t magic; uint32_t write_idx; uint32_t last_acked_seq; uint32_t crc32; } storage_footer_t;写入时先写数据再更新 footer最后写 magic启动时先校验 magic 和 CRC如果发现当前区损坏就回退到上一个完整区。这样即使写数据时突然掉电最坏情况也只是丢掉最后几条未确认数据而不是整个缓存全部报废。要注意 Flash 是扇区擦除的不能每来一条数据就写一次。我一般是攒够一批比如 10 条或者 128 字节再提交一次写间隔控制在秒级这样既保证完整性又不磨损 Flash。这里的结构还要和协议无关。不管是用 MQTT 还是 HTTP 补传读的都是同一份缓存发送后从同一个last_acked_seq推进。千万别为每个协议单独维护一个水位线否则切换协议时会出现“MQTT 已经确认到 2000HTTP 还从 1500 开始补”的重复上报平台会被搞疯。4.3 采样速度、缓存容量与溢出策略估算设计缓存容量之前先算清楚断网多久数据不能丢。我给一个简单的计算基准一条温湿度记录按前面的结构大约 12 字节不包括文件系统开销。如果 10 秒采一次一天是 8640 条大约 101KB30 秒采一次一天 2880 条大约 34KB60 秒采一次一天 1440 条大约 17KB。按这个算1MB 的 Flash 在 10 秒采样间隔下大概能存 9 天多8MB 能存 70 多天绝大多数现场都够用了。如果断网时间实在太长缓存满了怎么办不能什么都不做那样新数据也存不进去。我常用的策略分三级第一级断线超过半小时后自动降低采样频率从 10 秒降到 30 秒省缓存第二级缓存使用率达到 80% 后主动丢弃最旧但尚未确认的数据同时把故障状态上报到平台第三级如果丢数据量超过阈值本地 LED 故障灯点亮等着现场处理。说实话真到这一步说明网络故障已经不是一天两天继续死撑没有意义不如把“缓存溢出、有数据丢失”这个事实明确告诉平台避免后面数据分析时拿不完整曲线当完整曲线用。5. 现场常见问题与排查经验5.1 半开连接TCP 没报错数据就是不出去最常见的问题不是网线断了而是网络中间设备重启导致半开连接。现象是采集器侧 socket 还显示 established但平台侧早已没有这个连接采集器一直发数据平台一直收不到。我现场排查时用netstat -an看到连接状态是 ESTABLISHED但抓包发现 TCP 重传了很多次一直没有 ACK 回来。这种情况下应用程序如果只等着收数据可能永远等不到错误事件。解决思路就是前面说的三层探活物理层 link、TCP keepalive、应用层心跳缺一不可。Linux 下可以用SO_KEEPALIVE但默认很难用我一般把空闲时间调到 10 秒、探测间隔 5 秒、探测次数 3 次MCU 上直接自己实现应用层心跳更可控。连续 3 次心跳无响应就主动 close 当前 socket进入 RETRYING。宁可主动断掉一个可能还活着的连接也不能让数据在一个假连接上无限阻塞。5.2 补传重复、乱序怎么根治补传重复的直接原因是设备发的 ack 确认在网络上丢了设备超时后从旧水位线重发平台收到两条一模一样的数据。如果平台没有去重机制库里就会出现重复记录温度曲线出现毛刺。乱序则往往是因为补传线程和实时上报线程同时工作实时数据 seq 大的先到了补传 seq 小的后到。这两个问题本质上都是协议设计问题可以在两端同时解决。设备端坚持“单线程补传”补传期间暂停实时上报或者把实时数据也合并进同一个发送队列保证seq严格递增。平台端必须给每个设备维护一个连续序号表落库时以(device_id, seq)为主键重复直接忽略如果收到 2000 号数据但 1980 号还没到可以把 2000 先放到一个乱序缓冲里等这个设备的 seq 缺口补齐后再写正式表。补传窗口可以做得小一点比如一次 100 条服务端返回ack_seq后设备基于最新的ack_seq继续这样就算重复范围也很可控。5.3 协议切换后的状态残留支持多协议的设备最怕现场从 MQTT 切到 HTTP 后旧 MQTT 连接还在偷偷发数据。原因通常是协议切换逻辑只改了配置没有把旧协议的发送线程停掉也没有关闭旧连接。我做过一个比较极端的现场后台配置已经切到 HTTP但设备里 MQTT 线程因为一直没有收到断开指令还在按自己的重连策略往老 broker 上连结果同一批温湿度数据同时从两个协议通道到了平台查了半天才发现是协议切换状态残留。正确的切换流程是下发新配置后先把旧协议连接主动 close等发送线程退出再清空当前发送队列但千万不能清空last_acked_seq。断点续传的“断点”是全局的应该跟着设备走而不是跟着协议走。新协议上线后从同一个last_acked_seq 1开始补传才能做到不丢不重。另外 MQTT 连接切换时还有一个隐蔽坑旧连接没有正常断开时新连接复用同一个 clientID 会被 broker 判定冲突直接踢掉。先 close 旧连接再等待几秒和 broker 完成会话清理后再 connect 新连接这个问题就消失了。最后说一点个人体会。我在实际测试里不会只拿正常网络的电脑试设备一定会造几种故障拔网线、交换机断电、后台服务重启、路由器改 IP、同时给多台设备断电再上电。每一轮测试下来断线重连和断点续传都能暴露几个新问题。刚开始做这套设计的时候我也觉得“只要把协议打通就行”后来发现链路的生命周期管理才是真正拉开设备稳定性的地方。你现在做以太网温湿度采集如果还没有专门写一个断线重连状态机和last_acked_seq的概念建议尽早补上越早设计后面现场维护越是省心。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →