2018年9月11日 星期二

當 ESP8266 遇上中華電信 IoT 智慧聯網大平台 { 入門 - 04 } - 了解 MQTT 協議,學習如何發佈 MQTT 消息

網頁最後修改時間:2018/09/30

中華電信 IoT 智慧聯網大平台 ( 下面簡稱 平台 或 CHT IoT SP ) 的API 文件介紹可以知道,CHT IoT SP 支援 RESTful、MQTT 和 WebSocket 三種傳輸協定;關於 RESTful,已經在入門網頁 [01][02][03] 做過介紹。從這篇開始到接下來的幾篇,將以 MQTT 協議為主,先簡單的說明 MQTT 通訊所會用的控制封包的格式,再利用 ESP8266 做驗證,最後將這些結果配合 Arduino 開發板完成一個可以自動發佈消息的 MQTT 物聯網裝置。

此篇網頁以了解基本 MQTT 協議的控制封包格式為主,配合 Wireshark 抓取特定TCP 連線的封包為輔,直接由電腦發送不同的 MQTT 控制封包 (Control Packets):CONNECT (連接伺服器)、PUBLISH (發佈消息)、PINGREQ (心跳請求)、SUBSCRIBE (訂閱主題) 和 DISCONNECT (斷開伺服器) ... 等,得到完整的發送與接收格式 。利用這樣的方式可得到完整的客戶端 (Client) 與服務端 (Server) 之間一來一回的 Request 和 Response 控制封包格式,不但可用來直接與 MQTT 規格文件做對照來加速了解用法,而且寫程式的時候也可以直接套用。

在本篇的最後,用影片演示了 ESP8266 在 AT 指令的透傳模式下,如何與 CHT IoT SP 進行 MQTT 通訊並發佈消息,以此作為接下來撰寫程式的依據。
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
觀看此篇網頁前,建議先看過下面網頁!
  • ESP8266 AT 指令下的透傳模式,請自行參閱 [A] 和 [B]
  • CHT IoT SP 基本設置與說明,請自行參閱 [01][02] [03]
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*

軟體:

下面是網頁接下來會用到的工具軟體,對於 MQTT 開發上有很大的助益,更是學習的一大助手!
  • MQTT.fx Version 1.7.0
    MQTT 的發布與訂閱之用,用來取代前面幾篇網頁所使用的 MQTTlens;請安裝最新版本。
  • Wireshark ( 阿榮福利味下載可攜版 )
    網路封包分析工具
MQTT.fx 的用法在網頁中會說明,至於 Wireshark 在網頁中用到的部分只有其中一小部分,這一部分只要參考 Wireshark-基礎教學 ( 來自於政大資訊科學系 張鴻慶老師 "計算機網路課程" 的教學資料 ) PDF 文件的介紹就足夠,所以這裡不再特別贅述!

新建專案:

為了測試上的方便,可先在 CHT IoT SP 上建立一個新的專案。

新專案設置的 "權限資料" 頁面中,預設已先建立一個 admin 的權限,一般的情況下不需要用到如此高的權限,最多只需要讀和寫的權限即可,因此請手動設定一個並先記下來,下面測試時會用到;假設新增的 read_write 權限的內容為 PK4LQMHCHE1ZTQFQRW (此權限在網頁發佈後就會移除,使用時請自行更換為自己的權限內容)

專案存取權限設置
新專案中,新增一設備並設置三個感測器;感測器的名稱如欄位上方粉紅色字所示

專案設置完成畫面
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*
為了避免混淆,網頁中有一些名詞如下定義:

  • Client,稱為客戶端或裝置端,負責發送數據到遠端儲存的裝置或設備
  • Server,稱為伺服端或通訊端,負責接收數據(或儲存)的裝置或設備,這裡指的是 MQTT Broker
/*-/--*-*/*/*/*/***//-*-*-**-*/*-*-/*/*/*-*-/-////--/**/**--**/--///--//**----**//--**//**----***//*-**//*

平台 MQTT 協定格式:

專案設置小節裡,圖上的 ${PROJECT_KEY}${device_id} 和 ${sensor_id} 欄位值很重要,在跟 MQTT Broker 通訊時會用到,別忘了可到這裡找。

CHT IoT SP MQTT API 文件

一般使用 MQTT 軟體 ( 例如 MQTTLens 或 MQTT.fx 等 ) 的情況下,大概只需要 MQTT Broker IP 地址和 Port 號碼、使用者帳號和密碼、MQTT Topic ... 等資訊,就能執行連線 ( CONNECT )、斷線 ( DISCONNECT )、發佈 ( PUBLISH ) 和訂閱主題 ( SUBSCRIBE ) 的動作,而且也不用自己處理來自 MQTT Broker  ( CONNACKPUBACKSUBACK ... 等 ) 的回應或是裝置端與 MQTT Broker 的心跳請求 ( PINGREQ ) 和心跳響應 ( PINGRESP )。雖然對於初學者的門檻較低,但是也容易導致使用者知其然而不知所以然的情況出現,而且對於要撰寫 MQTT 通訊程式的使用者來說,遠遠不夠!只能做為測試通訊之用,需要再深入了解其控制封包格式才行。

另外,不是所有的 MQTT Broker 都使用相同版本的協議規範 (當然有些支援多版本),開始之前,要先知道 CHT IoT SP 支援什麼 ?
經過測試,CHT IoT SP 至少接受 MQTT v3.1 和 MQTT v3.1.1 兩種版本的協議規範。
由於 MQTTLens ( 平台/服務說明/如何開始 ) 採用 MQTT v3.1 版本協議規範,因此在這裡不使用它;原因是因為連線設定時,只要在 "Add a new Connection" 使用到某些欄位,與 CHT IoT SP 就別想連線 ( 其他的 MQTT Broker 沒試過 );但改用 MQTT.fx 就沒有這個問題。

MQTT.fx 支援 MQTT v3.1 和 v3.1.1 兩種協議規範,預設值是 MQTT v3.1.1 ( single-page HTML, PDF ) ,使用上與 MQTTLens 沒有太大不同,基本上都是以協議規範為主;但,越了解協議規範,越得心應手!

後續本文所談論的協議規範,都是以 MQTT v3.1.1 為依據。

之前在入門網頁 [1] [2],已分別說明了使用 RESTful HTTP GET 和 POST 取回與儲存數據的方式,現在換成了 MQTT 的方式,兩者有什麼不同 ? 也可以用同樣的手動方式來達到相同數據取回與儲存的目的嗎 ?

答案是:即使兩者通訊協議格式不同,但都可以使用入門網頁 [1] 和 [2] 所談及的方法,在 ESP8266 AT 指令下的透傳模式進行 MQTT 發佈、訂閱主題 ... 等的動作,只不過比較麻煩的是,需要先了解 MQTT 各種的控制封包格式 (如 CONNECTPUBLISHSUBSCRIBE ... 等 ) 才行,雖然這過程有點煩,但是不困難,有點耐心就行!

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* ESP8266 AT 指令下的透傳模式與 RESTful 和 MQTT 通訊步驟:

首先,必須先了解下圖中的設定參數。這些部分在之前的網頁已經說過,不同的是在右下方的參數部分;MQTT Broker 的 IP 地址沒變,但 Port 號碼要改成 1883。
ESP8266 AT 指令下的透傳模式需要的參數
有了需要的基本網路通訊參數之後,接下來的動作就是使用 AT 指令設置 ESP8266 進入到透傳模式 ( 就是下圖粉色框的部分 )。

同樣的 AT 指令順序,只差在 Port 號碼的不同,用來區隔接下來要使用的 MQTT 或是 RESTful 的通訊協議。雖然 MQTT 通訊協議相比下比較複雜,但兩者都可想像是一來一回的通訊方式,只不過 MQTT 定義較多的發送與接收的通訊協議方式而已。

ESP8266 AT 指令下的透傳模式與 RESTful 和 MQTT 通訊步驟
RESTfule HTTP 不管是 GET、POST、PUT 和 DELETE,都是發送 Request 格式,接收 Response 格式,只要單次往返就可以達到目的。

但對於 MQTT 來說,即便連上 MQTT Broker 進入透傳模式,是不能直接對其取回與儲存數據的。要達到這樣的目的,裝置端 ( Client ) 必須依照 MQTT 協議規範的控制封包格式 ( Control Packet Format ) 和順序才能與通訊端 ( Broker ) 進行溝通;依照不同的通訊目的,MQTT有其對應的控制封包,熟悉這些通訊控制封包格式 ( Control Packet Format ) 就是了解 MQTT 協議規範的不二法門,但這也是最困難的一部分!還好,我們有工具軟體 Wireshark 可以用,下面就會用到。

在繼續深入控制封包格式說明之前,我們先著眼在 MQTT 控制封包上,舉個 PUBLISH 的例子,簡單的來說明一下整個通訊流程。

下圖中,IoT Device ( 指的是 ESP8266,也就是裝置端 ) 進入到透傳模式後,要與 MQTT Broker ( 指的是 iot.cht.com.tw/1883,也就是通訊端 ) 開始通訊,IoT Device 必須先行傳送 CONNECT 控制封包 (封包就是一長串的無符號字元 (unsigned char) 陣列,MQTT Broker 收到後會回傳 CONNACK 控制封包;只有成功完成 CONNECT 發送與接收到 CONNACK 後,才能開始傳送其他的 MQTT 控制封包。

PUBLISH 控制封包包含了 MQTT Topic 和儲存數據的資料 (只考慮 QoS = 0 的情況 ),成功發送之後會收到 MQTT Broker 回應 PUBACK 的控制封包。

CONNECT 控制封包中會包含一個 2-byte 長度叫做 Keep Alive 的數值 ( 單位:秒 ),這個數值表示 IoT Device 最後一筆控制封包發送給 MQTT Broker 後,超過 Keep Alive 時間後必須發一次 PINGREQ 控制封包到 MQTT Broker 去的時間 ( 表示 IoT Device 還活著 ),然後接收 MQTT Broker 回應的 PINGRESP 控制封包才算 OK。

MQTT Broker 最晚能接受接收 PINGEREQ 的時間,一般為 1.5 倍的 Keep Alive 設定值,不過這不一定,有的 Broker 可更長。

DISCONNECT 控制封包,顧名思義就是斷開與 MQTT Broker 的連線 (但此時的 TCP 連線是還存在的 ),MQTT Broker 不回傳任何東西;要重新連線只要再重新發送 CONNECT 控制封包即可。
MQTT 裝置端 ( Client ) 到通訊端 ( Broker ) 通訊協議
/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* MQTT 控制封包格式說明:

這一部分應該在網路上可找到很多的參考資料,不管是協議規範或是二次說明。對於完全沒看過 MQTT 協議的初學者來說,要看懂 MQTT v3.1 或是 MQTT v3.1.1 協議規範就是個問題。這裡建議,兩個版本不管找到的是繁體、簡體、中文和英文都下載,盡量找有索引的看,版本間可以互相對照著,對於理解控制封包格式很有幫助。

MQTT 控制封包格式 ( Control Packet Format ) 基本結構,如下表格所示,分說如下:

不管是哪一種的類型的控制封包,其格式基本可劃分為三個部分:Fixed Header ( 固定標頭 ) + Variable Header ( 可變標頭 ) + Payload ( 訊息本體 ),除 Fixed Header 一定會有之外,後面兩個部分不一定有,整體長度 ( 2 ... 5 + Remaining Length ) 未定,要依不同功能位元 (bit) 指定後才知道 (下面會解釋)。

** Fixed Header ( 固定標頭 ):

Fixed Header 可再細分為兩個部分:MQTT Control Packet Type and Flags ( MQTT 控制封包類型與旗標設定 ),和 Remaining Length ( 剩餘長度 ) 兩個部分;前者占 1 個 byte ( 位元組),後者占 1 至 4 個 byte。

Control Packer Type 佔此位元組的高四位元 ( Bit 7 ... 4 ) ,而 Flags 佔此位元組的低四位元 ( Bit 3 ... 0 )。此位元組的設定照著下表依所使用的 Control Packet Type 設定即可,但像是 PUBLISH Flags 每個位元設定都有其意義,使用前就要先了解。

MQTT 控制封包格式 - Fixed Header 
Remaining Length 這個數值就有一點麻煩!它至少要 1 個 byte ,最多可擴展到 4 個 byte 的長度,主要是用來告知 MQTT Broker 剩下的 Variable Header + Payload 還有多少個 byte。所以對於一個完整的控制封包來說,整體的長度就是  [ 2 ... 5 bytes (Fixed Header) ] + [ n bytes (Variable Header + Payload) ],n bytes 就是 Remaining Length 所要表示的剩餘資料長度。

Remain Length 並不是直接將 n bytes 轉換為 16 進制得到,而是以循環除 128 的方式得到。所以對於 n < 128 bytes 範圍 0x00 ~ 0x7F 的 Remaining Length 只會佔據 1 個 byte,不需要額外再做計算。

不同長度的 Remaining Length 所佔的 bytes 大小與範圍如下所示:
  • 2-byte:127 < n ≤ 16,383
  • 3-byte:16,383 < n ≤ 2,097,15
  • 4-byte:2,097,151 < n ≤ 268,435,455
計算的方式在 MQTT 文件上面有範例,不過下面舉個使用 3 個 bytes 的 Remaining Length 的例子,假設如下:
  1. 固定標頭 (Fixed Header) 的 MQTT 控制封包類型與旗標設定 (MQTT Control Packet Type and Flags):1 bytes
  2. 可變標頭 (Variable Header):30 bytes
  3. 訊息本體 (Payload):16,488 bytes
  4. 固定標頭 (Fixed Header) 的剩餘長度 (Remaining Length):???
根據前面所說的,可以知道 Remaining Length = Variable Header + Payload 等於 16,518 bytes,但為了方便 MQTT Broker 知道需要接收的長度,所以每個表示 Remaining Length 的最高位元 ( bit 7 ),用來代表後面是否還是 Remaining Length 位元組,所以每個位元組能代表長度的只有 128 ( bit 6 - 0 ),所以若是 n = 127 bytes,則 Remaining Length 這個欄位只會佔用 1 個 byte,內容值為 0x7F;對於 CONNECT 來說,Fixed Header = 0x01 0x7F

再舉個例子:若 Remaining Length 為 188,則此數值會需要 2 個 byte

188 = 60 + 1 * 128 = 0x3C + 0x01 * 128

Remaining Length = (0x80 OR 0x3C) (0x01) = 0xbc 0x01

Fixed Header = 0x30 0xbc 0x01 (for PUBLISH, DUP=QoS level=RETAIN=0)

以上面所提有 3 個 bytes 的例子,此 Remaining Length 為 16,390 bytes

16,518 = 6 + 1 * 128 + 1 * 128 * 128 = 0x06 + 0x01 * 128 + 0x01 * 128 * 128

Remaining Length = (0x80 OR 0x06) (0x80 OR 0x01) (0x01)= 0x86 0x81 0x01

Fixed Header = 0x30 0x86 0x81 0x01 (for PUBLISH, DUP=QoS1=QoS0=RETAIN=0)

因為 Remain Length 位於封包前面的位置,所以除非已能事先限定整個接下來 Variable Header 和 Payload 的長度,否則只能在最後才能知道剩餘的長度值。另外,若是不要一次傳送多個 MQTT Topic 或是很大的資料量 (例如,圖片和音視訊),其實是可以藉由限定發送的長度來限制 Remaining Length 所佔據的字元組數目;這對於記憶體比較小的 MCU 是很有用的。

** Variable Header & Payload (可變標頭和訊息本體):

可變標頭和訊息本體的內容根據控制封包類型的不同而不同,但並非每一個控制封包類型都有,而且其內部還會包含封包類型需要的次級項目,這些次級項目依照封包類型的規定有其一定的順序 (必須依照此順序),但不一定全部都有 (不需要的就直接忽略掉),結合完整三個部分的內容就能建立出特定完整的控制封包格式。

這部分所牽扯到的東西比較多,非三言兩語就能解釋清楚,一開始不容易理解這是必然,但是只要借助 Wireshark 抓取 MQTT.fx 與 CHT IoT SP 間的通訊封包後,對照文件中的說明很容易就能夠理解箇中用意!

/*--*//**---/*///**---*-*////***--*/*///***----*///--*/*///**--*/*//**--**/*//
* 抓取 MQTT 通訊封包

要抓取 MQTT.fx 與 CHT IoT SP 通訊的 MQTT 控制封包,可以使用 Wireshark。但為了能夠確實抓取到需要的部分,要先設定通訊封包過濾的規則,只留下真正與 CHT IoT SP 通訊的部分,其他的則一律捨棄掉!

使用管理者權限開啟 Wireshark,...use this filter: 欄位中輸入

host iot.cht.com.tw && port 1883

完成後按下 ENTER,就會進入到 Wireshark 的封包抓取的畫面

Wireshark 過濾器設定
一般情況下,此時的 Wireshark 不會抓取到任何資訊在畫面上,必須要有 MQTT 的通訊才會有

Wireshark 設定 Filter 後的畫面
接著打開 MQTT.fx,按下 "connect" 按鈕旁邊的齒輪圖示,或是選取選單 "Extras / Edit Connection Profiles" 開啟 MQTT 連線設定視窗,視窗下方有兩個頁面 ( "General""User Credentials" ) 需要一起設定。

屬於視窗上方的部分:Profile Name 是這個連線設定檔的名稱,可以自己設定;Client ID 除了可自己設定,也可以使用旁邊的 "Generate" 按鈕產生,這是一個唯一的 ID,測試方便的緣故,所以也可直接設定跟下圖一樣;其他的部分照著填就可以,因為這些不能自己定義。

視窗下方分為兩個部分,如下圖為 "General" 頁面。Keep Alive Interval 這裡的數值可以自己設定,最長可以為 65,535 秒 = 18 小時 12 分鐘 15 秒,不清楚就使用預設值就行;MQTT Version 是使用的協議規範版本,這裡使用預設的 v3.1.1 版本即可;其他的部分可以不需要再做修改,除非自己知道這些欄位的定義為何。

MQTT.fx 連線設定 - 01
 另一個需要設定的頁面 "User Credentials",主要是用來設定連線的 User NamePassword ,這兩個欄位值都是使用相同的 ${PROJECT_KEY} 來填入就可以,其他下半部的頁面就採用預設值,不需要再額外做設定。

完成設定之後按下 "OK" 回到主畫面下

MQTT.fx 連線設定 - 02
** MQTT 發佈感測資料格式:

以發佈一個字串 "hELLo" 到 SayHello 感測器為例。

要發佈資料到 MQTT Broker,先查閱一下 CHT Iot SP API 文件中的說明

平台 MQTT 發佈感測資料格式
Topic: /v1/device/${device_id}/rawdata
Message: 修改為下面樣式 (資料只顯示,不儲存在 CHT IoT SP )
[{"id":"SayHello","save":false,"value":["hELLo"]}]

${device_id} 是專案設備編號,要使用自己建立的;在這裡用的是 7581817213

接著於 MQTT.fx 主畫面按下 "Connect" 與 MQTT Broker 連線 ( 這時 Wireshark 畫面會開始出現抓取的封包,先不用管它 ),輸入上面的資料 (如下圖畫面) 再按下 "Publish"


PUBLISH 感測資料到 SayHello 感測器
如果成功傳送的話,平台上面的 SayHello 感測器上面的值就會更新為下圖這樣

MQTT PUBLISH SayHello 感測器值的結果
等待 PINGREQ 訊息出現之後,等待 14 秒後再按下一次 "Publish",再繼續等待 PINGREQ 出現後按下 "Disconnect" 結束測試。

打開 Wireshark 並按下左上方紅色方形按鈕,停止封包抓取!為了讓畫面的資料更加的簡潔只出現 MQTT 相關的通訊,所以上方 Apply a display filter ... <Ctrl-/> 欄位輸入 mqtt ,就會出現跟下圖類似的畫面顯示

MQTT 通訊封包抓取結果
這裡面包含了使用 MQTT.fx 與平台 MQTT Broker 之間 CONNECT、CONNACK、PUBLISH、PUBACK、PINGREQ、PINGRESP 和 DISCONNECT 的完整通訊封包格式,而且每個格式裡面的欄位,不管是位元組 (byte) 或位元 (bit) 個別的含意也在中間區域解釋的非常清楚。所以經由這些抓取到的封包資訊可以清楚了解,在不同的 MQTT.fx 欄位參數設定下的 MQTT 控制封包格式字串如何組合產生,在撰寫 MQTT 程式時才能正確地在問題中找出癥結所在!

或許到這裡有人會問:「用函式庫就好!為什麼要深入去了解控制封包格式裡面的意思?」

原因是,下篇網頁開始,我們不會採用 MQTT 函式庫,而是直接像在撰寫 RESTfule API 程式的那三篇入門網頁一樣,都是進入到 ESP8266 AT 指令下的透傳模式,直接來處理發送 MQTT 控制封包格式和接收,所有的 MQTT 控制封包格式都要自己建立和接收處理,所以必須要懂!

下面開始解析 Wireshark 抓取的 MQTT 控制封包格式來做說明,有不詳細的部分,請用戶自行參閱 MQTT 協議規範文件。

** MQTT CONNECT 控制封包格式:

照前面的格式說明,CONNECTFixed Header = 0x01 ????;???? 表示還未知,需要先得知 Variable Header 和 Payload 字元組數量才能計算。

Variable Header = [ Protocol Name (6-byte) ] + [ Protocol Level (1-byte) ] + [ Connect Flags (1-byte) ] + [ Keep Alive (2-byte) ]
  • Protocol Name: 協議名稱;包含兩個字元的協議名稱長度位元組,和四個協議名稱的位元組,完整字串為 0x00 0x04 0x4d 0x51 0x54 0x54(黑體代表 MQTT)
  • Protocol Level: MQTT 版本;對於 MQTT v3.1.1 協議規範來說,此位元組值為 0x04
  • Keep Alive: 發送 PINGREQ 的間隔時間;此時間佔用兩個位元組,依所設定的時間值 (60),完整字串為 0x00 0x3c
Connect Flags 位元組定義如下:

MQTT CONNECT - Control Flags byte
由於與平台 MQTT Broker 連線時需要帳號與密碼,故 Bit 7 和 6 必須設定為 1,並且為了開始一個新會話需要丟棄之前的任何會話,Clean Session Bit 1 必須設定為 1;其他的欄位暫時沒有用到,所以全部設為 0 即可 ( 欲知其他位元欄位設定所代表的詳細意思,請參閱協議規範文件的解釋或自行使用軟體做測試 ),所以 Control Flags 的完整字串為 0xc2

到此,CONNECT 封包格式 = 10 ???? 00 04 4d 51 54 54 04 c2 ...

Payload = [ Client ID ] + [ Will Topic ] + [ Will Message ] + [ User Name ] + [ Password ]

根據 Control Flags 的設定,Payload 可省略為

Payload [ Client ID ] + [ User Name ] + [ Password ]
  • Client ID: 可自行定義或是產生,前面 2-byte 表示後面的 Client ID 佔多少位元組;這裡採自行定義的值 ( MQTT_FX_Client),完整字串為 0x00 0x0e 0x4d 0x51 0x54 0x54 0x5f 0x46 0x58 0x5f 0x43 0x6c 0x69 0x65 0x6e 0x74
    (黑體代表 MQTT_FX_Client)
  • User Name 或 Password: 使用者帳號和密碼,前面 2-byte 表示後面的  User Name 或 Password  佔多少位元組;兩者都是使用 ${PROJECT_KEY} = PK4LQMHCHE1ZTQFQRW,完整字串都是 0x00 0x12 0x50 0x4b 0x34 0x4c 0x51 0x4d 0x48 0x43 0x48 0x45 0x31 0x5a 0x54 0x51 0x46 0x51 0x52 0x57 (黑體代表 PK4LQMHCHE1ZTQFQRW)
所以由上面的 Variable HeaderPayload,可以來算算總共用了多少 bytes ?

Variable Header 使用 10 bytes (6+1+1+2),Payload 使用 56 bytes ( (2+14)+(2+18)+(2+18) ),兩個部分共使用 66 bytes,因為沒超過 127 bytes,所以 Fixed HeaderRemaining Length 要填入 0x42

所以整合上面的資料後,可得到一個完整的 CONNECT 控制封包格式

[Fixed Header] [Varible Header] {[Client ID][User Name][Password]}

10 42 00 04 4d 51 54 54 04 c2 00 3c 00 0e 4d 51 54 54 5f 46 58 5f 43 6c 69 65 6e 74 00 12 50 4b 34 4c 51 4d 48 43 48 45 31 5a 54 51 46 51 52 57 00 12 50 4b 34 4c 51 4d 48 43 48 45 31 5a 54 51 46 51 52 57

在 Wireshark 最上方畫面點選 Connect Command,中間部份選擇並打開 MQ Telemetry Transport Protocol, Connect Command ,在最下面的畫面就會出現與上面相同的結果

MQTT CONNECT 控制封包格式抓取結果
除了 DISCONNECT 控制封包的發送不會收到 MQTT Broker 的回應之外,其他的都會收到。下面來看看發送 CONNECT 控制封包後,MQTT Broker 所回應的 CONNACK 控制封包所代表的意思。

** MQTT CONNACK 控制封包格式:

照前面的格式說明,CONNACK 的 Fixed Header = 0x20 0x02

Variable Header = [ Connect Acknowledge Flags (1 byte) ] + [ Connect Return code (1 byte) ]

Connect Acknowledge Flags (連線確認旗標) 位元組定義如下 (只需要考慮 Bit 0):

MQTT CONNACK - Connect Acknowledge Flags
Connect Return code (連線返回碼) 位元組定義如下:

MQTT CONNACK - Connect Return code
依照 MQTT CONNECT - Control Flags 位元組中 Clean Ssession 位元設定值,這兩個位元組可以一起解釋:
  • Clean Session = 0
    若 MQTT Broker (伺服端) 已保存了 Client ID 對應的裝置端會話狀態,接收的 CONNACK 控制封包中的旗標 SP (Session Present,當前會話) 會被設置為 1;否則,接收的 CONNACK 控制封包中的旗標 SPConnect Return code (返回碼) 都會是 0。
  • Clean Session = 1
    接收的 CONNACK 控制封包中的旗標 SP 和 Connect Return code (返回碼) 都會是 0。
如果 MQTT Broker 要發送的 CONNACK 控制封包中的 Connect Return code (返回碼) 不是 0,那麼 MQTT Broker 發送時必須將 SP 設置為 0,接收才是正確。

所以 Variable Header = 0x00 0x00

由於 CONNACK 不需要 Payload 部分,所以對於 Clean Session = 1 且與 MQTT Broker 連線成功;依照上面的描述,則完整的 CONNACK 控制封包格式為

[Fixed Header] [Varible Header]

20 02 00 00

在 Wireshark 最上方畫面點選 Connect Ack,中間部份選擇並打開 MQ Telemetry Transport Protocol, Connect Ack ,在最下面的畫面就會出現與上面相同的結果

MQTT CONNACK 控制封包格式抓取結果
** MQTT PUBLISH 控制封包格式:

PUBLISH 同時可由裝置端或是通訊端發送;當裝置端向通訊端訂閱一個主題時,通訊端的主題內容有了變更,便會向裝置端發送 PUBLISH 控制封包傳遞相關變更數據,然後接收端再回覆一個 PUBACK 的控制封包回去 (這裡用的是 QoS = 0)。

PUBLISHFixed Header 如下表所示

MQTT PUBLISH - Fixed Header
首先,先解釋 byte 1 低四位元的意思
  • DUP flag: 是否為副本;若為 0,表示這是裝置端或是通訊端第一次發送這個 PUBLISH 控制封包;若此位元被裝置端或是通訊端設置為 1,表示這可能是一個之前控制封包請求的重發。當 QoS level 被設定為 0 時,DUP flag 也必須設置為 0。
  • QoS level: 傳輸品質;表示裝置端與通訊端之間的的傳輸品質,但若接收到這兩個位元組都為 1 的情況時,不管是裝置端或是通訊端都必須關閉網路連線。
MQTT PUBLISH - QoS definitions
關於 QoS level 更詳細的解釋、說明與用法,請參考這一篇網頁中的內容,就不再贅述!
  • RETAIN: 是否保留;若裝置端發送給通訊端的 PUBLISH 控制封包中 RETAIN 位元設定為 0,則通訊端將不能儲存這個訊息,也不能移除或是取代任何現有的保留訊息;反之,則必須儲存這個訊息和 QoS level,以便可以在未來分發與主題名相吻合的訂閱者。
Variable Header = [ Topic Name ] + [ Packet Identifier ]
  • Topic Name: 主題名稱;包含兩個字元的主題名稱長度位元組,和 n 個主題名稱的位元組,完整字串為
    00 1d 2f 76 31 2f 64 65 76 69 63 65 2f 37 35 38 31 38 31 37 32 31 33 2f 72 61 77 64 61 74 61
    ( 黑體代表 /v1/device/7581817213/rawdata )
  • Packet Identifier: 封包識別號;QoS = 0,沒有 Packet Identifier
Payload = [ Application Message  ]
  • Application Message: 採用 JSON 格式發佈的 Topic 訊息的部分,完整的字串為
    5b 7b 22 69 64 22 3a 22 53 61 79 48 65 6c 6c 6f 22 2c 22 73 61 76 65 22 3a 66 61 6c 73 65 2c 22 76 61 6c 75 65 22 3a 5b 22 68 45 4c 4c 6f 22 5d 7d 5d
    ( 黑體代表 [{"id":"SayHello","save":false,"value":["hELLo"]}] )

所以由上面的 Variable Header 和 Payload,可以來算算總共用了多少 bytes ?

Variable Header 使用 31 bytes (2+29),Payload 使用  52 bytes,兩個部分共使用 81 bytes,因為沒超過 127 bytes,所以 Fixed Header 的 Remaining Length 要填入 0x51

所以整合上面的資料後,可得到一個完整的 CONNECT 控制封包格式

[Fixed Header] [Varible Header] {           Payload            }

[Fixed Header] [  Topic Name  ] { Application Message }

30 51 00 1d 2f 76 31 2f 64 65 76 69 63 65 2f 37 35 38 31 38 31 37 32 31 33 2f 72 61 77 64 61 74 61 5b 7b 22 69 64 22 3a 22 53 61 79 48 65 6c 6c 6f 22 2c 22 73 61 76 65 22 3a 66 61 6c 73 65 2c 22 76 61 6c 75 65 22 3a 5b 22 68 45 4c 4c 6f 22 5d 7d 5d

在 Wireshark 最上方畫面點選 Publish Message,中間部份選擇並打開 MQ Telemetry Transport Protocol, Publish Message ,在最下面的畫面就會出現與上面相同的結果

MQTT PUBLISH 控制封包格式抓取結果
** MQTT PUBACK 控制封包格式:

通訊端會根據 PUBLISH 控制封包的 QoS level 回覆相對應的 PUBACK  控制封包,並表示在 Variable HeaderPacket Identifier 欄位

PUBACK - Expected Publish Packet response
因網頁使用 QoS = 0,因此通訊端不須回應,裝置端也不用理會與處理;但實際發送 PUBLISH (with QoS=0)  到 CHT IoT SP MQTT Broker 卻是會收到通訊端的 PUBACK 回覆 (with Packet Identifier = 0x00 0x00),不理會或是要處理都可以。

若是選擇要處理,則

Fixed Header 是固定格式,依據前面表格可得完整字串為 0x40 0x02

Variable Header 只有 Packet Identifier,所以完整字串為 0x00 0x00

由於 PUBACK 不需要 Payload 部分,依照上面的描述,完整的 PUBACK 控制封包格式為

[Fixed Header] [Varible Header]

40 02 00 00

在 Wireshark 最上方畫面點選 Publish Ack,中間部份選擇並打開 MQ Telemetry Transport Protocol, Publish Ack ,在最下面的畫面就會出現與上面相同的結果

MQTT PUBACK 控制封包格式抓取結果
若現在的情況是我們想要訂閱一個主題,當裝置端發送一個 SUBSCRIBE (with QoS=0) 給通訊端後,通訊端除了會回覆 SUBACK 之外,會再發送一個包含訂閱主題感測器資料的 PUBLISH 控制封包給裝置端;由於 QoS=0,所以裝置端可以不回應 PUBACK

如果對此有疑問的話,可以實際動手自己試試!詳細的說明,會在其他接續的網頁中再度提及。

** MQTT PINGREQ 和 PINGRESP 控制封包格式:

PINGREQ 對於 MQTT 協議來說並不一定要有,取決於裝置端發送的間隔時間與 Keep Alive 時間差距;PINGRESP 則是通訊端的回覆。

隨便取一個上方 Wireshark 抓取結果圖就可得知,Keep Alive 的時間設定為 60 秒,只要裝置端在 60 秒鐘沒有發送任何控制封包,那麼裝置端就要發送一次 PINGREQ 控制封包到通訊端,否則最晚延遲 1.5 倍的時間後就會被通訊端關閉網路連線。而且不管在任何時候,裝置端都可以發送 PINGREQ 控制封包給通訊端,並且用 PINGRESP 判斷網路與和通訊端的狀態。

這兩個控制封包都沒有 Variable HeaderPayload 的部分,而且 Fixed Header 位元組也沒有任何位元需要再額外設定,所以控制封包內容都是固定值

完整的 PINGREQ 發送控制封包格式為

[Fixed Header]

c0 00

完整的 PINGRESP 控制封包格式為

[Fixed Header]

d0 00

在 Wireshark 最上方畫面點選 Ping Request 或 Ping Response,中間部份選擇並打開 MQ Telemetry Transport Protocol, Ping Request 和  Ping Response,在最下面的畫面就會出現與上面相同的結果

MQTT PINGREQ 控制封包格式抓取結果
MQTT PINGRESP 控制封包格式抓取結果
** MQTT DISCONNECT 控制封包格式:

DISCONNECT 是從裝置端發送給通訊端的最後一個控制封包,表示裝置端正常斷開連接。

這個控制封包只有 Fixed Header 的部分,而且通訊端也不需要回覆任何訊息。

裝置端發送 DISCONNECT 控制封包後,必須關閉網路連線 ( TCP Connection ),而且不能再通過那個網路連線發送任何控制封包。

完整的 DISCONNECT 發送控制封包格式為

[Fixed Header]

e0 00

在 Wireshark 最上方畫面點選 Disconnect Req,中間部份選擇並打開 MQ Telemetry Transport Protocol, Disconnect Req ,在最下面的畫面就會出現與上面相同的結果

MQTT DISCONNECT 控制封包格式抓取結果

以上就是關於與 CHT IoT SP 之間採用 MQTT 協議規範 Publish Message (訊息發佈) 的過程與 MQTT 控制封包的格式說明。

有了這些資訊,實際用 ESP8266 來跑跑看,看在 AT 指令下如何發送 MQTT 控制封包。

ESP8266 AT 指令下的 MQTT 控制封包發送與接收:

*********************************************************************************
這一小節所用的材料可自行準備,或選用新版本的升級套件
更多 ESP8266 相關商品,請至分類賣場
*********************************************************************************

我將一整個操作的過程做成影片。

影片一開始會先使用 Postman 清除 CHT IoT SP 上 SayHello 的感測資料,然後將 ESP8266 與電腦做 UART 通訊;藉由一連串的 AT 指令發送,ESP8266 就會與 CHT IoT SP 的 MQTT Broker 處於透傳模式下。在此模式下,所鍵即所傳,所見即所收!

MQTT 的控制封包格式裡面包含一般 ASCII 碼或是非字元碼,因此 (不管是使用何種軟體) 都要先將發送與接收的字元格式切換為 16 進位格式,否則發送會出現錯誤,而且也看不到通訊端的回覆。

為了要發佈資料到 SayHello,首先要做的就是先發送 CONNECT 控制封包 ( 會收到 20 02 00 00 的 CONNACK 回覆 ),發送之後就能 PUBLISH 資料出去 (這時 SayHello 感測資料顯示為 hELLo,並收到 40 02 00 00PUBACK 回覆 )。

一般情況之下若是發佈的時間小於 Keep Alive 所設定的時間值,是不用額外發送 PINGREQ,不過在這裡我們同樣演示一下當發送 PINGREQ 控制封包出去後,通訊端就會回傳 D0 00PINGRESP 控制封包回來,用來表示雙方的通訊還是存在的。

最後就是斷開與 MQTT 的連結;此時要發送 DISCONNECT 控制封包給通訊端。不過這並不會得到任何來自通訊端的回覆,裝置端與通訊端的 TCP 連線這時還是存在的,裝置端必須自行關閉與 MQTT Broker 的 TCP 連線。這就如影片中所下的 AT 指令 AT+CIPCLOSE,這樣才是真正斷開。

只有真正的將這些操作過程實際走過一遍,才能深入了解 MQTT 協議。


結論:

對於 MQTT,本篇網頁只是蜻蜓點水、引領入門而已!

若是使用封裝好的函式庫或是軟體,其實大可不必了解到控制封包的格式組成到如此詳細的地步,只需要了解控制封包中參數對於 MQTT 通訊的影響為何即可。

本篇網頁並未對 MQTT SUBSCRIBE (訂閱主題) 相關控制封包或 QoS > 0 的部分著墨,大部分的原因是那一部分的程式還未撰寫,所以就先不在這裡說明了,待日後弄好之後,會在這裡或是其他篇網頁再做補充。

接下來的網頁,就讓我們開始來撰寫 MQTT PUBLISH 的程式吧!


<< 部落格相關文章 >>

2 則留言:

  1. 請問大大 是否任何nb-iot的 modem卡在完成連線設定之後‘在沒有相關的mqtt函式庫之下(例如 PubSubClient.h)
    都要撰寫如此繁複的通訊程式 才能控制通訊晶片完成 publish & subscribe的收發動作

    回覆刪除
    回覆
    1. 對!
      PubSubClient.h 就是把一些底層的動作包裝起來用,實際去看它的程式碼,也會是類似的東西。

      刪除

留言屬名為"Unknown"或"不明"的用戶,大多這樣的留言都會直接被刪除掉,不會得到任何回覆!

發問問題,請描述清楚你(妳)的問題,別人回答前不會想去 "猜" 問題是什麼?

不知道怎麼發問,請看 [公告] 部落格提問須知 - 如何問問題 !