MQTT協議 -- 訊息報文格式

409253645發表於2019-04-18

雖然學習協議是枯燥的,但是熟悉協議本身卻是很重要的事情。如果能把其細節弄清楚,並且配合一些實驗來學習,就不會那麼枯燥了。

訊息報文格式

MQTT協議是應用層協議,需要藉助TCP/IP協議進行傳輸,類似HTTP協議。MQTT協議也有自己的格式,如下表:

[ Fixed Header | Variable Header | Payload]

Fixed Header: 固定頭部,MQTT協議分很多種型別,如連線,釋出,訂閱,心跳等。其中固定頭是必須的,所有型別的MQTT協議中,都必須包含固定頭。

**Variable Header:**可變頭部,可變頭部不是可選的意思,而是指這部分在有些協議型別中存在,在有些協議中不存在。

**Payload:**訊息載體,就是訊息內容。與可變頭一樣,在有些協議型別中有訊息內容,有些協議型別中沒有訊息內容。


固定頭

固定頭包含兩部分內容,首位元組(位元組1)和剩餘訊息報文長度(1-4位元組)。

Bit 7 6 5 4 3 2 1 0
Byte 1 MQTT Control Packet type Flags specific to each MQTT Control Packet type
Byte 2... Remaining Length

為了避免翻譯不準確,這裡都使用官方的原始術語。其中MQTT Control Packet type可以簡單理解為位元組位Bit[7-4]用於確定報文型別。Flags specific to each MQTT Control Packet type意思是位元組位Bit[3-0]用作某些報文的特殊標記。

首位元組

首位元組用於表示MQTT訊息的報文型別以及某些型別的控制標記,如上圖。高4位(bit7~bit4)表示協議型別,總共可以表示16種協議型別,其中0000和1111是保留欄位。MQTT訊息報文型別如下。

報文型別 欄位值 資料方向 描述
保留 0 禁用 保留
CONNECT 1 Client ---> Server 客戶端連線到伺服器
CONNACK 2 Server ---> Client 連線確認
PUBLISH 3 Client <--> Server 釋出訊息
PUBACK 4 Client <--> Server 發不確認
PUBREC 5 Client <--> Server 訊息已接收(QoS2第一階段)
PUBREL 6 Client <--> Server 訊息釋放(QoS2第二階段)
PUBCOMP 7 Client <--> Server 釋出結束(QoS2第三階段)
SUBSCRIBE 8 Client ---> Server 客戶端訂閱請求
SUBACK 9 Server ---> Client 服務端訂閱確認
UNSUBACRIBE 10 Client ---> Server 客戶端取消訂閱
UNSUBACK 11 Server ---> Client 服務端取消訂閱確認
PINGREQ 12 Client ---> Server 客戶端傳送心跳
PINGRESP 13 Server ---> Client 服務端回覆心跳
DISCONNECT 14 Client ---> Server 客戶端斷開連線請求
保留 15 禁用 保留

首位元組的低4位(bit3~bit0)用來表示某些報文型別的控制欄位,實際上只有少數報文型別有控制位,如下圖。

報文型別 固定頭標記 Bit 3 Bit 2 Bit 1 Bit 0
CONNECT 保留 0 0 0 0
CONNACK 保留 0 0 0 0
PUBLISH Used in MQTT 3.1.1 DUP QoS QoS RETAIN
PUBACK 保留 0 0 0 0
PUBREC 保留 0 0 0 0
PUBREL 保留 0 0 1 0
PUBCOMP 保留 0 0 0 0
SUBSCRIBE 保留 0 0 1 0
SUBACK 保留 0 0 0 0
UNSUBACRIBE 保留 0 0 1 0
UNSUBACK 保留 0 0 0 0
PINGREQ 保留 0 0 0 0
PINGRESP 保留 0 0 0 0
DISCONNECT 保留 0 0 0 0

當釋出PUBLISH訊息時,如果DUP欄位(bit 3)設定為1,表明這是一條重複訊息,否則是第一次釋出訊息。為了保證訊息的可靠性傳遞,當QoS設定為1時,客戶端或伺服器釋出訊息時,需要得到對方的確認(PUBACK),如果一段時間後沒收到PUBACK,那麼會再次傳送當前訊息,並將DUP欄位標記為1。

QoS用來表明QoS等級,如果Bit 1和Bit 2都為0,表示QoS 0。如果Bit 1為1,表示QoS 1。如果Bit 2為1,表示QoS 2。如果同時將Bit 1和Bit 2都設定成1,那麼客戶端或伺服器認為這是一條非法的訊息,會關閉當前連線。

目前Bit[3-0]只在PUBLISH協議中使用有效,並且表中指明瞭是MQTT 3.1.1版本。對於其它MQTT協議版本,內容可能不同。所有固定頭標記為"保留"的協議型別,Bit[3-0]必須保持與表中保持一致,如SUBSCRIBE協議,其Bit 1必須為1。如果接收方接收到非法的訊息,會強行關閉當前連線。

Remaining Length

Remaining Length意思是剩餘長度,即Variable Header + Payload的長度。剩餘長度從Byte 2開始,最長可達4位元組。所以剩餘長度範圍是Byte[2-5]。那麼怎樣確定其長度到底是1還是4呢,這取決於位元組的最高位Bit 7(預設都是高位元組在前),如果這個值是1,那麼就繼續計算位元組長度,如果是0,那麼就不再計算位元組長度。

訊息長度可以簡單理解為128進位制的資料,4位長度最大可以表示128*128*128*128Byte=256MB。但是這個長度的計算有些特別,就是低位在前,高位在後(因為正常的表示方法是高位在前,低位在後),位元組最高位Bit7用於標記是否需要繼續計算訊息長度。以下是訊息長度的長度範圍:

位元組 最小值 最大值
1 0(0x00) 127(0x7F)
2 128 (0x80, 0x01) 16 383 (0xFF, 0x7F)
3 16 384 (0x80, 0x80, 0x01) 2 097 151 (0xFF, 0xFF, 0x7F)
4 2 097 152 (0x80, 0x80, 0x80, 0x01) 268 435 455 (0xFF, 0xFF, 0xFF, 0x7F)

稍微注意一下,0x80=1000 0000,不是 1000。剛開始以為是1000,所以就沒明白。

舉個例子。

訊息假設長度是[0X60],其二進位制是01100000,位元組最高位Bit7(從左邊起第0位)是0,所以不需要繼續往後計算。那麼訊息長度就是0X60,十進位制數是96。

如果訊息長度是[0XC1, 0XC2, 0X33],那麼他們的二進位制分別如下,

0xC1=1100 0001

0xC2=1100 0010

0x33=0011 0011,

第一位元組最高位是1,那麼需要繼續向後計算,去掉標記位(0xC1%128),得到100 0001=41

第二位元組最高位是1,那麼需要繼續向後計算,去掉標記位(0xC2%128),得到100 0010=42

第三位元組最高位是0,不需要向後計算,其結果就是0x33=51

因為低位在前,高位在後,那麼長度計算為Length=41 + 42*128 + 51*128*128=841001 B = 821KB

需要注意的是,訊息長度=可變頭部長度+訊息內容長度。不包括首位元組和訊息長度本身,如果訊息長度為5,那麼說明這條訊息後邊還有5位元組,整條訊息長度為7(首位元組+1位長度位元組+5)。

另外如果訊息長度為4位元組,最後一位不能超過0X7F=127,因為如果超出這個值,其最高位Bit7是1,還需要往後計算,這與訊息最大長度為4位元組矛盾。所以如果出現[0XFF, 0XFF, 0XFF, 0XFF]這樣的訊息長度,那麼接收方認為這是一條非法的訊息。


Variable Header

Variable Header的意思是可變化的訊息頭部。有些報文型別包含可變頭部,如PUBLISH,SUBSCRIBE,CONNECT等等。可變頭部在固定頭部和訊息內容之間,其內容根據報文型別不同而不同。

Packet Identifier(訊息ID)是一種常見的可變頭部,一個訊息ID包含2位元組,高位元組在前,低位元組在後。包含Packet Identifier的協議型別包括:

報文型別 包含可變頭
PUBLISH YES(QoS > 0)
PUBACK YES
PUBREC YES
PUBREL YES
PUBCOMP YES
SUBSCRIBE YES
SUBACK YES
UNSUBSCRIBE YES
UNSUBACK YES

訊息ID預設是從1開始並自增,如果一個訊息ID被用完後,這個訊息ID可以被重用。對於PUBLISH (QoS 1)來說,如果傳送端接收到PUBACK,那麼這個訊息ID就用完了。對於PUBLISH(QoS 2),如果接收方收到PUBCOMP,那麼這個訊息ID就用完了。對於SUBSCRIBE和UNSUBSCRIBE,訊息ID使用完成的標記是傳送方收到了對應的SUBACK和UNSUBACK。

另外客戶端和服務端的訊息ID是獨立分配的,客戶端和服務端可以同時使用同一個訊息ID。比如

	Client                     Server

   PUBLISH Packet Identifier=0x1234--->

   <--PUBLISH Packet Identifier=0x1234

   PUBACK Packet Identifier=0x1234--->

   <--PUBACK Packet Identifier=0x1234
複製程式碼

上邊訊息客戶端給服務端傳送一條訊息,使用的訊息ID是0x1234,同時服務端給客戶端傳送了一條訊息,也使用了訊息ID 0x1234。然後客戶端回覆服務端,傳送PUBACK,最後是客戶端收到服務端的回覆PUBACK。

另外其它協議如CONNECT和CONNACK也有可變頭部,具體請參見MQTT-Packet CONNECT Variable Header


Payload

有些報文型別是包含Payload的,Payload意思是訊息載體的意思,如PUBLISH的Payload就是指訊息內容。而CONNECT的Payload則包含Client Identifier,Will Topic,Will Message,Username,Password等資訊。具體請參見MQTT-Packet CONNECT Payload

包含Payload的報文型別如下:

報文型別 是否包含Payload
CONNECT YES
PUBLISH 可選
SUBSCRIBE YES
SUBACK YES
UNSUBSCRIBE YES

除了上面列出的報文型別,其它的報文型別都沒有Payload。

抓包測試

我們使用Wire Shark抓包,來探測一下MQTT訊息內容。

1. 開啟Wireshark,選擇你的網路卡,新增以下過濾條件,並點選開始捕獲。

tcp.port==1883
複製程式碼

2. 開啟終端,輸入以下命令,釋出一條訊息。如果你不理解一下命令,請參看我的前一篇文章MQTT快速入門

$ mosquitto_pub -d -p 1883 -h 10.69.94.176 -q 1 -t topic1 -m "Hello MQTT"
Client mosqpub|2052-SCNWCL0121 sending CONNECT
Client mosqpub|2052-SCNWCL0121 received CONNACK (0)
Client mosqpub|2052-SCNWCL0121 sending PUBLISH (d0, q1, r0, m1, 'topic1', ... (10 bytes))
Client mosqpub|2052-SCNWCL0121 received PUBACK (Mid: 1)
Client mosqpub|2052-SCNWCL0121 sending DISCONNECT
複製程式碼

3. 進入Wireshark捕獲視窗,發現捕獲到了一些TCP和MQTT協議,如下:

packet-capture

4. 檢視其中一條MQTT訊息,比如Publish Message,點選這一行。檢視MQTT訊息內容。其位元組碼為

32 14 00 06 74 6f 70 69 63 31 00 01 48 65 6c 6c 6f 20 4d 51 54 54
複製程式碼

5. 來具體看一下訊息內容

  • 首位元組 0x32=0011 0010,對照首位元組中的表,4位高位元組為0011=3,表示PUBLISH,4位低位元組0010,分別表示DUP 0,QoS 1(佔兩位),Retain 0。

  • Remaining Length 0x14=20,表示剩餘訊息長度為20。

  • PUBLISH (QoS>0)報文訊息包含可變頭部,其可變頭部包含topic name和Packet Identifier。其格式為:

  • 00 06表示topic name的長度,所以topic name長度是6

  • 74 6f 70 69 63 31表示topic name的UTF8字串,其值為"topic1"。

  • 00 01是Packet Identifier,所以訊息ID為1。

  • 48 65 6c 6c 6f 20 4d 51 54 54是Payload,表示“Hello MQTT"的UTF8位元組碼。


總結

我們介紹了MQTT協議的訊息格式,MQTT訊息格式包含Fixed Header, Variable Header和Payload。因為MQTT訊息格式非常精簡,所以可以高效的傳輸資料。

Fixed Header中包含首位元組,高4位用來表示報文型別,低4位用於型別控制。目前只有PUBLISH使用了型別控制欄位。其它控制欄位被保留並且必須與協議定義保持一致。

Fixed Header同時包含Remaining Length,這是剩餘訊息長度,最大長度為4位元組,理論上一條MQTT最大可以傳輸256MB資料。Remaining Length=Variable Header+Payload長度。

Variable Header是可變頭部,有些報文型別中需要包含可變頭部,可變頭部根據報文型別不同而不同。比如Packet Identifier在釋出,訂閱/取消訂閱等報文中都使用到。

Payload是訊息內容,也只在某些報文型別中出現,其內容和格式也根據報文型別不同而不同。


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

相關文章