概述
1. 什么是 MQTT?
MQTT (Message Queuing Telemetry Transport,消息队列遥测传输) 是一种基于 TCP/IP 协议族的应用层协议。MQTT 协议是专门针对硬件性能低下 & 网络状况不稳定的场景设计的,这使得 MQTT 在物联网和移动应用等受限场景得到广泛应用。
1.1 MQTT 协议的发展历史
- 1999 年:Andy Stanfork-Clark (IBM) 和 Arlen Nipper 发布 MQTT 协议,用于通过卫星连接石油管道遥测系统,MQTT 中的 TT (Telemetry Transport) 就是源于这样一个遥测系统;
- 2010 年:MQTT 协议免费发布;
- 2014 年:MQTT 协议正式成为 OASIS 标准,经过多年的发展,MQTT 协议已经成为互联网 (IoT) 的主要协议之一。
目前,MQTT 主要分为两个大版本:
- MQTT v3: 其中 v3.1.1 是最常用的版本,我们的实战篇也是基于协议版本;
- MQTT v5: 2018 年发布,目前应用有限。
1.2 MQTT 协议和 HTTP 协议有什么区别?
特性 | MQTT 协议 | HTTP 协议 |
---|---|---|
传输层 | TCP | TCP 或 UDP |
消息传递 | 发布 - 订阅模型 | 请求 - 响应模型 |
消息分发 | 1 对 0/1/N | 1 对 1 |
数据安全 | 使用 SSL/TLS | 不一定采用 HTTPS |
加密 | 应用层对有效载荷加密 | 不在应用层加密 |
消息大小 | 较小 | 较大 |
-
1、MQTT 协议基于传输层 TCP 协议,而 HTTP 可以基于 TCP 或 UDP;
-
2、MQTT 协议采用发布 - 订阅模型,同一个设备既可以是发布者也可以是订阅者;而 HTTP 协议采用请求 - 响应模型,一个设备作为请求方,另一个设备作为响应方;
-
3、MQTT 消息分发可以是 1 对 0/1/N,而 HTTP 消息分发是 1 对 1;
-
4、MQTT 协议使用 SSL/TLS 来确保安全,而 HTTP 协议并不规定使用 HTTPS;
-
5、MQTT 协议在应用层对有效载荷 (payloads) 加密,而 HTTPS 协议不在应用层加密,在传输数据前不会加密数据;
-
6、MQTT 消息报文较小,而 HTTP 消息报文较大;
1.3 为什么 MQTT 协议适合物联网和移动应用场景?
物联网和移动应用场景的特点是硬件性能低下和网络状况不稳定,而 MQTT 协议就是专门针对这种环境设计的,主要在四个方面有优势:
-
1、架构设计: MQTT 协议采用发布 - 订阅模型,使得消息发布者和消息订阅者互相解耦,当一个客户端断线时,整个系统可以继续工作。这使得 MQTT 在网络质量的场景下更具优势;
-
2、消息大小: MQTT 协议具有非常小的消息头,这使得 MQTT 协议更适应低带宽网络环境;
-
3、交付能力: MQTT 协议提供了更可靠的消息交付保证,它定义了三种消息发布服务质量 (QoS):“最多发一次”、“最少发一次” 和 “正好发一次”。其中,“正好一次” 用于计费系统和 IM App 推送中,能确保用户收到且只收到一次;
-
4、特性: MQTT 提供了遗嘱消息和保留消息的特性。遗嘱消息使得客户端端断开连接时,所有订阅的客户端都能收到来自代理的消息;保留消息意味着新订阅的客户端可以立即获得保留的消息。这使得 MQTT 协议更适合 间歇性连接的场景。
1.4 谁更适合物联网(HTTP/2 & WebSocket & MQTT)?
-
HTTP/2 是 HTTP/1.x 的升级,主要体现在:利用 “多路复用和二进制分帧” 来解决队首阻塞问题,降低了通信时延;利用 “头部压缩” 减少消息头部,降低了传输开销;实现了 服务器推送,允许在不发起请求的情况下将数据推送到客户端,弥补了 Http/1.x 依赖 Websockets 才能实现推送的缺陷。这些改进使得 HTTP/2 也具有适应物联网场景的条件。
-
Websockets 是在 Web 浏览器和 Web 服务器之间进行握手的协议,它降低了使用 Http/1.x 进行双工通信的开销。随着 HTTP/2 成为标准,对 websockets 的需求可能会下降。
-
MQTT 是基于发布订阅模型的协议,因其带宽消耗小而被广泛应用于物联网协议。
结论:这三种协议并没有绝对的优胜者,最好的协议取决于具体的需求和限制条件。但如果只从带宽、电池、功能多样性这些基本条件看,MQTT 在其中是更占优的选择。
1.5 为什么 MQTT 协议基于 TCP,可以基于 UDP 协议吗?
MQTT 协议的设计特性中包含了一项 “高可靠性交付”,它需要一个保证可靠的底层传输层协议,因此 TCP 协议、TLS 协议、WebSocket 协议都可以作为 MQTT 的底层协议。而无连接的 UDP 协议会丢失或重排数据,不能满足 MQTT 协议的传输需要。
1.6 MQTT 协议的工作模型
MQTT 是基于发布 - 订阅模型 (pub/sub) 的消息传递协议,与请求 - 响应模型不同,发布 - 订阅模型主要有三种角色:publisher & publisher & subscriber:
-
publisher & subscriber (发布者 & 订阅者): 是指通过网络连接到 MQTT broker 的设备,也叫 客户端 (client)。一个客户端既可以作为消息发布者,也可以作为消息订阅者;
-
broker (代理): 代理是整个发布 - 订阅模型的核心,也叫 服务端。
当 client 发布某个主题的消息时,broker 会将该消息分发给任何已订阅该主题的 client。通常来说,client 不会存储消息,一旦消息被发送到这些 client,消息就会从 broker 上删除。另外,保留消息、持久连接和服务质量 QoS 可能会导致消息临时存储在 broker 上。
发布 - 订阅模式使得 消息的发布者和订阅者解耦,主要体现为空间解耦和时间解耦:
-
空间解耦 / 设备解耦: 发布者和订阅者通过 broker 进行消息传递,相互之间感知不到对方的存在。当一个客户端断线时,整个系统可以继续工作。
-
时间解耦: publisher 和 subscriber 不一定需要同时运行;
图片引用自 juejin.cn/post/697644… —— cxuan 著
2. MQTT 协议消息格式
2.1 MQTT 协议消息的特点
-
1、基于二进制: MQTT 是一种基于二进制的协议,所谓基于二进制,是指 MQTT 协议操作的元素是二进制数据而不是文本数据;
-
2、命令 & 命令确认格式: MQTT 采用命令 & 命令确认的格式,每个命令消息都有一个关联的命令确认消息;
-
3、消息头小: MQTT 消息头最小只需要 2 字节。
2.2 MQTT 协议消息基本结构
一个 MQTT 消息由三部分组成:
MQTT 消息结构 | 描述 | 长度 |
---|---|---|
固定报头(Fixed header) | 存在于所有 MQTT 消息中 | 2 到 5 字节 |
可变报头(Variable header) | 存在于部分 MQTT 消息中 | 0 或 N 字节 |
载荷(Payloads) | 存在于部分 MQTT 消息中 | 0 或 N 字节 |
1、固定报头: 每一个 MQTT 消息都包含一个固定报头,包含消息类型、标志位和剩余长度三个部分。固定报头长度为 2 ~ 5 字节,具体取决于 “剩余长度” 的大小,格式如下:
- 消息类型(MQTT Control Packet type):
消息类型 | 值 | 消息流转方向 | 描述 | 需要有效载荷 |
---|---|---|---|---|
Reserved | 0 | 禁止 | 保留 | / |
CONNECT | 1 | => | 客户端请求连接服务器 | ✔ |
CONNACK | 2 | <= | CONNECT 消息确认 | ✖ |
PUBLISH | 3 | <==> | 客户端发布消息 | 可选 |
PUBACK | 4 | <==> | PUBLISH 消息确认(QoS 1) | ✖ |
PUBREC | 5 | <==> | 发布收到(保证交付第一步) | ✖ |
PUBREL | 6 | <==> | 发布释放(保证交付第二步) | ✖ |
PUBCOMP | 7 | <==> | 发布完成(保证交付第三步) | ✖ |
SUBSCRIBE | 8 | => | 客户端订阅消息 | ✔ |
SUBACK | 9 | <= | SUBSCRIBE 消息确认 | ✔ |
UNSUBSCRIBE | 10 | => | 客户端取消订阅 | ✔ |
UNSUBACK | 11 | <= | UNSUBSCRIBE 消息确认 | ✖ |
PINGREQ | 12 | => | 心跳请求 | ✖ |
PINGRESP | 13 | <= | PINGREQ 消息确认 | ✖ |
DISCONNECT | 14 | => | 客户端断开连接 | ✖ |
Reserved | 15 | 禁止 | 保留 | / |
- 剩余长度 (Remaining Length): 剩余长度表示当前消息剩余部分的字节数,包括可变报头和有效载荷的长度,但不包括剩余长度字段本身的字节数。
提示: 如何判断剩余长度的字节数,采用的是前缀无歧义的表示法。
2、可变报头: 不同消息的可变报头内容不一样,不过其中有一个比较通用的字段:
- 包唯一标识(Packet Identifier): SUBSCRIBE,UNSUBSCRIBE,PUBLISH(QoS > 0)的消息中会包含一个 2 字节的唯一标识字段,每次 client 发送这些消息时,必须分配一个未使用过的唯一标识。而这些消息的应答消息,如 PUBACK、PUBREC、PUBREL、UNSUBACK 必须与对应消息携带相同的唯一标识。
3、载荷: 某些 MQTT 消息会包含一个有效载荷,对于 PUBLISH 消息来说,有效载荷就是应用消息。
3. MQTT 协议中的消息
3.1 连接消息
MQTT 的连接总是发生在 client 和 broker 之间,两个 client 之间不会互相感知。请求连接时,client 会向 broker 发送 CONNECT 连接消息,broker 接受连接后会响应 CONNACK 连接确认消息。一旦连接建立,连接会一直保持打开状态,直到 client 发送 DISCONNECT 断开连接消息或连接异常中断。
3.1.1 CONNECT 请求连接
CONNECT 是 client 发送给 broker 的首个消息,并且在一次连接中,client 只能发送一次 CONNECT 消息,发送的第二个 CONNECT 消息会被 broker 当作违反协议处理,并断开连接。在 CONNECT 消息中,主要包含以下内容:
-
ClientId 客户端名称: 所有 client 都需要一个名称,broker 会根据 client 名称来跟踪会话,因此 client 名称必须是 唯一的。如果连接到 broker 时已经有一个重名的 clientId,那么会先断开现有 client 的连接,这将可能导致断开和连接的死循环,因为大多数 MQTT client 有断线重连机制。
-
CleanSession 持久会话: 当 client 连接到 broker 时,可以使用持久连接或非持久连接,CleanSession 标志决定是否使用持久连接(当 CleanSession = 0 时表示持久连接),对于持久会话,broker 会存储会话状态;而对于非持久会话,broker 不会存储 client 的任何内容,具体见第 4.2 节 · 会话状态。
-
UserName & Password 用户名 & 密码: 用于 broker 认证和授权。
-
KeepAlive 保活时间间隔: KeepAlive 是以秒单位的时间间隔,指 client 发送两次消息的最大时间间隔,当 client 和 borker 之间在一段时间内没有数据交互时,client 会发送PINGREQ 探测消息 用于判断连接是否正常,来决定是否要关闭该连接。KeepAlive 是 MQTT 协议的保活机制,从作用上看与 TCP 的 Keepalive 保活机制是非常类似的,不过 MQTT 协议的保活机制是应用层 client 实现的,而 TCP 的保活机制是 “内核” 实现的。
-
Last Will Message 遗嘱消息: 遗嘱消息用于通知意外停机的 client,每个 client 在连接时可以设置一个遗嘱消息,这个遗嘱消息会存储在 broker 上。当 client 因 “非正常原因” 断开连接时,broker 会将遗嘱消息分发给订阅了 “Will” 主题的 client。另外,这条遗嘱消息还可以设置是否为保留消息(Will Retain 标志)以及服务质量等级(Will Qos)。
3.1.2 CONNACK 连接确认
CONNACK 消息用于确认 CONNECT 消息。CONNECT 是 client 发送给 broker 的首个消息,相应地,broker 发送给 client 的首个消息一定是 CONNACK 消息。在 CONNACK 消息中,主要包含以下内容:
-
SessionPresent 持久会话: SessionPresent 标志表示当前 broker 是否持有与 client 的持久会话。当 broker 接受了一个非持久会话连接(CleanSession = 1),SessionPresent 的值始终为 0;而当 broker 接受了一个持久会话连接(CleanSession = 0),则 SessionPresent 的值取决于 broker 是否存储了 ClientId 的会话状态。
-
ReturnCode 响应码: 用于表示连接请求是否成功,如果响应码不为 0,则表示连接失败。具体取值如下表:
返回码 | 描述 |
---|---|
0 | 连接已接受 |
1 | 连接被拒绝,不可接受的协议版本 |
2 | 连接被拒绝,标识符被拒绝 |
3 | 连接被拒绝,服务器不可用 |
4 | 连接被拒绝,用户名或密码错误 |
5 | 连接被拒绝,未授权 |
3.1.3 DISCONNECT 断开连接
DISCONNECT 消息由 client 发送给 broker,用于断开连接。DISCONNECT 消息没有可变报头和有效载荷,也没有对应的确认应答消息,表示一个干净利索地断开连接操作。断开连接后,client 不能再发送除 CONNECT 消息之外的消息,broker 也需要丢弃和当前会话有环的遗嘱消息。
3.2 订阅消息
MQTT 是基于发布订阅模型的协议,在建立连接后,client 可以向 broker 订阅感兴趣的一个或多个话题。
3.2.1 SUBSCRIBE 订阅
SUBSCRIBE 消息由 client 发送给 broker,用于订阅感兴趣的话题,SUBSCRIBE 消息主要包含以下内容:
- 主题过滤器列表: SUBSCRIBE 消息的有效载荷中至少需要包含一个话题过滤器,每个过滤器由一个 Topic 和 QoS 组成,其中的 QoS 指定了指定 client 接受的最大 OoS 等级。
3.2.2 SUBACK 订阅确认
SUBACK 消息用于确认 SUBSCRIBE 消息。SUBACK 消息主要包含以下内容:
- 返回码列表: 每个返回码都与 SUBSCRIBE 消息中的话题过滤器一一对应。具体取值如下表:
返回码 | 描述 |
---|---|
0x00 | 订阅成功,最大 QoS 为 0 |
0x01 | 订阅成功,最大 QoS 为 1 |
0x02 | 订阅成功,最大 QoS 为 2 |
0x80 | 订阅失败 |
3.2.3 UNSUBSCRIBE 退订
UNSUBSCRIBE 消息由 client 发送给 broker,用于退订不感兴趣的话题,UNSUBSCRIBE 消息主要包含以下内容:
- 话题列表: UNSUBSCRIBE 消息的有效载荷中至少需要包含一个话题。
3.2.4 UNSUBACK 退订确认
UNSUBACK 消息用于确认 UNSUBSCRIBE 消息。UNSUBACK 消息非常简单,只有一个包唯一标识(位于可变报头)。
3.3. 发布消息
当 MQTT client 在连接到 broker 之后就可以发送消息了,每条 PUBLISH 消息都包含一个 topic ,broker 会根据 topic 将消息发送给感兴趣的 client。除此之外,每条消息还会包含一个 Payload,Payload 是真正发布的应用消息,载荷的内容和格式由应用层决定,MQTT 协议层不关心。
3.3.1 PUBLISH 发布
PUBLISH 消息可以由 client 发送给 broker,也可以由 broker 发送给 client,用来运送应用层消息。PUBLISH 消息主要包含以下内容:
-
QoS 发布服务质量标志: 标记当前 PUBLISH 消息传送的交付保证水平,分为三个等级,具体见第 4.3 节 · 发布服务质量:
- QoS 0(默认): 最多发一次
- QoS 1: 最少发一次
- QoS 2: 正好发一次
-
RETAIN 保留消息标志: 标记当前 PUBLISH 消息是否为保留消息,当 client 发送给 broker 的 PUBLISH 消息标记 RETAIN = 1 时,broker 会存储该消息,当新的 client 注册订阅时,并且匹配该消息主题时,该保留消息会发送给订阅者,具体见第 4.4 节 · 保留消息
-
DUP 重传标志: 标记当前的 PUBLISH / PUBREL 消息是否为重复发送消息。** MQTT 协议规定了两种消息重传的场景,具体见第 4.5 节 · 消息重传**
-
TopicName 话题名: 表示载荷数据的发布通道;
-
包唯一标识: 只有 QoS1 和 OoS2 的 PUBLISH 消息中存在;
-
应用消息: PUBLISH 消息的载荷是真正发布的应用消息,载荷的内容和格式由应用层决定,MQTT 协议层不关心。另外,载荷的数据长度等于:固定报头中的保留长度(Remaining Lenght)- 可变报头的长度,载荷长度也可以为零。
3.3.2 发布确认
PUBLISH 消息的接收方需要发送确认应答,不同 QoS 等级的 PUBLISH 消息响应的消息不同:
发布服务质量等级 QoS | 期望的确认应答 |
---|---|
QoS 0 | 无确认应答 |
OoS 1 | PUBACK 消息 |
OoS 2 | PUBREC 消息 PUBREL 消息 PUBCOMP 消息 |
3.4 Ping 心跳探测
当 client 和 broker 在一段时间内没有数据交互时,client 会发送 PINGREQ 探测消息,用于判断连接是否正常,来决定是否要关闭该连接,这就是 MQTT 协议的保活机制。
3.4.1 PINGREQ 探测消息
PINGREQ 消息由 client 发送给 broker。
3.4.2 PINGRESP 探测确认
PINGRESP 消息由 broker 发送给 client,代表 client 是存活的。
4. MQTT 协议核心特性
4.1 主题和主题过滤器
MQTT 主题本质上是一种 “寻址形式”,用于将应用层消息分发到期望的客户端。MQTT 主题是一种类似于文件系统的分层结构,使用 “/” 正斜杠 作为分隔符。
4.1.1 主题格式规范
- 1、区分大小写;
- 2、采用 UTF-8 编码的字符串;
- 3、非空字符串,至少包含一个字符才有效;
- 4、可以包含空;
- 5、一个主题增加 “/” 前缀或后缀后是不同主题。
4.1.2 主题通配符
客户端订阅主题时,可以订阅确定的主题(例如 “group/group123”),也可以使用 “通配符” 来同时订阅多个主题。需要注意的是:在发布消息是不允许使用主题通配符,client 每次发布消息只能发布到单个主题。
- 单级通配符:
+
是单级通配符,单级通配符可以用于任何一个主题级别,但只能匹配一个级别。例如:
主题 | 匹配主题举例 |
---|---|
group/+/123 | group/vip/123 group/temp/123 |
- 多级通配符:
#
是多级通配符,多级通配符可以匹配多个连续级别。需要注意,多级通配符只能用于主题的最后一个级别。例如:
主题 | 匹配主题举例 |
---|---|
group/# | group group/123 group/vip/123 group/temp/123 |
4.1.3 $SYS 主题
$SYS 主题是 broker 上默认创建的只读主题,除此之外,broker 不会默认创建任何主题,所有主题都是由客户端订阅或发布才创建的,都不是永久性的。关于 $SYS 主题的更多介绍在 这里
4.1.4 主题的生存周期
-
创建主题:某个客户端订阅该主题,或者某个客户端向主题发布消息,同时设置为保留消息;
-
删除主题:订阅该主题的最后一个客户端断开连接,同时连接为非持久会话(CleanSession = 1)。
4.2 会话状态
当 client 连接到 broker 时,可以使用持久连接或非持久连接,这是通过 CONNECT 消息中的 CleanSession 标志来决定的(当 CleanSession = 0 时表示持久连接)。对于持久会话,broker 会存储会话状态;而对于非持久会话,broker 不会存储 client 的任何内容。会话状态主要包含以下内容:
4.2.1 客户端存储的会话状态
- 已经发送 broker 但没有收到确认的 QoS1 和 QoS2 PUBLISH 消息
- 接收到的 QoS2 消息,但还没有收到确认的
4.2.2 服务端存储的会话状态
- 客户端的订阅;
- 已经发送到 client 的但没有得到确认的 QoS1 和 QoS2 PUBLISH 消息;
- 从客户端收到但还没有确认的 QoS2 PUBLISH 消息;
- 等待发送到 client 的 QoS1 和 QoS2 PUBLISH 消息;
- (可选项)等待发送到客户端的QoS 0消息。
提示: 保留消息不属于会话状态,在会话结束时不会被删除,broker 应该一直存储保留消息知道被 client 删除。
4.3 QoS 发布服务质量等级
- QoS 0(默认): 最多发一次(不保证消息交付)
- QoS 1: 最少发一次(保证消息交付,当可能出现重复)
- QoS 2: 正好发一次(保证没有重复的消息交付)
QoS 0 等级的 PUBLISH 消息的交付能力完全依赖于底层传输层,QoS 1 和 QoS 2 等级开始在应用层提高 PUBLISH 消息的交付能力。当消息丢失时,发送端会重新发送早前尝试发送过的 PUBLISH 消息(DUP = 1),接收者收到消息也会发送确认响应消息。
4.3.1 QoS 0 · 最多发一次
在 QoS 0 的等级的 PUBLISH 消息中不包含包唯一标识。发送者不考虑消息交付结果,接收者也不发送响应。接收者最多只能收到一次消息,也有可能一次也收不到。
4.3.2 OoS 1 · 最少发一次
在 QoS 1 等级的 PUBLISH 消息中包含包唯一标识,发送方会一直将该消息当作 “未确认” 的消息,直到收到对应的 PUBACK 确认消息。具体消息流如下:
提示: 实际的消息传递是在 client 和 broker 之间进行的,这里描述了发送方和接收方之间的消息传递。
- 1、发送方存储应用消息;
- 2、发送方发送 PUBLISH(QoS = 1, DUP = 0, <PID>)消息;
- 3、接收方响应 PUBACK(<PID>)确认消息;
- 4、发送方删除存储的应用消息。
4.3.3 QoS2 · 正好发一次
QoS 2 是最高的服务质量,保证消息不会丢失也不会重复,缺点是会增加开销。在 QoS 2 等级的 PUBLISH 消息中包含包唯一标识,发送者会一直将该消息当作 “未确认” 的消息,知道收到对应的 PUBCOMP 确认消息。
- 1、发送方存储消息;
- 2、发送方发送 PUBLISH(QoS = 2, DUP = 0, <PID>)消息;
- 3、接收方存储消息;
- 4、接收方响应 PUBREC(<PID>)消息;
- 5、发送方发送 PUBREL(<PID>)消息;
- 6、接收方向上层应用通知消息;
- 7、接收方响应 PUBCOMP()消息;
- 8、发送方删除存储的应用消息。
4.4 RETAIN 保留消息
当 client 发布某个主题的消息时,broker 会将该消息分发给任何已订阅该主题的 client,随后这条消息会从 broker 上删除。可以设置 RETAIN 保留标志设置该 PUBLISH 消息为保留消息,broker 会存储该主题的最后一条保留消息,当新的 client 注册订阅时,并且匹配该消息主题时,该保留消息会发送给订阅者。需要注意:broker 只会为每个主题保存最近一条保留消息,新收到的 RETAIN = 1 的消息会覆盖原本那条保留消息;
持久会话 & 服务质量等级 & 保留消息都会影响新订阅者是否接受消息,总结如下表:
4.5 消息重传
标记 DUP = 1 的消息是重复发送的消息,MQTT 消息重传有两种场景:
- 1、PUBLISH / PUBREL 消息发送后,在规定时间内没有收到确认应答消息,则重传这个消息;
- 2、在使用持久会话时,client 重新连接后,broker 会自动重传未确认的消息。
需要注意:DUP 标志只对 OoS > 0 的消息有效,所有 QoS = 0 的消息 DUP 标志必须设置为 0;
TCP 协议有报文重传机制,为什么 MQTT 协议还有消息重传机制?
TCP 协议的报文重传机制是对所有 TCP 报文有效的重传机制,而 MQTT 协议的消息重传机制只对一小部分消息有效,用于实现更可靠的消息交付保证。虽然 TCP 协议在一般情况下可以保证不丢包,但是这并不是绝对的,依然存在请求超时或者连接中断等情况。而 MQTT 协议的 QoS 1 和 QoS 2 要求更可靠的交付能力,并且需要在客户端重连后也能保证交付。因此,MQTT 协议也定义了一个消息重传机制。
消息报文格式
MQTT协议是应用层协议,需要借助TCP/IP协议进行传输,类似HTTP协议。MQTT协议也有自己的格式,如下表:
[ Fixed Header | Variable Header | Payload]
Fixed Header: 固定头部,MQTT协议分很多种类型,如连接,发布,订阅,心跳等。其中固定头是必须的,所有类型的MQTT协议中,都必须包含固定头。
**Variable Header:**可变头部,可变头部不是可选的意思,而是指这部分在有些协议类型中存在,在有些协议中不存在。
**Payload:**消息载体,就是消息内容。与可变头一样,在有些协议类型中有消息内容,有些协议类型中没有消息内容。
固定头
固定头包含两部分内容,首字节(字节1)和剩余消息报文长度(1-4字节)。
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Byte 1 | MQTT | Control | Packet | type | Flags specific | to each | MQTT Control | Packet type |
Byte 2... | Remaining | Length |
为了避免翻译不准确,这里都使用官方的原始术语。其中MQTT Control Packet type可以简单理解为字节位Bit[7-4]用于确定报文类型。Flags specific to each MQTT Control Packet type意思是字节位Bit[3-0]用作某些报文的特殊标记。
首字节
首字节用于表示MQTT消息的报文类型以及某些类型的控制标记,如上图。高4位(bit7~bit4)表示协议类型,总共可以表示16种协议类型,其中0000和1111是保留字段。MQTT消息报文类型如下。
报文类型 | 字段值 | 数据方向 | 描述 |
---|---|---|---|
保留 | 0 | 禁用 | 保留 |
CONNECT | 1 | Client ---> Server | 客户端连接到服务器 |
CONNACK | 2 | Server ---> Client | 连接确认 |
PUBLISH | 3 | Client <--> Server | 发布消息 |
PUBACK | 4 | Client <--> Server | 发不确认 |
PUBREC | 5 | Client <--> Server | 消息已接收(QoS2第一阶段) |
PUBREL | 6 | Client <--> Server | 消息释放(QoS2第二阶段) |
PUBCOMP | 7 | Client <--> Server | 发布结束(QoS2第三阶段) |
SUBSCRIBE | 8 | Client ---> Server | 客户端订阅请求 |
SUBACK | 9 | Server ---> Client | 服务端订阅确认 |
UNSUBACRIBE | 10 | Client ---> Server | 客户端取消订阅 |
UNSUBACK | 11 | Server ---> Client | 服务端取消订阅确认 |
PINGREQ | 12 | Client ---> Server | 客户端发送心跳 |
PINGRESP | 13 | Server ---> Client | 服务端回复心跳 |
DISCONNECT | 14 | Client ---> Server | 客户端断开连接请求 |
保留 | 15 | 禁用 | 保留 |
首字节的低4位(bit3~bit0)用来表示某些报文类型的控制字段,实际上只有少数报文类型有控制位,如下图。
报文类型 | 固定头标记 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|
CONNECT | 保留 | 0 | 0 | 0 | 0 |
CONNACK | 保留 | 0 | 0 | 0 | 0 |
PUBLISH | Used in MQTT 3.1.1 | DUP | QoS | QoS | RETAIN |
PUBACK | 保留 | 0 | 0 | 0 | 0 |
PUBREC | 保留 | 0 | 0 | 0 | 0 |
PUBREL | 保留 | 0 | 0 | 1 | 0 |
PUBCOMP | 保留 | 0 | 0 | 0 | 0 |
SUBSCRIBE | 保留 | 0 | 0 | 1 | 0 |
SUBACK | 保留 | 0 | 0 | 0 | 0 |
UNSUBACRIBE | 保留 | 0 | 0 | 1 | 0 |
UNSUBACK | 保留 | 0 | 0 | 0 | 0 |
PINGREQ | 保留 | 0 | 0 | 0 | 0 |
PINGRESP | 保留 | 0 | 0 | 0 | 0 |
DISCONNECT | 保留 | 0 | 0 | 0 | 0 |
当发布PUBLISH消息时,如果DUP字段(bit 3)设置为1,表明这是一条重复消息,否则是第一次发布消息。为了保证消息的可靠性传递,当QoS设置为1时,客户端或服务器发布消息时,需要得到对方的确认(PUBACK),如果一段时间后没收到PUBACK,那么会再次发送当前消息,并将DUP字段标记为1。
QoS用来表明QoS等级,如果Bit 1和Bit 2都为0,表示QoS 0。如果Bit 1为1,表示QoS 1。如果Bit 2为1,表示QoS 2。如果同时将Bit 1和Bit 2都设置成1,那么客户端或服务器认为这是一条非法的消息,会关闭当前连接。
目前Bit[3-0]只在PUBLISH协议中使用有效,并且表中指明了是MQTT 3.1.1版本。对于其它MQTT协议版本,内容可能不同。所有固定头标记为"保留"的协议类型,Bit[3-0]必须保持与表中保持一致,如SUBSCRIBE协议,其Bit 1必须为1。如果接收方接收到非法的消息,会强行关闭当前连接。
Remaining Length
Remaining Length意思是剩余长度,即Variable Header + Payload的长度。剩余长度从Byte 2开始,最长可达4字节。所以剩余长度范围是Byte[2-5]。那么怎样确定其长度到底是1还是4呢,这取决于字节的最高位Bit 7(默认都是高字节在前),如果这个值是1,那么就继续计算字节长度,如果是0,那么就不再计算字节长度。
消息长度可以简单理解为128进制的数据,4位长度最大可以表示128*128*128*128Byte=256MB。但是这个长度的计算有些特别,就是低位在前,高位在后(因为正常的表示方法是高位在前,低位在后),字节最高位Bit7用于标记是否需要继续计算消息长度。以下是消息长度的长度范围:
字节 | 最小值 | 最大值 |
---|---|---|
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) |
稍微注意一下,0x80=1000 0000,不是 1000。刚开始以为是1000,所以就没明白。
举个例子。
消息假设长度是[0X60],其二进制是01100000,字节最高位Bit7(从左边起第0位)是0,所以不需要继续往后计算。那么消息长度就是0X60,十进制数是96。
如果消息长度是[0XC1, 0XC2, 0X33],那么他们的二进制分别如下,
0xC1=1100 0001
0xC2=1100 0010
0x33=0011 0011,
第一字节最高位是1,那么需要继续向后计算,去掉标记位(0xC1%128),得到100 0001=41
第二字节最高位是1,那么需要继续向后计算,去掉标记位(0xC2%128),得到100 0010=42
第三字节最高位是0,不需要向后计算,其结果就是0x33=51
因为低位在前,高位在后,那么长度计算为Length=41 + 42*128 + 51*128*128=841001 B = 821KB
需要注意的是,消息长度=可变头部长度+消息内容长度。不包括首字节和消息长度本身,如果消息长度为5,那么说明这条消息后边还有5字节,整条消息长度为7(首字节+1位长度字节+5)。
另外如果消息长度为4字节,最后一位不能超过0X7F=127,因为如果超出这个值,其最高位Bit7是1,还需要往后计算,这与消息最大长度为4字节矛盾。所以如果出现[0XFF, 0XFF, 0XFF, 0XFF]这样的消息长度,那么接收方认为这是一条非法的消息。
Variable Header
Variable Header的意思是可变化的消息头部。有些报文类型包含可变头部,如PUBLISH,SUBSCRIBE,CONNECT等等。可变头部在固定头部和消息内容之间,其内容根据报文类型不同而不同。
Packet Identifier(消息ID)是一种常见的可变头部,一个消息ID包含2字节,高字节在前,低字节在后。包含Packet Identifier的协议类型包括:
报文类型 | 包含可变头 |
---|---|
PUBLISH | YES(QoS > 0) |
PUBACK | YES |
PUBREC | YES |
PUBREL | YES |
PUBCOMP | YES |
SUBSCRIBE | YES |
SUBACK | YES |
UNSUBSCRIBE | YES |
UNSUBACK | YES |
消息ID默认是从1开始并自增,如果一个消息ID被用完后,这个消息ID可以被重用。对于PUBLISH (QoS 1)来说,如果发送端接收到PUBACK,那么这个消息ID就用完了。对于PUBLISH(QoS 2),如果接收方收到PUBCOMP,那么这个消息ID就用完了。对于SUBSCRIBE和UNSUBSCRIBE,消息ID使用完成的标记是发送方收到了对应的SUBACK和UNSUBACK。
另外客户端和服务端的消息ID是独立分配的,客户端和服务端可以同时使用同一个消息ID。比如
Client Server
PUBLISH Packet Identifier=0x1234--->
<--PUBLISH Packet Identifier=0x1234
PUBACK Packet Identifier=0x1234--->
<--PUBACK Packet Identifier=0x1234
复制代码
上边消息客户端给服务端发送一条消息,使用的消息ID是0x1234,同时服务端给客户端发送了一条消息,也使用了消息ID 0x1234。然后客户端回复服务端,发送PUBACK,最后是客户端收到服务端的回复PUBACK。
另外其它协议如CONNECT和CONNACK也有可变头部,具体请参见MQTT-Packet CONNECT Variable Header
Payload
有些报文类型是包含Payload的,Payload意思是消息载体的意思,如PUBLISH的Payload就是指消息内容。而CONNECT的Payload则包含Client Identifier,Will Topic,Will Message,Username,Password等信息。具体请参见MQTT-Packet CONNECT Payload
包含Payload的报文类型如下:
报文类型 | 是否包含Payload |
---|---|
CONNECT | YES |
PUBLISH | 可选 |
SUBSCRIBE | YES |
SUBACK | YES |
UNSUBSCRIBE | YES |
除了上面列出的报文类型,其它的报文类型都没有Payload。
MQTT通配符使用
我们在做设备需求开发时会遇见一些问题,主要如下所述:
在一个业务场景中,有各种不同得设备或者是采集器需要介入至平台
那么,为了区分不同的设备,通常建议开发者对MQTT的发布和订阅的主题做细分,以做到对设备的精确控制。
区分设备
嵌入式开发时,开发者应该将设备的主题规划如下:
订阅:/sys/device/8685754894158765/ctrl
上报:/sys/device/8685754894158765/reply
其中8685754894158765
就是设备的IMEI。如此一来,只要服务端向某个设备的ctrl主题发布数据,设备收到后即可做出相应响应;设备也可以根据自己的逻辑,及时上报数据到reply主题。
如此一来,每个设备的逻辑很清晰了,但是服务端端呢?难道要去订阅每一个设备的不同主题吗?实际上并不复杂哦,使用MQTT的通配符就能轻松解决。
通配符
主题层级
譬如在上文的例子中:
订阅:/sys/device/8685754894158765/ctrl
上报:/sys/device/8685754894158765/reply
每一个 /
都是分隔符,用来分割主题的每一层级。以订阅的主题为例,它就被分割成了4个层级:
/sys/device/8685754894158765/ctrl
层级1. sys
层级2. device
层级3. 8685754894158765
层级4. ctrl
不要小看层级哦,区分设备,使用通配符,全靠他们了。
多层通配符#
#
是可以匹配主题中任意层级次数的通配符。
比如,如果你订阅了 /sys/device/#
,那么,你可以接收到以下这些主题的消息:
/sys/device
/sys/device/8685754894158765/reply
/sys/device/8685754894158766/reply
/sys/device/8685754894158767/reply
/sys/device/abce/efg/h/ijkl
...
通过示例我们可以看出,#
可以匹配大于等于0的层级。
服务端使用通配符 #
订阅主题。设备上报数据,服务端收到数据后,再根据设备的上报的 真实主题 和 payload 进行处理。
单层通配符+
+
只可匹配主题的某一层级。
比如,如果你订阅了 /sys/device/+
,那么,你可以接收到以下这些主题的消息:
/sys/device/8685754894158765
/sys/device/8685754894158766
/sys/device/8685754894158767
/sys/device/abce
...
但是不能收到如下主题的消息:
/sys/device/8685754894158767/reply
/sys/device/abce/efg/h/ijkl
/sys/device
因为他们都超过了 +
1层级的要求。需要注意的是,/sys/device
因为是0层级,所以也不符合要求,无法收到数据。
扩展用法
-
- 如果开发者想要订阅所有主题,那么连接到服务器后,订阅
#
就可以啦; /sys
和sys
是两个不同的主题。所以,如果开发者想要使用+
订阅/sys
,那么要写成这样:+/+
;- 小心使用
#
,以免造成不可预估的后果; - 主题中的其他字符,如
*
、$
等,均当作普通字符串处理,无其他特殊含义。
- 如果开发者想要订阅所有主题,那么连接到服务器后,订阅
最后
以上就是飞快冷风为你收集整理的MQTT1. 什么是 MQTT?2. MQTT 协议消息格式3. MQTT 协议中的消息4. MQTT 协议核心特性MQTT通配符使用的全部内容,希望文章能够帮你解决MQTT1. 什么是 MQTT?2. MQTT 协议消息格式3. MQTT 协议中的消息4. MQTT 协议核心特性MQTT通配符使用所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复