1 什么是 MQTT 控制报文?
MQTT 数据传输的最小单元。[MQTT 客户端]和服务端通过交换控制报文来实现:
- 订阅主题
- 发布消息
MQTT控制报文类型按功能分为连接、发布、订阅三类:
CONNECT 报文用于客户端向服务端发起连接,CONNACK 报文则作为响应返回连接的结果。如果想要结束通信,或者遇到了一个必须终止连接的错误,客户端和服务端可以发送一个 DISCONNECT 报文然后关闭网络连接。
AUTH 报文是 MQTT 5.0 引入的全新的报文类型,它仅用于增强认证,为客户端和服务端提供更安全的身份验证。
PINGREQ 和 PINGRESP 报文用于连接保活和探活,客户端定期发出 PINGREQ 报文向服务端表示自己仍然活跃,然后根据 PINGRESP 报文是否及时返回判断服务端是否活跃。
PUBLISH 报文用于发布消息,余下的四个报文分别用于 QoS 1、2 消息的确认流程。
SUBSCRIBE 报文用于客户端向服务端发起订阅,UNSUBSCRIBE 报文则正好相反,SUBACK 和 UNSUBACK 报文分别用于返回订阅和取消订阅的结果。
2 MQTT 报文格式
MQTT无论是什么类型的控制报文,它们都由三个部分组成:
-
固定报头
固定存在于所有控制报文
-
可变报头
-
有效载荷
可变报头、有效载荷是否存在及其内容取决于具体报文类型。如:
- 用于维持连接的 PINGREQ 报文就只有一个固定报头
- 用于传递应用消息的 PUBLISH 报文则完整地包含这三部
2.1 固定报头
固定报头由报文类型、标识位和报文剩余长度三个字段组成。
固定报头第一个字节的高 4 位,无符号整数,表示当前报文的类型,如 1 表示这是 CONNECT 报文等。
Name | Value | Direction of flow | Description |
---|---|---|---|
Reserved | Forbidden | Reserved | |
CONNECT | 1 | Client to Server | Connection request |
CONNACK | 2 | Server to Client | Connect acknowledgment |
PUBLISH | 3 | Client to Server orServer to Client | Publish message |
PUBACK | 4 | Client to Server orServer to Client | Publish acknowledgment (QoS 1) |
PUBREC | 5 | Client to Server orServer to Client | Publish received (QoS 2 delivery part 1) |
PUBREL | 6 | Client to Server orServer to Client | Publish release (QoS 2 delivery part 2) |
PUBCOMP | 7 | Client to Server orServer to Client | Publish complete (QoS 2 delivery part 3) |
SUBSCRIBE | 8 | Client to Server | Subscribe request |
SUBACK | 9 | Server to Client | Subscribe acknowledgment |
UNSUBSCRIBE | 10 | Client to Server | Unsubscribe request |
UNSUBACK | 11 | Server to Client | Unsubscribe acknowledgment |
PINGREQ | 12 | Client to Server | PING request |
PINGRESP | 13 | Server to Client | PING response |
DISCONNECT | 14 | Client to Server orServer to Client | Disconnect notification |
AUTH | 15 | Client to Server or Server to Client | Authentication exchange |
除了报文类型和剩余长度这两个字段,MQTT 报文剩余部分的内容基本都取决于具体的报文类型,所以这个字段也决定接收方应该如何解析报文的后续内容。
固定报头第一个字节中剩下的低 4 位包含了由控制报文类型决定的标识位。不过到 MQTT 5.0 为止,只有 PUBLISH 报文的这四个比特位被赋予了明确的含义:
- Bit 3:DUP,表示当前 PUBLISH 报文是否是一个重传的报文
- Bit 2,1:QoS,表示当前 PUBLISH 报文使用的服务质量等级
- Bit 0:Retain,表示当前 PUBLISH 报文是否是一个保留消息
其他所有的报文中,这 4 位都仍是保留的,即它们是一个固定的,不可随意变更的值。
最后的剩余长度指示了当前控制报文剩余部分的字节数,也就是可变报头和有效载荷这两个部分的长度。所以
MQTT 控制报文的总长度实际 = 固定报头的长度 + 剩余长度。
可变字节整数
但固定报头长度并不是固定,为尽可能减少报文大小,MQTT 将剩余长度字段设计成可变字节整数。
在 MQTT 中,存在很多长度不确定的字段,例如 PUBLISH 报文中的 Payload 部分就用来承载实际的应用消息内容,而应用消息的长度显然不固定。所以我们需要一个额外的字段来指示这些不定长内容的长度,以便接收端正确地解析。
一个 2 兆大小,也就是总共 2,097,152 个字节的应用消息,我们就需要一个 4 字节长度的整数才能够指示它的长度。但并不是所有的应用消息都有这么大,更多情况下是几 KB 甚至几个字节。用一个 4 字节长度的整数来指示一个总共 2 个字节长度的应用消息,显然浪费。
所以 MQTT 的可变字节整数就被设计出来,将每个字节的:
- 低 7 位编码数据
- 最高的有效位指示是否还有更多字节
这样,长度小于 128 字节时可变字节整数只需要一个字节就可指示。可变字节整数的最大长度为 4 个字节,所以最多可以指示长度为 (2^28 - 1) 字节,即256 MB数据。
2.2 可变报头
可变报头的内容取决于具体报文类型。如:
- CONNECT 报文的可变报头按顺序包含了协议名、协议级别、连接标识、Keep Alive 和属性五字段
- PUBLISH 报文的可变报头则按顺序包含了主题名、报文标识符和属性三字段
可变报头中字段出现的顺序须严格遵循协议规范,因为接收端只会按协议规定字段顺序解析。不能随意遗漏某个字段,除非协议明确要求或允许的。如:
- CONNECT报文的可变报头中,如果协议名之后直接就是连接标识,那么就会导致报文解析失败
- 而 PUBLISH 报文的可变报头中,报文标识符就只有在 QoS 不为 0 的时候才能存在
属性
MQTT 5.0 引入的一个概念。属性字段基本上都是可变报头的最后一部分,由属性长度和紧随其后的一组属性组成,这里的属性长度指的是后面所有属性的总长度。
所有的属性都是可选的,因为它们通常都有一个默认值,如果没有任何属性,那么属性长度的值就为 0。
每个属性都由一个定义了属性用途和数据类型的标识符和具体的值组成。不同属性的数据类型可能不同,比如一个是双字节长度的整数,另一个则是 UTF-8 编码的字符串,所以我们需要按照标识符所声明的数据类型对属性进行解析。
属性之间的顺序可以是任意的,这是因为我们可以根据标识符知道这是哪个属性,以及它的长度是多少。
属性通常都是为了某个专门的用途而设计的,比如在 CONNECT 报文中就有一个用于设置会话过期时间的的 Session Expiry Interval 属性,但显然我们在 PUBLISH 报文中就不需要这个属性。所以 MQTT 也严格定义了属性的使用范围,一个合法的 MQTT 控制报文中不应该包含不属于它的属性。
2.3 有效载荷
可将报文的可变报头看作它的附加项,而有效载荷则用于实现这个报文的核心目的。
如PUBLISH 报文中,Payload 用于承载具体应用消息内容,这也是 PUBLISH 报文最核心的功能。而 PUBLISH 报文的可变报头中的 QoS、Retain 等字段,则是围绕应用消息提供一些额外的能力。
SUBSCRIBE 报文也是如此,Payload 包含了想要订阅的主题以及对应的订阅选项,这也是 SUBSCRIBE 报文最主要的工作。