MQTT 協議 -- CONNECT & CONNACK
概述
今天來學習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。
測試
講了這麼多,我們現在來抓包測試一下,驗證我們上面講述的內容。
-
開啟Wire Shark,監聽本地環回網路卡,過濾條件輸入tcp.port==1883
-
開啟終端,輸入以下命令,傳送一條訊息。
mosquitto_pub -d -t topic1 --will-qos 2 --will-topic "will_topic" --will-payload "I'm offline!" -u "zengbiaobiao" -P "password" -m "Hello MQTT" 複製程式碼
-
回到Wire Shark,檢視CONNECT資料包,如下圖。
當我們檢視CONNECT協議時,可以看到其詳細資料內容,
第一塊區域,顯示固定頭以及訊息長度,Msg Len: 85,之後是協議名稱長度以及協議名稱。
第二塊區域,顯示了CONNECT Flags,對應著我們傳送訊息是設定的引數。
第三塊區域,顯示了Payload的內容,每一項內容之前,都有兩位元組用於表示內容長度。
總結
今天介紹了MQTT中CONNECT 以及CONNACK協議型別。CONNECT協議型別包含協議頭,可變頭和訊息體。重點介紹了CONNECT Flags以及連線報文中的一些重要特性,這些特性包括Clean Session,Will Topic,Keep Alive,User/Password等等,這些特性都很適用。
另外我們也介紹了CONNACK,最後通過一個實驗進行抓包測試,驗證我們所講的內容。
如果你有