一篇文章带你彻底搞懂MQTT协议

第一章:MQTT介绍

1.1.定义

        MQTT(Message Queuing Telemetry Transport)是一种轻量级、基于发布-订阅模式的消息传输协议,适用于资源受限的设备和低带宽、高延迟或不稳定的网络环境。它在物联网应用中广受欢迎,能够实现传感器、执行器和其它设备之间的高效通信 。 简单的说客户端只需要订阅这个主题,当有其他客户端向这个主题发布消息时,这个客户端就可以收到这条消息 。

1.2.特点

MQTT 所具有的适用于物联网特定需求的特点和功能,使其成为物联网领域最佳的协议之一。它的主要特点包括:

  • 轻量级:物联网设备通常在处理能力、内存和能耗方面受到限制。MQTT 开销低、报文小的特点使其非常适合这些设备,因为它消耗更少的资源,即使在有限的能力下也能实现高效的通信。

  • 可靠:物联网网络常常面临高延迟或连接不稳定的情况。MQTT 支持多种 QoS 等级、会话感知和持久连接,即使在困难的条件下也能保证消息的可靠传递,使其非常适合物联网应用。

  • 安全通信:安全对于物联网网络至关重要,因为其经常涉及敏感数据的传输。为确保数据在传输过程中的机密性,MQTT 提供传输层安全(TLS)和安全套接层(SSL)加密功能。此外,MQTT 还通过用户名/密码凭证或客户端证书提供身份验证和授权机制,以保护网络及其资源的访问。

  • 双向通信:MQTT 的发布-订阅模式为设备之间提供了无缝的双向通信方式。客户端既可以向主题发布消息,也可以订阅接收特定主题上的消息,从而实现了物联网生态系统中的高效数据交换,而无需直接将设备耦合在一起。这种模式也简化了新设备的集成,同时保证了系统易于扩展。

  • 连续、有状态的会话:MQTT 提供了客户端与 Broker 之间保持有状态会话的能力,这使得系统即使在断开连接后也能记住订阅和未传递的消息。此外,客户端还可以在建立连接时指定一个保活间隔,这会促使 Broker 定期检查连接状态。如果连接中断,Broker 会储存未传递的消息(根据 QoS 级别确定),并在客户端重新连接时尝试传递它们。这个特性保证了通信的可靠性,降低了因间断性连接而导致数据丢失的风险。

  • 大规模物联网设备支持:物联网系统往往涉及大量设备,需要一种能够处理大规模部署的协议。MQTT 的轻量级特性、低带宽消耗和对资源的高效利用使其成为大规模物联网应用的理想选择。通过采用发布-订阅模式,MQTT 实现了发送者和接收者的解耦,从而有效地减少了网络流量和资源使用。此外,协议对不同 QoS 等级的支持使得消息传递可以根据需求进行定制,确保在各种场景下获得最佳的性能表现。

  • 语言支持:物联网系统包含使用各种编程语言开发的设备和应用。MQTT 具有广泛的语言支持,使其能够轻松与多个平台和技术进行集成,从而实现了物联网生态系统中的无缝通信和互操作性。

1.3.相关名词

客户端:使用MQTT的程序或设备。客户端总是通过网络连接到服务端。它可以

  • 发布应用消息给其它相关的客户端。

  • 订阅以请求接受相关的应用消息。

  • 取消订阅以移除接受应用消息的请求。

  • 从服务端断开连接。

服务端:一个程序或设备,作为发送消息的客户端和请求订阅的客户端之间的中介。它可以

  • 接受来自客户端的网络连接。

  • 接受客户端发布的应用消息。

  • 处理客户端的订阅和取消订阅请求。

  • 转发应用消息给符合条件的已订阅客户端。

订阅:订阅包含一个主题过滤器和一个最大的服务质量等级。订阅与单个会话关联。一个会话可以包含多个的订阅。会话的每个订阅都有一个不同的主题过滤器。 (与消息队列消费者订阅消息类似)

主题名:附加在应用消息上的一个标签,服务端已知且与订阅匹配。服务端发送应用消息的一个副本给每一个匹配的客户端订阅。 (与消息队列主题名类似)

主题过滤器:订阅中包含的一个表达式,用于表示相关的一个或多个主题。主题过滤器可以使用通配符。 (消费者过滤消息的条件)

会话:客户端和服务端之间的状态交互。一些会话持续时长与网络连接一样。(就是客户端到服务端的一次连接)

控制报文:通过网络连接发送的信息数据包。MQTT规范定义了十四种不同类型的控制报文,其中一个(PUBLISH报文)用于传输应用消息。 (可以理解为发布订阅的消息主体)

1.4.工作流程

  1. 客户端使用MQTT协议与 Broker 建立连接,可以选择使用 TLS/SSL 加密来实现安全通信。客户端提供认证信息,并指定会话类型(Clean Session 或 Persistent Session)。

  2. 客户端既可以向特定主题发布消息,也可以订阅主题以接收消息。当客户端发布消息时,它会将消息发送给 MQTT Broker;而当客户端订阅消息时,它会接收与订阅主题相关的消息。

  3. MQTT Broker 接收发布的消息,并将这些消息转发给订阅了对应主题的客户端。它根据 QoS 等级确保消息可靠传递,并根据会话类型为断开连接的客户端存储消息。

第二章:MQTT控制报文格式

2.1.MQTT控制报文结构

2.2.固定报头

固定报头是MQTT协议开头,2个字节,分为三个部分:标志位、报文类型、剩余长度 。

2.2.1.控制报文的类型

表示为4位无符号值,位置在固定报头的第一个字节的前4位。

2.2.2.标志位

固定报头第1个字节的剩余的4位包含每个MQTT控制报文类型特定的标志。控制报文与标志位是相互对应的,必须设置为这些值, 如果收到非法的标志,接收者必须关闭网络连接。

2.2.3.剩余长度

从固定报头的第二个字节开始,表示当前报文剩余部分的字节数(剩余部分=可变报头+有效负载)。 剩余长度不包括用于编码剩余长度字段本身的字节数。 剩余长度的字节数是不固定的,至少一个字节,最多4个字节,所以固定报头中包含的一个字节就是剩余长度的第一个字节。剩余长度的每一个字节的最高位是一个标志位,用来表示下一个字节是否也属于剩余长度。

例:

01000000 最高位为0,则说明后面的字节数据不属于剩余长度了。

10100000 00000001 第一个字节最高位为1,则说明后面的字节数据属于剩余长度,第二个字节最高位为0,则说明后面没有属于剩余长度的字节了,那么此剩余长度则为这2个字节。

因为剩余长度的每个字节的最高位为标志位,所以其真实值并不是这些字节直接合并而成 。

如果第一个字节的最高位为0,则说明剩余长度只有一个字节,并且其值就是这个字节的值,如:01000000 那么剩余长度的真实值就是:64

如果剩余长度超过一个字节,那么就需要将每个字节的高字节去掉,然后组成一个新的数据,如: 10100000 00000001 去掉标志位即 0100000 0000001 重新合并,合并时后面的字节在高位即 10100000 则剩余长度的值为十进制的160;

2.3.可变报头

某些MQTT控制报文包含一个可变报头部分。它在固定报头和负载之间。可变报头的内容根据报文类型的不同而不同,一般是包含和报文类型相关的数据。例如客户端连接服务器报文,包含了协议名,协议级别、连接标志、保持连接等。

2.3.1.报文标识符

很多控制报文的可变报头部分包含一两字节的报文标识符字段。

不同报文类型的报文标识符有不同的作用:

SUBSCRIBE,UNSUBSCRIBE和PUBLISH(QoS大于0)控制报文必须包含一个非零的16位报文标识符。客户端每次发送一个新的这些类型的报文时都必须分配一个当前未使用的报文标识符。如果一个客户端要重发这个特殊的控制报文,在随后重发那个报文时,它必须使用相同的标识符。当客户端处理完这个报文对应的确认后,这个报文标识符就释放可重用。

PUBACK, PUBREC, PUBREL报文必须包含与最初发送的PUBLISH报文相同的报文标识符。

SUBACK和UNSUBACK必须包含在对应的SUBSCRIBE和UNSUBSCRIBE报文中使用的报文标识符。

2.4.有效载荷

某些MQTT控制报文类型的最后一个部分包含有效载荷,对于PUBLISH来说有效载荷就是消息主体。

第三章:MQTT控制报文

3.1.CONNECT – 连接服务端

客户端到服务端的网络连接建立后,客户端发送给服务端的第一个报文必须是CONNECT报文,在一个网络连接上,客户端只能发送一次CONNECT报文。服务端必须将客户端发送的第二个CONNECT报文当作协议违规处理并断开客户端的连接。此报文类型可以理解为对此次会话的一系列设置。

3.1.1.固定报头

3.1.2.可变报头

CONNECT报文的可变报头按下列次序包含四个字段:协议名(Protocol Name),协议级别(Protocol Level),连接标志(Connect Flags)和保持连接(Keep Alive)。

  • 协议名:是表示协议名 MQTT 的UTF-8编码的字符串。MQTT规范的后续版本不会改变这个字符串的偏移和长度。如果协议名不正确服务端可以断开客户端的连接,也可以按照某些其它规范继续处理CONNECT报文。

  • 协议级别:客户端用8位的无符号值表示协议的修订版本。对于3.1.1版协议,协议级别字段的值是4。如果发现不支持的协议级别,服务端必须给发送一个返回码为0x01(不支持的协议级别)的CONNACK报文响应CONNECT报文,然后断开客户端的连接。

  • 连接标志:连接标志字节包含一些用于指定MQTT连接行为的参数。它还指出有效载荷中的字段是否存在。 服务端必须验证CONNECT控制报文的保留标志位(第0位)是否为0,如果不为0必须断开客户端连接。

清理会话 Clean Session: 连接标志字节的第1位。

客户端和服务端可以保存会话状态,以支持跨网络连接的可靠消息传输。这个标志位用于控制会话状态的生存时间。

如果清理会话标志被设置为0,服务端必须基于当前会话(使用客户端标识符识别)的状态恢复与客户端的通信。如果没有与这个客户端标识符关联的会话,服务端必须创建一个新的会话。当连接断开后,客户端 和服务端必须保存会话信息。当清理会话标志为0的会话连接断开之后,服务端必须将之后的QoS 1和QoS 2级别的消息保存为会话状态的一部分,如果这些消息匹配断开连接时客户端的任何订阅。服务端也可以保 存满足相同条件的QoS 0级别的消息。

如果清理会话(CleanSession)标志被设置为1,客户端和服务端必须丢弃之前的任何会话并开始一个新的会话。会话仅持续和网络连接同样长的时间。与这个会话关联的状态数据不能被任何之后的会话重用。

客户端的会话状态包括:①已经发送给服务端,但是还没有完成确认的QoS 1和QoS 2级别的消息。②已从服务端接收,但是还没有完成确认的QoS 2级别的消息。

服务端的会话状态包括:①会话是否存在,即使会话状态的其它部分都是空。②客户端的订阅信息。③已经发送给客户端,但是还没有完成确认的QoS 1和QoS 2级别的消息。④即将传输给客户端的QoS 1和QoS 2级别的消息。⑤已从客户端接收,但是还没有完成确认的QoS 2级别的消息。⑥可选,准备发送给客户端的QoS 0级别的消息。

遗嘱标志 Will Flag: 连接标志的第2位。

遗嘱标志(Will Flag)被设置为1,表示如果连接请求被接受了,遗嘱(Will Message)消息必须被存储在服务端并且与这个网络连接关联。之后网络连接关闭时,服务端必须发布这个遗嘱消息,除非服务端收到 DISCONNECT报文时删除了这个遗嘱消息 。

遗嘱消息发布的条件,包括但不限于:①服务端检测到了一个I/O错误或者网络故障。②客户端在保持连接(Keep Alive)的时间内未能通讯。③客户端没有先发送DISCONNECT报文直接关闭了网络连接。④由于 协议错误服务端关闭了网络连接。

如果遗嘱标志被设置为1,连接标志中的Will QoS和Will Retain字段会被服务端用到,同时有效载荷中必须包含Will Topic和Will Message字段。

一旦被发布或者服务端收到了客户端发送的DISCONNECT报文,遗嘱消息就必须从存储的会话状态中移除。

如果遗嘱标志被设置为0,连接标志中的Will QoS和Will Retain字段必须设置为0,并且有效载荷中不能包含Will Topic和Will Message字段 。

如果遗嘱标志被设置为0,网络连接断开时,不能发送遗嘱消息 。

服务端应该迅速发布遗嘱消息。在关机或故障的情况下,服务端可以推迟遗嘱消息的发布直到之后的重启。如果发生了这种情况,在服务器故障和遗嘱消息被发布之间可能会有一个延迟。

遗嘱QoS Will QoS: 连接标志的第4和第3位。

这两位用于指定发布遗嘱消息时使用的服务质量等级。

如果遗嘱标志被设置为0,遗嘱QoS也必须设置为0。

如果遗嘱标志被设置为1,遗嘱QoS的值可以等于0,1,2。它的值不能等于3。

遗嘱保留 Will Retain: 连接标志的第5位。

如果遗嘱消息被发布时需要保留,需要指定这一位的值。

如果遗嘱标志被设置为0,遗嘱保留标志也必须设置为0 [MQTT-3.1.2-15]。

如果遗嘱标志被设置为1:①如果遗嘱保留被设置为0,服务端必须将遗嘱消息当作非保留消息发布。②如果遗嘱保留被设置为1,服务端必须将遗嘱消息当作保留消息发布。

用户名标志 User Name Flag: 连接标志的第7位。

如果用户名(User Name)标志被设置为0,有效载荷中不能包含用户名字段。

如果用户名(User Name)标志被设置为1,有效载荷中必须包含用户名字段。

密码标志 Password Flag: 连接标志的第6位。

如果密码(Password)标志被设置为0,有效载荷中不能包含密码字段。

如果密码(Password)标志被设置为1,有效载荷中必须包含密码字段。

如果用户名标志被设置为0,密码标志也必须设置为0。

  • 保持连接:保持连接是一个以秒为单位的时间间隔,表示为一个16位的字,它是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。客户端负责保证控制报文发送的时间间隔不超过保持连接的值。如果没有任何其它的控制报文可以发送,客户端必须发送一个PINGREQ报文。

    不管保持连接的值是多少,客户端任何时候都可以发送PINGREQ报文,并且使用PINGRESP报文判断网络和服务端的活动状态。

    如果保持连接的值非零,并且服务端在一点五倍的保持连接时间内没有收到客户端的控制报文,它必须断开客户端的网络连接,认为网络连接已断开。

    客户端发送了PINGREQ报文之后,如果在合理的时间内仍没有收到PINGRESP报文,它应该关闭到服务端的网络连接。

    保持连接的值为零表示关闭保持连接功能。这意味着,服务端不需要因为客户端不活跃而断开连接。注意:不管保持连接的值是多少,任何时候,只要服务端认为客户端是不活跃或无响应的,可以断开客户端的连接。

3.1.3.有效载荷

CONNECT报文的有效载荷包含一个或多个以长度为前缀的字段,可变报头中的标志决定是否包含这些字段。如果包含的话,必须按这个顺序出现:客户端标识符,遗嘱主题,遗嘱消息,用户名,密码 。

  • 客户端标识符

    服务端使用客户端标识符识别客户端。连接服务端的每个客户端都有唯一的客户端标识符。客户端和服务端都必须使用客户端标识符识别两者之间的MQTT会话相关的状态。

    客户端标识符 (ClientId) 必须存在而且必须是CONNECT报文有效载荷的第一个字段。

    服务端必须允许1到23个字节长的UTF-8编码的客户端标识符,客户端标识符只能包含这些字符:“0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”(大写字母,小写字母和数字)

  • 遗嘱主题 Will Topic

    如果遗嘱标志被设置为1,有效载荷的下一个字段是遗嘱主题(Will Topic)

  • 遗嘱消息 Will Message

    如果遗嘱标志被设置为1,有效载荷的下一个字段是遗嘱消息。遗嘱消息定义了将被发布到遗嘱主题的应用消息。这个字段由一两字节的长度和遗嘱消息的有效载荷组成,表示为零字节或多个字节序列。长度给出了跟在后面的数据的字节数,不包含长度字段本身占用的两个字节。

    遗嘱消息被发布到遗嘱主题时,它的有效载荷只包含这个字段的数据部分,不包含开头的两个长度字节。

  • 用户名 User Name

    如果用户名(User Name)标志被设置为1,有效载荷的下一个字段就是它。服务端可以将它用于身份验证和授权。

  • 密码 Password

    如果密码(Password)标志被设置为1,有效载荷的下一个字段就是它。密码字段包含一两字节的长度字段,长度表示二进制数据的字节数(不包含长度字段本身占用的两个字节),后面跟着0到65535字节的二进制数据。

3.1.4.响应

服务器可以在同一个TCP端口或其他网络端点上支持多种协议(包括本协议的早期版本)。如果服务器确定协议是MQTT 3.1.1,那么它按照下面的方法验证连接请求。

  1. 网络连接建立后,如果服务端在合理的时间内没有收到CONNECT报文,服务端应该关闭这个连接。

  2. 服务端必须按照要求验证CONNECT报文,如果报文不符合规范,服务端不发送CONNACK报文直接关闭网络连接。

  3. 服务端可以检查CONNECT报文的内容是不是满足任何进一步的限制,可以执行身份验证和授权检查。如果任何一项检查没通过,它应该发送一个适当的、返回码非零的CONNACK响应,并且必须关闭这个网络连接。

如果验证成功,服务端会执行下列步骤。

  1. 如果ClientId表明客户端已经连接到这个服务端,那么服务端必须断开原有的客户端连接。

  2. 服务端必须按照要求执行清理会话的过程。

  3. 服务端必须发送返回码为零的CONNACK报文作为CONNECT报文的确认响应。

  4. 开始消息分发和保持连接状态监视。

允许客户端在发送CONNECT报文之后立即发送其它的控制报文;客户端不需要等待服务端的CONNACK报文。如果服务端拒绝了CONNECT,它不能处理客户端在CONNECT报文之后发送的任何数据。

3.2.CONNACK – 确认连接请求

服务端发送CONNACK报文响应从客户端收到的CONNECT报文。服务端发送给客户端的第一个报文必须是CONNACK。如果客户端在合理的时间内没有收到服务端的CONNACK报文,客户端应该关闭网络连接。

3.2.1.固定报头

3.2.2.可变报头

  • 连接确认标志

    第1个字节是 连接确认标志,7-1位是保留位且必须设置为0。 第0 (SP)位 是当前会话(Session Present)标志。

    如果服务端收到清理会话(CleanSession)标志为1的连接,除了将CONNACK报文中的返回码设置为0之外,还必须将CONNACK报文中的当前会话设置(Session Present)标志为0 。

    如果服务端收到一个CleanSession为0的连接,当前会话标志的值取决于服务端是否已经保存了ClientId对应客户端的会话状态。如果服务端已经保存了会话状态,它必须将CONNACK报文中的当前会话标志设置为1。如果服务端没有已保存的会话状态,它必须将CONNACK报文中的当前会话设置为0。还需要将CONNACK报文中的返回码设置为0。

    当前会话标志使服务端和客户端在是否有已存储的会话状态上保持一致。

    一旦完成了会话的初始化设置,已经保存会话状态的客户端将期望服务端维持它存储的会话状态。如果客户端从服务端收到的当前的值与预期的不同,客户端可以选择继续这个会话或者断开连接。客户端可以丢弃客户端和服务端之间的会话状态,方法是,断开连接,将清理会话标志设置为1,再次连接,然后再次断开连接。

    如果服务端发送了一个包含非零返回码的CONNACK报文,它必须将当前会话标志设置为0 。

  • 连接返回码

    连接返回码字段使用一个字节的无符号值。如果服务端收到一个合法的CONNECT报文,但出于某些原因无法处理它,服务端应该尝试发送一个包含非零返回码的CONNACK报文。如果服务端发送了一个包含非零返回码的CONNACK报文,那么它必须关闭网络连接。如果认为下表中的所有连接返回码都不太合适,那么服务端必须关闭网络连接,不需要发送CONNACK报文。

3.2.3.有效载荷

CONNACK报文没有有效载荷 。

3.3.PUBLISH – 发布消息

PUBLISH控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息。

3.3.1.固定报头

DUP:第1个字节,第3位。

如果DUP标志被设置为0,表示这是客户端或服务端第一次请求发送这个PUBLISH报文。如果DUP标志被设置为1,表示这可能是一个早前报文请求的重发。

客户端或服务端请求重发一个PUBLISH报文时,必须将DUP标志设置为1 。对于QoS 0的消息,DUP标志必须设置为0 。

服务端发送PUBLISH报文给订阅者时,收到(入站)的PUBLISH报文的DUP标志的值不会被传播。发送(出站)的PUBLISH报文与收到(入站)的PUBLISH报文中的DUP标志是独立设置的,它的值必须单独 的根据发送(出站)的PUBLISH报文是否是一个重发来确定。

QOS: 第1个字节,第2-1位。 这个字段表示应用消息分发的服务质量等级保证。

PUBLISH报文不能将QoS所有的位设置为1。如果服务端或客户端收到QoS所有位都为1的PUBLISH报文,它必须关闭网络连接。

保留标志 RETAIN: 第1个字节,第0位。

如果客户端发给服务端的PUBLISH报文的保留(RETAIN)标志被设置为1,服务端必须存储这个应用消息和它的服务质量等级(QoS),以便它可以被分发给未来的主题名匹配的订阅者。一个新的订阅建立时,对每个匹配的主题名,如果存在最近保留的消息,它必须被发送给这个订阅者。如果服务端收到一条保留(RETAIN)标志为1的QoS 0消息,它必须丢弃之前为那个主题保留的任何消息。它应该将这个新的QoS 0消息当作那个主题的新保留消息,但是任何时候都可以选择丢弃它 — 如果这种情况发生了,那个主题将没有保留消息。

服务端发送PUBLISH报文给客户端时,如果消息是作为客户端一个新订阅的结果发送,它必须将报文的保留标志设为1。当一个PUBLISH报文发送给客户端是因为匹配一个已建立的订阅时,服务端必须将保留标志设为0,不管它收到的这个消息中保留标志的值是多少。

保留标志为1且有效载荷为零字节的PUBLISH报文会被服务端当作正常消息处理,它会被发送给订阅主题匹配的客户端。此外,同一个主题下任何现存的保留消息必须被移除,因此这个主题之后的任何订阅者都不会收到一个保留消息。当作正常意思是现存的客户端收到的消息中保留标志未被设置。服务端不能存储零字节的保留消息。

如果客户端发给服务端的PUBLISH报文的保留标志位0,服务端不能存储这个消息也不能移除或替换任何现存的保留消息。

保留消息的应用场景:某个mqtt客户端A每小时向某个特定的topic发布一条消息,所有订阅这个topic的客户端将会收到该消息,这是正常流程,如果客户端A刚刚发布消息, 此时有一个新的客户端B订阅该topic,也就是“订阅”是在“发布”后,这个时候客户端B将接收不到该消息。Retain 功能就是为了解决这一问题,当客户端A发布小时时,将 retain标志置1,那么broker就会保存该消息,当有新的客户端订阅该topic时,会立刻将该条消息推送给客户端B。所以官方的协议中是这样介绍该功能:“如果客户端发给服务端的 PUBLISH 报文的保留(RETAIN)标志被设置为 1,服务端 必须存储这个应用消息和它的服务质量等级(QoS),以便它可以被分发给未来的主题名匹配的订阅者”。

3.3.2.可变报头

可变报头按顺序包含主题名和报文标识符。

  • 主题名

    主题名(Topic Name)用于识别有效载荷数据应该被发布到哪一个信息通道。

    主题名必须是PUBLISH报文可变报头的第一个字段。

    PUBLISH报文中的主题名不能包含通配符。

    服务端发送给订阅客户端的PUBLISH报文的主题名必须匹配该订阅的主题过滤器。

  • 报文标识符 Packet Identifier

    只有当QoS等级是1或2时,报文标识符(Packet Identifier)字段才能出现在PUBLISH报文中。

3.3.3.有效载荷

有效载荷包含将被发布的应用消息。

3.3.4.响应

PUBLISH报文的接收者必须按照根据PUBLISH报文中的QoS等级发送响应。

3.3.5.其他

客户端使用PUBLISH报文发送应用消息给服务端,目的是分发到其它订阅匹配的客户端。

服务端使用PUBLISH报文发送应用消息给每一个订阅匹配的客户端。

客户端使用带通配符的主题过滤器请求订阅时,客户端的订阅可能会重复,因此发布的消息可能会匹配多个过滤器。对于这种情况,服务端必须将消息分发给所有订阅匹配的QoS等级最高的客户端。服务端之后可以按照订阅的QoS等级,分发消息的副本给每一个匹配的订阅者。

收到一个PUBLISH报文时,接收者的动作取决于QoS等级。

如果服务端实现不授权某个客户端发布PUBLISH报文,它没有办法通知那个客户端。它必须按照正常的QoS规则发送一个正面的确认,或者关闭网络连接。

3.4.PUBACK –发布确认

PUBACK报文是对QoS 1等级的PUBLISH报文的响应。

3.4.1.固定报头

3.4.2.可变报头

包含等待确认的PUBLISH报文的报文标识符。

3.4.3.有效载荷

PUBACK报文没有有效载荷。

3.5.PUBREC – 发布收到(QoS 2,第一步)

PUBREC报文是对QoS等级2的PUBLISH报文的响应。它是QoS 2等级协议交换的第二个报文。

3.5.1.固定报头

3.5.2.可变报头

可变报头包含等待确认的PUBLISH报文的报文标识符。

3.5.3.有效载荷

PUBREC报文没有有效载荷。

3.6.PUBREL – 发布释放(QoS 2,第二步)

PUBREL报文是对PUBREC报文的响应。它是QoS 2等级协议交换的第三个报文。

3.6.1.固定报头

PUBREL控制报文固定报头的第3,2,1,0位是保留位,必须被设置为0,0,1,0。服务端必须将其它的任何值都当做是不合法的并关闭网络连接。

3.6.2.可变报头

可变报头包含与等待确认的PUBREC报文相同的报文标识符。

3.6.3.有效载荷

PUBREL报文没有有效载荷。

3.7.PUBCOMP – 发布完成(QoS 2,第三步)

PUBCOMP报文是对PUBREL报文的响应。它是QoS 2等级协议交换的第四个也是最后一个报文。

3.7.1.固定报头

3.7.2.可变报头

可变报头包含与等待确认的PUBREL报文相同的报文标识符。

3.7.3.有效载荷

PUBCOMP报文没有有效载荷。

3.8.SUBSCRIBE - 订阅主题

客户端向服务端发送SUBSCRIBE报文用于创建一个或多个订阅。每个订阅注册客户端关心的一个或多个主题。为了将应用消息转发给与那些订阅匹配的主题,服务端发送PUBLISH报文给客户端。SUBSCRIBE报文也(为每个订阅)指定了最大的QoS等级,服务端根据这个发送应用消息给客户端。

3.8.1.固定报头

SUBSCRIBE控制报固定报头的第3,2,1,0位是保留位,必须分别设置为0,0,1,0。服务端必须将其它的任何值都当做是不合法的并关闭网络连接。

3.8.2.可变报头

可变报头包含报文标识符。

3.8.3.有效载荷

SUBSCRIBE报文的有效载荷包含了一个主题过滤器列表,它们表示客户端想要订阅的主题。服务端应该支持包含通配符的主题过滤器。如果服务端选择不支持包含通配符的主题过滤器,必须拒绝任何包含通配符过滤器的订阅请求。每一个过滤器后面跟着一个字节,这个字节被叫做服务质量要求(Requested QoS)。它给出了服务端向客户端发送应用消息所允许的最大QoS等级。

SUBSCRIBE报文的有效载荷必须包含至少一对主题过滤器 和 QoS等级字段组合。没有有效载荷的SUBSCRIBE报文是违反协议的。

3.8.4.响应

服务端收到客户端发送的一个SUBSCRIBE报文时,必须使用SUBACK报文响应。SUBACK报文必须和等待确认的SUBSCRIBE报文有相同的报文标识符。

允许服务端在发送SUBACK报文之前就开始发送与订阅匹配的PUBLISH报文。

如果服务端收到一个SUBSCRIBE报文,报文的主题过滤器与一个现存订阅的主题过滤器相同,那么必须使用新的订阅彻底替换现存的订阅。新订阅的主题过滤器和之前订阅的相同,但是它的最大QoS值可以不同。与这个主题过滤器匹配的任何现存的保留消息必须被重发,但是发布流程不能中断。

如果主题过滤器不同于任何现存订阅的过滤器,服务端会创建一个新的订阅并发送所有匹配的保留消息。

如果服务端收到包含多个主题过滤器的SUBSCRIBE报文,它必须如同收到了一系列的多个SUBSCRIBE报文一样处理那个,除了需要将它们的响应合并到一个单独的SUBACK报文发送。

服务端发送给客户端的SUBACK报文对每一对主题过滤器 和QoS等级都必须包含一个返回码。这个返回码必须表示那个订阅被授予的最大QoS等级,或者表示这个订阅失败。服务端可以授予比订阅者要求的低一些的QoS等级。为响应订阅而发出的消息的有效载荷的QoS必须是原始发布消息的QoS和服务端授予的QoS两者中的最小值。如果原始消息的QoS是1而被授予的最大QoS是0,允许服务端重复发送一个消息的副本给订阅者。

3.9.SUBACK – 订阅确认

服务端发送SUBACK报文给客户端,用于确认它已收到并且正在处理SUBSCRIBE报文。

SUBACK报文包含一个返回码清单,它们指定了SUBSCRIBE请求的每个订阅被授予的最大QoS等级。

3.9.1.固定报头

3.9.2.可变报头

可变报头包含等待确认的SUBSCRIBE报文的报文标识符。

3.9.3.有效载荷

有效载荷包含一个返回码清单。每个返回码对应等待确认的SUBSCRIBE报文中的一个主题过滤器。返回码的顺序必须和SUBSCRIBE报文中主题过滤器的顺序相同。

3.10.UNSUBSCRIBE –取消订阅

3.10.1.固定报头

UNSUBSCRIBE报文固定报头的第3,2,1,0位是保留位且必须分别设置为0,0,1,0。服务端必须认为任何其它的值都是不合法的并关闭网络连接。

3.10.2.可变报头

可变报头包含一个报文标识符。

3.10.3.有效载荷

UNSUBSCRIBE报文的有效载荷包含客户端想要取消订阅的主题过滤器列表。UNSUBSCRIBE报文中的主题过滤器必须是连续打包的。 UNSUBSCRIBE报文的有效载荷必须至少包含一个消息过滤器。没有有效载荷的UNSUBSCRIBE报文是违反协议的 。

3.10.4.响应

UNSUBSCRIBE报文提供的主题过滤器(无论是否包含通配符)必须与服务端持有的这个客户端的当前主题过滤器集合逐个字符比较。如果有任何过滤器完全匹配,那么它(服务端)自己的订阅将被删除,否则不会有进一步的处理。

如果服务端删除了一个订阅:

  • 必须停止分发任何新消息给这个客户端。

  • 必须完成分发任何已经开始往客户端发送的QoS 1和QoS 2的消息。

  • 可以继续发送任何现存的准备分发给客户端的缓存消息。

服务端必须发送UNSUBACK报文响应客户端的UNSUBSCRIBE请求。UNSUBACK报文必须包含和UNSUBSCRIBE报文相同的报文标识符。即使没有删除任何主题订阅,服务端也必须发送一个UNSUBACK响应 。

如果服务端收到包含多个主题过滤器的UNSUBSCRIBE报文,它必须如同收到了一系列的多个UNSUBSCRIBE报文一样处理那个报文,除了将它们的响应合并到一个单独的UNSUBACK报文外。

3.11.UNSUBACK – 取消订阅确认

服务端发送UNSUBACK报文给客户端用于确认收到UNSUBSCRIBE报文。

3.11.1.固定报头

3.11.2.可变报头

可变报头包含等待确认的UNSUBSCRIBE报文的报文标识符。

3.11.3.有效载荷

UNSUBACK报文没有有效载荷。

3.12.PINGREQ – 心跳请求

客户端发送PINGREQ报文给服务端的。用于:

  1. 在没有任何其它控制报文从客户端发给服务的时,告知服务端客户端还活着。

  2. 请求服务端发送响应确认它还活着。

  3. 使用网络以确认网络连接没有断开。

3.12.1.固定报头

3.12.2.可变报头

PINGREQ报文没有可变报头。

3.12.3.有效载荷

PINGREQ报文没有有效载荷。

3.13.PINGRESP – 心跳响应

服务端发送PINGRESP报文响应客户端的PINGREQ报文。表示服务端还活着。

3.13.1.固定报头

3.13.2.可变报头

PINGRESP报文没有可变报头。

3.13.3.有效载荷

PINGRESP报文没有有效载荷。

3.14.DISCONNECT –断开连接

DISCONNECT报文是客户端发给服务端的最后一个控制报文。表示客户端正常断开连接。

3.14.1.固定报头

3.14.2.可变报头

DISCONNECT 报文没有可变报头。

3.14.3.有效载荷

DISCONNECT 报文没有有效载荷。

3.14.4.响应

客户端发送DISCONNECT报文之后:

  • 必须关闭网络连接。

  • 不能通过那个网络连接再发送任何控制报文。

服务端在收到DISCONNECT报文时:

  • 必须丢弃任何与当前连接关联的未发布的遗嘱消息,具体描述见 3.1.2.5节。

  • 应该关闭网络连接,如果客户端还没有这么做。

第四章:技术点

4.1.服务质量等级

MQTT 中的 QoS 指的是发布者与订阅者之间消息传递的保证级别。它提供三个服务级别:

  • QoS 0 – 最多交付一次

  • QoS 1 – 至少交付一次

  • QoS 2 – 只交付一次

其中,使用 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.1.QoS 0:最多分发一次

QoS 0 是最低的 QoS 等级。QoS 0 消息即发即弃,不需要等待确认,不需要存储和重传,因此对于接收方来说,永远都不需要担心收到重复的消息。

对于QoS 0的分发协议,发送者

  • 必须发送QoS等于0,DUP等于0的PUBLISH报文。

对于QoS 0的分发协议,接收者

  • 接受PUBLISH报文时同时接受消息的所有权。

为什么 QoS 0 消息会丢失?

当我们使用 QoS 0 传递消息时,消息的可靠性完全依赖于底层的 TCP 协议。

而 TCP 只能保证在连接稳定不关闭的情况下消息的可靠到达,一旦出现连接关闭、重置,仍有可能丢失当前处于网络链路或操作系统底层缓冲区中的消息。这也是 QoS 0 消息最主要的丢失场景。

4.1.2.QoS 1: 至少分发一次

为了保证消息到达,QoS 1 加入了应答与重传机制,发送方只有在收到接收方的 PUBACK 报文以后,才能认为消息投递成功,在此之前,发送方需要存储该 PUBLISH 报文以便下次重传。

QoS 1 需要在 PUBLISH 报文中设置 Packet ID,而作为响应的 PUBACK 报文,则会使用与 PUBLISH 报文相同的 Packet ID,以便发送方收到后删除正确的 PUBLISH 报文缓存。

对于QoS 1的分发协议,发送者

  • 每次发送新的应用消息都必须分配一个未使用的报文标识符。

  • 发送的PUBLISH报文必须包含报文标识符且QoS等于1,DUP等于0。

  • 必须将这个PUBLISH报文看作是 未确认的 ,直到从接收者那收到对应的PUBACK报文。

注意:允许发送者在等待确认时使用不同的报文标识符发送后续的PUBLISH报文。

对于QoS 1的分发协议,接收者

  • 响应的PUBACK报文必须包含一个报文标识符,这个标识符来自接收到的、已经接受所有权的PUBLISH报文。

  • 发送了PUBACK报文之后,接收者必须将任何包含相同报文标识符的入站PUBLISH报文当作一个新的消息,并忽略它的DUP标志的值。

  1. Sender 向 Receiver 发送一个带有消息数据的 PUBLISH 包, 并在本地保存这个 PUBLISH 包。

  2. Receiver 收到 PUBLISH 包以后,向 Sender 发送一个 PUBACK 数据包,PUBACK 数据包没有消息体(Payload),在可变报头中(Variable header)中有一个包标识(Packet Identifier),和它收到的 PUBLISH 包中的 Packet Identifier 一致。

  3. Sender 收到 PUBACK 之后,根据 PUBACK 包中的 Packet Identifier 找到本地保存的 PUBLISH 包,然后丢弃掉,一次消息的发送完成。

  4. 如果 Sender 在一段时间内没有收到 PUBLISH 包对应的 PUBACK,它将该 PUBLISH 包的 DUP 标识设为 1(代表是重新发送的 PUBLISH 包),然后重新发送该 PUBLISH 包。重复这个流程,直到收到 PUBACK,然后执行第 3 步。

  • 为什么 QoS 1 消息会重复?

    对于发送方来说,没收到 PUBACK 报文分为以下两种情况:

    1. PUBLISH 未到达接收方

    2. PUBLISH 已经到达接收方,接收方的 PUBACK 报文还未到达发送方

    在第一种情况下,发送方虽然重传了 PUBLISH 报文,但是对于接收方来说,实际上仍然仅收到了一次消息。

    但是在第二种情况下,在发送方重传时,接收方已经收到过了这个 PUBLISH 报文,这就导致接收方将收到重复的消息。

虽然重传时 PUBLISH 报文中的 DUP 标志会被设置为 1,用以表示这是一个重传的报文。但是接收方并不能因此假定自己曾经接收过这个消息,仍然需要将其视作一个全新的消息。

这是因为对于接收方来说,可能存在以下两种情况:

第一种情况,发送方由于没有收到 PUBACK 报文而重传了 PUBLISH 报文。此时,接收方收到的前后两个 PUBLISH 报文使用了相同的 Packet ID,并且第二个 PUBLISH 报文的 DUP 标志为 1,此时它确实是一个重复的消息。

第二种情况,第一个 PUBLISH 报文已经完成了投递,1024 这个 Packet ID 重新变为可用状态。发送方使用这个 Packet ID 发送了一个全新的 PUBLISH 报文,但这一次报文未能到达对端,所以发送方后续重传了这个 PUBLISH 报文。这就使得虽然接收方收到的第二个 PUBLISH 报文同样是相同的 Packet ID,并且 DUP 为 1,但确实是一个全新的消息。

由于我们无法区分这两种情况,所以只能让接收方将这些 PUBLISH 报文都当作全新的消息来处理。因此当我们使用 QoS 1 时,消息的重复在协议层面上是无法避免的。

甚至在比较极端的情况下,例如 Broker 从发布方收到了重复的 PUBLISH 报文,而在将这些报文转发给订阅方的过程中,再次发生重传,这将导致订阅方最终收到更多的重复消息。

在下图表示的例子中,虽然发布者的本意只是发布一条消息,但对接收方来说,最终却收到了三条相同的消息:

4.1.3.QoS 2: 仅分发一次

QoS 2 解决了 QoS 0、1 消息可能丢失或者重复的问题,但相应地,它也带来了最复杂的交互流程和最高的开销。每一次的 QoS 2 消息投递,都要求发送方与接收方进行至少两次请求/响应流程。

QoS 2的消息可变报头中有报文标识符。QoS 2的PUBLISH报文的接收者使用一两步确认过程来确认收到。

对于QoS 2的分发协议,发送者

  • 必须给要发送的新应用消息分配一个未使用的报文标识符。

  • 发送的PUBLISH报文必须包含报文标识符且报文的QoS等于2,,DUP等于0。

  • 必须将这个PUBLISH报文看作是 未确认的 ,直到从接收者那收到对应的PUBREC报文。

  • 收到PUBREC报文后必须发送一个PUBREL报文。PUBREL报文必须包含与原始PUBLISH报文相同的报文标识符。

  • 必须将这个PUBREL报文看作是 未确认的 ,直到从接收者那收到对应的PUBCOMP报文。

  • 一旦发送了对应的PUBREL报文就不能重发这个PUBLISH报文。

注意:允许发送者在等待确认时使用不同的报文标识符发送后续的PUBLISH报文。

对于QoS 2的分发协议,接收者

  • 响应的PUBREC报文必须包含报文标识符,这个标识符来自接收到的、已经接受所有权的PUBLISH报文。

  • 在收到对应的PUBREL报文之前,接收者必须发送PUBREC报文确认任何后续的具有相同标识符的PUBLISH报文。 在这种情况下,它不能重复分发消息给任何后续的接收者。

  • 响应PUBREL报文的PUBCOMP报文必须包含与PUBREL报文相同的标识符。

  • 发送PUBCOMP报文之后,接收者必须将包含相同报文标识符的任何后续PUBLISH报文当作一个新的发布。

  1. 首先,发送方存储并发送 QoS 为 2 的 PUBLISH 报文以启动一次 QoS 2 消息的传输,然后等待接收方回复 PUBREC 报文。这一部分与 QoS 1 基本一致,只是响应报文从 PUBACK 变成了 PUBREC。

  2. 当发送方收到 PUBREC 报文,即可确认对端已经收到了 PUBLISH 报文,发送方将不再需要重传这个报文,并且也不能再重传这个报文。所以此时发送方可以删除本地存储的 PUBLISH 报文,然后发送一个 PUBREL 报文,通知对端自己准备将本次使用的 Packet ID 标记为可用了。与 PUBLISH 报文一样,我们需要确保 PUBREL 报文到达对端,所以也需要一个响应报文,并且这个 PUBREL 报文需要被存储下来以便后续重传。

  3. 当接收方收到 PUBREL 报文,也可以确认在这一次的传输流程中不会再有重传的 PUBLISH 报文到达,因此回复 PUBCOMP 报文表示自己也准备好将当前的 Packet ID 用于新的消息了。

  4. 当发送方收到 PUBCOMP 报文,这一次的 QoS 2 消息传输就算正式完成了。在这之后,发送方可以再次使用当前的 Packet ID 发送新的消息,而接收方再次收到使用这个 Packet ID 的 PUBLISH 报文时,也会将它视为一个全新的消息。

为什么 QoS 2 消息不会重复?

QoS 2 消息保证不会丢失的逻辑与 QoS 1 相同,所以这里我们就不再重复了。

与 QoS 1 相比,QoS 2 新增了 PUBREL 报文和 PUBCOMP 报文的流程,也正是这个新增的流程带来了消息不会重复的保证。

在我们更进一步之前,我们先快速回顾一下 QoS 1 消息无法避免重复的原因。

当我们使用 QoS 1 消息时,对接收方来说,回复完 PUBACK 这个响应报文以后 Packet ID 就重新可用了,也不管响应是否确实已经到达了发送方。所以就无法得知之后到达的,携带了相同 Packet ID 的 PUBLISH 报文,到底是发送方因为没有收到响应而重传的,还是发送方因为收到了响应所以重新使用了这个 Packet ID 发送了一个全新的消息。

所以,消息去重的关键就在于,通信双方如何正确地同步释放 Packet ID,换句话说,不管发送方是重传消息还是发布新消息,一定是和对端达成共识了的。

而 QoS 2 中增加的 PUBREL 流程,正是提供了帮助通信双方协商 Packet ID 何时可以重用的能力。

QoS 2 规定,发送方只有在收到 PUBREC 报文之前可以重传 PUBLISH 报文。一旦收到 PUBREC 报文并发出 PUBREL 报文,发送方就进入了 Packet ID 释放流程,不可以再使用当前 Packet ID 重传 PUBLISH 报文。同时,在收到对端回复的 PUBCOMP 报文确认双方都完成 Packet ID 释放之前,也不可以使用当前 Packet ID 发送新的消息。

因此,对于接收方来说,能够以 PUBREL 报文为界限,凡是在 PUBREL 报文之前到达的 PUBLISH 报文,都必然是重复的消息;而凡是在 PUBREL 报文之后到达的 PUBLISH 报文,都必然是全新的消息。

一旦有了这个前提,我们就能够在协议层面完成 QoS 2 消息的去重。

4.1.4.不同 MQTT QoS 的适用场景和注意事项

QoS 0:QoS 0 的缺点是可能会丢失消息,消息丢失的频率依赖于你所处的网络环境,并且可能使你错过断开连接期间的消息,不过优点是投递的效率较高。

所以我们通常选择使用 QoS 0 传输一些高频且不那么重要的数据,比如传感器数据,周期性更新,即使遗漏几个周期的数据也可以接受。

QoS 1:QoS 1 可以保证消息到达,所以适合传输一些较为重要的数据,比如下达关键指令、更新重要的有实时性要求的状态等。

但因为 QoS 1 还可能会导致消息重复,所以当我们选择使用 QoS 1 时,还需要能够处理消息的重复,或者能够允许消息的重复。

在我们决定使用 QoS 1 并且不对其进行去重处理之前,我们需要先了解,允许消息的重复,可能意味着什么。

如果我们不对 QoS 1 进行去重处理,我们可能会遭遇这种情况,发布方以 1、2 的顺序发布消息,但最终订阅方接收到的消息顺序可能是 1、2、1、2。如果 1 表示开灯指令,2 表示关灯指令,我想大部分用户都不会接受自己仅仅进行了开灯然后关灯的操作,结果灯在开和关的状态来回变化。

QoS 2:QoS 2 既可以保证消息到达,也可以保证消息不会重复,但传输成本最高。如果我们不愿意自行实现去重方案,并且能够接受 QoS 2 带来的额外开销,那么 QoS 2 将是一个合适的选择。通常我们会在金融、航空等行业场景下会更多地见到 QoS 2 的使用。

4.2.心跳检测

客户端要定时向服务端发送心跳请求(PINGREQ)报文,这个心跳时间间隔是客户端时进行设置的。设置好心跳时间间隔后,客户端就知道多久要发送一条心跳请求给服务端。在客户端连接服务端时,会将心跳时间间隔信息放入CONNECT报文,用于告知服务端心跳时间间隔的。客户端在心跳间隔时间内,如果有消息发布,那就直接发布消息而不发布心跳请求,但是在心跳间隔时间内,客户端没有消息发布,那么它就会发布一条心跳请求给服务端,这个心跳请求的目的就是为了告诉服务端我还在线。

另外,在实际运行中,如果服务端没有在1.5倍心跳时间间隔内收到客户端发布消息(PUBLISH)或发来心跳请求(PINGREQ),那么服务端就会认为这个客户端已经掉线。

举例来说,如果心跳时间间隔是60秒。那么服务端在90秒内没有收到客户端发布的消息也没有收到PINGREQ请求,那么它就会认为客户端已经掉线。

另外,心跳机制不仅仅用于服务端判断客户端是否在线。客户端也可以利用这一机制来判断自己是否与服务端仍保持连接。如果客户端发送了心跳请求(PINGREQ)给服务端一段时间后,仍然没有收到服务端回复的心跳确认。那么客户端也会认为自己已经断开了与服务端的连接。

4.3.主题和主题通配符

4.3.1.MQTT 主题

MQTT 主题本质上是一个 UTF-8 编码的字符串,是 MQTT 协议进行消息路由的基础。MQTT 主题类似 URL 路径,使用斜杠 / 进行分层:

chat/room/1
sensor/10/temperature
sensor/+/temperature
sensor/#

为了避免歧义且易于理解,通常不建议主题以 / 开头或结尾,例如 /chatchat/

不同于消息队列中的主题(比如 Kafka 和 Pulsar),MQTT 主题不需要提前创建。MQTT 客户端在订阅或发布时即自动的创建了主题,开发者无需再关心主题的创建,并且也不需要手动删除主题。

下图是一个简单的 MQTT 订阅与发布流程, APP 1 订阅了sensor/2/temperature 主题后,将能接收到 Sensor 2 发布到该主题的消息。

4.3.2.MQTT 主题通配符

MQTT 主题通配符包含单层通配符 + 及多层通配符 #,主要用于客户端一次订阅多个主题。 注意:通配符只能用于订阅,不能用于发布。

  • 单层通配符:加号 (“+”) 是用于单个主题层级匹配的通配符。在使用单层通配符时,单层通配符必须占据整个层级,例如:

+ 有效
sensor/+ 有效
sensor/+/temperature 有效
sensor+ 无效(没有占据整个层级)

如果客户端订阅了主题 sensor/+/temperature,将会收到以下主题的消息:

sensor/1/temperature
sensor/2/temperature
...
sensor/n/temperature

但是不会匹配以下主题:

sensor/temperature
sensor/bedroom/1/temperature

  • 多层通配符: 井字符号(“#” )是用于匹配主题中任意层级的通配符。多层通配符表示它的父级和任意数量的子层级,在使用多层通配符时,它必须占据整个层级并且必须是主题的最后一个字符,例如:

# 有效,匹配所有主题
sensor/# 有效
sensor/bedroom# 无效(没有占据整个层级)
sensor/#/temperature 无效(不是主题最后一个字符)

如果客户端订阅主题 senser/#,它将会收到以下主题的消息:

sensor
sensor/temperature
sensor/1/temperature

  • 以 $ 开头的主题:$SYS/ 开头的主题为系统主题,系统主题主要用于获取 MQTT 服务器自身运行状态、消息统计、客户端上下线事件等数据。目前,MQTT 协议暂未明确规定 $SYS/ 主题标准,但大多数 MQTT 服务器都遵循该标准建议

    例如,EMQX 服务器支持通过以下主题获取集群状态。

  • 共享订阅

共享订阅是 MQTT 5.0 引入的新特性,用于在多个订阅者之间实现订阅的负载均衡,MQTT 5.0 规定的共享订阅主题以 $share 开头。

下图中,3 个订阅者用共享订阅的方式订阅了同一个主题 $share/g/topic,其中topic 是它们订阅的真实主题名,而 $share/g/ 是共享订阅前缀(g/ 是群组名,可为任意 UTF-8 编码字符串)。

4.4.保留消息

发布者发布消息时,如果 Retained 标记被设置为 true,则该消息即是 MQTT 中的保留消息(Retained Message)。MQTT 服务器会为每个主题存储最新一条保留消息,以方便消息发布后才上线的客户端在订阅主题时仍可以接收到该消息。

何时使用 MQTT 保留消息?

发布订阅模式虽然能让消息的发布者与订阅者充分解耦,但也存在一个缺点,即订阅者无法主动向发布者请求消息。订阅者何时收到消息完全依赖于发布者何时发布消息,这在某些场景中就产生了不便。

借助保留消息,新的订阅者能够立即获取最近的状态,而不需要等待无法预期的时间,例如:

  • 智能家居设备的状态只有在变更时才会上报,但是控制端需要在上线后就能获取到设备的状态;

  • 传感器上报数据的间隔太长,但是订阅者需要在订阅后立即获取到最新的数据;

  • 传感器的版本号、序列号等不会经常变更的属性,可在上线后发布一条保留消息告知后续的所有订阅者。

保留消息将保存多久?如何删除?

服务器只会为每个主题保存最新一条保留消息,保留消息的保存时间与服务器的设置有关。若服务器设置保留消息存储在内存,则 MQTT 服务器重启后消息即会丢失;若存储在磁盘,则服务器重启后保留消息仍然存在。

保留消息虽然存储在服务端中,但它并不属于会话的一部分。也就是说,即便发布这个保留消息的会话已结束,保留消息也不会被删除。删除保留消息有以下几种方式:

  • 客户端往某个主题发送一个 Payload 为空的保留消息,服务端就会删除这个主题下的保留消息;

  • 在 MQTT 服务器上删除,比如 EMQX MQTT 服务器提供了在 Dashboard 上删除保留消息的功能;

  • MQTT 5.0 新增了消息过期间隔属性,发布时可使用该属性设置消息的过期时间,不管消息是否为保留消息,都将会在过期时间后自动被删除。

4.5.遗嘱消息

在现实世界中,一个人可以制定一份遗嘱,声明在他去世后应该如何分配他的财产以及应该采取什么行动。在他去世后,遗嘱执行人会将这份遗嘱公开,并执行遗嘱中的指示。

在 MQTT 中,客户端可以在连接时在服务端中注册一个遗嘱消息,与普通消息类似,我们可以设置遗嘱消息的主题、有效载荷等等。当该客户端意外断开连接,服务端就会向其他订阅了相应主题的客户端发送此遗嘱消息。这些接收者也因此可以及时地采取行动,例如向用户发送通知、切换备用设备等等。

假设我们有一个传感器监控一个很少变化的值,普通的实现是定期发布最新数值,但更好的实现是仅在数值发生变化时以保留消息的形式发送它。这使得任何新的订阅者总能立即获得当前值,而不必等待传感器再一次发布。不过订阅者也因此没有办法根据是否及时收到消息来判断传感器是否离线。借助遗嘱消息,我们可以立即得知传感器保持活动超时,而且不必总是获取传感器发布的值。

遗嘱消息总是在客户端“死亡”后被发布,在某种意义上,它也是客户端发出的最后一个消息。所以主题别名在遗嘱消息中没有任何意义。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值