物联网网关4G长连接老掉线怎么破:TCP KeepAlive与心跳重连七步讲透

现场最常见的故障不是模组坏了,而是"平台显示在线、数据不动"——连接还在,通路已经被中间设备悄悄回收。本文基于 TCP/IP 连接管理与重传机制、MQTT 规范与 4G 模组 AT 指令,讲清三类掉线的判别方法、心跳周期的实测取值、指数退避加重连抖动的状态机,附 STM32 可移植代码与七步排查清单。

2026-10-20
4G网关TCP KeepAliveMQTT心跳断线重连物联网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 次),真正报错往往要十几分钟。这段时间里,平台看到的是"在线、无数据",运维看到的是"设备失联",两边对不上。

二、TCP KeepAlive 为什么救不了场

很多人第一反应是"打开 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 倍)适用场景
15s5760约 7.3MB22.5s实时控制、AGV 调度
30s2880约 3.6MB45s产线设备、告警联动
60s1440约 1.8MB90s大多数工况监测(推荐起点)
120s720约 0.9MB180s水务、供暖等慢变场景
300s288约 0.36MB450s电池供电、可容忍延迟

表里只算了上行报文,没算 ACK、TLS 记录头与重传。若走 MQTT over TLS,一次心跳的实际字节数会明显增加,周期选择要按"流量预算 ÷ 单包字节"反算。另外提醒一句:心跳不是越短越好。周期缩到 15s 以下,除了流量上升,还会让本就拥塞的蜂窝小区更拥堵,反而推高丢包率。

四、MQTT 侧的三条硬规则

用 MQTT 上云的话,心跳这件事协议已经替你定了一半规矩:CONNECT 报文里的 Keep Alive 是 16 位秒数(0 表示关闭),服务端在 1.5 倍 Keep Alive 时间内没有收到任何控制报文,就必须断开这个网络连接。客户端则应在空闲时发 PINGREQ、等服务端回 PINGRESP。三条实践规则:

Keep Alive服务端断开阈值(1.5×)建议客户端判定建议 PINGREQ 触发PINGRESP 超时
30s45s35s空闲 15s8s
60s90s70s空闲 30s10s
120s180s140s空闲 60s15s

还有一个常被忽略的点:QoS1 的重复投递。断线重连后若开启持久会话,broker 会补发未确认消息,设备端必须用报文标识符(Packet Identifier)去重,否则一次重连就可能产生重复工单。工程上更稳的做法是业务层自带单调递增序号,落库时按序号幂等。

五、模组侧:先把"网"确认清楚,再谈"连"

很多"重连失败"其实是注网就没成功。STM32 通过串口驱动 4G 模组时,建议把下面这条 AT 序列固化成开机自检流程,每一步都带超时与重试:

顺序指令作用正常返回失败处理
1AT串口连通性OK查波特率、查流控
2AT+CPIN?SIM 是否就绪+CPIN: READY查卡座、查卡是否欠费
3AT+CSQ信号强度(0~31,99 为未知)+CSQ: 18,99小于 10 视为弱信号,检查天线
4AT+CGREG?分组域注网状态+CGREG: 0,1(1 为本地网)0/2 时等待,必要时重启射频
5AT+CGACT=1,1激活 PDPOK检查 APN 配置
6AT+CIPSTART="TCP",......建立 TCPCONNECT OK先 AT+CIPCLOSE 再重试
7AT+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、CESQCSQ≥10 且 CGREG=1,否则先修射频
2 看连接确认 socket 状态与最后收包时间状态 ESTABLISHED 但长时间无收包即为"假在线"
3 测老化心跳拉到 600s 跑 24h,记录临界点得到本条链路的可用保活窗口
4 定周期取临界值 1/3,核对流量预算周期既短于老化,又不超月流量
5 配 MQTTKeep 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小时内响应

🎧 本文已制作播客节目
双主持对话音频,随时随地收听本文内容
收听播客 →