上一节简要介绍了 MQTT 协议的特性和 MQTT 协议通信流程。这节我们将继续通过图解的方式,和大家一起来分析 MQTT 协议报文。
MQTT协议报文
MQTT协议报文分为三部分:固定报文、可变报文、有效载荷。
固定报文:存在于所有MQTT
数据包中,表示数据包类型及数据包的分组类标识。固定报文的长度并非固定不变,而是相对于可变报文来说,固定报文在不同类型的消息中,都固定有一个字节消息首部和一至四个字节的用于表示剩余长度的部分。
可变报文:存在于部分MQTT
数据包中,数据包类型决定了可变头是否存在及其具体内容。在连接Broker时包括协议名称、协议版本、连接标记、心跳间隔时长等内容;在发布消息时包括主题名称、消息ID 报文信息;在连接确认时含有连接返回码报文。其他类型消息没有报文内容。
有效载荷:存在于部分MQTT
数据包中,表示客户端收到的具体内容。
2.1 固定报头
固定报头由报文类型、标识位和报文剩余长度三个字段组成。第一部分是固定的一个字节,从高位到低位分别是消息类型(占 4bit)、重传标识(占 1 bit)、Qos 标识(占 2bit)、保留位 (占1bit);第二部分是用于存放剩余数据长度,至少有一个字节,最多有 4 个字节。
消息类型
在第一个报头的 4~7 位,占 4 个 bit。总共有 16 个值,以下是每个值对应的消息类型:
消息类型 | 字段值 | 数据方向 | 描述 |
---|---|---|---|
CONNECT | 1 | Client → Server | 客户端发起连接请求,携带用户名、密码、会话选项(如Clean Session)、遗嘱主题与遗嘱消息等信息。 |
CONNACK | 2 | Server → Client | 服务器对连接请求的响应,包含连接确认码(CONNACK Code),表示连接是否成功建立。 |
PUBLISH | 3 | Both | 发布消息到指定的主题。携带QoS级别(0、1、2)、主题名、消息标识符(对于QoS 1和2)以及负载(Payload)。数据方向取决于消息的发布或传递过程。 |
PUBACK | 4 | Both | 对QoS 1级别的PUBLISH消息的确认响应。客户端收到QoS 1消息时向服务器发送,服务器收到客户端的QoS 1发布确认时向客户端发送。 |
PUBREC | 5 | Both | QoS 2级别PUBLISH消息的第一阶段确认。由接收方(无论客户端还是服务器)发送给发送方。 |
PUBREL | 6 | Both | QoS 2级别PUBLISH消息的第二阶段释放确认。作为对PUBREC的响应,由原发送方发送给接收方。 |
PUBCOMP | 7 | Both | QoS 2级别PUBLISH消息的完成确认。作为对PUBREL的响应,由原接收方发送给原发送方,标志着消息传输完成。 |
SUBSCRIBE | 8 | Client → Server | 客户端向服务器发送订阅请求,包含一个或多个主题过滤器及其对应的QoS等级。 |
SUBACK | 9 | Server → Client | 服务器对SUBSCRIBE请求的响应,包含每个订阅主题的返回码,表示订阅是否成功以及分配的QoS级别。 |
UNSUBSCRIBE | 10 | Client → Server | 客户端请求取消订阅一个或多个主题。 |
UNSUBACK | 11 | Server → Client | 服务器确认UNSUBSCRIBE请求已被处理,表示客户端已成功退订所指定的主题。 |
PINGREQ | 12 | Client → Server | 客户端发送心跳请求,用于保持长连接的活跃状态。 |
PINGRESP | 13 | Server → Client | 服务器对PINGREQ的响应,确认客户端的心跳请求。 |
DISCONNECT | 14 | Client → Server | 客户端主动通知服务器即将断开连接。 |
AUTH | 15 | Both | MQTT 5.0 引入的全新的报文类型,它仅用于增强认证,为客户端和服务端提供更安全的身份验证。 |
注:值为 0 为系统保留字段。
重传标识
第三位为重传标识(DUP),如果值为 1 表示这个数据包是一条重复的消息,否则该数据包就是第一次发布。
Qos发布消息服务质量
服务质量是 MQTT 的一个重要特性。当我们使用 TCP/IP 时,连接已经在一定程度上受到保护。但是在无线网络中,中断和干扰很频繁,MQTT 在这里帮助避免信息丢失及其服务质量水平。这些级别在发布时使用。如果客户端发布到 MQTT 服务器,则客户端将是发送者,MQTT 服务器将是接收者。当MQTT服务器向客户端发布消息时,服务器是发送者,客户端是接收者。
第 2 至 1 位表示发布消息的服务质量,它有 四个值分别是:
00
:Qos0 最多一次,即:<=101
:Qos1 至少一次,即:>=110
:Qos2 刚好一次,即:=111
:预留
QoS 0
QoS等级0,被称为“最多一次”传输,是MQTT协议中最基本的服务质量等级。在这个级别上,消息从发布者发送到订阅者时,不进行额外的确认和重传机制。这意味着消息可能会丢失,但在网络条件良好的情况下可以快速传输。适合于那些对数据传输速度要求高而对数据丢失容忍度较高的场景,如实时环境监测或快速数据采集。
QoS 1
QoS等级1,被称为“至少一次”传输,确保消息至少被送达一次。消息被接收方接收后,会发送回一个确认响应。如果发布者没有收到确认,它可能会再次发送消息,这可能导致消息重复。这种级别提供了比QoS 0更高的消息可靠性,适用于需要确保消息送达但可以容忍消息重复的场景。如智能家居控制或设备状态更新。
为什么 QoS 1 消息会重复?
对于发送方来说,没收到 PUBACK 报文分为以下两种情况:
- PUBLISH 未到达接收方
- PUBLISH 已经到达接收方,接收方的 PUBACK 报文还未到达发送方
如何为 QoS 1 消息去重?
QoS 1 消息的重复在协议层面上是无法避免的。所以如果我们想要对 QoS 1 消息进行去重,只能从业务层面入手。
一个比较常用且简单的方法是,在每个 PUBLISH 报文的 Payload 中都带上一个时间戳或者一个单调递增的计数,这样上层业务就可以根据当前收到消息中的时间戳或计数是否大于自己上一次接收的消息中的时间戳或计数来判断这是否是一个新消息。
QoS 2
QoS等级2是MQTT协议中最高级别的服务质量,被称为“恰好一次”传输。这个级别保证了消息在不丢失和不重复的前提下被准确送达,适用于对消息准确性要求极高的场景。QoS 2通过一个复杂的四步握手过程来确保消息的唯一性和可靠性。这包括发布者和订阅者之间的多次信息交换,确保每条消息只被接收一次。QoS 级别 2 在网络中具有最高的开销,因为在发送方和接收方之间需要两个流。QoS 2适合于需要严格消息准确性和可靠性的场景,如财务交易、关键任务控制系统等。
- 首先,发送方存储并发送 QoS 为 2 的 PUBLISH 报文以启动一次 QoS 2 消息的传输,然后等待接收方回复 PUBREC 报文。这一部分与 QoS 1 基本一致,只是响应报文从 PUBACK 变成了 PUBREC。
- 当发送方收到 PUBREC 报文,即可确认对端已经收到了 PUBLISH 报文,发送方将不再需要重传这个报文,并且也不能再重传这个报文。所以此时发送方可以删除本地存储的 PUBLISH 报文,然后发送一个 PUBREL 报文,通知对端自己准备将本次使用的 Packet ID 标记为可用了。与 PUBLISH 报文一样,我们需要确保 PUBREL 报文到达对端,所以也需要一个响应报文,并且这个 PUBREL 报文需要被存储下来以便后续重传。
- 当接收方收到 PUBREL 报文,也可以确认在这一次的传输流程中不会再有重传的 PUBLISH 报文到达,因此回复 PUBCOMP 报文表示自己也准备好将当前的 Packet ID 用于新的消息了。
- 当发送方收到 PUBCOMP 报文,这一次的 QoS 2 消息传输就算正式完成了。在这之后,发送方可以再次使用当前的 Packet ID 发送新的消息,而接收方再次收到使用这个 Packet ID 的 PUBLISH 报文时,也会将它视为一个全新的消息。
剩余消息报文长度
MQTT协议中的“剩余消息报文长度”(Remaining Length)是指除固定报头之外,整个报文中剩余部分(包括可变报头和负载)的总字节数。剩余长度字段是MQTT报文固定报头的一部分,用于指示接下来跟随的可变报头和负载的具体长度,从而允许接收方正确解析整个报文。
编码方式:
- 剩余长度采用一种特殊的变长度编码方案,以节省空间。这种编码不是直接使用固定的多字节整数表示,而是采用一种类似于基数为128(即二进制10000000,十六进制0x80)的计数系统。
- 每个字节的低7位用于存储有效数字,而最高位(bit 7)用作继续位(Continuation Bit),表示是否有更多的字节用于表示剩余长度。如果最高位为1,则表示后面还有更多字节用于表示剩余长度;若为0,则表示当前字节是剩余长度编码的最后一个字节。
最大长度:
- 使用这种编码方式,剩余长度可以被编码为1到4个字节。这意味着最大可表示的剩余长度为:
- 1字节:7位有效数字,最大值为0x7F(127)
- 2字节:14位有效数字,最大值为0x7FFF(32767)
- 3字节:21位有效数字,最大值为0x7FFFFF(8388607)
- 4字节:28位有效数字,最大值为0x7FFFFFFF(268435455)
- 实际上,MQTT规范可能限制了报文的最大长度,例如在MQTT v3.1.1中,剩余长度的最大值为268,435,455字节(即4个字节所能表示的最大值),但实际应用中可能会受到实现者或配置的进一步限制。
下面我们举一个例子来讲解一下:
如收到的剩余长度数据为: ab 01
十六进制 ab 对应的二进制为1010 1011,第一个字节的高位为 1,表示后面字节属于剩余长度,接下来看第二个字节 01。
十六进制 01 对应的二进制为0000 0001,高位为 0,表示后面没有剩余长度字节,去除第一个字节和第二个字节的标识位,后面的作为高位,即得到最终标识剩余长度的二进制为 000 0001 010 1011,对应的十进制是128 +32 + 8 + 2 + 1 = 剩余171字节 。
2.2 可变报文
可变报文(Variable Header)位于固定报头之后、有效载荷之前。可变报文的内容和格式根据具体的报文类型而变化,也就是说,不同的MQTT控制报文可能包含不同的可变报文字段或不包含可变报文。
常见可变报文字段:
以下是一些MQTT控制报文中可能出现的可变报文字段示例:
- Connect报文:
- Protocol Name:MQTT协议的名称字符串。
- Protocol Level:使用的MQTT协议版本号。
- Connect Flags:一组标志位,用于指定连接参数,如Clean Session、Will Flag、Will QoS、Will Retain、Password Flag、User Name Flag等。
- Keep Alive:客户端期望的最长无数据传输时间间隔。
- Properties:MQTT 5.0 引入,由属性长度和紧随其后的一组属性组成,这里的属性长度指的是后面所有属性的总长度。
- Publish报文(QoS 1和2):
- Topic Name:
- Packet Identifier:
- Message Identifier(MsgID):用于确认和重传机制的唯一标识符。
- Subscribe报文:
- Message Identifier(MsgID):用于关联订阅请求和服务器的确认响应。
- Suback、Unsuback报文:
- Message Identifier(MsgID):与对应的Subscribe或Unsubscribe请求中的MsgID相同,用于关联响应。
- Puback、Pubrec、Pubrel、Pubcomp报文(QoS 1和2的确认报文):
- Message Identifier(MsgID):与对应PUBLISH报文中的MsgID相同,用于关联确认过程。
- Disconnect报文(在某些MQTT扩展中):
- Session Expiry Interval(可选):指定会话过期时间。
这些字段只是部分示例,并不代表所有MQTT报文类型的可变报文都包含这些内容。实际上,某些报文(如PINGREQ和PINGRESP)可能没有可变报文部分,而其他报文(如CONNACK)则可能包含特定于其功能的额外字段,如连接确认码(CONNACK Return Code)。
Connect 可变报文
Connect可变报头按顺序包含了协议名、协议级别、连接标识、Keep Alive 和属性这五个字段。
协议名称
协议名是表示协议名 MQTT 的UTF-8编码的字符串。MQTT规范的后续版本不会改变这个字符串的偏移和长度。如果协议名不正确,服务端需要断开客户端的连接。
Description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
---|---|---|---|---|---|---|---|---|---|
Protocol Name | |||||||||
byte 1 | Length MSB (0) | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
byte 2 | Length 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 协议级别 3:这是 MQTT 协议的最初版本,也被称为 MQTT v3.1。它提供了基本的消息发布和订阅功能,支持 QoS 0(至多一次)和 QoS 1(至少一次)等级。
MQTT 协议级别 4:这是 MQTT 协议的改进版本,也被称为 MQTT v3.1.1。相对于 MQTT v3.1,它修复了一些 bug,并添加了一些新的特性,如支持消息保持、遗嘱消息、认证机制等。
MQTT 协议级别 5:这是 MQTT 协议的最新版本,也被称为 MQTT v5.0。相对于 MQTT v3.1.1,它引入了一些重要的改进,如支持双向通信、属性、共享订阅、消息排队等功能,并提供更灵活的 QoS 控制。
Protocol Level 协议级别 | Description 描述 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|
byte 7 | Version(5) 版本 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
使用一个字节表示协议的修订版本。对于5.0版协议,协议级别字段的值是5(0x05)。如果发现不支持的协议级别,服务端必须给发送一个返回码为0x84(不支持的协议级别)的CONNACK报文响应CONNECT报文,然后断开客户端的连接。
**连接标志 **
Connect Flags 字节包含多个参数,用于指定 MQTT 连接的行为。它还指示有效负载中是否存在字段。
标识名称 | 位置 | 含义 |
---|---|---|
Clean Session | 1 | 告诉服务端是否需要基于客户端标识符,在对应的会话(session)中保存连接断开过程中收到的级别为Qos1和Qos2的消息并在连接恢复后想客户端进行分发 |
Will Flag | 2 | 是否开启遗嘱消息模式,如果为YES,则服务端需要存储负载中客户端发送的will topic和will message |
Will Qos | 3-4 | 遗嘱消息的安全级别,占用2byte,值为0、1、2 |
Will Retain | 5 | 遗嘱消息是否需要作为保留消息发布 |
Password Flag | 6 | 是否需要密码 |
User Name Flag | 7 | 是否需要用户名 |
Keep alive
Keep Alive 是一个双字节整数,它是一个以秒为单位的时间间隔。它是客户端完成传输一个 MQTT ControlPacket 的点和它开始发送下一个 MQTT ControlPacket 点之间允许经过的最大时间间隔。客户端有责任确保发送的 MQTT 控制数据包之间的间隔不超过 Keep Alive 值。如果 KeepAlive 为非零,并且没有发送任何其他 MQTT 控制数据包,则客户端必须发送 PINGREQ 数据包。
如果服务器在 CONNACK 数据包上返回 Server Keep Alive,则客户端必须使用该值,而不是作为 Keep Alive 发送的值。
客户端可以随时发送 PINGREQ,而不考虑 Keep Alive 值,并检查相应的 PINGRESP 以确定网络和服务器是否可用。
如果 Keep Alive 值为非零,并且服务器在 Keep Alive 时间段的一倍半内未收到来自客户端的 MQTT 控制数据包,则必须关闭与客户端的网络连接,就像网络出现故障一样。
如果客户端在发送 PINGREQ 后的合理时间内未收到 PINGRESP 数据包,则应关闭与服务器的网络连接。
如果 Keep Alive 值为 0,则会关闭 KeepAlive 机制。如果 Keep Alive 为 0,则客户端没有义务按任何特定计划发送 MQTTControl 数据包。
Bit 位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
byte 9 | Keep Alive MSB | |||||||
byte 10 | Keep Alive LSB |
属性
所有的属性都是可选的,因为它们通常都有一个默认值,如果没有任何属性,那么属性长度的值就为 0。
每个属性都由一个定义了属性用途和数据类型的标识符和具体的值组成。不同属性的数据类型可能不同,比如一个是双字节长度的整数,另一个则是 UTF-8 编码的字符串,所以我们需要按照标识符所声明的数据类型对属性进行解析。
属性之间的顺序可以是任意的,这是因为我们可以根据标识符知道这是哪个属性,以及它的长度是多少。
属性通常都是为了某个专门的用途而设计的,比如在 CONNECT 报文中就有一个用于设置会话过期时间的的 Session Expiry Interval 属性,但显然我们在 PUBLISH 报文中就不需要这个属性。所以 MQTT 也严格定义了属性的使用范围,一个合法的 MQTT 控制报文中不应该包含不属于它的属性。
2.3 有效载荷
最后是有效载荷部分。我们可以将报文的可变报头看作是它的附加项,而有效载荷则用于实现这个报文的核心目的。
比如在 PUBLISH 报文中,Payload 用于承载具体的应用消息内容,这也是 PUBLISH 报文最核心的功能。而 PUBLISH 报文的可变报头中的 QoS、Retain 等字段,则是围绕着应用消息提供一些额外的能力。
SUBSCRIBE 报文也是如此,Payload 包含了想要订阅的主题以及对应的订阅选项,这也是 SUBSCRIBE 报文最主要的工作。
总结:
每个MQTT控制报文由三部分组成:
- 固定报头(Fixed Header)
- 报文类型(Packet Type):占1位,标识当前报文的类型。MQTT规范定义了十四种不同的报文类型,如CONNECT、CONNACK、PUBLISH、PUBACK、SUBSCRIBE、SUBACK等。
- 标志位(Flags):紧跟在报文类型后的若干位,其数量和含义取决于报文类型。例如,PUBLISH报文的标志位包括QoS等级(0, 1, or 2)、是否retain消息、是否有消息标识符等。
- 剩余长度(Remaining Length):一个变长字段,表示后续可变报头和有效载荷的总长度。剩余长度的编码规则允许其表示长达268,435,455字节的数据,但编码方式可能会导致实际占用1至4个字节。
- 可变报头(Variable Header)
- 可选存在:并非所有控制报文都包含可变报头。其内容根据报文类型的不同而变化。
- 内容:可能包含诸如协议名称与版本、连接标志(如会话清洁、遗嘱标志等)、报文标识符(Packet Identifier)、主题名通配符、订阅ID列表、用户属性等特定于报文类型的字段。
- 有效载荷(Payload)
- 内容:承载具体应用数据或附加信息。例如,在PUBLISH报文中,有效载荷包含实际的消息内容;在SUBSCRIBE报文中,有效载荷包含一系列主题过滤器和相应的QoS级别。
下一节我们将以一个实际的例子来解析报文内容。
参考:
MQTT Version 5.0 (oasis-open.org)
[MQTT 5.0 报文(Packets)入门指南 | EMQ (emqx.com)](
本文由mdnice多平台发布