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

相关推荐
小李不想当小白7 小时前
PCB结构与组成(PCB设计入门学习笔记)
经验分享·笔记·单片机·嵌入式硬件·学习·pcb工艺·pcb
新晨单片机设计7 小时前
S004R-基于STM32单片机智能门禁系统(密码、指纹、人脸、刷卡、蓝牙)【Proteus仿真+Keil程序+原理图】
stm32·单片机·嵌入式硬件·proteus
至为芯7 小时前
PY32F071至为芯支持32位ARM内核的高主频MCU单片机
单片机·集成电路·芯片
桀人8 小时前
MCU系统架构——总线——统一编址
stm32·单片机·嵌入式硬件
学生小羊8 小时前
C++ 初阶 学习博客
c语言·c++·c++与c语言的区别·c++基础学习
天天喝旺仔8 小时前
Linux 性能排查实战:用 perf、strace 与火焰图定位 CPU、内存与 IO 瓶颈
linux·性能优化·运维开发
LuminousCPP8 小时前
Linux系统编程(二):从目录树到命令执行|路径、环境变量、alias 与文件操作入门
linux·经验分享·笔记
第十人i8 小时前
Linux 系统初始化配置清单:新服务器快速部署
linux·服务器
LongRunning8 小时前
【Linux】RK3568-OpenCV+YOLO(七)
linux
j7~8 小时前
【Linux】三十八.C++ 手写 HTTP 服务器:裸 socket + fork 多进程,从 HTTP 报文解析到浏览器打开网页
linux·网络·网络协议·学习·http·网络编程