MQTT 客戶端自動重連最佳實踐|構建可靠 IoT 裝置連線

EMQX發表於2023-03-30

背景

MQTT 是一個基於 TCP 協議的釋出/訂閱模型協議,它被廣泛應用於物聯網、感測器網路和其他低頻寬、不穩定網路環境中。在這些網路環境中,網路連線往往不穩定,可能會出現網路故障、訊號弱化、丟包等問題,這可能會導致 MQTT 客戶端與伺服器之間的連線中斷。物聯網應用中,常見的觸發斷線重連的場景包括:

  1. 網路環境惡劣或者斷網,造成 MQTT 客戶端連線超時斷開。
  2. 由於業務需要服務端升級切換,服務端主動關閉斷開。
  3. 裝置重啟或客戶端重啟,客戶端主動重連。
  4. 其他網路因素造成 TCP/IP 傳輸層斷開導致 MQTT 連線重連。

為了確保 MQTT 客戶端與伺服器之間的穩定連線,MQTT 客戶端需要實現重連邏輯,幫助 MQTT 客戶端自動重新連線伺服器,並恢復之前的訂閱關係、保持會話等狀態。

為什麼 MQTT 客戶端重連程式碼需要良好的設計

MQTT 裝置重連是很多物聯網應用中不可避免的情況。設計 MQTT 客戶端重連邏輯時需要注意使用正確的事件回撥方法,每次重連設定合理的隨機退避時間,以保證客戶端和服務端的長時間穩定執行,從而確保業務的正常開展。

不合理的重連邏輯設計可能會造成諸多問題:

  1. 重連邏輯失效導致客戶端靜默不再接受 Broker 訊息。
  2. 客戶端頻繁重連,無重連退避時間導致形成 DDOS 攻擊服務端 Broker。
  3. 客戶端頻繁上下線導致 Broker 服務端資源過量不必要的消耗。

而合理的重連邏輯既可以提高 MQTT 客戶端的穩定性和可靠性,避免因網路連線中斷而導致的資料丟失、延遲等問題,還可以降低由於頻繁連線對伺服器端的壓力。

如何設計一段 MQTT 客戶端重連程式碼

在進行 MQTT 客戶端重連程式碼設計時需要考慮以下幾個方面:

  • 設定正確的連線保活時間 MQTT 客戶端的連線保活時間即 Keep Alive,負責檢測當前連線的健康狀態。Keep Alive 超時會觸發客戶端重連和服務端關閉客戶端連線。該數值會影響到服務端和客戶端檢測到連線斷開不可用的時長,使用者需要根據自身網路狀態,以及期望的最長等待時間來設定合理的 Keep Alive。
  • 重連策略和退避 使用者應該根據網路環境的不同,制定不同的重連策略。例如,當網路連線中斷時,可以設定一個初始等待時間,並在每次重連嘗試後逐漸增加等待時間,以避免網路連線中斷導致的大量重連嘗試。建議使用指數退避演算法或隨機 + 階梯延時來留出足夠的退避時隙。
  • 連線狀態管理 需要在客戶端中維護連線狀態,包括連線狀態的記錄、連線斷開的原因、已訂閱的主題列表等資訊。當連線中斷時,客戶端應該記錄下連線斷開的原因,並進行相應的重連嘗試。但如果使用會話保持功能,則不需要客戶端自己儲存這些資訊。
  • 異常處理 在連線過程中可能會發生各種異常情況,例如伺服器不可用、認證失敗、網路異常等。需要在客戶端中新增異常處理邏輯,根據異常情況進行相應的處理。MQTT 5 協議提供了詳實的此類斷開連線原因,客戶端可以根據這些資訊記錄異常日誌、斷開連線、再次重連等。
  • 最大嘗試次數限制 對於一些低功耗裝置,為避免重連次數過多導致客戶端資源消耗過大,有時候需要考慮限制最大重連嘗試次數。當超過最大嘗試次數後,客戶端應該中止重連嘗試進入休眠狀態,避免無意義的重連。
  • 退避演算法 有兩種常用的重連退避方法:指數補償演算法和隨機退避。指數補償演算法是透過負反饋機制指數增加等待時間來找到合適的傳送/連線速率。隨機退避即透過設定等待時間的上下限,每次重連都等待隨機的延時時間,由於其易於實現而有廣泛使用。

重連程式碼示例

我們將以 Paho MQTT C 的庫為例,示範如何使用非同步程式設計模型優雅完成自動重連功能。Paho 提供了豐富的回撥函式,請注意不同回撥方法觸發條件和設定方式不同,分別有全域性回撥、API 回撥和非同步方法回撥。API 回撥有相當的靈活性,但當開啟自動重連功能時,建議只使用非同步回撥。此處對三種回撥函式都提供了例程,使用者可以使用此例程驗證三種回撥函式的觸發。

// 是 Async 使用的回撥方法
// 連線成功的非同步回撥函式,在連線成功的地方進行Subscribe操作。
void conn_established(void *context, char *cause)
{
    printf("client reconnected!\n");
    MQTTAsync client = (MQTTAsync)context;
    MQTTAsync_responseOptions opts = MQTTAsync_responseOptions_initializer;
    int rc;

    printf("Successful connection\n");

    printf("Subscribing to topic %s\nfor client %s using QoS%d\n\n"
           "Press Q<Enter> to quit\n\n", TOPIC, CLIENTID, QOS);
    opts.onSuccess = onSubscribe;
    opts.onFailure = onSubscribeFailure;
    opts.context = client;
    if ((rc = MQTTAsync_subscribe(client, TOPIC, QOS, &opts)) != MQTTASYNC_SUCCESS)
    {
        printf("Failed to start subscribe, return code %d\n", rc);
        finished = 1;
    }
}


// 以下為客戶端全域性連線斷開回撥函式
void conn_lost(void *context, char *cause)
{
    MQTTAsync client = (MQTTAsync)context;
    MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer;
    int rc;

    printf("\nConnection lost\n");
    if (cause) {
        printf("     cause: %s\n", cause);
    }
    printf("Reconnecting\n");
    conn_opts.keepAliveInterval = 20;
    conn_opts.cleansession = 1;
    conn_opts.maxRetryInterval = 16;
    conn_opts.minRetryInterval = 1;
    conn_opts.automaticReconnect = 1;
    conn_opts.onFailure = onConnectFailure;
    MQTTAsync_setConnected(client, client, conn_established);
    if ((rc = MQTTAsync_connect(client, &conn_opts)) != MQTTASYNC_SUCCESS)
    {
        printf("Failed to start connect, return code %d\n", rc);
        finished = 1;
    }
}

int main(int argc, char* argv[])
{
    // 建立非同步連線客戶端需要使用的屬性結構體
    MQTTAsync client;
    MQTTAsync_connectOptions conn_opts = MQTTAsync_connectOptions_initializer;
    MQTTAsync_disconnectOptions disc_opts = MQTTAsync_disconnectOptions_initializer;
    int rc;
    int ch;
    // 建立非同步連線客戶端,不使用 Paho SDK 內建的持久化來處理快取訊息
    if ((rc = MQTTAsync_create(&client, ADDRESS, CLIENTID, MQTTCLIENT_PERSISTENCE_NONE, NULL))
            != MQTTASYNC_SUCCESS)
    {
        printf("Failed to create client, return code %d\n", rc);
        rc = EXIT_FAILURE;
        goto exit;
    }
    // 設定非同步連線回撥,注意此處設定的回撥函式為連線層面的全域性回撥函式
    // conn_lost 為連線斷開觸發,有且只有連線成功後斷開才會觸發,在斷開連線的情況下進行重連失敗不觸發。
    // msgarrvd 收到訊息時觸發的回撥函式
    // msgdeliverd 是訊息成功傳送的回撥函式,一般設定為NULL
    if ((rc = MQTTAsync_setCallbacks(client, client, conn_lost, msgarrvd, msgdeliverd)) != MQTTASYNC_SUCCESS)
    {
        printf("Failed to set callbacks, return code %d\n", rc);
        rc = EXIT_FAILURE;
        goto destroy_exit;
    }
    // 設定連線引數
    conn_opts.keepAliveInterval = 20;
    conn_opts.cleansession = 1;
    // 此處設定 API呼叫失敗會觸發的回撥,接下來進行connect操作所以設定為 onConnectFailure 方法
    conn_opts.onFailure = onConnectFailure;
    // 此處設定 客戶端連線API呼叫成功會觸發的回撥,由於例程使用非同步連線的 API,設定了會導致2個回撥都被觸發,所以建議不使用此回撥
    //conn_opts.onSuccess = onConnect;
    // 注意第一次發起連線失敗不會觸發自動重連,只有曾經成功連線並斷開後才會觸發
    conn_opts.automaticReconnect = 1;
    //開啟自動重連,並且設定 2-16s 的隨機退避時間
    conn_opts.maxRetryInterval = 16;
    conn_opts.minRetryInterval = 2;
    conn_opts.context = client;
    // 設定非同步回撥函式,此與之前的 API 回撥不同,每次連線/斷開都會觸發
    MQTTAsync_setConnected(client, client, conn_established);
    MQTTAsync_setDisconnected(client, client, disconnect_lost);
    // 啟動客戶端連線,之前設定的 API 回撥只會在這一次操作生效
    if ((rc = MQTTAsync_connect(client, &conn_opts)) != MQTTASYNC_SUCCESS)
    {
        printf("Failed to start connect, return code %d\n", rc);
        rc = EXIT_FAILURE;
        goto destroy_exit;
    }

    ......
}
檢視 MQTTAsync_subscribe.c 詳細程式碼。

更多選擇:NanoSDK 內建重連策略

NanoSDK 是除了 Paho 以外的又一 MQTT SDK 選擇。NanoSDK 基於 NNG-NanoMSG 專案開發,使用 MIT License,對開源和商業都很友好。相較於 Paho 其最大的不同在於內建的全非同步 I/O 和 支援 Actor 程式設計模型,當使用 QoS 1/2 訊息時可以獲得更高的訊息吞吐速率。而且 NanoSDK 支援 MQTT over QUIC 協議,與大規模物聯網 MQTT 訊息伺服器 EMQX 5.0 結合可解決弱網下的資料傳輸難題。這些優勢使得它已經在車聯網和工業場景中得到了廣泛的使用。

在 NanoSDK 中,重連策略已經完全內建,無需使用者手動實現。

// nanosdk 採用自動撥號機制,預設進行重連
nng_dialer_set_ptr(*dialer, NNG_OPT_MQTT_CONNMSG, connmsg);
nng_dialer_start(*dialer, NNG_FLAG_NONBLOCK);

總結

本文介紹在 MQTT 客戶端程式碼實現過程中,重連邏輯設計的重要性與最佳實踐。透過本文,讀者可以設計更為合理的 MQTT 裝置重連程式碼,降低客戶端與伺服器端的資源開銷,構建更加穩定可靠的物聯網裝置連線。

版權宣告: 本文為 EMQ 原創,轉載請註明出處。

原文連結:https://www.emqx.com/zh/blog/mqtt-client-auto-reconnect-best-practices

相關文章