背景
之前工作中參與有關協議調試的時候,發現對於協議幀的解析是比較重要的。
參考:《MQTT協議 -- 消息報文格式》、《基於STM32實現MQTT》、《MQTT協議從服務端到客戶端詳解》
英文資料:《MQTT Control Packets》
MQTT協議數據包結構
此圖是 PUBLISH 報文的組成
在MQTT協議中,一個MQTT數據包由:固定頭(Fixed header)、可變頭(Variable header)、消息體(payload)三部分構成。
- (1)固定頭(Fixed header)。存在於所有MQTT數據包中,表示數據包類型及數據包的分組類標識。
- (2)可變頭(Variable header)。存在於部分MQTT數據包中,數據包類型決定了可變頭是否存在及其具體內容。
- (3)消息體(Payload)。存在於部分MQTT數據包中,表示客戶端收到的真正內容。 與可變頭一樣,在有些協議類型中有消息內容,有些協議類型中沒有消息內容。
MQTT固定頭
MQTT協議分很多種類型,如連接,發布,訂閱,心跳等。所有類型的MQTT協議中,都必須包含固定頭。
固定頭包含兩部分內容,首字節(Byte1)和剩余消息報文長度(從Byte2開始,最多占用4個字節)。
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 1 | MQTT報文類型 | 報文類型標志位 | ||||||
byte 2.. | 剩余長度 |
MQTT Control Packet type 報文類型
Byte1的 Bit[7-4]: MQTT Control Packet type,報文類型。總共可以表示16種報文類型,其中0000和1111是保留字段。
報文類型 | Bit[7-4]值 | 數據方向 | 描述 |
---|---|---|---|
保留 | 0000 | 禁用 | 保留 |
CONNECT | 0001 | Client ---> Server | 客戶端連接到服務器 |
CONNACK | 0010 | Server ---> Client | 連接確認 |
PUBLISH | 0011 | Client <--> Server | 發布消息 |
PUBACK | 0100 | Client <--> Server | 發不確認 |
PUBREC | 0101 | Client <--> Server | 消息已接收(QoS2第一階段) |
PUBREL | 0110 | Client <--> Server | 消息釋放(QoS2第二階段) |
PUBCOMP | 0111 | Client <--> Server | 發布結束(QoS2第三階段) |
SUBSCRIBE | 1000 | Client ---> Server | 客戶端訂閱請求 |
SUBACK | 1001 | Server ---> Client | 服務端訂閱確認 |
UNSUBACRIBE | 1010 | Client ---> Server | 客戶端取消訂閱 |
UNSUBACK | 1011 | Server ---> Client | 服務端取消訂閱確認 |
PINGREQ | 1100 | Client ---> Server | 客戶端發送心跳 |
PINGRESP | 1101 | Server ---> Client | 服務端回復心跳 |
DISCONNECT | 1110 | Client ---> Server | 客戶端斷開連接請求 |
保留 | 1111 | 禁用 | 保留 |
Flags specific to each MQTT Control Packet type 報文類型標志位
Byte1的 Bit[3-0]: Flags specific to each MQTT Control Packet type,字節位用作某些報文類型的標志位。
實際上只有少數報文類型有控制位,如下表。
報文類型 | 固定頭標記 | 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 報文中有關標志位的意義與用法,容易對學習造成困擾。
剩余長度
剩余長度的計算從理解上是一大難點。注意理解好下面2句加粗的句子。
Remaining Length意思是剩余長度,即可變頭(Variable header) + 消息體(payload)
的長度。
剩余長度從Byte 2開始,最長可達4字節。即:剩余長度范圍是Byte2到Byte5。
計算: 剩余長度 所占用的字節數
MQTT協議規定,byte2(最高到byte5)的bit7(最高位)若為1,則表示還有后續字節存在。
記 N 為 消息報文中的 第n個byte, (2 < N < 5), (Byte 5 的 bit7肯定是0)
如果byte N 的 bit7 是1,那么Byte M (M = N + 1, M < 5 ) 作為剩余長度
的一部分,可用於繼續計算字節長度;
如果byte N 的 bit7 是0,那么Byte M (M = N + 1, M < 5 ) 就不能看作是剩余長度
的一部分計算字節長度。
所以單個字節最大值:01111111,即:0x7F,10進制為127。
MQTT協議最多允許4個字節表示剩余長度。那么最大長度為:0xFF,0xFF,0xFF,0x7F。
計算:剩余長度 所代表長度(以Byte為單位)
消息長度可以簡單理解為128進制的數據,4位長度最大可以表示128128128*128Byte=256MB。
注意:長度的計算有些特別,即低位在前,高位在后。
以下是消息長度的長度范圍:
占用字節 | 長度范圍的最小值 | 長度范圍的最大值 |
---|---|---|
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) |
剩余長度 的有關計算
為了方便讀者理解,我們舉例並計算一下。
若現收到一段MQTT數據報文:
0x20 0x02 0xAA 0xBB
,一共4個字節根據 MQTT 數據結構可知,
0x20
代表了CONNACK 報文
。第二個字節開始,與 剩余長度有關
Remaining Length
。顯然 , Byte2 (0x02)中的byte2[7] 為 0,代表后面的0xAA 0xBB與剩余長度無關。
再有,Byte2 (0x02) Byte2[6:0] 值 = 2,代表后續的報文長度還有2個字節,它們是
0xAA 0xBB
(我們先不關心與固定頭無關的部分
0xAA 0xBB
代表了什么意思,實際上是我亂舉例的。)至此,固定頭計算完畢。
這個例子比較簡單,我們再來看一段稍微復雜一點的報文。
若現收到一段以
0x30 0x9B 0x01 ...
開頭的 MQTT數據報文:根據 MQTT 數據結構可知,
0x30
代表了PUBLISH 報文
。第二個字節開始,與 剩余長度有關
Remaining Length
。顯然,Byte 2 (0x9B) 中的bit7 為
1
,代表后面的0x01
與剩余長度有關。再有,Byte 3 (0x01) 中的bit7 為
0,代表
剩余長度`有關的報文在此字節為止。知道了剩余長度有關的報文字節是
0x9B
與0x01
,那么就是計算具體的剩余長度。注意:要低位在前,高位在后。
Byte 2 中的 0x9b 中能夠計算長度的只有 byte2[6:0] 即
(0x9b)&~(0x80) = 0x1B
那么:
len = (0x01)* 128 + 0x1B = 155
,即:后面的報文還有155個字節。我們也可以通過這個例子知道報文的長度實際上是128進制的存儲方式。
至此,固定頭計算完畢。
以此類推,我們很容易知道,如果一段報文以 0x20 0xFF 0xFF 0xFF 0x7E
開頭,那么剩下的還有 266338303 個 報文字節
我們甚至可以寫出一段"報文字節剩余長度計算"的c語言代碼。
/*
# Copyright from Web, All Rights Reserved
#
# File Name: endecode_for_rl.c
# Created : Mon, Feb 3, 2020 7:47:02 PM
*/
#include <stdio.h>
typedef unsigned int uint32;
typedef unsigned short uint16;
typedef unsigned char uint8;
/*
* buf 存放剩余長度 段的 容器
* length 設置的長度
* 返回值: buf 占用的 字節數
* */
int MQTTPacketSetPacketLenth(uint8 *buf, unsigned long length)
{
// ref : https://blog.csdn.net/weixin_42381351/article/details/89397776
unsigned long rc = 0;
unsigned char d;
do {
d = length % 128;
length /= 128;
/* if there are more digits to encode, set the top bit of this digit */
if (length > 0) {
d |= 0x80;
}
buf[rc++] = d;
} while (length > 0);
return rc;
}
/*
* buf 作為 剩余長度 幀 的首地址
* */
unsigned long MQTTPacketGetPacketLenth(uint8 *buf)
{
// 改編自中文版文檔中的偽代碼
char encodedByte;
unsigned int multiplier = 1;
unsigned long rc = 0;
int i = 0;
do {
encodedByte = buf[i++];
rc += (encodedByte & 0x7f) * multiplier;
if (multiplier > 128*128*128)
break; //throw Error(Malformed Remaining Length)
else
multiplier *= 128;
}while ((encodedByte & 0x80) != 0);
return rc;
}
int main(void)
{
int i;
unsigned long rl;
int length_step;
uint8 packet[256] = {0x80, 0x80, 0x80, 0x01}; // 除了剩余長度以外,沒有其他部分
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的長度為 : %ld\n", rl);
length_step = MQTTPacketSetPacketLenth(packet, 16383);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的長度為 : %ld, 應該是 16383\n", rl);
length_step = MQTTPacketSetPacketLenth(packet, 2097151);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的長度為 : %ld, 應該是 2097151\n", rl);
length_step = MQTTPacketSetPacketLenth(packet, 268435455);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的長度為 : %ld, 應該是 268435455\n", rl);
length_step = MQTTPacketSetPacketLenth(packet, 321);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的長度為 : %ld, 應該是 321\n", rl);
return 0;
}
MQTT 可變頭
Variable Header的意思是可變化的消息頭部。MQTT
數據包中包含一個可變頭,它駐位於可變頭(Variable header)與消息體(payload)之間。
有些報文類型包含可變頭部,如PUBLISH,SUBSCRIBE,CONNECT等等。可變頭部在固定頭部和消息內容之間,其內容根據報文類型不同而不同。
學習固定頭的時候,我們可以一個個字節位進行分析計算,但學習可變頭我個人認為應該根據具體的報文類型進行完整的分析。
可變頭部不是可選的意思,而是指這部分在有些協議類型中存在,在有些協議中不存在。
可變頭的內容因數據包類型而不同,較常的應用是做為包的標識:
Bi | 7 6 5 4 3 2 1 0 |
---|---|
byte 1 | 包標簽符(MSB) |
byte 2… | 包標簽符(LSB) |
使用大端序(big-endian,高位字節在低位字節前面)。這意味着一個16位的字在網絡上表示為最高有效字節(MSB),后面跟着最低有效字節(LSB)。
后面的字段也用到了這種編碼,這里需要特意強調一下:
有關字符串,MQTT采用的是修改版的UTF-8編碼,一般形式為如下,需要牢記:
bit 7 6 5 4 3 2 1 0 byte 1 String Length MSB byte 2 String Length LSB bytes 3 ... Encoded Character Data 頭2個字節(byte1、byte2)組成為一個完整的無符號的16位數字,代表從byte3開始后面字符串字節長度。
后面的n個字節才是字符串真正的內容。
前后共2+n個字節。
可變頭的 報文標識符
Packet Identifier 也可以叫做 Message Identifier,以后在文章中出現的 報文標識符,都以 Packet Identifier 指代。
報文標識符用來區分報文,特別是在重發的報文中用來標識是否是同一個報文,並在需要應答的場景中用於確定是對哪個發送報文的應答。可變報頭的報文標識符(Packet Identifier)字段存在於在多個類型的報文里(占用2個字節)。這些報文是:
PUBLISH(QoS > 0時)
, PUBACK
,PUBREC
,PUBREL
,PUBCOMP
,SUBSCRIBE,
SUBACK
,UNSUBSCRIBE
,UNSUBACK
。
其實是這樣的。因為 在 MQTT 協議 中 ,有些報文在發出以后 需要有收到對應響應報文;為了避免不被混淆,所以才用 Packet Identifier 來 "綁定" 處理這些消息。如果沒有 Packet Identifier 那么在通信中,連續多條一樣的報文就變得無法處理。發送者不知道現在第幾條消息被有效處理了,不知道第幾條消息被拒絕了。
Bit | 7 - 0 |
---|---|
byte 1 | 報文標識符 MSB |
byte 2 | 報文標識符 LSB |
Package ID默認是從1(0x01)開始並自增,最大為255(0xff)。
SUBSCRIBE
,UNSUBSCRIBE
和PUBLISH(QoS大於0)
控制報文必須包含一個非零的16位報文標識符(Packet Identifier)。
- 客戶端每次發送一個新的這些類型的報文時都必須分配一個當前未使用的報文標識符。
- 如果一個客戶端要重發這個特殊的控制報文,在隨后重發那個報文時,它必須使用相同的標識符。
當客戶端處理完這個報文對應的確認(ACK, CMP)后,這個報文標識符就釋放可重用。
例如:QoS 1的PUBLISH對應的是
PUBACK
,QoS 2的PUBLISH對應的是PUBCOMP
,與SUBSCRIBE或UNSUBSCRIBE對應的分別是SUBACK
或UNSUBACK
。
發送一個QoS 0的PUBLISH報文時,相同的條件也適用於服務端。
QoS等於0的PUBLISH報文不能包含報文標識符。
PUBACK
, PUBREC
, PUBREL
報文必須包含與最初發送的PUBLISH報文相同的報文標識符。類似地,SUBACK
和UNSUBACK
必須包含在對應的SUBSCRIBE和UNSUBSCRIBE報文中使用的報文標識符。
需要報文標識符的控制報文在 下表 - 包含報文標識符的控制報文 Control Packets that contain a Packet Identifier`中列出。
控制報文 | 報文標識符字段 |
---|---|
PUBLISH | YES(QoS > 0) |
PUBACK | YES |
PUBREC | YES |
PUBREL | YES |
PUBCOMP | YES |
SUBSCRIBE | YES |
SUBACK | YES |
UNSUBSCRIBE | YES |
UNSUBACK | YES |
客戶端和服務端彼此獨立地分配報文標識符。因此,客戶端服務端組合使用相同的報文標識符可以實現並發的消息交換。
換句話說, 假設客戶端發送標識符為0x1234的PUBLISH報文,它有可能會在收到那個報文的PUBACK之前,先收到服務端發送的另一個不同的但是報文標識符也為0x1234的PUBLISH報文。Client Server PUBLISH Packet Identifier=0x1234---> <--PUBLISH Packet Identifier=0x1234 PUBACK Packet Identifier=0x1234---> <--PUBACK Packet Identifier=0x1234
上邊消息客戶端給服務端發送一條消息,使用的Packet ID是0x1234,同時服務端給客戶端發送了一條消息,也使用了Packet ID 0x1234。
然后客戶端回復服務端,發送PUBACK,最后是客戶端收到服務端的回復PUBACK。
Payload消息體
並非所有的報文類型需要包含Payload。
下表 - 包含有效載荷的控制報文 Control Packets that contain a Payload 列出了需要有效載荷的控制報文。
控制報文 | 是否包含Payload |
---|---|
CONNECT | 需要 |
CONNACK | 不需要 |
PUBLISH | 可選 |
PUBACK | 不需要 |
PUBREC | 不需要 |
PUBREL | 不需要 |
PUBCOMP | 不需要 |
SUBSCRIBE | 需要 |
SUBACK | 需要 |
UNSUBSCRIBE | 需要 |
UNSUBACK | 不需要 |
PINGREQ | 不需要 |
PINGRESP | 不需要 |
DISCONNECT | 不需要 |
根據上表我們可以知道,Payload消息體作為MQTT數據包的第三部分,被包含於CONNECT
、SUBSCRIBE
、SUBACK
、UNSUBSCRIBE
、PUBLISH
這些類型報文里面:
1)CONNECT,消息體內容主要是:客戶端的ClientID、訂閱的Topic、Message以及用戶名和密碼。
2)SUBSCRIBE,消息體內容是一系列的要訂閱的主題以及QoS。
3)SUBACK,消息體內容是服務器對於SUBSCRIBE所申請的主題及QoS進行確認和回復。
4)UNSUBSCRIBE,消息體內容是要訂閱的主題。
5)PUBLISH, 消息體內容是要實際消息的內容(雖然是可選的,可是PUBLISH
的報文確實比較常用的)。
除了上面列出的報文類型,其它的報文類型都沒有Payload。
接下來我們根據不同的報文類型進行分析。