背景
之前工作中参与有关协议调试的时候,发现对于协议帧的解析是比较重要的。
参考:《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
", rl);
length_step = MQTTPacketSetPacketLenth(packet, 16383);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld, 应该是 16383
", rl);
length_step = MQTTPacketSetPacketLenth(packet, 2097151);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld, 应该是 2097151
", rl);
length_step = MQTTPacketSetPacketLenth(packet, 268435455);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld, 应该是 268435455
", rl);
length_step = MQTTPacketSetPacketLenth(packet, 321);
rl = MQTTPacketGetPacketLenth(packet);
printf("求出的长度为 : %ld, 应该是 321
", 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。
接下来我们根据不同的报文类型进行分析。