现场最常见的故障不是模组坏了,而是"平台显示在线、数据不动"——连接还在,通路已经被中间设备悄悄回收。本文基于 TCP/IP 连接管理与重传机制、MQTT 规范与 4G 模组 AT 指令,讲清三类掉线的判别方法、心跳周期的实测取值、指数退避加重连抖动的状态机,附 STM32 可移植代码与七步排查清单。
做物联网远程运维,最费人的故障不是"连不上",而是"看着在线却不收数":平台侧设备状态是绿的,曲线却是平的,重启一下又好了,过几小时再犯。结论先给三条:第一,这类故障十有八九不是模组掉网,而是链路被运营商侧 NAT 表项回收,TCP 还停在 ESTABLISHED,设备自己毫不知情;第二,TCP 自带的 KeepAlive 默认 7200s 才探一次(Linux 默认 tcp_keepalive_time=7200、intvl=75、probes=9),远慢于 NAT 老化,指望它保活等于没做;第三,正确做法是在应用层做心跳,周期取"实测 NAT 老化时间的三分之一",配合 MQTT 的 1.5 倍判定,断线后按 1→2→4→…→60s 指数退避并加 ±20% 抖动重连,避免几千台设备同时冲击服务器。
现场把"掉线"当成一个词,排查就会原地打转。实际上至少有三类,现象、根因、恢复动作完全不同。
| 类型 | 典型现象 | 根因 | 怎么确认 | 恢复动作 |
|---|---|---|---|---|
| 链路断(真掉网) | 信号格消失、AT+CSQ 返回 99、CGREG 为 0 或 2 | 基站切换、隧道/地下室无覆盖、SIM 欠费、模组复位 | 读 AT+CSQ、AT+CGREG?,看注网状态 | 等待重新注网;连续失败则 AT+CFUN=1,1 复位模组 |
| NAT 表项老化(假在线) | TCP 仍是 ESTABLISHED,send 返回成功,但没有下行数据 | 运营商侧 NAT 把长时间无流量的映射删了,公网侧回包无人认领 | 看"最后成功收包时间"是否停住;抓包可见只有重传没有 ACK | 主动发心跳保活;已失效则必须重建连接 |
| 平台侧超时断链 | 设备认为在线,broker 已把会话踢掉 | MQTT 服务端按 1.5 倍 keepalive 判定客户端失联 | 对比设备日志与平台连接日志时间戳 | 客户端心跳要早于服务端判定周期 |
三类里最坑的是第二类。因为从设备视角看,socket 没有报错、send() 返回的字节数也正确——数据只是进了黑洞。TCP 要等到重传次数耗尽才会报错,而已建立连接上的重传由 tcp_retries2 控制(Linux 默认 15 次),真正报错往往要十几分钟。这段时间里,平台看到的是"在线、无数据",运维看到的是"设备失联",两边对不上。
很多人第一反应是"打开 SO_KEEPALIVE 不就行了"。问题在默认值。RFC 1122 明确要求保活间隔的默认值不得小于 2 小时,Linux 的实现是空闲 7200s 后开始探测、每隔 75s 探一次、连续 9 次无响应才判定断开——也就是最坏要 2 小时 11 分 15 秒才发现对端没了。而运营商 NAT 的老化时间通常在数分钟到数十分钟量级(不同省份、不同 APN 差异很大,没有公开统一值,必须实测)。保活周期比老化周期长一个数量级,连接自然早就被回收了。
还有两个容易被忽略的细节。其一,KeepAlive 只在连接空闲时才触发:如果应用每 10 分钟发一次数据,保活探测永远不会发出,NAT 靠业务数据刷新——这反而没问题;真正出问题的是"夜间无数据上报"的时段。其二,保活探测包不携带应用数据,某些中间设备对这类"无载荷的确认段"处理不一致,可能直接丢弃而不回 RST,设备端只能靠重传超时兜底。
| 对比项 | TCP KeepAlive | 应用层心跳 |
|---|---|---|
| 默认周期 | 7200s(Linux),RFC 要求不小于 2h | 由应用决定,常见 30~120s |
| 能否穿越 NAT | 周期太长,通常等不到下一次探测 | 周期短于老化时间即可维持映射 |
| 能否发现"假在线" | 只能发现对端无响应,发现慢 | 可要求对端回包,收到即证明双向通 |
| 能否携带业务状态 | 不能 | 能(电量、信号、固件版本、缓存条数) |
| 流量开销 | 极小,但周期不可控 | 可控,可随场景调整 |
| 结论 | 作为兜底保留,不能作为主保活手段 | 必须做,且要有回包确认 |
不要抄别人的 60s,也不要凭感觉给 300s。正确顺序是:先把心跳周期设成一个明显偏长的值(例如 600s),连续跑 24 小时,记录每次心跳的发出时间与收到回包的时间;找到"发出后收不到回包"的那个临界点,就是这条链路(这张卡、这个 APN、这个地区)的实际可用保活窗口。工程上取实测临界值的三分之一作为心跳周期,留出两次容错余量。
周期定了,还要算流量账。一次心跳的最小开销是 TCP/IP 头 40B 加 2B 负载,合 42B;若平台回一个 42B 的确认,一次往返约 84B。按 30 天计算:
| 心跳周期 | 每天上行包数 | 每月上行流量(42B/包) | 最坏断链发现时延(1.5 倍) | 适用场景 |
|---|---|---|---|---|
| 15s | 5760 | 约 7.3MB | 22.5s | 实时控制、AGV 调度 |
| 30s | 2880 | 约 3.6MB | 45s | 产线设备、告警联动 |
| 60s | 1440 | 约 1.8MB | 90s | 大多数工况监测(推荐起点) |
| 120s | 720 | 约 0.9MB | 180s | 水务、供暖等慢变场景 |
| 300s | 288 | 约 0.36MB | 450s | 电池供电、可容忍延迟 |
表里只算了上行报文,没算 ACK、TLS 记录头与重传。若走 MQTT over TLS,一次心跳的实际字节数会明显增加,周期选择要按"流量预算 ÷ 单包字节"反算。另外提醒一句:心跳不是越短越好。周期缩到 15s 以下,除了流量上升,还会让本就拥塞的蜂窝小区更拥堵,反而推高丢包率。
用 MQTT 上云的话,心跳这件事协议已经替你定了一半规矩:CONNECT 报文里的 Keep Alive 是 16 位秒数(0 表示关闭),服务端在 1.5 倍 Keep Alive 时间内没有收到任何控制报文,就必须断开这个网络连接。客户端则应在空闲时发 PINGREQ、等服务端回 PINGRESP。三条实践规则:
| Keep Alive | 服务端断开阈值(1.5×) | 建议客户端判定 | 建议 PINGREQ 触发 | PINGRESP 超时 |
|---|---|---|---|---|
| 30s | 45s | 35s | 空闲 15s | 8s |
| 60s | 90s | 70s | 空闲 30s | 10s |
| 120s | 180s | 140s | 空闲 60s | 15s |
还有一个常被忽略的点:QoS1 的重复投递。断线重连后若开启持久会话,broker 会补发未确认消息,设备端必须用报文标识符(Packet Identifier)去重,否则一次重连就可能产生重复工单。工程上更稳的做法是业务层自带单调递增序号,落库时按序号幂等。
很多"重连失败"其实是注网就没成功。STM32 通过串口驱动 4G 模组时,建议把下面这条 AT 序列固化成开机自检流程,每一步都带超时与重试:
| 顺序 | 指令 | 作用 | 正常返回 | 失败处理 |
|---|---|---|---|---|
| 1 | AT | 串口连通性 | OK | 查波特率、查流控 |
| 2 | AT+CPIN? | SIM 是否就绪 | +CPIN: READY | 查卡座、查卡是否欠费 |
| 3 | AT+CSQ | 信号强度(0~31,99 为未知) | +CSQ: 18,99 | 小于 10 视为弱信号,检查天线 |
| 4 | AT+CGREG? | 分组域注网状态 | +CGREG: 0,1(1 为本地网) | 0/2 时等待,必要时重启射频 |
| 5 | AT+CGACT=1,1 | 激活 PDP | OK | 检查 APN 配置 |
| 6 | AT+CIPSTART="TCP",...... | 建立 TCP | CONNECT OK | 先 AT+CIPCLOSE 再重试 |
| 7 | AT+CPSMS=1,,,"00000001","00000001" | 开启 PSM(电池设备) | OK | 非低功耗场景不要开 |
PSM 要单独说一句:它省电的代价是设备在休眠期不可达。平台下发的远程指令只能等设备下一次 TAU 唤醒或主动上报时才能拿到,所以"实时控制"类业务不要开 PSM,改用 eDRX 折中。信号强度也别只看 CSQ,4G 下更建议读 AT+CESQ 拿 RSRP/RSRQ,弱信号时优先检查天线与金属壳体遮挡,而不是急着改代码。
一次基站闪断,几千台设备如果同时立刻重连,会产生典型的"惊群":服务器连接数瞬间打满,认证队列堆积,部分设备超时后又立刻重试,形成雪崩。正确做法是指数退避 + 随机抖动:首次 1s,之后 2、4、8、16、32s,封顶 60s;每次重试前叠加 ±20% 的随机量,把重连请求摊平到一个时间窗里。
typedef enum {
LK_IDLE = 0, LK_NET_WAIT, LK_TCP_CONNECT, LK_ONLINE, LK_BACKOFF
} lk_state_t;
static lk_state_t st = LK_IDLE;
static uint32_t backoff_ms = 1000, t_tick = 0, fail_cnt = 0;
static uint32_t last_rx_ms = 0; /* 最后成功收包时刻 */
/* 1ms 周期调用 */
void link_tick(void)
{
t_tick++;
switch (st) {
case LK_IDLE:
if (net_is_registered()) { st = LK_TCP_CONNECT; t_tick = 0; }
break;
case LK_NET_WAIT:
if (net_is_registered()) { st = LK_TCP_CONNECT; t_tick = 0; }
else if (t_tick > 60000) { modem_reset(); t_tick = 0; } /* 60s 未注网 */
break;
case LK_TCP_CONNECT:
if (tcp_connect_async() == 0) {
st = LK_ONLINE; backoff_ms = 1000; fail_cnt = 0;
last_rx_ms = t_tick;
} else if (t_tick > 20000) { /* 20s 连接超时 */
tcp_close(); st = LK_BACKOFF; t_tick = 0;
}
break;
case LK_ONLINE:
/* 心跳:周期 60000ms,由业务层发送;这里只做超时判定 */
if ((t_tick - last_rx_ms) > 90000) { /* 1.5 倍心跳 */
tcp_close(); st = LK_BACKOFF; t_tick = 0;
fail_cnt++;
}
break;
case LK_BACKOFF: {
/* ±20% 抖动:先转有符号,避免无符号相减回绕 */
int32_t jitter = (int32_t)(backoff_ms / 5) * ((int32_t)(rand() % 3) - 1);
uint32_t wait = (uint32_t)((int32_t)backoff_ms + jitter);
if (t_tick >= wait) { st = LK_TCP_CONNECT; t_tick = 0; }
break;
}
}
}
/* 重连失败后调用:指数退避,封顶 60s;连续 5 次失败复位模组 */
void link_on_fail(void)
{
fail_cnt++;
backoff_ms = (backoff_ms < 60000) ? backoff_ms * 2 : 60000;
if (fail_cnt >= 5) { modem_reset(); fail_cnt = 0; backoff_ms = 1000; }
st = LK_BACKOFF; t_tick = 0;
}
这段代码有四个刻意设计。第一,收包即刷新 last_rx_ms:不只看心跳回包,任何业务下行都算"链路活着",避免误判。第二,退避封顶 60s:再长会让恢复变慢,再短会加剧雪崩。第三,连续 5 次失败才复位模组:模组复位本身要十几秒,频繁复位反而永远连不上。第四,抖动用整数运算实现,不引入浮点,Cortex-M0 也能跑。
还有两件配套的事:一是把"最后成功收发时间"写进 RTC 备份寄存器或 FRAM,设备重启后能判断"上次是不是异常掉线",便于统计;二是给链路状态机配一个独立看门狗,若 30 分钟仍无法上线,触发整机硬件复位,避免设备"卡死在重连里"。
| 步骤 | 动作 | 判据 |
|---|---|---|
| 1 看注网 | 读 CSQ、CGREG、CESQ | CSQ≥10 且 CGREG=1,否则先修射频 |
| 2 看连接 | 确认 socket 状态与最后收包时间 | 状态 ESTABLISHED 但长时间无收包即为"假在线" |
| 3 测老化 | 心跳拉到 600s 跑 24h,记录临界点 | 得到本条链路的可用保活窗口 |
| 4 定周期 | 取临界值 1/3,核对流量预算 | 周期既短于老化,又不超月流量 |
| 5 配 MQTT | Keep Alive、PINGREQ 超时、遗嘱消息 | 客户端判定早于服务端 1.5 倍 |
| 6 改重连 | 指数退避 1→60s、±20% 抖动、5 次失败复位 | 批量断电恢复时服务器连接曲线平滑 |
| 7 做验证 | 拔天线、关基站、断服务器各 30 分钟 | 三种场景均能自愈且无重复工单 |
4G 物联网网关的"假在线",本质是 TCP 连接的生命周期比 NAT 映射长:TCP 自己并不知道中间设备的映射已经消失。解决办法不是加大重连频率,而是用短于 NAT 老化时间三分之一的应用层心跳维持映射、用 MQTT 的 1.5 倍规则做双向确认、用 1→60s 指数退避加 ±20% 抖动把重连请求摊平。心跳周期与流量是一笔明账:60s 周期每月约 1.8MB、断链最坏 90s 发现,是大多数工况监测场景的推荐起点。
本文基于《TCP/IP 技术大全》中 TCP 连接管理、重传与定时器相关章节,结合 OASIS MQTT 3.1.1 规范、3GPP 27.007 系列 AT 指令定义及 4G 模组现场调试实践整理优化。
沧州艾诺威电子 — 国家高新技术企业,20+项国家专利
嵌入式系统开发 · 物联网方案 · AI智能硬件 · 一站式交付
电话:13930711029 | 邮箱:tech@czinv.com | 24小时内响应