MQTT 協議 -- CONNECT & CONNACK

409253645發表於2019-09-11

概述

今天來學習MQTT協議中關於connect部分,connect是很重要的部分,因為它是Client 與MQTT Broker通訊的基礎,並且提供了很多很有用的特性,很多場景中都可以用到這些特性。

還是理論結合著實踐來講吧,否則擔心小夥伴們看了睡覺。~~~

前面已經講過了,MQTT是一種基於釋出訂閱的訊息傳輸協議,所以MQTT釋出客戶端可以釋出訊息到1個或多個訂閱客戶端。這個模式很像電視或者收音機的廣播,電臺釋出節目,千家萬戶的觀眾接收節目。所有的訊息都是發給MQTT Broker,MQTT Broker再將訊息轉發給它的訂閱者。在這個過程中,需要注意以下幾點:

  • 所有客戶端都有一個唯一Id,這個Id只是一個標記,不是客戶地址。釋出端釋出訊息時,只能發給某個topic,而不能將訊息發給某個地址或Id。
  • 客戶端的Id不能重複。如果有一個客戶端Client A連到MQTT Broker,如果之後又有一個客戶端Client B連到MQTT Broker,此時MQTT Broker會斷開Client A的連線。因為MQTT 客戶端有自動連線功能,Client A斷開連線之後,嘗試重新連線到MQTT Broker,此時Client B又會斷開,之後Client B又進行重連,然後兩個Client就會進入斷開-連線-斷開的死迴圈。
  • MQTT Broker負責接收訊息,並進行過濾,將訊息轉發給訂閱了相關主題的Client。
  • Publisher和Subscriber沒有直接的關聯。他們都只與MQTT Broker進行連線。
  • 客戶端既可以傳送訊息,也可以接收訊息。
  • 通常情況下,MQTT Broker是不儲存訊息的。

現在應該對MQTT Client和Broker有一個比較清楚的認識了。我們來討論MQTT Connect的格式吧。


CONNECT

Fixed Header

MQTT的固定頭部包含了首位元組和可變長度。其中首位元組的高4位(bit7~bit4)用於表示報文型別,1表示connect。其它標記位元組(bit3~bit0)都為0,如下表。

Bit 7 6 5 4 3 2 1 0
Byte 1 0 0 0 1 0 0 0 0
Byte 2... Remaining Length
Variable Header

在Connect報文中,可變頭部包含10個位元組,如下表:

Byte Description bit7 bit6 bit5 bi4 bit3 bit2 bit1 bit0
Byte 1 Length MSB (0) 0 0 0 0 0 0 0 0
Byte 2 Length LSB (4) 0 0 0 0 0 1 0 0
Byte 3 'M' 0 1 0 0 1 1 0 1
Byte 4 'Q' 0 1 0 1 0 0 0 1
Byte 5 'T' 0 1 0 1 0 1 0 0
Byte 6 'T' 0 1 0 1 0 1 0 0
Byte 7 Level(4) 0 0 0 0 0 1 0 0
Byte 8 Connect Flag User Name Password Will Retain Will QoS Will QoS Will Flag Clean Session Reserved
Byte 9 Keep Alive MSB 0 0 0 0 0 0 0 0
Byte 10 Keep Alive MSB 0 1 1 0 0 0 0 0

可變頭部的內容包含Protocol Name, Protocol Level, Connect Flags以及Keep Alive時間。下面分別介紹:

Protocol Name: 位元組1-6,這部分內容是固定的,其中位元組1和位元組2表示協議名稱長度,其內容是0x04。位元組3-位元組6表示協議名稱"MQTT"的UTF-8編碼。

Protocol Level: 位元組7,表示協議等級,MQTT 3.1.1協議版本的協議等級是4。

Connect Flag: 位元組8,連線標記,每一位都表示一個標記,Bit0是保留標記。從Bit1~Bit7,分別表示Clean Session, Will Flag等內容。這些標記確定了Payload是否包含對應的資訊。例如,如果Bit7和Bit6的值都為1,那麼表示此次連線的Payload中包含User Name和Password。後邊會分別介紹各個標記的作用。

Keep Alive: 位元組9和位元組10,客戶端與伺服器心跳間隔,高位元組在前,低位元組在後。單位是S,預設是60S。關於Keep Alive,需要注意的事項包括:

  • 當客戶端和伺服器之間沒有訊息傳輸時,客戶端會每隔60S(keep alive值)向MQTT Broker傳送PINGREQ資料包文。伺服器需要回復PINGRESP資料包文。
  • 如果客戶端在傳送PINGREQ資料包一段時間後沒有收到PINGRESP資料包,客戶端會斷開連線。
  • 如果Keep Alive的值設定為大於0(假設60S),在沒有資料互動的情況下,伺服器如果在超過1.5倍Keep Alive時間(90S)後沒有收到PINGREQ資料包,則伺服器會斷開與當前客戶端的連線。
  • Keep Alive可以設定為0,那麼客戶端不會傳送PINGREQ資料包,伺服器也不會因為沒有收到PINGREQ而斷開客戶端連線。

我們來測試一下Keep Alive,觀察PINGREQ和PINGRESP資料包。在命令列終端輸入以下命令,會得到相應結果:

$ mosquitto_sub -t topic001 -k 5 -d
Client mosqsub|16532-SCNWCL012 sending CONNECT
Client mosqsub|16532-SCNWCL012 received CONNACK (0)
Client mosqsub|16532-SCNWCL012 sending SUBSCRIBE (Mid: 1, Topic: topic001, QoS: 0)
Client mosqsub|16532-SCNWCL012 received SUBACK
Subscribed (mid: 1): 0
Client mosqsub|16532-SCNWCL012 sending PINGREQ
Client mosqsub|16532-SCNWCL012 received PINGRESP
Client mosqsub|16532-SCNWCL012 sending PINGREQ
Client mosqsub|16532-SCNWCL012 received PINGRESP
Client mosqsub|16532-SCNWCL012 sending PINGREQ
Client mosqsub|16532-SCNWCL012 received PINGRESP
複製程式碼

上面命令中,-t topic001表示訂閱topic為topic001的主題,使用 -k 5表示設定keep alive時間間隔為5S,-d表示啟用Debug模式。從輸出結果可以看到每隔5S,客戶端會傳送PINGREQ,並收到從伺服器返回的PINGRESP。

Connect Flag

因為Connect Flag的內容比較多,所以單獨用一小節來介紹。

Reserved

第8位元組的Bit0。保留位,必須為0。

Clean Session

第8位元組的Bit1,用於表示是否需要清除Session,如果值為0,表示保留Session,如果為1,表示清除Session。

Client和Broker可以儲存一些Session狀態資訊,用於訊息的可靠傳輸。我們知道MQTT是通過定義QoS等級來保證訊息的可靠傳輸的,所以Session狀態資訊中最重要的就是QoS訊息的狀態。以Broker為例,對於QoS為1或者2的訊息,如果Broker無法成功投遞訊息到Client A,那麼訊息狀態會保留下來,當下次Client A重新連線時,伺服器會根據Session狀態,重新投遞之前失敗的訊息。

所以,如果客戶端釋出或訂閱某個topic,並且設定了QoS > 0,那麼Clean Session必須設定為0。

Will Flag

第8位元組的Bit2,用於標記是否傳送遺願訊息。

遺願訊息是指當客戶端非正常斷開時,客戶端希望傳送一條訊息給一個指定的topic,通知對方自己掉線了。遺願訊息的一個應用場景是裝置掉線提醒,當裝置掉線後,訂閱方(通常是後臺伺服器)就知道裝置已經掉線了。

當Will Flag設定為1時,表示需要設定遺願訊息,那麼Broker會儲存遺願訊息並且在客戶端異常掉線之後,傳送遺願訊息到指定的topic。

如果Will Flag設定為1,那麼必須要有Will Topic(遺願主題)和Will Message(遺願訊息),並且Will QoS和Will Retain也會被讀取。遺願訊息也可以設定QoS的,這樣可以確保遺願訊息的可靠傳遞。

那麼什麼情況下Broker會傳送Will Message呢?當以下任何一種情況發生時,Broker會傳送遺願訊息。

  • 網路異常
  • 伺服器在Keep Alive超時後沒有收到PINGREQ
  • 客戶端在關閉連線之前,沒有先傳送DISCONNECT
  • 伺服器因為協議錯誤關閉客戶端連線

需要注意的是,如果客戶端正常關閉連線,在關閉連線之前傳送了DISCONNECT,遺願訊息是不會被髮送的。當客戶端傳送DISCONNECT請求後,遺願訊息會被Broker刪除。

Will QoS

第8位元組的Bit3和Bit4,用來設定遺願訊息的QoS,因為QoS有3個值,0,1和2,所以用兩位來表示,高位在前,低位在後。

Will Retain

第8位元組的Bit5,用於標記是否保留遺願訊息。

Will Retain這個標記也很有用,如果設定成1,當客戶端掉線後,之後所有新的訂閱者訂閱Will Topic時,都能收到遺願訊息。QoS > 0只能報紙之前訂閱過的訂閱者收到訊息,Will Retain能確保新的訂閱者也接收到訊息。

User Name Flag

用於標記Payload中是否包含User Name資訊。如果設定成1,payload中必須包含User Name資訊。

Password Flag

用於標記Payload中是否包含Password資訊。如果設定成1,payload中必須包含Password資訊。需要注意的是,如果User Name Flag設定成0,Password Flag必須設定成0。但是可以只包含使用者名稱,不包含密碼,所以User Name Flag設定成1時,Password Flag也可以設定成0.

Keep Alive

可變頭中第9和第10位元組用來表示Keep Alive時間。這個時間是MQTT的心跳時間,單位是秒,預設值是60S。在Client和Broker沒有資料互動的情況下,Client需要傳送PINGREQ給Broker,Broker回覆PINGRESP,用於檢測客戶端是否線上。關於Keep Alive,需要注意一下幾點:

  • 客戶端負責傳送心跳PINGREQ,服務端只管在接收到心跳時回覆PINGRESP。
  • 客戶端在傳送PINGREQ一段時間後,未收到回覆,客戶端將關閉連線。
  • 服務端在Keep Alive的1.5被時間之後,沒有收到客戶端的任何資料,包括PINGREQ,也會關閉連線。
  • Keep Alive可以設定為0,表示不啟用心跳機制,那麼客戶端,伺服器都不會因為未收到心跳或回覆而關閉連線。
示例

我們來看一個CONNECT報文的可變頭的例子,加深理解。

描述 7 6 5 4 3 2 1 0
位元組1 型別長度高位 (0) 0 0 0 0 0 0 0 0
位元組2 型別長度低位 (4) 0 0 0 0 0 1 0 0
位元組3 'M' 0 1 0 0 1 1 0 1
位元組4 'Q' 0 1 0 1 0 0 0 1
位元組5 'T' 0 1 0 1 0 1 0 0
位元組6 'T' 0 1 0 1 0 1 0 0
字7 Level (4)
位元組8 見備註 1 1 0 0 1 1 0 0
Keep Alive
位元組9 Keep Alive高位 0 0 0 0 0 0 0 0
位元組10 Keep Alive低位 0 0 0 0 1 0 1 0

以上位元組8,分別表示 User Name Flag (1),Password Flag (1),Will Retain (0),Will QoS (01),Will Flag (1),Clean Session (1),Reserved (0)

Payload

介紹完可變頭,我們來看訊息體。

CONNECT協議是包含訊息體的,其內容分別是Client Id/[Will Topic]/[Will Message]/[User Name]/[Password]。

以上型別順序不能變化,除了Client Id之外,其它內容都是可選的,只有Connect Flag中對應的值為1時,其Payload中才包含相關內容。如只有可變頭中CONNECT Flag部分,其Will Flag為1時,Payload中才會出現Will Topic和Will Message。

對於所有Payload中的內容,前兩位是長度,後面是資料內容。我們後邊會通過Wire Shark抓包來驗證這一點。

Client Id

Client Id是客戶端的唯一標識,這個Id不能重複,如果兩個客戶端使用了相同的Id,那麼就會出現互相踢對方的現象,如果你的客戶端一直在斷開-連線-斷開這樣的進行迴圈,就要考慮是否是Client Id重複了。關於Client Id,需要注意以下事項:

  • Client Id要唯一,並且最好能有一定意義並且可讀,如我們公司是使用<客戶端型別>_<裝置編號>作為Client Id。如前面提到的,雖然不能直接給Client Id發訊息,但是在問題排查時,Client Id還是有用的。如EMQX的後臺管理工具,可以直接根據Client Id判斷客戶端是否線上,以及追蹤某個Client Id傳送和接收的所有訊息。這對於線上排查問題很有幫助。
  • Client Id可以為空,如果為空,伺服器會自動分配一個Id,之後也會一直使用分配的Id,直到網路斷開。但是生產環境中,不建議這樣使用。
  • Client Id是和Session關聯的,所以如果你的專案中使用到了QoS > 0,那麼不能使用隨機的Client Id。
Will Topic & Will Message

當Will Flag設定成1時,Payload中就包含Will Topic和Will Message。Will Topic和Will Message已經講過,這裡不再贅述。

User Name & Password

如果這兩個Connection Flag被設定成1,那麼Payload中就包含User Name和Password。通過User Name和Password可以對Client進行身份驗證和授權。身份驗證可以決定是否允許客戶端連線,授權可以限制客戶端允許訪問某些資源(比如topic)。這個屬於Broker中客戶端管理的內容,我們後邊會介紹。


CONNACK

當客戶端傳送一個CONNECT報文後,伺服器需要回復一個CONNACK報文。如果客戶端傳送CONNECT給Broker,在一段時間內沒有收到CONNACK,那麼客戶端需要關閉當前連線。

Fixed Header

CONNACK固定頭如下:

Bit 7 6 5 4 3 2 1 0
Byte 1 0 0 1 0 0 0 0 0
Byte 2 0 0 0 0 0 0 1 0

再次回憶一下固定頭的格式,第一位元組高4位表示協議型別,2表示CONNACK,低4位是某些協議的標記位,對於CONNACK,這是保留欄位。

固定頭的後邊部分是Remaining Length,CONNACK的協議,其內容長度是2,用一個位元組表示。

Variable Header

可變頭包含兩個位元組,其格式如下:

Bit 7 6 5 4 3 2 1 0
Byte 1 0 0 0 0 0 0 0 X
Byte 2 X X X X X X X X

第一位元組Bit7-Bit1是保留位,Bit 0用於表示Session Present。

第二位元組是Return Code。我們分別介紹。

Session Present

Session Present用來表明服務端是否存在Session狀態。前邊講過,當Clean Session設定為0時,服務端會保留Session的一些狀態,當客戶端重新連線時,那麼伺服器會根據Client Id判斷是否存在Session Status,如果存在,該值為1,否則為0。關於Session Present,需要注意以下兩點:

  • 當客戶端發起連線並且設定Clean Session為1時,不管伺服器是否存在Session Status,Session Present總是為0.

  • 如果Broker返回的Return Code不為0,那麼Session Present必須為0.

Return Code

CONNACK可變頭的第二位元組表示Return Code,表明連線的返回狀態。0表示成功,其他數字表示異常,Return Code列表如下:

描述
0X00 正常
0X01 伺服器不支援協議當前協議版本
0X02 拒絕連線,Client Id沒有連線許可權
0X03 拒絕連線,服務端不可用
0X04 拒絕連線,使用者名稱密碼錯誤
0X05 拒絕連線,沒有授權
0X06-0XFF 保留

上邊0X02和0X05的區別,0X02是身份驗證拒絕連線,0X05是授權驗證失敗,拒絕連線。

Payload

CONNACK沒有Payload。


測試

講了這麼多,我們現在來抓包測試一下,驗證我們上面講述的內容。

  1. 開啟Wire Shark,監聽本地環回網路卡,過濾條件輸入tcp.port==1883

  2. 開啟終端,輸入以下命令,傳送一條訊息。

    mosquitto_pub -d -t topic1 --will-qos 2 --will-topic "will_topic" --will-payload "I'm offline!" -u "zengbiaobiao" -P "password" -m "Hello MQTT"
    複製程式碼
  3. 回到Wire Shark,檢視CONNECT資料包,如下圖。

mqtt-connect

當我們檢視CONNECT協議時,可以看到其詳細資料內容,

第一塊區域,顯示固定頭以及訊息長度,Msg Len: 85,之後是協議名稱長度以及協議名稱。

第二塊區域,顯示了CONNECT Flags,對應著我們傳送訊息是設定的引數。

第三塊區域,顯示了Payload的內容,每一項內容之前,都有兩位元組用於表示內容長度。


總結

今天介紹了MQTT中CONNECT 以及CONNACK協議型別。CONNECT協議型別包含協議頭,可變頭和訊息體。重點介紹了CONNECT Flags以及連線報文中的一些重要特性,這些特性包括Clean Session, Will Topic, Keep Alive, User/Password等等,這些特性都很適用。

另外我們也介紹了CONNACK,最後通過一個實驗進行抓包測試,驗證我們所講的內容。

如果你有


所有文章在Github上同步,你也可以訪問我的個人部落格點選檢視

相關文章