lwIP MQTT 心跳 Bug 分析与修复

一、背景

在使用 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 中的计数逻辑也能解决问题。

相关推荐
AlfredZhao2 天前
生产环境里,为什么不建议把普通端口直接暴露到公网?
linux·https·443·80
戴为沐3 天前
Linux内存扩容指南
linux
zylyehuo3 天前
Linux 彻底且安全地删除文件
linux
用户805533698034 天前
主线 U-Boot 上 RK3506:和闭源 rkbin 拔河的三个隐性契约
linux·嵌入式
用户034095297914 天前
linux fcitx 5 雾凇拼音 设置在中文输入法下仍然输入英文标点
linux
Web3探索者6 天前
可视化服务器管理和传统命令行区别是什么?新手教程:Linux 运维到底该用图形界面还是 SSH 命令行?
linux·ssh
zylyehuo6 天前
Linux系统中网线与USB网络共享冲突
linux
Sokach10157 天前
Linux Shell 脚本从零到能用:一个新手的一天学习总结
linux
FreakStudio8 天前
W55MH32L-EVB 上手测评:硬件 TCP/IP 加持的以太网单片机,MicroPython 零门槛开发
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
AlfredZhao8 天前
Docker 容器时区不对,`timedatectl` 不存在怎么办?
linux·timezone