
目录
6.2 Session Expiry Interval 介绍
1. 简介
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于 TCP/IP 协议上,由IBM 在 1999 年发布。MQTT 最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。

MQTT 是基于 TCP 的应用层协议,与 http、ftp 协议是处于同一层。几乎所有使用 ESP32接入 IOT 物联网的项目中,都用的是 MQTT 协议,因此我们有必要学习 MQTT 协议基本内容。
2. 工作原理
MQTT 是基于发布-订阅模式的通信协议,由 MQTT 客户端通过主题(Topic)发布或订阅消息,通过 MQTT Broker 集中管理消息路由,并依据预设的服务质量等级(QoS)确保端到端消息传递可靠性。
2.1 MQTT 客户端
任何运行 MQTT 客户端库的应用或设备都是 MQTT 客户端。例如,使用 MQTT 的即时通讯应用是客户端,使用 MQTT 上报数据的各种传感器是客户端,各种 MQTT 测试工具也是客户端。
一些运行MQTT客户端库的应用:
MQTT 测试工具:
这里我在Winodws环境下部署了一个MQTT Broker,并且使用MQTTX客户端,感兴趣可以了解一下:
2.2 MQTT Broker
MQTT Broker 是负责处理客户端请求的关键组件,包括建立连接、断开连接、订阅和取消订阅等操作,同时还负责消息的转发。一个高效强大的 MQTT Broker 能够轻松应对海量连接和百万级消息吞吐量,从而帮助物联网服务提供商专注于业务发展,快速构建可靠的 MQTT 应用。
2.3 发布-订阅模式
发布-订阅模式与客户端-服务器模式的不同之处在于,它将发送消息的客户端(发布者)和接收消息的客户端(订阅者)进行了解耦。发布者和订阅者之间无需建立直接连接,而是通过 MQTT Broker 来负责消息的路由和分发。
举个例子,温度传感器作为客户端连接到 MQTT Broker,并通过发布操作将温度数据发布到一个特定主题(例如 Temperature)。MQTT Broker 接收到该消息后会负责将其转发给订阅了相应主题(Temperature)的订阅者客户端:

2.4 主题
MQTT 协议根据主题来转发消息。主题通过 / 来区分层级,类似于 URL 路径,例如:
chat/room/1
sensor/10/temperature
sensor/+/temperature
MQTT 主题支持以下两种通配符:+ 和 #。
+:表示单层通配符,例如 a/+ 匹配 a/x 或 a/y。
#:表示多层通配符,例如 a/# 匹配 a/x、a/b/c/d。
2.5 QoS
MQTT 提供了三种服务质量(QoS),在不同网络环境下保证消息的可靠性。
- QoS 0:消息最多传送一次。如果当前客户端不可用,它将丢失这条消息。
- QoS 1:消息至少传送一次。
- QoS 2:消息只传送一次。
3. 控制报文
3.1 简介
控制报文(数据报文)是网络中交换与传输的数据最小单元,通俗来讲就是站点一次性要发送的“数据块”。它包含了将要发送的完整数据信息,其长短不一致,长度不限且可变。MQTT 客户端和服务端通过交换控制报文来完成它们的工作,比如订阅主题和发布消息。
截止到目前为止,MQTT 定义了 15 种控制报文类型,我们按照功能分类一下如下:

详细可以参考官方给的协议文档:
3.2 报文格式
在MQTT协议中,一个MQTT数据包由:固定报头、可变报头、消息体(也称为“有效载荷”)三部分构成。
| 固定报头(Fixed header) | 可变报头(Variable header) | 有效载荷(payload) |
| 所有报文都包含 | 部分报文包含 | 部分报文包含 |
3.2.1 固定报头
固定报头由报文类型、标识位和报文剩余长度三个字段组成:

3.2.1.1 报文类型
报文类型位于固定报头第一个字节的高 4 位(也就是MQTT Contorl Packet Type对应的位置),它是一个无符号整数,很显然,它表示当前报文的类型,例如 1 表示这是一个 CONNECT 报文,2 表示 CONNACK 报文等等,详细的列表可以参考官方文档,找到这个表格:

3.2.1.2 标识位
固定报头第一个字节中剩下的低 4 位(Flags对应的位置),包含了由控制报文类型决定的标识位。不过到 MQTT 5.0 为止,只有 PUBLISH 报文的这四个比特位被赋予了明确的含义:
- Bit 3:DUP,表示当前 PUBLISH 报文是否是一个重传的报文。
- Bit 2,1:QoS,表示当前 PUBLISH 报文使用的服务质量等级。
- Bit 0:Retain,表示当前 PUBLISH 报文是否是一个保留消息。
其他所有的报文中,这 4 位都仍是保留的,即它们是一个固定的,不可随意变更的值,如下是官方手册的内容:

3.2.1.3 剩余长度
剩余长度指示了当前控制报文剩余部分的字节数,也就是可变报头和有效载荷这两个部分的长度。所以 MQTT 控制报文的总长度实际上等于固定报头的长度加上剩余长度:

对于剩余长度的计算,MQTT对该字段使用一种特殊的可变字节编码方式,其可以在保证可以表示很大长度的同时,尽可能节省空间。
编码规则:
- 每个字节使用低 7 位(bit 0~6)来存储数据本身。也就是说,单个字节可以描述的最大长度是 127 字节
- 最高位(第 7 位,bit 7)用作延续位(Continue Bit)。

举个例子:

至于为什么这么设计,我们可以想一下,在 MQTT 中,存在很多长度不确定的字段,例如 PUBLISH 报文中的 Payload 部分就用来承载实际的应用消息内容,而应用消息的长度显然是不固定的。所以我们需要一个额外的字段来指示这些不定长内容的长度,以便接收端正确地解析。
一个 2 兆大小,也就是总共 2,097,152 个字节的应用消息,我们就需要一个 4 字节长度的整数才能够指示它的长度。但并不是所有的应用消息都有这么大,更多情况下是几 KB 甚至几个字节。用一个 4 字节长度的整数来指示一个总共 2 个字节长度的应用消息,显然是过于浪费了。
所以 MQTT 的可变字节整数就被设计出来了,它将每个字节中的低 7 位用于编码数据,最高的有效位用于指示是否还有更多的字节。这样,长度小于 128 字节时可变字节整数只需要一个字节就可以指示。可变字节整数的最大长度为 4 个字节,所以最多可以指示长度为 (2^28 - 1) 字节,也就是 256 MB 的数据:

3.2.2 可变报头
可变报头的内容取决于具体的报文类型。
例如,CONNECT 报文的可变报头按顺序包含了协议名、协议级别、连接标识、Keep Alive 和属性这五个字段:

PUBLISH 报文的可变报头则按顺序包含了主题名、报文标识符和属性这三个字段:

需要注意这里提到的顺序,可变报头中字段出现的顺序必须严格遵循协议规范,因为接收端只会按照协议规定的字段顺序进行解析。我们也不能随意地遗漏某个字段,除非是协议明确要求或允许的。例如,在 CONNECT 报文的可变报头中,如果协议名之后直接就是连接标识,那么就会导致报文解析失败。而在 PUBLISH 报文的可变报头中,报文标识符就只有在 QoS 不为 0 的时候才能存在。
属性是 MQTT 5.0 引入的一个概念。属性字段基本上都是可变报头的最后一部分,由属性长度和紧随其后的一组属性组成,这里的属性长度指的是后面所有属性的总长度:

所有的属性都是可选的,因为它们通常都有一个默认值,如果没有任何属性,那么属性长度的值就为 0。
每个属性都由一个定义了属性用途和数据类型的标识符和具体的值组成。不同属性的数据类型可能不同,比如一个是双字节长度的整数,另一个则是 UTF-8 编码的字符串,所以我们需要按照标识符所声明的数据类型对属性进行解析。

属性之间的顺序可以是任意的,这是因为我们可以根据标识符知道这是哪个属性,以及它的长度是多少。
属性通常都是为了某个专门的用途而设计的,比如在 CONNECT 报文中就有一个用于设置会话过期时间的的 Session Expiry Interval 属性,但显然我们在 PUBLISH 报文中就不需要这个属性。所以 MQTT 也严格定义了属性的使用范围,一个合法的 MQTT 控制报文中不应该包含不属于它的属性。

3.2.3 有效载荷
最后是有效载荷部分。我们可以将报文的可变报头看作是它的附加项,而有效载荷则用于实现这个报文的核心目的。
比如在 PUBLISH 报文中,Payload 用于承载具体的应用消息内容,这也是 PUBLISH 报文最核心的功能。而 PUBLISH 报文的可变报头中的 QoS、Retain 等字段,则是围绕着应用消息提供一些额外的能力。
SUBSCRIBE 报文也是如此,Payload 包含了想要订阅的主题以及对应的订阅选项,这也是 SUBSCRIBE 报文最主要的工作。
3.3 报文验证
这里我使用wireshark这个抓包工具,官网链接:
下载完,安装非常简单,一直下一步即可,安装完双击图标打开:


这里我需要捕获自己本地部署的broker,双击打开:

在上方搜索框搜索mqtt,进行只查看mqtt数据:

我们来到MQTTX客户端服务器点击,随便找一个我们之前创建的链接,点击连接:

可以看到Command 和 ACK ,我们连接的信息:

想查看信息,可以点击想要查看的信息,下面是一些信息:

可以对照一下,使我们连接的一些基本信息:

这些信息是一个心跳检测机制,证明客户端和服务器还在正常连接:

此时我们在MQTTX客户端发送一个消息:

来到抓包工具可以看到相关信息:

剩下可以自己发送接收查看一下,这里不再做过多验证。
4. QoS简介
MQTT协议的传输,其实其底层是TCP传输协议,在不稳定的网络环境中,使用 TCP 传输协议的 MQTT 备可能会面临确保可靠通信的挑战。而为了解决这个问题,MQTT 引入了一种 QoS 机制,提供多种消息交互选项,以满足用户在不同场景下对可靠消息传递的特定要求。
MQTT 中的 QoS 指的是发布者与订阅者之间消息传递的保证级别。它提供三个服务级别:
- QoS 0 – 最多交付一次(可能丢失消息)
- QoS 1 – 至少交付一次(可以保证收到消息,但消息可能重复)
- QoS 2 – 只交付一次(可以保证消息既不丢失也不重复)
QoS 等级从低到高,不仅意味着消息可靠性的提升,也意味着传输复杂程度的提升。
在一个完整的从发布者到订阅者的消息投递流程中,QoS 等级是由发布者在 PUBLISH 报文中指定的,大部分情况下 Broker 向订阅者转发消息时都会维持原始的 QoS 不变。不过也有一些例外的情况,根据订阅者的订阅要求,消息的 QoS 等级可能会在转发的时候发生降级。
例如,订阅者在订阅时要求 Broker 可以向其转发的消息的最大 QoS 等级为 QoS 1,那么后续所有 QoS 2 消息都会降级至 QoS 1 转发给此订阅者,而所有 QoS 0 和 QoS 1 消息则会保持原始的 QoS 等级转发。

4.1 QoS 0——最多交付一次
QoS 0 消息即发即弃,不需要等待确认,不需要存储和重传,因此对于接收方来说,永远都不需要担心收到重复的消息。

对于为什么Qos 0 消息会丢失呢,我们知道 TCP 只能保证在连接稳定不关闭的情况下消息的可靠到达,一旦出现连接关闭、重置,仍有可能丢失当前处于网络链路或操作系统底层缓冲区中的消息。而QoS 0 传递消息时,消息的可靠性完全依赖于底层的 TCP 协议,这也就导致了 QoS 0 消息的丢失。
4.2 QoS 1——至少交付一次
QoS 1 加入了应答与重传机制,发送方只有在收到接收方的 PUBACK 报文以后,才能认为消息投递成功,在此之前,发送方需要存储该 PUBLISH 报文以便下次重传。
运作机制,QoS 1 需要在 PUBLISH 报文中设置 Packet ID,而作为响应的 PUBACK 报文,则会使用与 PUBLISH 报文相同的 Packet ID,以便发送方收到后删除正确的 PUBLISH 报文缓存:

至于消息重复发送的原因有两种,第一种发送方消息因为某些原因并没有发送到接收方,这样接收方就不能正确的应答,需要发送方再次发送,接收到才能正确应答,这种情况下世界上接受方也就接收到了一次消息:

另一种原因,因为接收方一直没告诉发送方我已经接收到消息了,发送方以为接收方还没有接收到消息便继续发送,导致消息重复:

4.3 QoS 2——只交付一次
每一次的 QoS 2 消息投递,都要求发送方与接收方进行至少两次请求/响应流程。这样可以解决消息可能丢失或者重复的问题,不过相应的相应地,它也带来了最复杂的交互流程和最高的开销。
运作机制,我们先看张图:

①:发送方存储 PUBLISH 报文;
②:发送方,发送 QoS 为 2 的 PUBLISH 报文,以启动一次 QoS 2 消息的传输,然后等待接收方回复 PUBREC 报文。
③:当发送方收到 PUBREC 报文,即可确认对端已经收到了 PUBLISH 报文,发送方将不再需要重传这个报文,并且也不能再重传这个报文。所以此时发送方可以删除本地存储的 PUBLISH 报文。
④:发送方,发送一个 PUBREL 报文,通知对端自己准备将本次使用的 Packet ID 标记为可用了。
⑤:与 PUBLISH 报文一样,我们需要确保 PUBREL 报文到达对端,所以也需要一个响应报文,并且这个 PUBREL 报文需要被存储下来以便后续重传。
⑥:接收方收到 PUBREL 报文,确认在这一次的传输流程中不会再有重传的 PUBLISH 报文到达,因此回复 PUBCOMP 报文表示自己也准备好将当前的 Packet ID 用于新的消息了。
⑦:发送方收到 PUBCOMP 报文,这一次的 QoS 2 消息传输就算正式完成了。在这之后,发送方可以再次使用当前的 Packet ID 发送新的消息,而接收方再次收到使用这个 Packet ID 的 PUBLISH 报文时,也会将它视为一个全新的消息。
QoS 2 消息不会重复的原因在于,QoS 2 中增加的 PUBREL 流程,正是提供了帮助通信双方协商 Packet ID 何时可以重用的能力,QoS 2 规定,发送方只有在收到 PUBREC 报文之前可以重传 PUBLISH 报文。一旦收到 PUBREC 报文并发出 PUBREL 报文,发送方就进入了 Packet ID 释放流程,不可以再使用当前 Packet ID 重传 PUBLISH 报文。同时,在收到对端回复的 PUBCOMP 报文确认双方都完成 Packet ID 释放之前,也不可以使用当前 Packet ID 发送新的消息。
4.4 总结
4.4.1 QoS 0
QoS 0 消息即发即弃,不需要等待确认,不需要存储和重传,只要我手里有数据我就发,我不管接没接收到,不管我发没发过去,我只管发送,其他一律不管:

4.4.2 QoS 1
我虽然会了解你是否接收到了,但我也就只了解你是否接收到了,接收到几次我不管,反正我就一直发,直到你告诉我你接收到了,中间因为什么原因没接收到我不管,我只管你是否发给我你已经接收到了的值:

4.4.3 QoS 2
发送方和接收方规定一个标志位,在这个标志位由接收方反馈给发送方之前,发送方会发送之前的数据,接收方也默认为这是之前的消息,而如果这个标志位给到发送方,就认为这之后的消息就是新消息:

5. 主题(Topics)和通配符(Wildcards)
MQTT 的灵活性和高效性核心在于两个关键概念:主题(Topics)和通配符(Wildcards)。它们构成了 MQTT 发布-订阅模型的基础。
MQTT 主题是消息的寻址机制,使设备和应用能够组织和筛选信息流。主题提供了分层结构,可以代表 IoT 系统的不同方面,比如设备位置、传感器类型和数据类别。
MQTT 通配符,它们是增强协议灵活性的有力工具。通配符允许订阅者同时接收多个主题的消息,简化客户端逻辑并减少网络开销。
5.1 主题
MQTT 主题本质上是一个 UTF-8 编码的字符串,是 MQTT 协议进行消息路由的基础。MQTT 主题类似 URL 路径,使用斜杠 / 进行分层:
chat/room/1
sensor/10/temperature
sensor/+/temperature
sensor/#
为了避免歧义且易于理解,通常不建议主题以 / 开头或结尾:
/chat
chat/
MQTT 客户端在订阅或发布时即自动的创建了主题,开发者无需再关心主题的创建,并且也不需要手动删除主题。因此MQTT 主题不需要提前创建。
一个简单的 MQTT 订阅与发布流程, APP 1 订阅了sensor/2/temperature 主题后,将能接收到 Sensor 2 发布到该主题的消息:

5.2 通配符
MQTT 主题通配符包含单层通配符 + 及多层通配符 #,主要用于客户端一次订阅多个主题。
注意:通配符只能用于订阅,不能用于发布。
5.2.1 单层通配符(+)
加号 (“+” U+002B) 是用于单个主题层级匹配的通配符。在使用单层通配符时,单层通配符必须占据整个层级,例如:
+ 有效
sensor/+ 有效
sensor/+/temperature 有效
sensor+ 无效(没有占据整个层级)
简单来说单层通配符只能自己一个人存在,自己一个代表一个层级,例如客户端订阅了主题 sensor/+/temperature ,那么实际上就是订阅了:
sensor/1/temperature
sensor/2/temperature
...
sensor/n/temperature
5.2.2 多层通配符(#)
井字符号(“#” U+0023)是用于匹配主题中任意层级的通配符。多层通配符表示它的父级和任意数量的子层级,在使用多层通配符时,它必须占据整个层级并且必须是主题的最后一个字符,例如:
# 有效,匹配所有主题
sensor/# 有效
sensor/bedroom# 无效(没有占据整个层级)
sensor/#/temperature 无效(不是主题最后一个字符)
他比单层多加了一些限制条件,单层顾名思义就是表示当前一层,多层需要放到最后面,表示后面所有层,例如客户端订阅主题 senser/#,它将会收到以下主题的消息::
sensor
sensor/temperature
sensor/1/temperature
5.2.3 系统主题($SYS/)
除了上述两个通配符,其实在使用过程中,还有其他的通配符,只不过MQTT 协议暂未明确规定 $SYS/ 主题标准,但大多数 MQTT 服务器都遵循该标准建议。
以 $SYS/ 开头的主题为系统主题,系统主题主要用于获取 MQTT 服务器自身运行状态、消息统计、客户端上下线事件等数据。
详细了解,可以点击跳转官方文档:
主要支持客户端上下线事件、收发流量、消息收发、系统监控等丰富的系统主题,用户可通过订阅 $SYS/# 主题获取所有系统主题消息。
5.2.4 共享订阅($share)
共享订阅是 MQTT 5.0 引入的新特性,用于在多个订阅者之间实现订阅的负载均衡,MQTT 5.0 规定的共享订阅主题以 $share 开头。
下图中,3 个订阅者用共享订阅的方式订阅了同一个主题 $share/g/topic,其中topic 是它们订阅的真实主题名,而 $share/g/ 是共享订阅前缀(g/ 是群组名,可为任意 UTF-8 编码字符串):

需要注意一点,虽然 MQTT 协议在 5.0 版本才引入共享订阅,但是 EMQX 从 MQTT 3.1.1 版本开始就支持共享订阅。
6. 会话
MQTT 会话本质上就是一组需要服务端和客户端额外存储的上下文数据,这些数据可以仅持续与网络连接一样长的时间,也可以跨越多个连续的网络连接存在。当客户端与服务端借助这些会话数据恢复通信时,可以让网络中断就像从未发生过一样。
MQTT 为服务端和客户端分别定义了它们需要存储的会话状态。对于服务端来说,它需要存储以下内容:
- 会话是否存在。
- 客户端的订阅列表。
- 已发送给客户端,但是还没有完成确认的 QoS 1 和 QoS 2 消息。
- 等待传输给客户端的 QoS 0 消息(可选),QoS 1 和 QoS 2 消息。
- 从客户端收到的,但是还没有完成确认的 QoS 2 消息。
- 遗嘱消息与遗嘱过期间隔。
- 会话过期时间。
对于客户端来说,它需要存储以下内容:
- 已发送给服务端,但是还没有完成确认的 QoS 1 和 QoS 2 消息。
- 从服务端收到的,但是还没有完成确认的 QoS 2 消息。
显而易见的是,让服务端和客户端永久存储这些会话数据,不仅会带来很多额外的存储成本,而且在很多场景中也没有必要。譬如我们只是为了避免网络连接短暂中断导致的消息丢失,那么一般将会话数据设置为在连接断开后保留短暂的几分钟即可。
另外,当客户端与服务端会话状态不一致时,比如客户端设备因为重启导致会话数据丢失,那么它需要在连接时告知服务端丢弃原有的会话并创建一个全新的会话。
针对这两点,MQTT 5.0 提供了 Clean Start 和 Session Expiry Interval 这两个连接字段来控制会话的生命周期。
6.1 Clean Start 介绍
Clean Start 位于 CONNECT 报文的可变报头,客户端在连接时通过这个字段指定是否复用已存在的会话,它只有 0 和 1 两个可取值。
当 Clean Start 被设置为 0,如果服务端存在与客户端连接时指定的 Client ID 关联的会话,那么它必须使用这个会话来恢复通信。
当 Clean Start 设置为 1,客户端和服务端必须丢弃任何已存在的会话,并开始一个全新的会话。相应地,服务端也会把 CONNACK 报文中的 Session Present 字段设置为 0。

6.2 Session Expiry Interval 介绍
Session Expiry Interval 同样位于 CONNECT 报文的可变报头,不过它是一个可选的连接属性。它被用来指定会话在网络断开后能够在服务端保留的最长时间,如果到达过期时间但网络连接仍未恢复,服务端就会丢弃对应的会话状态。它有三个典型的值:
- 没有指定此属性或者设置为 0,表示会话将在网络连接断开时立即结束。
- 设置为一个大于 0 的值,则表示会话将在网络连接断开的多少秒之后过期。
- 设置为 0xFFFFFFFF,即 Session Expiry Interval 属性能够设置的最大值时,表示会话永不过期。
7. 消息
7.1 保留消息(Retained Message)
发布者发布消息时,如果 Retained 标记被设置为 true,则该消息即是 MQTT 中的保留消息(Retained Message)。MQTT 服务器会为每个主题存储最新一条保留消息,以方便消息发布后才上线的客户端在订阅主题时仍可以接收到该消息。
当客户端订阅主题时,如果服务端存在该主题匹配的保留消息,则该保留消息将被立即发送给该客户端:

我们来看一下具体使用,我们先将Retain的勾选去掉,然后发送一条普通消息,主题为example/test1,发送内容1:

然后把Retain勾选给勾上,创建一个新的主题example/test2,发送两次内容分别为1和2:

然后我们通过通配符订阅这两个主题,example/+:

我们将会看到该订阅能成功收到第二条保留消息,example/test1 的普通消息及 example/test2 的第一条保留消息都未收到。可见 MQTT 服务器只会为每个主题存储最新一条保留消息:

问题一:如何判断一条消息是否是保留消息?
当客户端订阅了有保留消息的主题后,即会收到该主题的保留消息,可通过消息中的保留标志位判断是否是保留消息。需要注意的是,在保留消息发布前订阅主题,将不会收到保留消息。需要待保留消息发布后,重新订阅该主题,才会收到保留消息。
问题二:保留消息将保存多久?如何删除?
服务器只会为每个主题保存最新一条保留消息,保留消息的保存时间与服务器的设置有关。若服务器设置保留消息存储在内存,则 MQTT 服务器重启后消息即会丢失;若存储在磁盘,则服务器重启后保留消息仍然存在。
补充:
保留消息的存储方式:内存存储(默认存储类型)、磁盘存储,我们找到EMQX,找到如下位置:


MQTT 可以通过Session Expiry Interval为离线客户端缓存尚未发送的消息,然后在客户端恢复连接时发送。但如果客户端离线时间较长,可能有一些寿命较短的消息已经没有必要必须发送给客户端了,继续发送这些过期的消息,只会浪费网络带宽和客户端资源。
例如,我们做一个环境检测的传感器设备,某天刚好起雾,设备此时建议开启雾灯,不过处理开启雾灯的设备掉线了,雾灯设备不能正常收到发送的消息,等雾灯设备恢复连接的时候,此时已经阳光明媚了,你在去把建议开启雾灯的消息,发给雾灯设备,其实已经不合适了,这条消息其实应当被丢弃,因此MQTT 5.0 引入的一个新特性:消息过期间隔。
它允许发布端为有时效性的消息设置一个过期间隔,如果该消息在服务端中停留超过了这个指定的间隔,那么服务端将不会再将它分发给订阅端。默认情况下,消息中不会包含消息过期间隔,这表示该消息永远不会过期:

例如上图,发布者发布两个主题,一个保留10s,另一个保留60s,此时订阅者掉线了30s,当30s后订阅者上线后,只能接收到第二个主题的消息。
注意:如果客户端在发布消息时设置了过期间隔,那么服务端在转发这个消息时也会包含过期间隔,但过期间隔的值会被更新为服务端接收到的值减去该消息在服务端停留的时间。这可以避免消息的时效性在传递的过程中丢失,特别是在桥接到另一个MQTT 服务器的时候。
怎么理解呢,还是看上图,我们发现订阅者在接收消息的时候并不是显示60s,而是60s减去掉线的30s(消息在服务端停留的时间)的时间,也就是30s。
我们来举例说明一下:
首先,我们将发布者连接到我们创建的服务器,然后来到订阅者将Clean Start开启,设置时间为300s:

然后订阅主题example/test:

订阅好以后先断开连接:

Clean Start 决定了客户端在连接服务器时,是否要建立一个全新的会话,并丢弃之前可能存在的任何会话信息。
Clean Start = true(清除/全新开始,勾选 )
客户端告诉 Broker:“请为我创建一个全新的、干净的会话。忽略并删除我之前的任何会话数据。”
Clean Start = false(继续/恢复会话,不勾选 )
客户端告诉 Broker:“尝试恢复我之前的会话。如果存在,请把我没确认的消息和订阅状态还给我。”
如果我们设置了 Session Expiry Interval 的时间。此时即使设置了 Clean Start = false,如果之前会话的过期时间已经到了,Broker 也会自动删除旧会话,此时效果就和 Clean Start = true 一样,会建立一个新会话。
这样做是为了模拟设备掉线的情况。
然后来到发布者,将Retain去掉,设置消息过期时长为10s,保存发送,发送消息为1:

同理设置一个60s的,发送消息为2:

分别发送出去,可以看到分别标注了各自的过期时间:

我们在回到订阅者,将Clean Start关掉:

点击连接可以发现只订阅了消息2的内容,并且过期是将刚好是停留设定的时间减去停留时间(可能会有毫秒的误差):

7.2 遗嘱消息(Will Message)
MQTT 最早在 3.1 协议规范的摘要中,提到了 Last Will and Testament(LWT)这个概念,实际上其也是遗嘱消息,只是表述不同,下面我们来了解一下。
在现实世界中,一个人可以制定一份遗嘱,声明在他去世后应该如何分配他的财产以及应该采取什么行动。在他去世后,遗嘱执行人会将这份遗嘱公开,并执行遗嘱中的指示。
在 MQTT 中,客户端可以在连接时在服务端中注册一个遗嘱消息,当该客户端意外断开连接,服务端就会向其他订阅了相应主题的客户端发送此遗嘱消息。这些接收者也因此可以及时地采取行动,例如向用户发送通知、切换备用设备等等。
与普通消息一样,我们可以为遗嘱消息设置主题(Will Topic)、保留消息标识位(Will Retain)、属性(Will Properties)、QoS(Will QoS)和有效载荷(Will Payload):

虽然上面提到的字段的用法与它们在普通消息中时完全相同,但是遗嘱消息可用的属性与普通应用消息略有不同,下表列出了它们的具体区别:
| 属性 (Property) | Will Message (遗嘱消息) | Normal Message (普通消息) | 说明 |
|---|---|---|---|
| Will Delay Interval (遗嘱延迟间隔) | ✔ | ✗ | 仅遗嘱消息可用。指定客户端断开后,Broker 延迟发布遗嘱消息的时间。 |
| Payload Format Indicator (有效负载格式指示符) | ✔ | ✓ | 两者均可用。指示消息内容是否为 UTF-8 编码的合法 JSON(1)或未指定(0)。 |
| Message Expiry Interval (消息过期间隔) | ✔ | ✓ | 两者均可用。消息在 Broker 中存储的过期时间(秒),0 表示立即过期。 |
| Content Type (内容类型) | ✔ | ✓ | 两者均可用。描述消息内容类型的字符串(如 application/json)。 |
| Topic Alias (主题别名) | ✗ | ✓ | 仅普通消息可用。用一个数字别名来替代完整的主题名,以减少传输开销。 |
| Response Topic (响应主题) | ✔ | ✓ | 两者均可用。用于指定一个主题,接收方应向该主题发布响应消息。 |
| Correlation Data (关联数据) | ✔ | ✓ | 两者均可用。用于匹配请求与响应的标识数据(如请求ID)。 |
| User Property (用户属性) | ✔ | ✓ | 两者均可用。可传递多个自定义的键值对字符串,用于传递额外元数据。 |
上表可以看出遗嘱消息只是多了一个专属属性:Will Delay Interval, 这个属性决定了服务端将在网络连接关闭后延迟多久发布遗嘱消息,并以秒为单位。默认情况下,服务端总是在网络连接意外关闭时立即发布遗嘱消息。但是很多时候,网络连接的中断是短暂的,所以客户端往往能够重新连接并继续之前的会话。这导致遗嘱消息可能被频繁地且无意义地发送。
- 如果没有指定 Will Delay Interval 或者将其设置为 0,服务端将仍然在网络连接关闭时立即发布遗嘱消息。
- 如果将 Will Delay Interval 设置为一个大于 0 的值,并且客户端能够在 Will Delay Interval 到期前恢复连接,那么该遗嘱消息将不会被发布。
需要注意一点的是,遗嘱消息是会话状态的一部分,当会话结束,遗嘱消息也无法继续单独存在。但是在遗嘱消息延迟发布期间,会话可能过期,也可能因为客户端在新的连接中设置Clean Start 为 1 所以服务端需要丢弃之前的会话。为了避免丢失遗嘱,此时服务端必须发布该遗嘱消息,即便 Will Delay Interval 还没有到期。所以服务端最终何时发布遗嘱消息,取决于 Will Delay Interval 到期和会话结束这两种情况谁先发生。
我们来看一下遗嘱消息的使用,首先我们先找到发布者的参数设定打开Clean Start,将会话过期事件设置为300,这里还需要注意一点自动重连时间,因为我想把遗嘱消息时间设置为5s,因此这里的自动重连时间设置为6000ms,不然遗嘱消息还没发出去,就又连上了,还发啥遗嘱消息:

然后将设置界面往下滑,找到遗嘱消息的设置,遗嘱消息的主体example/will(自己设定不需要一样),将延迟时间设为5s:

来到订阅者这边,订阅遗嘱的主题:

来到发布者,发送一个主题为空,但设置了主题别名的消息,由于我们还未建立主题与主题别名的映射,所以这会让服务端认为客户端的行为不符合协议规范而关闭连接,并且发送遗嘱消息:

点击发送会发现:

订阅者,也在5s后正常接收到遗嘱消息:

7.3 延迟发布
延迟发布是 EMQX 支持的 MQTT 扩展功能。当客户端使用特殊主题前缀 $delayed/{DelayInteval} 发布消息时,将触发延迟发布功能,可以实现按照用户配置的时间间隔延迟发布消息。格式如下:
$delayed/{DelayInterval}/{TopicName}
- $delayed:使用 $delay 作为主题前缀的消息都将被视为需要延迟发布的消息。延迟间隔由下一主题层级中的内容决定。
- {DelayTime}:指定该 MQTT 消息延迟发布的时间间间隔或时间戳,单位是秒,允许的最大间隔是 42949669 秒(大约497天)。如果 {DelayTime} 无法被解析为一个整型数字,或者不在允许的范围内,EMQX 将丢弃该消息,客户端不会收到任何信息。
- {TopicName}:MQTT 消息的主题名称。
举个例子:
- $delayed/15/x/y:15 秒后将 MQTT 消息发布到主题 x/y。
- $delayed/60/a/b:1 分钟后将 MQTT 消息发布到 a/b。
- $delayed/3600/$SYS/topic:1 小时后将 MQTT 消息发布到 $SYS/topic。
我们来演示一下,来到 EMQX Dashboard 中,找到延迟发布,点击右边的设置:

看看是否启用:

来到MQTTX,来到订阅者,订阅一个主题:

然后来到发布者,发布主题,记得调用$delayed,以及需要延时的时间:

点击发送:

可以看到订阅者,在10s后订阅到消息:

8. 订阅
8.1 订阅选项
8.1.1 简介
在 MQTT 中,一个订阅由一个 主题过滤器 和对应的订阅选项组成。所以理论上,我们可以为每个订阅都设置不同的订阅选项。
- 主题过滤器:决定了服务端将向我们转发哪些主题下的消息
- 订阅选项:是允许我们进一步定制服务端的转发行为
MQTT 5.0提供了4个订阅选项:QoS、No Local、Retain As Published、Retain Handling。
8.1.2 QoS
QoS 是最常用的一个订阅选项,它表示服务端在向订阅端发送消息时可以使用的最大 QoS 等级。
客户端可能会在订阅时指定一个小于 2 的 QoS,因为它的实现不支持 QoS 1 或者 QoS 2。而如果服务端支持的最大 QoS 小于客户端订阅时请求的最大 QoS,那么显然服务端将无法满足客户端的要求,这时服务端就会通过订阅的响应报文(SUBACK)告知订阅端最终授予的最大 QoS 等级,订阅端可以自行评估是否接受并继续通信,简单来说:
服务端支持的最大 QoS < 客户端订阅时请求的最大QoS

当我们订阅时请求的最大 QoS,小于消息发布时的 QoS 时,为了尽可能地投递消息,服务端不会忽略这些消息,而是会在转发时对这些消息的 QoS 进行降级处理:
订阅时请求的最大QoS < 消息发布时的QoS

来演示一下,首先创建一个主题QoS选择为1,然后订阅该主题QoS选择为0:

我们发布主题看一下,可以看到进行降级处理了:

8.1.3 No Local
No Local 只有 0 和 1 两个可取值,为 1 表示服务端不能将消息转发给发布这个消息的客户端,为 0 则相反。
这个选项通常被用在桥接场景中。桥接本质上是两个 MQTT Server 建立了一个 MQTT 连接,然后相互订阅一些主题,Server 将客户端的消息转发给另一个 Server,而另一个 Server 则可以将消息继续转发给它的客户端:

官方给了我们一个例子,假设两个 MQTT Server 分别是 Server A 和 Server B,它们分别向对方订阅了 # 主题。现在,Server A 将一些来自客户端的消息转发给了 Server B,而当 Server B 查找匹配的订阅时,Server A 也会位于其中。如果 Server B 将消息转发给了 Server A,那么同样 Server A 在收到消息后又会把它们再次转发给 Server B,这样就陷入了无休止的转发风暴。
这个怎么理解呢,我们前面了解知道MQTT客户端既可以做发送端,又可以做接收端,假如你发送端发送一个主题 example/test,接收端也发送一个主题 example/test,发送端想要接收,接收端的主题,不想要自己的主题,但是由于两者订阅的主题都是example/test,这样导致当发送端发送主题的时候,自己也会订阅到自己的主题:

而如果我们将No Local勾选上,再次点击发送会发现:

不会在接收到自己的主题:

8.1.4 Retain As Published
Retain As Published 同样只有 0 和 1 两个可取值,为 1 表示服务端在向此订阅转发应用消息时需要保持消息中的 Retain 标识不变,为 0 则表示必须清除。
Retain As Published 与 No Local 一样,同样也是主要适用于桥接场景。我们知道当服务端收到一条保留消息时,除了将它存储起来,还会将它像普通消息一样转发给当前已经存在的订阅者,并且在转发时会清除消息的 Retain 标识。
具体使用,我们设定两个订阅主题,一个勾选false,一个勾选true:


可以发现勾选true的显示了保留消息的标识:

8.1.5 Retain Handling
Retain Handling 这个订阅选项被用来向服务端指示当订阅建立时,是否需要发送保留消息。我们知道默认情况下,只要订阅建立,那么服务端中与订阅匹配的保留消息就会下发。
但某些时候,客户端可能并不想接收保留消息,比如客户端在连接时复用了会话,但是客户端无法确认上一次连接中是否成功创建了订阅,所以它可能会再次发起订阅。如果订阅已经存在,那么可能保留消息已经被消费过了,也可能服务端已经在会话中缓存了一些离线期间到达的消息,这时客户端可能并不希望服务端发布保留消息。
另外,客户端也可能在任何时刻都不想收到保留消息,即使是第一次订阅。比如我们将开关状态作为保留消息发送,但对某个订阅端来说,开关事件将触发一些操作,那么在这种情况下不发送保留消息是很有用的。
这三种不同的行为,我们可以通过 Retain Handling 来选择。
- 将 Retain Handling 设置为 0,表示只要订阅建立,就发送保留消息;
- 将 Retain Handling 设置为 1,表示只有建立全新的订阅而不是重复订阅时,才发送保留消息;
- 将 Retain Handling 设置为 2,表示订阅建立时不要发送保留消息。
我们来看一下这三个值都有什么不同的效果。
再开始前我们先检查一下,MQTTX是否开启了自动重连:

然后查看一下那个主题下有保留消息:

这里我使用example/test1。
8.1.4.1 Retain Handling 设置为 0
先将Clean Start勾选上,开启一个新的会发:

将保留消息设置为0:

点击确定以后会发现,先发送了一次保留消息:

然后再将Clean Start关掉,获取之前会话:

再次连接可以发现重复发送了保留消息:

8.1.4.2 Retain Handling 设置为 1
此时将值设为1:

订阅时会发送一次保留消息:

此时在断开和连接就不会再次发送保留消息:

8.1.4.3 Retain Handling 设置为 2
现在设置为2:

此时订阅就不会发送保留消息:

8.2 共享订阅
在普通的订阅中,我们每发布一条消息,所有匹配的订阅端都会收到该消息的副本。当某个订阅端的消费速度无法跟上消息的生产速度时,我们没有办法将其中一部分消息分流到其他订阅端中来分担压力。这使订阅端容易成为整个消息系统的性能瓶颈:

所以 MQTT 5.0 引入了共享订阅特性,它使得 MQTT 服务端可以在使用特定订阅的客户端之间均衡地分配消息负载。这表示,当我们有两个客户端共享一个订阅时,那么每个匹配该订阅的消息都只会有一个副本投递给其中一个客户端:

使用共享订阅,我们不需要对客户端的底层代码进行任何改动,只需要在订阅时使用遵循以下命名规范的主题即可:
$share/{Share Name}/{Topic Filter}
- $share 是一个固定的前缀,以便服务端知道这是一个共享订阅主题。
- {Share Name} 是一个由客户端指定的字符串,表示当前共享订阅使用的共享名。
- {Topic Filter} 则是我们实际想要订阅的主题。
EMQX 支持两种格式的共享订阅前缀,分别为带群组的共享订阅(前缀为 $share/<group-name>/)和不带群组的共享订阅(前缀为 $queue/)。两种共享订阅格式示例如下:
| 前缀格式 | 示例 | 前缀 | 真实主题名 |
|---|---|---|---|
| 带群组格式 | $share/abc/t/1 | $share/abc/ | t/1 |
| 不带群组格式 | $queue/t/1 | $queue/ | t/1 |
8.2.1 带群组的共享订阅
可以通过在原始主题前添加 $share/<group-name> 前缀为分组的订阅者启用共享订阅。组名可以是任意字符串。EMQX 同时将消息转发给不同的组,属于同一组的订阅者可以使用负载均衡接收消息。
例如,如果订阅者 s1、s2 和 s3 是组 g1 的成员,订阅者 s4 和 s5 是组 g2 的成员,而所有订阅者都订阅了原始主题 t1。共享订阅的主题必须是 $share/g1/t1 和 $share/g2/t1。当 EMQX 发布消息 msg1 到原始主题 t1 时:

- EMQX 将 msg1 发送给 g1 和 g2 两个组。
- s1、s2、s3 中的一个订阅者将接收 msg1。
- s4 和 s5 中的一个订阅者将接收 msg1。
8.2.2 不带群组的共享订阅
以 $queue/ 为前缀的共享订阅是不带群组的共享订阅。它是 $share 订阅的一种特例。您可以将其理解为所有订阅者都在一个订阅组中,如 $share/$queue。




4万+





