教你动手写网络协议栈系列文章

序号 内容
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
  • MQTT的标准可以参考如下网站:http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718030

MQTT报文

MQTT报文格式

  • MQTT的报文字段主要包含3部分,如下表:
名称 说明
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报文类型的特定的标志。
  • 注意:如果接收方收到非法的标志,接受者必须关闭网络连接。标志如下表:
  • 其中:
    • DUP:控制报文的重复分发标志
    • QoS:PUBLISH报文的服务质量等级
    • RETAIN:PUBLISH报文的保留标志
类型 报文标志 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
    • 0xC1 = 11000001
    • 0xC2 = 11000010
    • 0x33 = 00110011
  1. 消息长度是0x60,其二进制是01100000b,字节最高位Bit7位0,所以不需要往后计算,其十进制是96(即消息长度为96个字节)。
  2. 消息长度是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]

  • MQTT中有些报文类型是包含Payload
  • 如PUBLISH的Payload指消息内容。
  • 如CONNECT的Payload指Client Identifier,Will Topic,Will Message,Username,Password等信息
  • 包含Payload的报文类型如下:
类型 包含Payload
CONNECT
PUBLISH 可选
SUBSCRIBE
SUBACK
UNSUBSCRIBE

通过wireshark分析MQTT报文

  • 因为我们的服务器和客户端是在PC上搭建的,所以需要通过wireshark的回环抓包,来分析报文类型CONNECT和CONNACK。
  1. 打开wireshark,选择进行回环抓包
教你动手写网络协议栈-MQTT报文解析6-解析_MQTT
  1. 使用MQTT.fx创建一个客户端,点击连接,便可以抓到CONNECT和CONNACK的报文。
教你动手写网络协议栈-MQTT报文解析6-解析_MQTT_02
  1. 红色圈子的报文类型CONNECT的内容:
教你动手写网络协议栈-MQTT报文解析6-解析_MQTT_03
内容:
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
  1. 报文类型CONNECT内容分析:
  • 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"。
  • 0x04:代表MQTT版本号:v3.1.1
  • 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。
  1. 红色圈子的报文类型CONNACK的内容:
教你动手写网络协议栈-MQTT报文解析6-解析_MQTT_04
内容:
20
02
00
00
  1. 报文类型CONACK内容分析:
  • 20:高四位0010,代表报文类型:CONNACK。
  • 02:二进制-0000 0010,Bit为0,所以剩余长度只有一个字节长,即0x02十进制:2个字节。
  • 00:可变头部的第一个字节的第0位连接确认。
教你动手写网络协议栈-MQTT报文解析6-解析_MQTT_05
  • 00:可变头部的第二个字节。
返回码响应 描述
0 0x00连接已接受 连接已被服务器接受
1 0x01连接已拒绝,不支持的协议版本 服务器不支持客户端请求的协议版本
2 0x02连接已拒绝,不合格的客户端ID 客户端ID是正确的UTF-8码,但服务器不允许使用
3 0x03连接已拒绝,服务端不可用 网络连接已建立,但MQTT服务不可用
4 0x04连接已拒绝,无效的用户名或密码 用户名或密码的数据格式无效
5 0x05连接已拒绝,未授权 客户端未被授权连接到此服务器
6-255 Reserved 保留

 

教你动手写网络协议栈-MQTT报文解析6-解析_MQTT_06

 

RT-Thread线下技术培训:MQTT网关

 

本次培训RT-Thread将以ART-Pi为硬件平台+RT-Thread物联网操作系统提供MQTT网关原型,希望帮助工程师快速熟悉基于RT-Thread的项目开发流程,熟悉MQTT网关开发中所涉及到的技术要点如Modbus、MQTT、连网、文件系统、FTP、Bootloader、OTA、裁剪优化,提升产品开发速度。

 

我们将到访的城市有:上海、深圳、南京、广州、郑州、北京、武汉、天津、成都、西安

 

教你动手写网络协议栈-MQTT报文解析6-解析_MQTT_07