雖然學習協議是枯燥的,但是熟悉協議本身卻是很重要的事情。如果能把其細節弄清楚,並且配合一些實驗來學習,就不會那麼枯燥了。
訊息報文格式
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協議,如下:
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是訊息內容,也只在某些報文型別中出現,其內容和格式也根據報文型別不同而不同。