一、背景
? ? ? ?在使用 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()
中實現
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) {// 發送心跳包 PINGREQmqtt_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
:
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
中的計數邏輯也能解決問題。