MQTT -- MQTT 特性:QoS、Retained 消息、LWT 以及 Keepalive
- QoS 及其最佳实践
- MQTT 协议中的 QoS 等级
- QoS 0
- QoS 1
- PUBACK 数据包
- QoS 2
- PUBREC 数据包
- PUBREL 数据包
- PUBCOMP 数据包
- 实际的订阅者 QoS
- QoS 的最佳实践
- QoS 与会话
- QoS 的选择
- Retained 消息
- LWT 遗嘱消息
- Keepalive
- PINGREQ 数据包
- PINGRESP 数据包
- MQTT 5 新特性
- 用户属性
- 共享订阅
- 消息过期
- 重复主题
- Broker 能力查询
- 双向 DISCONNECT
QoS 及其最佳实践
MQTT 协议中的 QoS 等级
MQTT 协议最初被用于网络带宽窄、信号不稳定的环境下,因此其设计了一套保证消息稳定传输的机制,这套机制提供了 3 种不同层次的 QoS(Quality of Service):
-
QoS 0:至多一次,at most once;
-
QoS 1:至少一次,at least once;
-
QoS 2:确保一次:exactly once;
QoS 是消息的发送方与消息的接收方之间达成的一个协议:
QoS | 概述 |
---|---|
0 | 发送方发送一条消息,接收方最多能接收到一次。即发送方完成消息发送之后不关心消息发送是否成功。 |
1 | 发送方发送一条消息,接收方至少能接收到一次。即发送方完成消息发送之后,若发送失败,则继续重发直到接受方接收到消息为止。这种模式下可能会导致接收方收到重复的消息。 |
2 | 发送方发送一条消息,接收方一定且只能收到一次。即发送方完成消息发送之后,若发送失败,则继续重发直到接收方接收到消息为止,在这一过程中同时保证接收方不会因为消息重传而收到重复的消息。 |
QoS 0
QoS 0 等级下发送方和接收方之间的一次消息传递流程如下所示:
发送方将包含消息的 PUBLISH 包发出后,不关心发送结果如何,直接丢弃该 PUBLISH 包,完成一次消息发送。
QoS 1
QoS 1 需要保证消息送达接收方至少一次,因此此处需要一种应答机制,如下所示:
信息传递过程如下:
-
发送方向接收方发送一个包含数据的 PUBLISH 数据包,同时在本地保存该数据包;
-
接收方收到 PUBLISH 数据包后,向发送发回复一个 PUBACK 数据包,该数据包的可变头中的包标识与接收到的 PUBLISH 数据包的包标识一致;
-
发送方收到回复的 PUBACK 数据包后,根据包标识找到保存在本地的对应的 PUBLISH 数据包并将其丢弃,完成一次消息的发送;
-
若发送方在一定时间内未收到 PUBACK 数据包,则将对应的 PUBLISH 数据包的 DUP 标识设置为 1 以表示该包为重发数据包,然后重新发送给接收方,并重复 2、3、4 过程;
PUBACK 数据包
固定头
固定头中数据包类型字段为 4 代表 PUBACK 数据包,该数据包剩余长度字段固定为 2。
可变头
PUBACK 数据包的可变头包含一个占据两个字节大小的包标识符。
消息体
PUBACK 数据包不包含消息体。
QoS 2
QoS 2 在 QoS 1 的基础上,进一步保证消息不重复,因此需要更复杂的应答机制,如下:
QoS 2 采用四段数据交互以保证接收方收到的消息精确一次,因此其开销是最大的,但其安全性是最高的:
-
发送方发送 PUBLISH 数据包,并将该数据包保存本地,假设该数据包的包标识符为 Pid;
-
接收方收到 PUBLISH 数据包后,将其包标识符 Pid 保存在本地,并返回一个 PUBREC 数据包,该数据包的包标识符为 Pid,无消息体;
-
发送方接收到 PUBREC 数据包后,丢弃保存在本地的 PUBLISH 数据包,同时将收到的 PUBREC 数据包保存本地,随后向接收方发送 PUBREL 数据包,该数据包的额包标识符为 Pid。若发送方在一定时间内未收到 PUBACK 数据包,则将对应的 PUBLISH 数据包的 DUP 标识设置为 1 以表示该包为重发数据包,然后重新发送给接收方;
-
接收方收到 PUBREL 数据包后,丢弃掉保存的 Pid,并返回一个 PUBCOMP 数据包,该数据包的包标识符为 Pid,无消息体;
-
发送方收到对应的 PUBCOMP 数据包后,认为数据包传输完毕,随后丢弃保存本地的 PUBREC 数据包。若接收方在一定的时间内未收到 PUBCOMP 数据包,则会重新发送 PUBREL 数据包;
PUBREC 数据包
固定头
固定头中数据包类型字段为 5 代表 PUBREC 数据包,该数据包剩余长度字段固定为 2。
可变头
PUBREC 数据包的可变头包含一个占据两个字节大小的包标识符。
消息体
PUBREC 数据包不包含消息体。
PUBREL 数据包
固定头
固定头中数据包类型字段为 6 代表 PUBREL 数据包,该数据包剩余长度字段固定为 2。
可变头
PUBREL 数据包的可变头包含一个占据两个字节大小的包标识符。
消息体
PUBREL 数据包不包含消息体。
PUBCOMP 数据包
固定头
固定头中数据包类型字段为 7 代表 PUBCOMP 数据包,该数据包剩余长度字段固定为 2。
可变头
PUBCOMP 数据包的可变头包含一个占据两个字节大小的包标识符。
消息体
PUBCOMP 数据包不包含消息体。
实际的订阅者 QoS
实际上,我们使用 Client 向 Broker 订阅主题时,有时指定的 QoS 和实际的 QoS 不一致,这是因为,在 MQTT 协议中规定:从 Broker 到 Subscriber 的实际 QoS 等于 Publisher 发布消息时指定的 QoS 等级与订阅时指定的 QoS 等级中的较小者。
因此,如果使用者希望 Subscriber 至少收到一次消息,那就需要确保 Publisher 和 Subscriber 的 QoS 都不小于 1。
QoS 的最佳实践
QoS 与会话
如果 Client 想要接收离线消息,那么在连接 Broker 时必须指定使用持久会话,这样 Broker 才会存储 Client 在离线期间没有确认接收的且 QoS 大于 1 的消息。
QoS 的选择
-
QoS 0:
-
Client 与 Broker 之间的网络连接非常稳定;
-
允许丢失部分消息;
-
不需要接收离线消息;
-
-
QoS 1:
-
应用需要接收所有的消息;
-
应用拥有处理重复消息的能力;
-
不接受 QoS 2 带来的额外开销(QoS 1 发送消息的速度比 QoS 2 快很多);
-
-
QoS 2:
-
应用需要接收所有的消息;
-
应用不能处理重复的消息;
-
接受 QoS 2 带来的额外开销;
-
Retained 消息
Retained 消息是指在 PUBLISH 数据包中 Retain 表示为 1 的消息,Broker 收到消息后,将会为该主题保存该 Retained 消息。当有新的订阅者订阅该主题时,Broker 会将这个消息立即发送给新的订阅者。
Retained 消息存在以下特点:
-
一个 topic 只能存在一条 Retained 消息,发布新的 Retained 消息将会覆盖旧消息;
-
若订阅者使用通配符订阅主题,那么该订阅者将会收到所有的匹配主题的 Retained 消息;
-
只有新的订阅者才能够收到 Retained 消息;
需要注意:Retained 消息与持久会话没有任何关系。Retained 消息针对主题 topic,Broker 为每一个 topic 单独存储;持久会话针对客户端 Client,Broker 为每一个 Client 单独存储。
当 Retained 消息发送到订阅者时,PUBLISH 数据包中的 Retain 字段仍然为 1,订阅者可以根据该字段判断该消息是否是 Retained 消息从而进行相应的处理。
LWT 遗嘱消息
当 Broker 检测到 Client 非正常断开连接时,就会向 Client 遗嘱主题中发布相应的遗嘱消息 LWT(Last Will and Testament)。
遗嘱消息的相关设置是在建立连接时的 CONNECT 数据包中设定的:
-
Will Flag:是否使用 LWT 遗嘱消息;
-
Will Topic:遗嘱主题,不可使用通配符;
-
Will QoS:发布遗嘱消息时的 QoS 等级;
-
Will Retain:遗嘱消息的 Retain 标识;
-
Will Message:遗嘱消息内容;
在以下情况下,Broker 将认为 Client 非常长断开:
-
Broker 检测到底层的 I/O 异常;
-
Client 未能在 Keepalive 的间隔内和 Broker 之间进行消息交互;
-
Client 在关闭底层 TCP 连接前没有发送 DISCONNECT 数据包;
-
Broker 因为协议错误而关闭了与 Client 的连接;
Keepalive
在 MQTT 协议当中,Broker 需要知道 Client 是否非正常的断开,以判断是否发布遗嘱消息。MQTT 是基于 TCP 的一个应用层协议,理论上当 TCP 协议连接断开时会通知上层应用,但 TCP 协议存在半打开连接的问题,在这种状态下一端的 TCP 连接已经失效,但是另一端却无法立即感知,需要很长一段时间才能发现连接已断开。
因此,MQTT 协议设计了 Keepalive 机制。在建立连接时,传输一个以秒为单位的 Keepalive 参数,MQTT 协议规定:在 1.5 倍 Keepalive 时间间隔内,若 Broker 未收到 Client 的任何数据包,则 Broker 就会认为自身与 Client 之间的连接已断开。对于 Client 同理。
为此,MQTT 协议专门设计了一对 PINGREQ\PINGRESP 数据包以满足 Keepalive 的约定和连接状态的侦测。
Keepalive 机制存在以下几种特性:
-
若在一个 Keepalive 时间间隔内,Client 与 Broker 存在过数据包传输,那么 Client 就没有必要再发送 PINGREQ 数据包了;
-
Keepalive 的数值由 Client 指定,不同的 Client 可以指定不同的 Keepalive 参数;
-
Keepalive 最大值为 18 小时 12 分 15 秒;
-
Keepalive 值设置为 0 表示不使用 Keepalive 机制;
PINGREQ 数据包
当 Client 在一个 Keepalive 时间间隔内没有向 Broker 发送任何数据包时,它应该向 Broker 发送 PINGREQ 数据包以保持连接活性。数据包格式如下:
固定头
固定头中的数据包类型字段值为 12 表示 PINGREQ 数据包,其剩余长度字段固定值为 0。
可变头
PINGREQ 数据包无可变头。
消息体
PINGREQ 数据包无消息体。
PINGRESP 数据包
当 Broker 收到 Client 发送的 PINGREQ 数据包时,其应该回复一个 PINGRESP 数据包。
固定头
固定头中的数据包类型字段值为 13 表示 PINGRESP 数据包,其剩余长度字段固定值为 0。
可变头
PINGRESP 数据包无可变头。
消息体
PINGRESP 数据包无消息体。
MQTT 5 新特性
用户属性
MQTT 5 中可以在 PUBLISH、CONNECT 以及带有 Return Code 的数据包中携带一个或者多个用户属性数据。
-
PUBLISH 数据包中所携带的用户属性由发送方的应用定义,随消息被 Broker 转发到消息的订阅方;
-
CONNECT 数据包和 ACKs 数据包也可以携带发送者自定义的用户属性数据;
共享订阅
假设某个主题承载的数据量非常的大,一般情况下我们只能在订阅者中启动多个线程去进行处理以减轻负载。在 MQTT 5.0 中提供了共享订阅的功能,多个 Client 可以共同订阅一个共享主题,发布在该主题的消息就会被均衡的发送给所有的共享订阅 Client,实现负载均衡。
消息过期
MQTT 5.0 包含了消息过期功能,在进行消息发布时可以指定该消息多久后过期,Broker 不会将已过期的离线消息发送给 Client。
重复主题
MQTT 5.0 中,如果将 PUBLISH 数据包的主题名设置为长度为 0 的字符串,那么 Broker 将会使用上一次发布时的主题作为当前消息的主题,以此降低多次发布到统一主题的额外开销。
Broker 能力查询
MQTT 5.0 的 CONNACK 数据包包含了一些预定义的头部数据,用于标识 Broker 支持哪些功能,如下表所示:
预定义头 | 数据类型 | 描述 |
---|---|---|
Retain Available | Boolean | 是否支持 Retained 消息 |
Maximum QoS | Number | Client 可用于订阅和发布的最大 QoS |
Wildcard avaliable | Boolean | 订阅时是否可以使用通配符主题 |
Subscription identifiers avaliable | Boolean | 是否支持 Subscription identifier |
Shared Subscription avaliable | Boolean | 是否支持共享订阅 |
Maximum Message Size | Number | 可发送的最大消息长度 |
Server Keepalive | Number | Broker 支持的最大 Keepalive 值 |
双向 DISCONNECT
MQTT 5.0 中,Broker 主动断开与 Client 的连接时也会发送 DISCONNECT 数据包,双方互发的数据包当中都包含一个 Reason Code 用于标识断开的原因,如下图所示:
Reason Code | 发送方 | 描述 |
---|---|---|
0 | Client 或 Broker | 正常断开连接,不发布遗嘱消息 |
4 | Client | 正常断开连接,但是要求 Broker 发布遗嘱消息 |
129 | Client 或 Broker | MQTT 数据包格式错误 |
135 | Broker | 请求未授权 |
143 | Broker | 内部过滤器格式正确,但 Broker 不接收 |
144 | Client 或 Broker | 主题名格式正确,但 Client 或 Broker 不接收 |
153 | Client 或 Broker | 消息体格式不正确 |
154 | Broker | 不支持 Retained 消息 |
155 | Broker | QoS 等级不支持 |
158 | Broker | 不支持共享订阅 |
162 | Broker | 订阅时不支持通配符主题名 |