专栏说明:上一章完成 QoS 消息缓存、应答、超时重传逻辑。很多设备调试时出现一个很头疼现象:网络已经断开,MCU 完全感知不到,Broker 端判定设备在线;或者心跳参数配置不当,设备频繁被服务端踢下线。
MQTT 心跳不只是简单定时发 PINGREQ 报文,它包含心跳时序、双向存活判定、半断开检测、退避式重连、会话清理整套工程逻辑,是设备长期稳定运行的关键。本章面向量产设备,讲解理论 + 完整可移植 C 代码,解决线上设备掉线疑难杂症。
0 心跳与断线场景说明
物联网设备网络环境复杂:4G 模块信号波动、路由器重启、网线松动、NAT 网关超时切断长连接。TCP 协议属于 "惰性断开",单方面网络中断时,两端不会立刻收到断开信号,就会出现假在线现象。
MQTT 协议通过 Keep‑Alive 心跳字段解决假在线问题:
- 客户端定时发送 PINGREQ 报文;
- Broker 收到后回复 PINGRESP;
- 如果在规定时间没有收到心跳交互,任意一端可以主动关闭会话。
| 故障现象 | 根本原因 |
|---|---|
| 设备一段时间后被服务器踢掉 | 心跳间隔设置过大,或者 PINGREQ 发送不及时 |
| 网络断了设备还显示在线 | TCP 半断开,没有上层心跳检测 |
| 重连风暴,网络恢复瞬间疯狂建立连接 | 重连没有退避延时,无冷却机制 |
| 重连成功后旧消息状态错乱 | 重连后未清空 QoS 缓存、环形缓冲区 |
1 MQTT Keep‑Alive 协议原理
CONNECT 报文中携带keepAlive参数,单位秒。协议标准规定:
客户端必须在不超过 keepAlive 的时间间隔内发送 PINGREQ。实际工程建议在
keepAlive/2~keepAlive*0.75区间发送心跳报文,预留网络抖动余量。
举个例子:keepAlive=60s
- Broker 判定设备超时时间:1.5 × keepAlive = 90s
- 如果客户端 90 秒内没有任何报文(业务消息 / PINGREQ)到达,Broker 主动断开连接。
重点注意:发送业务 PUBLISH 报文,可以替代 PINGREQ 心跳。如果设备高频上报传感器数据,不需要额外发送 PINGREQ;只有长时间无业务报文时才发送心跳包。很多开发者忽略这一点,造成心跳冗余流量。
心跳双向判定规则
- 客户端视角:发送 PINGREQ 之后,等待 PINGRESP 应答。超时没有收到应答,判定链路失效,主动断开 TCP,触发重连流程。
- 服务端视角:统计客户端最近收到报文时间,超过 1.5 倍 keepAlive,关闭连接。
很多裸机 MQTT 开源代码只做到定时发 PINGREQ,没有等待 PINGRESP 应答校验。网络单向通(能发不能收)场景下设备永远不会触发断线,这是量产重大缺陷。
2 扩展 MQTT 客户端结构体,新增心跳状态
基于第 3 章 MqttClient_t 扩展,增加心跳应答超时检测字段:
typedef enum
{
MQTT_HEART_IDLE = 0,
MQTT_HEART_SENT_PINGREQ, /* 已经发出PINGREQ,等待PINGRESP应答 */
}MqttHeartState_e;
typedef struct
{
MqttClientState_e state;
RingBuffer_t rxRingBuf;
uint32_t keepAliveTimer; /* 距离上一次收发报文时间戳 */
uint32_t pingReqSendTimer; /* PINGREQ发送时刻 */
MqttHeartState_e heartState; /* 心跳子状态 */
uint32_t reconnectTimer;
uint16_t keepAliveInterval; /* 单位 s */
uint16_t pingRespTimeoutMs; /* 等待PINGRESP超时,默认3000ms */
uint8_t txBuf[512];
uint8_t parseBuf[512];
uint16_t parseIndex;
/* 引用QoS管理器 */
MqttQosManager_t *qosMgr;
}MqttClient_t;
结构体初始化示例
void mqtt_client_init(MqttClient_t *client, MqttQosManager_t *qosMgr)
{
memset(client,0,sizeof(MqttClient_t));
client->state = MQTT_STATE_TCP_DISCONNECT;
client->keepAliveInterval = 60;
client->pingRespTimeoutMs = 3000;
client->heartState = MQTT_HEART_IDLE;
client->qosMgr = qosMgr;
}
3 心跳保活轮询函数实现
整合到 mqtt_client_poll 轮询内部,完整心跳逻辑:
static void mqtt_heart_poll(MqttClient_t *client,uint32_t now)
{
if(client->state != MQTT_STATE_MQTT_CONNECTED)
{
client->heartState = MQTT_HEART_IDLE;
return;
}
switch(client->heartState)
{
case MQTT_HEART_IDLE:
{
/* 距离上一次报文时间达到阈值,发送PINGREQ */
uint32_t heartThreshold = client->keepAliveInterval * 1000 / 2;
if((now - client->keepAliveTimer) > heartThreshold)
{
uint16_t len = mqtt_build_pingreq(client->txBuf,sizeof(client->txBuf));
if(len > 0)
{
tcp_send(client->txBuf, len);
client->heartState = MQTT_HEART_SENT_PINGREQ;
client->pingReqSendTimer = now;
}
}
break;
}
case MQTT_HEART_SENT_PINGREQ:
{
/* 已经发送PINGREQ,等待PINGRESP应答,超时判定链路故障 */
if((now - client->pingReqSendTimer) > client->pingRespTimeoutMs)
{
/* 心跳无应答,链路异常,主动断开 */
tcp_disconnect();
client->state = MQTT_STATE_TCP_DISCONNECT;
ring_buffer_clear(&client->rxRingBuf);
/* 重置QoS缓存,旧消息根据业务决定是否保留 */
memset(client->qosMgr,0,sizeof(MqttQosManager_t));
client->reconnectTimer = now;
}
break;
}
default:
break;
}
}
收到任意有效报文,刷新心跳计时
每当收到任意 MQTT 合法报文(CONNACK / PUBLISH / PINGRESP 等),刷新keepAliveTimer。业务发送 PUBLISH 成功后,同样刷新该计时器,替代心跳包:
/* 在报文解析成功之后调用 */
void mqtt_refresh_heart_timer(MqttClient_t *client,uint32_t now)
{
client->keepAliveTimer = now;
/* 如果正在等待PINGRESP,收到回复则恢复空闲状态 */
client->heartState = MQTT_HEART_IDLE;
}
工程提示:设备高频上报数据场景,
mqtt_refresh_heart_timer会不断刷新计时,PINGREQ 报文不会重复发送,节约流量。
4 智能退避自动重连机制
简单固定 3 秒重连在网络抖动时会产生重连风暴。量产推荐阶梯退避策略:
- 初次失败重连间隔:2s
- 连续失败:2s →4s →8s →16s,最大上限 30s,到达上限不再继续增大;
- 一旦重连成功,退避计数器清零。
新增重连控制结构体字段:
uint16_t reconnectDelayBase; /* 当前重连基础延时 ms */
uint16_t reconnectDelayMax; /* 最大重连延时 */
uint16_t reconnectFailCount; /* 连续重连失败次数 */
重连逻辑示例:
void mqtt_reconnect_poll(MqttClient_t *client,uint32_t now)
{
if(client->state != MQTT_STATE_TCP_DISCONNECT)
return;
if((now - client->reconnectTimer) >= client->reconnectDelayBase)
{
/* 发起TCP连接 */
if(tcp_connect() == 0)
{
client->state = MQTT_STATE_TCP_CONNECTING;
}
else
{
/* TCP连接发起失败,增大退避时间 */
client->reconnectFailCount ++;
if(client->reconnectDelayBase < client->reconnectDelayMax)
{
client->reconnectDelayBase *= 2;
}
client->reconnectTimer = now;
}
}
}
重连成功后执行清零:
/* TCP握手成功回调 */
void mqtt_on_tcp_connect_ok(MqttClient_t *client,uint32_t now)
{
client->reconnectFailCount = 0;
client->reconnectDelayBase = 2000; /* 恢复初始2s */
client->state = MQTT_STATE_MQTT_CONNECT_SEND;
client->reconnectTimer = now;
}
5 半断开、异常会话清理要点
发生断线触发重连时,必须做资源清理,否则会出现诡异 bug:
- 清空 TCP 接收环形缓冲区,丢弃残留旧碎片报文;
- QoS 等待应答缓存:业务区分处理
- cleanSession=1:全部丢弃未应答消息;
- cleanSession=0:需要持久化保存待发送消息,重连之后重新投递;
- 心跳子状态重置为 IDLE;
- 重连退避计数器,只有连接真正建立成功才清零,不要每次断开就清零。
非常容易踩坑:cleanSession 标志。如果 CONNECT 设置 cleanSession=1,Broker 会直接丢弃该客户端历史订阅和排队消息;cleanSession=0 服务端保存会话。工业设备按需选择。
6 调试排坑清单
- keepAlive 不要设置为 0:0 代表关闭心跳,设备网络异常将无法检测掉线。
- keepAlive 不要设置过小,例如 5 秒:网络轻微抖动就会触发心跳超时,频繁重连。一般设备推荐 30‑120s。
- 只发送 PINGREQ,不等待 PINGRESP 应答:无法识别单向断网。
- 重连不加退避,网络故障瞬间疯狂调用 tcp_connect,占用 MCU 资源。
- 重连后不清空环形缓冲区:残留上一次会话碎片报文,解析错乱。
- 业务发送 PUBLISH 之后忘记刷新 keepAliveTimer,导致多余心跳包。
本章总结
本章完成量产级心跳保活与智能重连全套逻辑。
- 实现双向心跳校验:发送 PINGREQ 并且等待 PINGRESP 应答,解决 TCP 半断开假在线;
- 业务报文复用心跳计时,减少不必要流量;
- 阶梯退避重连,避免重连风暴;
- 断线后会话资源清理,保障重连之后协议栈状态干净。
到此协议栈已经具备长时间跑量产设备的基础稳定性。下一章预告:第 6 章 MQTT 主题、过滤器、通配符、订阅管理、拒收机制详解与代码实现
💖 点赞 + 收藏 + 关注,MQTT 物联网上云系列持续更新!