一、背景
在使用 lwIP 内置 MQTT 客户端时,如果你用的是 2.2.0 之前的版本 ,很可能会遇到一个恼人的问题:客户端和服务器正常连接,但一段时间后 会话被 broker 踢掉。
比如常见的现象:
-
Mosquitto / EMQX 日志显示客户端超时断开。
-
lwIP 端没有主动调用
mqtt_disconnect()
,却突然进入了MQTT_DISCONNECTED
状态。 -
配置的 keep-alive 时间是 60s,但实际上 90s 左右就会掉线。
经过排查,这其实是 心跳(keep-alive)定时逻辑的 bug。下面来分析一下原因,并给出解决方法
二、问题现象
在 lwIP 2.1.x 的 mqtt.c
里,心跳定时逻辑在 mqtt_cyclic_timer()
中实现
cpp
if (client->keep_alive > 0) {
client->server_watchdog++;
if ((client->server_watchdog * MQTT_CYCLIC_TIMER_INTERVAL) > (client->keep_alive + client->keep_alive / 2)) {
mqtt_close(client, MQTT_CONNECT_TIMEOUT);
restart_timer = 0;
}
/* keep-alive 超时检测 */
if ((client->cyclic_tick * MQTT_CYCLIC_TIMER_INTERVAL) >= client->keep_alive) {
// 发送心跳包 PINGREQ
mqtt_output_append_fixed_header(&client->output, MQTT_MSG_TYPE_PINGREQ, 0, 0, 0, 0);
client->cyclic_tick = 0;
} else {
client->cyclic_tick++;
}
}
看似合理,但这里有个细节:
-
只有在 else****分支中才会执行 cyclic_tick++。
-
如果进入
if (...)
发送了心跳包,就会直接cyclic_tick = 0
,漏掉了一次累加。
结果就是:
-
心跳计数器实际触发频率比预期低。
-
PINGREQ 的发送比配置的 keep-alive 更晚。
-
Broker 端在 1.5 倍 keep-alive 没收到心跳时,就会断开连接。
三、解决方法
只需要在进入分支判断之前,提前增加一次 cyclic_tick:
cpp
client->cyclic_tick++; // 修复点:每个周期都先自增
if ((client->cyclic_tick * MQTT_CYCLIC_TIMER_INTERVAL) >= client->keep_alive) {
mqtt_output_append_fixed_header(&client->output, MQTT_MSG_TYPE_PINGREQ, 0, 0, 0, 0);
client->cyclic_tick = 0;
} else {
client->cyclic_tick++;
}
这样就保证了:
-
每次定时器调用,
cyclic_tick
都会+1。 -
不会出现"少算一次"的情况。
-
心跳严格按照配置的 keep-alive 周期发送。
四、结论
-
lwIP 2.1.x 版本的 MQTT 实现存在心跳 bug,导致 PINGREQ 延迟发送,broker 判定超时。
-
原因在于
cyclic_tick++
的位置不对,导致计数器漏算。 -
解决办法:在 lwIP 2.2.0 中,官方已经调整了
mqtt_cyclic_timer()
的逻辑,把cyclic_tick
的自增位置放到固定地方,避免了这个 bug。因此,如果你的项目允许,推荐直接升级 lwIP 到 ≥ 2.2.0 。如果受限于平台或历史代码,直接修改mqtt.c
中的计数逻辑也能解决问题。