MQTT物聯網通訊協議入門及Demo實現

炒燜煎糖板栗發表於2022-04-19

一、MQTT協議概念

MQTT(Message Queuing Telemetry Transport,訊息佇列遙測傳輸協議),它是一個極其輕量級釋出/訂閱訊息傳輸協議,輕量級指的是較少的程式碼和頻寬。因為在物聯網行業有類似充電樁、娃娃機、遙控飛行器等等這樣的裝置,它們的網路可能存在不穩定的情況並且只需要傳輸少量的資料,MQTT就應運而生專為受限裝置和低頻寬、高延遲或不可靠的網路而設計。

釋出/訂閱機制

釋出/訂閱模型將傳送訊息的客戶端(釋出者)與接收訊息的客戶端(訂閱者)分離。釋出者和訂閱者從不直接聯絡。他們甚至不知道對方的存在,它們之間由一個第三方元件(代理)處理幫助篩選所有傳入訊息,並將其正確分發給訂閱者。訊息的釋出者和訂閱者都是客戶端,訊息代理是伺服器,訊息釋出者可以同時是訂閱者

image-20220416214035341

這個機制最重要的是將釋出者和訂閱者進行解耦

  1. 釋出者、訂閱者不需要交換埠知道對方的主機,只需要知道代理的主機和埠
  2. 釋出者、訂閱者不需要同時都執行,哪怕一方下線
  3. 釋出或接收期間,這兩個元件上的操作都不需要中斷

MQTT客戶端

釋出者和訂閱者都是客戶端,可以是裝置也可以是伺服器,簡單來說就是網路連線到MQTT代理的任何裝置

Broker代理(伺服器)

代理負責接收所有訊息、過濾訊息、確定誰訂閱了每條訊息,並將訊息傳送到這些訂閱的客戶端。代理還儲存具有持久會話的所有客戶端的會話資料,包括訂閱和丟失的訊息。代理的另一個職責是客戶端的身份驗證和授權。通常,代理是可擴充套件的,這有助於自定義身份驗證、授權和整合到後端系統中。

MQTT訊息結構

MQTT訊息包含三個部分:

  • 固定頭(Fixed header)

    image-20220417220241178

  • 可變頭(Variable header)

    image-20220417222930425

  • 訊息體(payload)

image-20220417222819163

二、MQTT協議實現原理

MQTT 客戶端需要連線到代理後立即釋出訊息,然後訂閱者從裡面訂閱資料,這裡涉及到六個部分:CONNECTPublishSubscribeUnsubscribeSUBACKUnsuback

MQTT連線

客戶端向代理髮送CONNECT訊息。代理響應一個CONNACK訊息和一個狀態碼。連線建立後,代理將保持連線開啟,直到客戶端傳送斷開連線命令或連線斷開

CONNECT訊息主要包含以下內容:

  • ClientId:代理使用ClientId來標識客戶端和客戶端當前狀態,對於每個客戶端和代理ClientId是唯一的

  • Clean Session:標誌告訴代理客戶端是否想要建立一個持久會話。如果為false代理會儲存客戶端的所有訂閱以及使用服務質量(QoS)級別1或2進行訂閱的客戶端的所有錯過的訊息。如果為true代理不為客戶端儲存任何內容,並清除以前任何持久會話中的所有資訊

  • Username/Password:使用者名稱和密碼用於客戶端身份驗證和授權。強烈建議使用者名稱和密碼與安全傳輸使用SSL證照驗證客戶端,因此不需要使用者名稱和密碼

  • Will Message:遺囑,當客戶端斷開連線時,此訊息通知其他客戶端

  • KeepAlive:客戶端指定並在連線建立時與代理通訊。這個間隔定義了代理和客戶端在不傳送訊息的情況下可以忍受的最長時間

  • LWT欄位:包含lastWillTopic、lastWillMessage、lastWillRetain、lastWillQos

    這個欄位可以幫助瞭解客戶端是正常斷開連線(使用 MQTT 斷開連線訊息)還是不正常斷開連線(沒有斷開連線訊息),檢測到客戶端已不正常地斷開連線。為了響應不正常的斷開連線,代理將最後一個將訊息傳送到最後一個將訊息主題的所有訂閱客戶端。如果客戶端使用正確的斷開連線訊息正常斷開連線,那麼代理將丟棄儲存的 LWT 訊息

代理收到 CONNECT 訊息時,返回連線確認標誌

MQTT訊息釋出

每條訊息都必須包含一個主題,代理可以使用該主題將訊息轉發給感興趣的客戶端

Publish訊息包含以下內容:

  • packetID:資料包識別符號在訊息在客戶端和代理之間流動時唯一標識訊息。資料包識別符號僅與大於零的 QoS 級別相關

  • topicName:主題名稱,主題區分大小寫

    主題格式就像URL:deviceName/1638791867

    1. +:表示任意匹配某一級主題,例如deviceName/+/weaved可以匹配deviceName/1638791867/weaved,但是無法匹配deviceName/1638791867/weaving
    2. #:表示匹配多級,例如deviceName/#可以匹配deviceName/1638791867/weaved
    3. $:是為 MQTT 代理的內部統計資訊保留的,客戶端無法向這些主題釋出訊息
  • QOS:服務級別質量,有3 個 QoS 級別

    1. 最多一次 (0)

      只會傳輸一次,不能保證對方一定會收到

      image-20220417161252851

    2. 至少一次 (1)常用

      至少保證對方能夠收到一次訊息,獲得接收方發來的 PUBACK資料包,如果傳送方在合理的時間內未收到 PUBACK 資料包,則傳送方將重新傳送 PUBLISH 資料包

      image-20220417161340386

    3. 正好一次 (2)

      QoS 2 是最安全、最慢的服務質量級別,由傳送方和接收方之間的至少兩個請求/響應流(四部分握手)提供。

      (1)、當接收方從傳送方獲取 QoS 2 PUBLISH 資料包時,它會相應地處理髮布訊息,並使用確認 PUBLISH 資料包的PUBREC 資料包回覆傳送方。如果傳送方未從接收方獲取 PUBREC 資料包,它將再次傳送帶有重複 (DUP) 標誌的 PUBLISH 資料包,直到收到確認。

      (2)、接收方收到 PUBREC 資料包,傳送方就可以安全地丟棄初始 PUBLISH 資料包。

      (3)、傳送方儲存來自接收方的 PUBREC 資料包,並使用PUBREL資料包進行響應

      (4)、接收方獲得 PUBREL 資料包後,它可以丟棄所有儲存的狀態並使用PUBCOMP資料包進行應答

      image-20220417162001595

    如果資料包在此過程中丟失,發件人負責在合理的時間內重新傳輸訊息

  • retainFlag:訊息是否由代理儲存為指定主題的最後一個已知正確值。當新客戶端訂閱某個主題時,它們會收到保留在該主題上的最後一條訊息

    保留的訊息可幫助新訂閱的客戶端在訂閱主題後立即獲取狀態更新,而不需要等到客戶端下一次推送訊息。保留的訊息消除了等待發布客戶端傳送下一個更新的時間

  • payload:訊息的實際內容包含影像,任何編碼的文字,加密資料以及二進位制的資料

  • dupFlag:標誌指示郵件是重複的,這個重複傳送跟QoS大於0的時候有關

客戶端將訊息傳送到 MQTT代理進行釋出時,代理將讀取訊息,確認訊息(根據 QoS 級別),並處理訊息。代理的處理包括確定哪些客戶端訂閱了主題並向它們傳送訊息

MQTT訂閱機制

MQTT客戶端傳送了訊息。如果沒人接收訊息將毫無意義,所以也會有客戶端來訂閱訊息,客戶端會向 MQTT 代理髮送一條 SUBSCRIBE訊息

Subscribe訊息包含以下內容:

  • packetID:資料包識別符號在訊息在客戶端和代理之間流動時唯一標識訊息。資料包識別符號僅與大於零的 QoS 級別相關

  • 訂閱列表:一個 SUBSCRIBE 訊息可以包含一個客戶端的多個訂閱,每個訂閱都由一個主題和一個 QoS 級別組成

MQTT訂閱確認

為了確認每個訂閱,代理向客戶端傳送 SUBACK確認訊息

SUBACK訊息包含以下內容:

  • packetID:資料包識別符號在訊息在客戶端和代理之間流動時唯一標識訊息
  • rerurnCode:每訂閱一個主題傳送一個返回程式碼
返回程式碼 返回程式碼響應
0 成功 - 最大 QoS 0
1 成功 - 最大 QoS 1
2 成功 - 最大 QoS 2
128 失敗

客戶端成功傳送 SUBSCRIBE 訊息並接收 SUBACK 訊息後,它將獲取與 SUBSCRIBE 訊息包含的訂閱中的主題匹配的每個已釋出訊息

MQTT取消訂閱

訊息可以訂閱那麼也可以取消訂閱,會刪除代理上客戶端的現有預訂

Unsubscribe訊息包含以下內容:

  • packetID:資料包識別符號在訊息在客戶端和代理之間流動時唯一標識訊息
  • List of Topic(主題列表):主題列表可以包含多個客戶要取消訂閱的主題。只需傳送主題

MQTT確認取消訂閱

要確認取消訂閱,代理會向客戶端傳送 Unsuback確認訊息

Unsuback訊息包含以下內容:

  • packetID:資料包識別符號在訊息在客戶端和代理之間流動時唯一標識訊息,這與取消訂閱訊息中的資料包識別符號相同

三、MQTT基本功能

持久會話

客戶端需要連線到代理並且訂閱主題,但是客戶端和代理之間如果連線在非持久會話中中斷,那麼主題會丟失,需要在重新連線時再次訂閱。為了避免這個問題可以使用持久會話功能,它主要是在代理中儲存了:

  • 客戶端的會話以及訂閱
  • QOS為1和2中沒有確認的訊息
  • 客戶端在斷聯時候錯過的訊息
  • 客戶端接收到的所有尚未完全確認的 QoS 2 訊息

為了開啟代理上的持久會話,在MQTT客戶端連線到代理伺服器的時候有個cleanSession欄位設定為false表示開啟持久會話,所有資訊和訊息都將保留,代理儲存會話,直到客戶端重新聯機並收到訊息,如果長時間不聯機,那麼會消耗記憶體

客戶端上的持久會話,當客戶端請求伺服器儲存會話資料時,客戶端負責儲存以下資訊:

  • QoS 1 或 2 流中尚未由代理確認的所有訊息
  • 從代理接收到的所有尚未完全確認的 QoS 2 訊息

四、MQTT Demo

搭建MQTT伺服器

官方文件:產品概覽 | EMQX 文件

EMQX (Erlang/Enterprise/Elastic MQTT Broker) 是基於 Erlang/OTP 平臺開發的開源物聯網 MQTT 訊息伺服器。

Erlang/OTP是出色的軟實時 (Soft-Realtime)、低延時 (Low-Latency)、分散式 (Distributed)的語言平臺。

MQTT 是輕量的 (Lightweight)、釋出訂閱模式 (PubSub) 的物聯網訊息協議。

EMQX 設計目標是實現高可靠,並支援承載海量物聯網終端的 MQTT 連線,支援在海量物聯網裝置間低延時訊息路由:

  1. 穩定承載大規模的 MQTT 客戶端連線,單伺服器節點支援 200 萬連線。
  2. 分散式節點叢集,快速低延時的訊息路由。
  3. 訊息伺服器內擴充套件,支援定製多種認證方式、高效儲存訊息到後端資料庫。
  4. 完整物聯網協議支援,MQTT、MQTT-SN、CoAP、LwM2M、WebSocket 或私有協議支援

使用Docker安裝EMQX

1、獲取Docker映象

docker pull emqx/emqx:4.4.3

image-20220419101256317

2、啟動Docker

docker run -d --name emqx -p 1883:1883 -p 8081:8081 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 emqx/emqx:4.4.3

3、訪問Web管理控制檯

控制檯地址: http://XXXXXX:18083,預設使用者: admin,密碼:public

image-20220419101524738

各個服務埠說明:
1883:MQTT 協議埠
8883:MQTT/SSL
8083:MQTT/WebSocket 埠
8080:HTTP API
18083:Dashboard 管理控制檯埠

搭建MQTT訊息推送客戶端

引入相關依賴包

  <dependencies>
        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
            <version>1.2.5</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <optional>true</optional>
        </dependency>
    </dependencies>

MQTT客戶端

import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.stereotype.Component;

/**
 * 訊息推送客戶端
 *
 * @author yanglingcong
 */
@Slf4j
@Component
public class MyMqttClient {

    private final static int QOS_1 = 1;

    private final static String USER_NAME = "ylc";

    private final static int PASSWORLD = 123456;

    private final static int KEEP_ALIVE = 60;

    /**
     * 連線地址
     * */
    public static final String HOST = "tcp://XXXXX:1883";

    /**
    * 訂閱主題
    * */
    public static final String TOPIC = "deviceName/";

    //客戶端唯一ID
    private static final String clientid = "pubClient";


    public static void main(String[] args) {
        MqttClient mqtt = createMqtt();
        publishMessage("Hello", TOPIC, mqtt);
    }

    public static MqttClient createMqtt() {
        MqttClient client = null;

        MqttConnectOptions connectOptions = new MqttConnectOptions();
        //斷開之後自動重聯
        connectOptions.setAutomaticReconnect(true);
        //設定會話心跳時間 代理和客戶端在不傳送訊息的情況下可以忍受的最長時間
        connectOptions.setKeepAliveInterval(KEEP_ALIVE);
        //不建立持久會話
        connectOptions.setCleanSession(true);
        //使用者名稱
        connectOptions.setUserName(USER_NAME);
        //密碼
        connectOptions.setPassword(String.valueOf(PASSWORLD).toCharArray());
        try {
            client = new MqttClient(HOST, clientid, new MemoryPersistence());
            //MQTT連線
            client.connect(connectOptions);
            //訊息回撥
            client.setCallback(new MqttCallBackHandle(client));
        } catch (MqttException e) {
            log.warn("MQTT訊息異常{}", e);

        }
        return client;

    }

    /**
     * 訊息推送
     *
     * @param message 訊息內容
     * @param topic   傳送的主題
     * @author yanglingcong
     * @date 2022/4/18 21:25
     */
    public static void publishMessage(String message, String topic, MqttClient mqttClient) {
        MqttMessage mqttMessage = new MqttMessage();
        mqttMessage.setQos(QOS_1);
        //保留在該主題上的最後一條訊息
        //mqttMessage.setRetained(true);
        mqttMessage.setPayload(message.getBytes());
        try {
            mqttClient.publish(topic, mqttMessage);
            log.info("MQTT訊息傳送成功:{}", message);
        } catch (MqttException e) {
            log.warn("MQTT訊息推送失敗");
            e.printStackTrace();
        }
    }

}

MQTT回撥介面

import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.MqttClient;

/**
 * MQTT訊息回撥方法
 */
@Slf4j
public class MqttCallBackHandle implements MqttCallbackExtended {


    private MqttClient client;

    public  MqttCallBackHandle(MqttClient client){
        this.client=client;
    }

    //訂閱主題
    private final static String CMD_TOP_FORMAT = "deviceName/";

    /**
     * 連線成功後呼叫該方法
     * @param reconnect
     * @param serverURI
     */
    @Override
    public void connectComplete(boolean reconnect, String serverURI) {
        try {
            //重新訂閱主題
            client.subscribe(CMD_TOP_FORMAT);
            log.info("=====MQTT重聯成功=====");
        } catch (MqttException e) {
            e.printStackTrace();
        }
    }

    /** 
     * 斷開連線後回撥方法
     * @param throwable
     */
    @Override
    public void connectionLost(Throwable throwable) {
        log.info("=====MQTT連線斷開=====");
    }

    /**
     * 接收訂閱到的訊息
     * @param topic
     * @param message
     * @throws Exception
     */
    @Override
    public void messageArrived(String topic, MqttMessage message) throws Exception {
        log.info("=====MQTT訊息訂閱成功=====");
        log.info("主題:{},內容:{}",topic,message);
    }

    /**
     * 傳送完成
     * @param iMqttDeliveryToken
     */
    @Override
    public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
        log.info("=====MQTT訊息傳送完畢=====");
    }
}

搭建MQTT訊息訂閱客戶端

import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.stereotype.Component;

/**
 * 訊息訂閱客戶端
 *
 * @author yanglingcong*/
@Component
@Slf4j
public class MyMqttSubClient {

    private final static int QOS_1 = 1;

    private final static String USER_NAME = "ylc";

    private final static int PASSWORLD = 123456;

    private final static int KEEP_ALIVE = 60;

    //連線地址
    public static final String HOST = "tcp://xxxx:1883";

    // 訂閱主題
    public static final String TOPIC = "deviceName/";

    //客戶端唯一ID
    private static final String clientid = "subClient";


    public static void main(String[] args) {
        subscribe();
    }

    public MyMqttSubClient() throws MqttException {
        //訂閱
        subscribe();
    }

    public  static void subscribe()  {
        MqttClient client=null;

        MqttConnectOptions connectOptions=new MqttConnectOptions();
        //斷開之後自動重聯
        connectOptions.setAutomaticReconnect(true);
        //設定會話心跳時間 代理和客戶端在不傳送訊息的情況下可以忍受的最長時間
        connectOptions.setKeepAliveInterval(KEEP_ALIVE);
        //不建立持久會話
        connectOptions.setCleanSession(true);
        //使用者名稱
        connectOptions.setUserName(USER_NAME);
        //密碼
        connectOptions.setPassword(String.valueOf(PASSWORLD).toCharArray());

        try {
            client=new MqttClient(HOST,clientid, new MemoryPersistence());
            //MQTT連線
            client.connect(connectOptions);

        } catch (MqttException e) {
            e.printStackTrace();
        }
        //訊息回撥
        client.setCallback(new MqttCallBackHandle(client));


        try {
            client.subscribe(TOPIC,QOS_1);
        } catch (MqttException e) {
            log.warn("MQTT訊息訂閱異常{}",e);
            e.printStackTrace();
        }
    }
}

環境測試

image-20220419115315933

1、MQTT客戶端pubClient向伺服器推送訊息

image-20220419120031352

2、MQTT客戶端subClient從伺服器訂閱訊息

image-20220419120043822

3、踢除客戶端,會自動重聯,因為設定了MQTT斷開自動重聯

image-20220419120159757

五、MQTT常見問題

MQTT訊息持久化

如果 cleanSession 設為true,一旦掉線客戶端不會儲存任何內容,並清除以前任何持久會話中的所有資訊

如果 cleanSession 設為false,重連後可以接收之前訂閱主題的訊息,還有離線時期未接收的訊息

MQTT訂閱恢復機制

MQTT掉線設定自動重聯之後,無法再進行訂閱。MqttCallbackExtended介面有一個connectComplete方法用於重新訂閱主題

MQTT和訊息佇列的區別

  • 訊息佇列可以儲存訊息,直到被消費為止

  • 訊息佇列只能被消費處理一次,不像MQTT訂閱的人都可以收到訊息

  • 訊息佇列需要先建立佇列,MQTT可以使用時候建立

  • MQTT是一種通訊協議,MQ是訊息通道

  • MQTT面向海量裝置連線、MQ是面向海量資料

相關文章