教你动手写网络协议栈系列文章
序号 |
内容 |
1 |
《教你动手写UDP协议栈-UDP协议栈格式》 |
2 |
《教你动手写UDP协议栈-DHCP报文解析》 |
3 |
《教你动手写UDP协议栈-OTA上位机》 |
4 |
《教你动手写UDP协议栈-DNS报文解析》 |
5 |
《教你动手写UDP协议栈-CoAP报文解析 》 |
6 |
《教你动手写网络协议栈-MQTT报文解析-实践 》 |
7 |
《教你动手写网络协议栈-MQTT报文解析-解析 》
|
概述
-
在上一篇文章,直接在本地搭建了服务器和客户端,简单的实践了MQTT的用法。而这一篇来解析MQTT的报文格式。MQTT的报文字段很精简。但是解析起来还是有些复杂的。
-
解析报文最好的工具是采用wireshark抓包,不过我发现,wireshark的2.xxx的版本无法进行回环抓包(即无法抓取127.0.0.1的数据报文)。通过一番度娘,发现新版本的wireshark用Npcap替换WinPcap,Npcap是基于WinPcap 4.1.3开发的,api兼容WinPcap。下载链接:https://www.wireshark.org/download.html
-
http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718030
MQTT报文
MQTT报文格式
名称 |
说明 |
Fixed header(固定报文头) |
所有MQTT报文都包含 |
Variable header(可变报文头) |
只有部分MQTT报文包含 |
Payload(MQTT数据段) |
只有部分MQTT报文包含 |
MQTT固定报文头[Fixed header]
-
每个MQTT报文都包含一个固定报文头,固定报文头部格式如下:
bit 7 6 5 4 3 2 1 0
+-----+-----+-----+-----+-----+-----+-----+-----+-----+
|Byte1| MQTT控制报文类型 |指定控制报文类型的标志 |
+-----+-----------------------------------------------|
|Byte2| 剩余长度 |
+-----+-----------------------------------------------|
| ... | 剩余长度 |
+-----------------------------------------------------|
MQTT控制报文类型
-
MQTT的控制报文类型在固定报文头的第1个字节的4 ~ 7bit,共4位无符号值。这些值如下表描述:
类型 |
值 |
报文方向 |
描述 |
RESERVED |
0 |
禁止 |
保留 |
CONNECT |
1 |
客户端到服务端 |
客户端请求连接服务器 |
CONNACK |
2 |
服务端到客户端 |
连接报文确认 |
PUBLISH |
3 |
双向 |
发布消息 |
PUBACK |
4 |
双向 |
QoS 1消息发布收到确认 |
PUBREC |
5 |
双向 |
发布收到(保证交付第一步) |
PUBREL |
6 |
双向 |
发布释放(保证交付第二步) |
PUBCOMP |
7 |
双向 |
QoS 2消息发布完成 |
SUBSCRIBE |
8 |
客户端到服务端 |
客户端订阅请求 |
SUBACK |
9 |
服务端到客户端 |
订阅请求报文确认 |
UNSUBSCRIBE |
10 |
客户端到服务端 |
客户端取消订阅请求 |
UNSUBACK |
11 |
服务端到客户端 |
取消订阅请求报文确认 |
PINGREQ |
12 |
客户端到服务端 |
心跳请求 |
PINGRESP |
13 |
服务端到客户端 |
心跳响应 |
DISCONNECT |
14 |
客户端到服务端 |
客户端断开连接 |
RESERVED |
15 |
禁止 |
保留 |
MQTT控制报文标志
-
MQTT的控制报文标志在固定报文头的第1个字节的4 ~ 7bit,包含每个MQTT报文类型的特定的标志。
-
注意:如果接收方收到非法的标志,接受者必须关闭网络连接。标志如下表:
-
类型 |
报文标志 |
Bit3 |
Bit2 |
Bit1 |
Bit0 |
CONNECT |
Reserved |
0 |
0 |
0 |
0 |
CONNACK |
Reserved |
0 |
0 |
0 |
0 |
PUBLISH |
Used in MQTT 3.1.1 |
DUP |
QoS |
QoS |
RETAIN |
PUBACK |
Reserved |
0 |
0 |
0 |
0 |
PUBREC |
Reserved |
0 |
0 |
0 |
0 |
PUBREL |
Reserved |
0 |
0 |
1 |
0 |
PUBCOMP |
Reserved |
0 |
0 |
0 |
0 |
SUBSCRIBE |
Reserved |
0 |
0 |
1 |
0 |
SUBACK |
Reserved |
0 |
0 |
0 |
0 |
UNSUBSCRIBE |
Reserved |
0 |
0 |
1 |
0 |
UNSUBACK |
Reserved |
0 |
0 |
0 |
0 |
PINGREQ |
Reserved |
0 |
0 |
0 |
0 |
PINGRESP |
Reserved |
0 |
0 |
0 |
0 |
DISCONNECT |
Reserved |
0 |
0 |
0 |
0 |
MQTT报文剩余长度
-
剩余长度字段从固定报文头的第2个字节开始,最长可达4个字节,所以剩余长度访问是Byte[2 ~ 5]。
-
剩余长度表示当前报文剩余部分的字节数,包含可变头部和Payload。
-
上面的描述,那么怎么确定其长度用几个字节来描述呢?答案:取决于字节的最高位Bit7(默认都是在搞自己在前);如果Bit7为1,那么需要继续计算字节长度,如果Bit7为0,那么不需要继续计算字节长度。
-
消息长度可以简单理解为128禁止的数据,4位长度最大可以表示:128 * 128 * 128 * 128 Byte = 256MB。
-
需注意计算规则,低位在前,高位在后,字节最高位Bit7标记是否继续计算消息长度。其消息长度范围如下表:
字节 |
最小值 |
最大值 |
1 |
0(0x00) |
127(0x7F) |
2 |
128(0x80, 0x01) |
16383(0xFF, 0X7F) |
3 |
16384(0x80, 0x80, 0x01) |
2097151(0xFF, 0xFF, 0x7F) |
4 |
2097152(0x80, 0x80, 0x80, 0x01) |
268435455(0xFF, 0xFF, 0xFF, 0x7F) |
-
-
第一字节最高位是1,需要继续向后计算,去掉标记位(0xC1%128),得到1000001=41
-
第二字节最高位是1,需要继续向后计算,去掉标记位(0xC2%128),得到1000010=42
-
第三字节最高位是0,不需要向后计算,其结果就是0x33=51
-
因为低位在前,高位在后,即消息长度为:41 + 42 * 128 + 51 * 128 * 128=841001Byte = 821KB
-
消息长度是0x60,其二进制是01100000b,字节最高位Bit7位0,所以不需要往后计算,其十进制是96(即消息长度为96个字节)。
-
消息长度是0xC1, 0xC2, 0x33。分别二进制为:
MQTT可变报文头[Variable header]
-
在某些MQTT控制报文包含了一个可变报文头部分,它在固定报文头和payload之间,可变报头的内容根据报文类型的不同而不同,可变报头的报文标识符(Packet Identifier)字段存在与多个类型的报文里。可变报头其实就是MQTT开发中使用的Packet ID,通过Packet ID 进行一些操作确认。包含Packet ID的报文类型如下:
类型 |
包含可变报文头 |
PUBLISH |
√(QoS > 0) |
PUBACK |
√ |
PUBREC |
√ |
PUBREL |
√ |
PUBCOMP |
√ |
SUBSCRIBE |
√ |
SUBACK |
√ |
UNSUBSCRIBE |
√ |
UNSUBACK |
√ |
-
Packet ID默认是从1开始并自增,如果一个Packet ID被用完后,这个Packet ID可以被重用。对于PUBLISH(QoS 1)来说,如果发送端接收到PUBACK,那么这个Packet ID就用完了。对于PUBLISH(QoS 2),如果接收方收到PUBCOMP,那么这个Packet ID就用完了。对于SUBSCRIBE和UNSUBSCRIBE,Packet ID使用完成的标记是发送方收到了对应的SUBACK和UNSUBACK。
MQTT数据段[Payload]
-
-
-
如CONNECT的Payload指Client Identifier,Will Topic,Will Message,Username,Password等信息
-
类型 |
包含Payload |
CONNECT |
√ |
PUBLISH |
可选 |
SUBSCRIBE |
√ |
SUBACK |
√ |
UNSUBSCRIBE |
√ |
通过wireshark分析MQTT报文
-
因为我们的服务器和客户端是在PC上搭建的,所以需要通过wireshark的回环抓包,来分析报文类型CONNECT和CONNACK。
-
-
使用MQTT.fx创建一个客户端,点击连接,便可以抓到CONNECT和CONNACK的报文。
-
内容:
10 25
00 04 4d 51 54 54
04
c2
00 3c
00 08 63 6c 69 65 6e 74 30 31
00 05 61 64 6d 69 6e
00 08 31 32 33 34 35 36 37 38
-
-
0x10:高四位0001,代表报文类型:CONNECT。
-
0x25:二进制-0010 0101,Bit7为0,所以剩余长度只有一个字节长,即0x25十进制:37个字节
-
0x00 0x04 0x4d 0x51 0x54 0x54:其中-0x00,0x04表示协议长度;0x4d 0x51 0x54 0x54对应 "M", "Q", "T", "T"。
-
-
0xc2: 次字节含义如下表,该字节描述紧跟数据有 User Name、Password,没有遗嘱设置,选择了清理会话方式与服务器连接。
Bit |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
0 |
含义 |
User Name Flag |
Password Flag |
Will Retain Flag |
Will QoS MSB |
Will QoS LSB |
Will Flag |
Clean session |
Rreserved |
值 |
1 |
1 |
0 |
0 |
0 |
0 |
1 |
0 |
-
00 3c:对应十进制为60,即保持连接(Keep Alive)60秒,以秒为单位,它是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。客户端负责保证控制报文发送的时间间隔不超过保持连接的值。如果没有任何其它的控制报文可以发送,客户端 必须发送一个PINGREQ 报文。客户端随时可以发送ping指令,服务器如果发现在KeepAalive时间内没有收到客户端的消息,会自动断开与客户端建立的连接。
-
00 08 63 6c 69 65 6e 74 30 31:其中-0x00, 0x08表示clientID的长度8个字节;0x63,0x6c,0x69,0x65,0x6e,0x74,0x30,0x31:代表client01。即是我们在MQTT.fx创建客户端的时候设置clientID。
-
00 05 61 64 6d 69 6e:其中-0x00,0x05表示User Name的的长度5个字节;0x61,0x64,0x6d,0x69,0x6e:代表User Name为admin,
即是我们在MQTT.fx创建客户端的时候设置User Name。
-
00 08 31 32 33 34 35 36 37 38:其中-0x00,0x08表示User Name的的长度8个字节;0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38:代表Password为12345678,
即是我们在MQTT.fx创建客户端的时候设置Password。
-
内容:
20
02
00
00
-
-
20:高四位0010,代表报文类型:CONNACK。
-
02:二进制-0000 0010,Bit为0,所以剩余长度只有一个字节长,即0x02十进制:2个字节。
-
值 |
返回码响应 |
描述 |
0 |
0x00连接已接受 |
连接已被服务器接受 |
1 |
0x01连接已拒绝,不支持的协议版本 |
服务器不支持客户端请求的协议版本 |
2 |
0x02连接已拒绝,不合格的客户端ID |
客户端ID是正确的UTF-8码,但服务器不允许使用 |
3 |
0x03连接已拒绝,服务端不可用 |
网络连接已建立,但MQTT服务不可用 |
4 |
0x04连接已拒绝,无效的用户名或密码 |
用户名或密码的数据格式无效 |
5 |
0x05连接已拒绝,未授权 |
客户端未被授权连接到此服务器 |
6-255 |
Reserved |
保留 |
RT-Thread线下技术培训:MQTT网关
本次培训RT-Thread将以ART-Pi为硬件平台+RT-Thread物联网操作系统提供MQTT网关原型,希望帮助工程师快速熟悉基于RT-Thread的项目开发流程,熟悉MQTT网关开发中所涉及到的技术要点如Modbus、MQTT、连网、文件系统、FTP、Bootloader、OTA、裁剪优化,提升产品开发速度。
我们将到访的城市有:上海、深圳、南京、广州、郑州、北京、武汉、天津、成都、西安
长按识别上方二维码报名
你
可以添加微信17775982065为好友,注明:公司+姓名,拉进 RT-Thread 官方微信交流群!