💬
结合shelly设备连接本地 MQTT brocker的wireshark抓包,学习MQTT协议(主要是v3.1.1)。
参考MQTT标准协议,结合用wireshark抓包的结果,加深对协议的理解,文章会先介绍控制报文的格式,然后再按照设备实际连接时候的报文顺序介绍不同的控制报文。
完整MQTT3.1.1协议参考《MQTT Version 3.1.1》或《MQTT协议中文版

🥯 1. 控制报文格式

1.1 MQTT控制报文的结构

MQTT协议通过交换预定义的MQTT控制报文来通信。一个MQTT报文包含三个部分:固定报头,可变报头和有效载荷,其中固定报头为所有MQTT控制报文都需要包含,可变报头和有效载荷为部分报文类型包含。通过这种方式,使得报文更为精简。
Fixed header
固定报头,所有控制报文都包含
Variable header
可变报头,部分控制报文包含
Payload
有效载荷,部分控制报文包含

1.2 固定报头 Fixed header

每个MQTT控制报文都包含的固定报头。主要包含描述当前MQTT报文的类型和报文的长度,程序就根据类型以及长度对剩余的部分进行一个有效的解析。
notion image

1.2.1 MQTT控制报文的类型

固定报头中第一字节的bit4-bit7
名字
报文流动方向
描述
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
禁止
保留

1.2.2 标志 Flags

固定报头中第一字节的bit0-bit3。从下表可以看到,大多数指令该标志都是0值。
控制报文
固定报头标志
Bit 3
Bit 2
Bit 1
Bit 0
CONNECT
Reserved
0
0
0
0
CONNACK
Reserved
0
0
0
0
PUBLISH
Used in MQTT 3.1.1
DUP1
QoS2
QoS2
RETAIN3
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
  • DUP1 =控制报文的重复分发标志
  • QoS2 = PUBLISH报文的服务质量等级
  • RETAIN3 = PUBLISH报文的保留标志

1.2.3 剩余长度 Remaining Length

从固定报头的第二字节开始就是剩余长度。
剩余长度是指当前包中的剩余字节,包括可变包头的数据以及载荷。剩余长度不包含用来编码剩余长度的字节。
剩余长度使用了一种可变长度的结构来编码,这种结构使用单一字节表示0-127的值。大于127的值如下处理。每个字节的低7位用来编码数据,最高位用来表示是否还有后续字节。因此每个字节可以编码128个值,再加上一个标识位。剩余长度最多可以用四个字节来表示,即应用发送最多256M大小的控制包。
 
字节数
最小值
最大值
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)
如果不采用可变长度的结构进行编码的话,4字节最大值为4294967295,也就是4GB,对于MQTT的使用场景,大部分时候都是几百字节的数据传输,使用变长的编码结构能更好的兼顾大部分时间小数据量和某些特定场景大数据量的传输。
 
示例1:如下Connect Command报文剩余长度是97,小于127,十六进制表示0x61。
notion image
示例2:另一个MQTT控制报文,剩余长度为170,大于了127,就需要被编码成两个字节表示,170(=42+1*128),低位在前。所以第一字节是42+128=170=0xaa=0b10101010(二进制第一bit表示后面还有一个字节),第二字节是0x01。
notion image
 
 

1.3 可变报头

某些类型的MQTT控制包包含一个可变包头结构。位于固定包头和载荷之间。可变包头的内容取决于包的类型。可变包头中的包标识符字段在大多类型的包中比较常见。

1.3.1 包唯一标识

notion image
很多类型的控制包的可变包头结构都包含了2字节的唯一标识字段。这些控制包是PUBLISH(QoS > 0),PUBACK,PUBREC,PUBREL,PUBCOMP,SUBSCRIBE,SUBACK,UNSUBSCRIBE,UNSUBACK。
示例:如下的订阅ack报文,就包含了id标识字段。
notion image
 
包含报文标识符的控制报文 Control Packets that contain a Packet Identifier
控制报文
报文标识符字段
CONNECT
不需要
CONNACK
不需要
PUBLISH
需要(如果QoS > 0)
PUBACK
需要
PUBREC
需要
PUBREL
需要
PUBCOMP
需要
SUBSCRIBE
需要
SUBACK
需要
UNSUBSCRIBE
需要
UNSUBACK
需要
PINGREQ
不需要
PINGRESP
不需要
DISCONNECT
不需要
客户端和服务端各自独立分配唯一标识。因此,一对客户端和服务端交换数据的时候可以使用相同的唯一标识。
 

1.4 有效载荷

某些MQTT控制报文在报文的最后部分包含一个有效载荷,比如publish报文
 
包含有效载荷的控制报文 Control Packets that contain a Payload
控制报文
有效载荷
CONNECT
需要
CONNACK
不需要
PUBLISH
可选
PUBACK
不需要
PUBREC
不需要
PUBREL
不需要
PUBCOMP
不需要
SUBSCRIBE
需要
SUBACK
需要
UNSUBSCRIBE
需要
UNSUBACK
不需要
PINGREQ
不需要
PINGRESP
不需要
DISCONNECT
不需要
示例:如在订阅报文中,有效载荷部分就包括了需要订阅的topic和QoS
notion image

🥮 2. 控制报文

2.1 CONNECT – 连接服务端

客户端和服务端建立网络连接后,如图,也就是在TCP三次握手。
notion image
协议规定第一个从客户端发送给服务端的包必须是CONNECT报文。且每个网络连接客户端只能发送一次CONNECT报文。服务端如果收到客户端发来的第二个CONNECT报文当作违反协议处理,并断开与客户端的连接。
notion image
 
CONNECT报文的有效载荷包含一个或多个编码字段。用来指定客户端的唯一标识,话题,信息,用户名和密码。除了客户端唯一标识,其他都是可选项,是否存在取决于可变包头里的标识。

2.1.1 固定报头 Fixed header

notion image
示例:
notion image
 

2.1.2 可变报头 Variable header

CONNECT报文的可变报头按下列次序包含四个字段:协议名(Protocol Name),协议级别(Protocol Level),连接标志(Connect Flags)和保持连接(Keep Alive)。
协议名 Protocol Name
说明
7
6
5
4
3
2
1
0
协议名
byte 1
长度 MSB (0)
0
0
0
0
0
0
0
0
byte 2
长度 LSB (4)
0
0
0
0
0
1
0
0
byte 3
‘M’
0
1
0
0
1
1
0
1
byte 4
‘Q’
0
1
0
1
0
0
0
1
byte 5
‘T’
0
1
0
1
0
1
0
0
byte 6
‘T’
0
1
0
1
0
1
0
0
协议名就是“MQTT”字符串,方便数据包检测软件,比如防火墙进行流量识别。
示例:
notion image
 
协议级别 Protocol Level
说明
7
6
5
4
3
2
1
0
协议级别
byte 7
Level(4)
0
0
0
0
0
1
0
0
客户端用8位的无符号值表示协议的修订版本。
对于3.1.1版协议,协议级别字段的值是4(0x04)。如
notion image
对于5.0版协议,协议级别字段的值是5(0x05)。如
notion image
 
连接标识
连接标识包含了一些参数来指定MQTT的连接行为,它也能表示负载中的字段是否存在。
notion image
连接标识包含了6个有效字段,分别是:
清理会话 Clean Session,表示客户端和服务端可以保存会话状态,以支持跨网络连接的可靠消息传输。这个标志位用于控制会话状态的生存时间。清理会话标志设置为0的客户端会收到所有在它连接断开期间发布的QoS 1和QoS 2级别的消息。
遗嘱标志 Will Flag,置1表示建立连接之后,网络连接关闭时,服务端会发布这个遗嘱消息,除非服务端收到DISCONNECT报文时删除了这个遗嘱消息。比如可以用来发布设备离线的消息。
遗嘱QoS Will QoS,用于指定发布遗嘱消息时使用的服务质量等级。
遗嘱保留 Will Retain,如果遗嘱消息被发布时需要保留,需要指定这一位的值。
如果遗嘱标志被设置为0,遗嘱保留(Will Retain)标志也必须设置为0。
如果遗嘱标志被设置为1:
如果遗嘱保留被设置为0,服务端必须将遗嘱消息当作非保留消息发布。
如果遗嘱保留被设置为1,服务端必须将遗嘱消息当作保留消息发布。
(补充什么是保留消息)
用户名标志 User Name Flag,用于标识有效载荷中是否包含用户名字段,设置为1为包含用户名字段。有个约束,如果用户名标志被设置为0,密码标志也必须设置为0。
密码标志 Password Flag,用于标识有效载荷中是否包含密码字段,设置为1为包含密码字段。
 
保持连接 Keep Alive
两字节,是一个以秒为单位的时间间隔,允许的最大值为18小时多一点。是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。
值为零表示关闭保持连接功能;非零标识启用间隔,服务端在一点五倍的保持连接时间内没有收到客户端的控制报文,它必须断开客户端的网络连接,认为网络连接已断开。
不过实际关于服务端1.5倍时间没有收到客户端报文需要断联,取决于服务端的实现,比如在EMQX中,是可以修改这个1.5倍时间设定的。
notion image
 

2.1.3 有效载荷 Payload

CONNECT报文的有效载荷(payload)包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段。如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码,服务端就按约定的顺序进行解析。
客户端标识符 Client Identifier
客户端标识符 (ClientId)是客户端连接时候一般都需要带上的字段,用来唯一标识客户端。
notion image
notion image
当不同的物理设备使用相同的客户端标识符 (ClientId)登录到MQTT服务器时,后连接的设备是会挤掉前连接的设备的,如果设备固件有做重连,那么就不可避免发生两个客户端互相挤对方掉线。
比如以EMQX作为服务端,两个客户端使用相同clientId,日志如下。
notion image
这时候的断联请求是服务端发起的,断联MQTT报文里会带上原因码。
notion image
 
遗嘱主题 Will Topic
如果遗嘱标志被设置为1,有效载荷的下一个字段是遗嘱主题(Will Topic)。
遗嘱消息 Will Message
如果遗嘱标志被设置为1,有效载荷的下一个字段是遗嘱消息。
实际可以使用遗嘱消息发布设备离线事件
notion image
但是遗嘱消息发布是有条件的,并不是所有断连都会发布。
notion image
 
用户名 User Name
如果用户名(User Name)标志被设置为1,有效载荷的下一个字段就是它。服务端可以将它用于身份验证和授权。
密码 Password
如果密码(Password)标志被设置为1,有效载荷的下一个字段就是它。
 

2.2 CONNACK – 确认连接请求

服务端发送CONNACK报文响应从客户端收到的CONNECT报文。

2.2.1 固定报头

notion image
剩余长度字段对于CONNACK报文这个值等于2。

2.2.2 可变报头

notion image
连接确认标志 Connect Acknowledge Flags
第1个字节是 连接确认标志,位7-1是保留位固定为0。
第0 (SP)位 是当前会话(Session Present)标志。 如果客户端的CleanSession标志为1,则服务端回复CONNACK时当前会话(Session Present)标志设置为0
连接返回码 Connect Return code
就是服务端对客户端新建MQTT连接的状态码,成功为0,其他原因如下图定义。
notion image
MQTT5.0 协议对于3.1.1协议新增了许多原因码

2.2.3 有效载荷

CONNACK报文没有有效载荷。
 
完整的CONNACK报文如:
notion image
 

2.3 SUBSCRIBE - 订阅主题

设备MQTT连上服务器之后,就会订阅自己业务相关的主题。

2.3.1 固定报头

notion image
SUBSCRIBE控制报固定报头的第3,2,1,0位是保留位,必须分别设置为0,0,1,0。
notion image
剩余长度字段
等于可变报头的长度(2字节)加上有效载荷的长度。

2.3.2可变报头

notion image

2.3.3 有效载荷

SUBSCRIBE报文的有效载荷包含了一个主题过滤器列表,它们表示客户端想要订阅的主题。有效载荷还包含了订阅主题的QOS信息
notion image
下面抓包例子中,一个TCP报文包含了多个SUBSCRIBE报文,每个SUBSCRIBE报文都只订阅了一个topic
notion image
实际也可以一个SUBSCRIBE报文直接订阅多个topic。
notion image
 

2.4 SUBACK – 订阅确认

服务端收到客户端的SUBSCRIBE报文后,就需要回复SUBACK报文,SUBACK报文包含一个返回码清单,它们指定了SUBSCRIBE请求的每个订阅被授予的最大QoS等级。

2.4.1 固定报头

notion image
剩余长度字段
等于可变报头的长度加上有效载荷的长度。

2.4.2 可变报头

notion image
可变报头包括了消息id

2.4.3 有效载荷

notion image
例如
notion image
 
订阅的QOS只是说明了客户端希望该topic发送过来消息的最大QOS,实际收到的消息QOS是依赖发送方的PUBLISH消息的。比如发送方发送的消息QOS是2,订阅方订阅QOS是1,服务端转发时候就会降发送方的消息降级为QOS1转发给订阅方;但如果发送方的消息是QOS1或0,就会保持原QOS转发给订阅方。也就是说订阅了QOS1的消息,可能会收到QOS1或0的消息,不会收到QOS2的消息。

2.5 PUBLISH – 发布消息

PUBLISH控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息。

2.5.1 固定报头

notion image
重发标志 DUP
如果DUP标志被设置为0,表示这是客户端或服务端第一次请求发送这个PUBLISH报文。如果DUP标志被设置为1,表示这可能是一个早前报文请求的重发。
服务质量等级 QoS
这个字段表示应用消息分发的服务质量等级保证。
notion image
保留标志 RETAIN
如果客户端发给服务端的PUBLISH报文的保留(RETAIN)标志被设置为1,服务端必须存储这个应用消息和它的服务质量等级(QoS),以便它可以被分发给未来的主题名匹配的订阅者。
对于发布者不定期发送状态消息这个场景,保留消息很有用。新的订阅者将会收到最近的状态。对于实际应用,可以使用保留消息让APP端订阅主题时获取到设备的最新状态数据。
剩余长度字段
等于可变报头的长度加上有效载荷的长度。

2.5.2 可变报头

可变报头按顺序包含主题名和报文标识符。
主题名 Topic Name
主题名(Topic Name)用于识别有效载荷数据应该被发布到哪一个信息通道。
报文标识符 Packet Identifier
只有当QoS等级是1或2时,报文标识符(Packet Identifier)字段才能出现在PUBLISH报文中。

2.5.3 有效载荷

有效载荷包含将被发布的应用消息。数据的内容和格式是应用特定的。有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度。包含零长度有效载荷的PUBLISH报文是合法的。
 
QOS0的 PUBLISH报文缺少报文标识符 Packet Identifier,如
notion image
notion image
notion image
PUBLISH报文是能和SUBSCRIBE报文在一个TCP包中发送给服务端的,如
notion image
 

2.6 PUBACK –发布确认(QOS 1)

PUBACK报文是对QoS 1等级的PUBLISH报文的响应。

2.6.1 固定报头

notion image
剩余长度字段
表示可变报头的长度。对PUBACK报文这个值等于2.

2.6.2 可变报头

可变报头包括消息id

2.6.3 有效载荷

PUBACK报文没有有效载荷。
notion image
 

2.7 PUBREC – 发布收到(QoS 2,第一步)

PUBREC报文是对QoS等级2的PUBLISH报文的响应。它是QoS 2等级协议交换的第二个报文。

2.7.1 固定报头

notion image
剩余长度字段
表示可变报头的长度。对PUBREC报文它的值等于2。

2.7.2 可变报头

可变报头包含消息id

2.7.3 有效载荷

PUBREC报文没有有效载荷。
notion image
 

2.8 PUBREL – 发布释放(QoS 2,第二步)

PUBREL报文是对PUBREC报文的响应。它是QoS 2等级协议交换的第三个报文。

2.8.1 固定报头

notion image
剩余长度字段
表示可变报头的长度。对PUBREL报文这个值等于2.

2.8.2 可变报头

可变报头包含消息id

2.8.3 有效载荷

PUBREL报文没有有效载荷。
notion image
 

2.9 PUBCOMP – 发布完成(QoS 2,第三步)

PUBCOMP报文是对PUBREL报文的响应。它是QoS 2等级协议交换的第四个也是最后一个报文。

2.9.1 固定报头

notion image
剩余长度字段
表示可变报头的长度。对PUBCOMP报文这个值等于2。

2.9.2 可变报头

可变报头包含消息id

2.9.3 有效载荷

PUBCOMP报文没有有效载荷。
notion image
 
所以,发布一个QOS2的消息,需要交互4个MQTT报文
notion image
 

2.10 PINGREQ – 心跳请求

心跳包也是常见的MQTT报文,对于一些低功耗场景的设备,交互消息频次较少,日常挂机时,更多的就是心跳包请求了。

2.10.1 固定报头

notion image
PINGREQ报文没有可变报头和有效载荷部分
notion image
 

2.11 PINGRESP – 心跳响应

服务端发送PINGRESP报文响应客户端的PINGREQ报文。表示服务端还活着。

2.11.1 固定报头

notion image
PINGRESP报文没有可变报头和有效载荷部分
notion image
 

2.12 DISCONNECT –断开连接

DISCONNECT报文是客户端发给服务端的最后一个控制报文。表示客户端正常断开连接。
在一般的物联网设备,只有设备OTA升级时候才会有正常断开连接。

2.12.1 固定报头

notion image

2.12.2 可变报头

DISCONNECT报文没有可变报头。

2.12.3 有效载荷

DISCONNECT报文没有有效载荷。
 
DISCONNECT报文抓包示例如下,也是一个两字节的报文
notion image
 

2.13 UNSUBSCRIBE –取消订阅

客户端发送UNSUBSCRIBE报文给服务端,用于取消订阅主题。
实际物联网设备,取消订阅是比较少的,这里只是顺带提一下。

2.13.1 固定报头

notion image
剩余长度字段
等于可变报头的长度加上有效载荷的长度。

2.13.2 可变报头

可变报头包含了消息id字段

2.13.3 有效载荷

UNSUBSCRIBE报文的有效载荷包含客户端想要取消订阅的主题过滤器列表。
 
notion image
 

2.14 UNSUBACK – 取消订阅确认

服务端发送UNSUBACK报文给客户端用于确认收到UNSUBSCRIBE报文。

2.14.1 固定报头

notion image
剩余长度字段
表示可变报头的长度,对UNSUBACK报文这个值等于2。

2.14.2 可变报头

可变报头包含等待确认的UNSUBSCRIBE报文的报文标识符。

2.14.3 有效载荷

UNSUBACK报文没有有效载荷。
notion image

📎 参考资料

 
从sentinel中“偷”段代码实现个JAVA限流器新手switch续航版硬解全过程详细记录
Loading...
Yibin
Yibin
一名平凡的程序员👨🏻‍💻
公告
📢 行远自迩,笃行不怠。
记录技术、工具和一些真实的折腾。