文章目录
一、MQTT5介绍
1.1 什么是MQTT
- MQTT(Message Queuing Telemetry Transport),即消息队列遥测传输协议,是一种基于发布/订阅的消息传输协议。其轻量、开放、简洁和易实现的特点能够适用于要求代码量小、网络带宽资源匮乏的情景,如机器间通信(M2M)、物联网等。
1.2 MQTT5 历史
- MQTT 的历史始于 1990 年代后期,当时 Andy Stanford-Clark 和 Arlen Nipper 发明了 MQTT以通过卫星网络监控石油和天然气管道。他们需要一种能够最大限度地减少网络带宽和设备资源要求、易于实施并且适用于不可靠网络的协议。当时,云计算、大量连接的设备以及MQTT 的各种物联网用例都没有考虑在内。
- 2013年,MQTT作为标准提交给OASIS标准组。第一个发布的版本是 3.1.1 版。直到 2015 年底,才开始对 MQTT 标准进行重大更新,称为 MQTT 5,以应对现代计算环境的复杂性。经过深思熟虑,MQTT 5 于 2019 年 3 月成为标准。
- MQTT v5 是 MQTT 协议的重大更新。为了响应 MQTT 用户的反馈,MQTT 5 添加了现代物联网应用程序所需的功能。这些新功能特别适用于部署到云端的应用程序,需要稳健性和可靠的错误处理来实现关键任务消息传递,并寻求更轻松地将 MQTT 消息集成到现有计算基础架构中。
1.3 MQTT5 设计目标
负责指定和标准化 MQTT 的 OASIS 技术委员会 (TC) 面临着复杂的平衡行为:
- 在不增加开销或降低易用性的情况下添加长期用户想要的功能。
- 在不增加不必要的复杂性的情况下提高性能和可扩展性。
TC 决定 MQTT 5 规范的以下功能目标: - 增强可扩展性和大型系统
- 改进的错误报告
- 形式化常见模式,包括能力发现和请求响应
- 包括用户属性在内的可扩展性机制
- 性能改进和对小客户的支持
MQTT 5 的设计旨在使 MQTT 代理更容易扩展到大量并发-连接的客户端
1.4 MQTT5应用场景
MQTT 5 规范已成为大多数物联网用例的明显选择。新的 MQTT 5 功能成功解决了 MQTT 3 的局限性,并为未来的创新开辟了道路。在接下来的几年里,我们预计 MQTT 在所有行业的采用都会大幅增长,包括制造、汽车、关键基础设施、物流、智慧城市等。MQTT 即将成为所有物联网的标准。
二、为什么要用MQTT5
为更健壮的系统更好地处理错误
- 会话过期
- 消息过期
- 否定确认
云原生计算的更多可扩展性
- 标准化共享订阅
- 主题别名
更大的灵活性和更容易的集成
- 用户属性
- 负载格式指示器
三、MQTT5
topic主题
指定了你要把消息发布到哪里或你要订阅哪些感兴趣的消息,你可以认为主题是一种逻辑通道。发布者将消息发布到某个通道中,订阅者监听来自这个通道中的消息。
MQTT主题层级
- 主题层级分隔符用于对主题名进行结构化。 如果存在分隔符, 它会将主题名分割为多个主题层级。
- 我们用"/“分隔的字符串来表示主题,斜杠”/"用于分割主题的每个层级, 它为主题名提供一个分层结构。
注意:主题是大小写敏感的,大小写不同则对应的是不同的主题。
cleanSession
只有在CleanSession =0,消息的QoS为1或2 的情况下,离线客户端才能收到断连期间的消息。
使用场景
- 客户端只需要发布消息,并需要订阅这些消息。你不需要MQTT服务端保存会话信息,或重发QoS 1和QoS 2级别的消息。
- 客户端不需要获取在它离线时发送的消息。
概念
- MQTT客户端与服务端的连接可能不是非常稳定,在不稳定的网络环境下,要想保证所有信息传输都能够做到准确无误,这是非常困难的。因此,我们就要根据客户端对系统运行的重要性来区别对待。
- 有些MQTT客户端对整个系统运行起着关键作用,这些客户端一定要准确无误的收到服务端发来的报文。比如一辆自动驾驶汽车的导航系统。假如这个导航系统错过了服务端发来的报文,可能会导致交通事故甚至人员伤亡。因此,即使网络不是非常稳定,我们仍然要求汽车导航系统一定要准确无误的收到服务端所发来的报文。
- 但是有些MQTT客户端对整个系统运行并不是很重要。比如同样是这辆自动驾驶汽车。它的音乐播放系统如果没有及时收到服务端发来的音乐播放报文,这对驾驶系统来说影响不大。
以上所举的两个例子说明,MQTT通讯中有些客户端必须准确无误的收到报文,有些则不需要。 - 为了保证重要的MQTT报文可以被客户端准确无误的收到。在服务端向客户端发送报文后,客户端会向服务端返回一个确认报文。如果服务端没有收到客户端返回的确认报文,那么服务端就会认为刚刚发送给客户端的报文没有被准确无误的送达。在这种情况下,服务端将会执行以下两个操作:
- 将尚未被客户端确认的报文保存起来
- 再次尝试向客户端发送报文,并且再次等待客户端发来确认信息
- 如果cleanSession 被设置为“true”。那么服务端不需要客户端确认收到报文,也不会保存任何报文。在这种情况下,即使客户端错过了服务端发来的报文,也没办法让服务端再次发送报文。服务端一旦发送完报文,就会把报文忘得“干干净净”了
- 反过来,如果我们将cleanSession 设置为”false”。那么服务端就知道,后续通讯中,客户端可能会要求我保存没有收到的报文。
- 如果某个客户端用于收发非常重要的信息(比如前文示例中汽车自动驾驶系统),那么该客户端在连接服务端时,应该将cleanSession设置为”false”。这样才能让服务端保存那些没有得到客户端接收确认的信息。以便服务端再次尝试将这些重要信息再次发送给客户端。
- 相反的,如果某个客户端用于收发不重要的信息(比如前文示例中车载音乐系统)那么该客户端在连接服务端时,应该将cleanSession设置为”true”。
- 如果需要服务端保存重要报文,光设置cleanSession 为false是不够的,还需要传递的MQTT信息QoS级别大于0
QoS
Qos选择
使用QoS 0:
- 消息的发送方和接收方处在一个很稳定的网络环境下
- 可以接受偶尔丢消息,某些消息的丢失是可以接受的,数据的重要性不是那么高
- 消息不需要入队列缓存。在断连的情况下,只有设置了持久会话的客户端的QoS 1和QoS 2消息才会被放到队列中缓存。
使用QoS 1:
- 你需要获取到每个消息,不容许消息丢失,但是可以接受消息重复。QoS 1服务质量等级是最长用的服务质量,因为它保证了消息至少会送达一次。
- 你无法忍受QoS 2的性能开销,QoS 1级别的消息会比QoS 2级别的消息快很多。
使用QoS 2:
- 消息丢失和重复都是不可接受的。但是要注意QoS 2的交互会耗费很多时间。
QoS 0
可以接受消息偶尔丢失的场景下可以选择 QoS 0
- 消息最多传递一次,如果当时 MQTT 客户端 不可用,则会丢失该消息。Sender 发送一条消息之后,就不再关心它有没有发送到对方,也不设置任何重发机制。
QoS 1
不能接收消息丢失,但是同时想要保证一定的性能,可以选择Qos 1,但是无法保证消息重复。
- 消息传递至少 1 次。包含了简单的重发机制,Sender 发送消息之后等待接收者的 ACK,如果没收到 ACK 则重新发送消息。这种模式能保证消息至少能到达一次,但无法保证消息重复。
-
为什么无法保证消息重复:
- 对于发送方来说,没收到 PUBACK 报文分为以下两种情况:
- PUBLISH 未到达接收方
- 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,但确实是一个全新的消息。
- 对于发送方来说,没收到 PUBACK 报文分为以下两种情况:
-
由于无法区分这两种情况,只能让接收方将这些 PUBLISH 报文都当作全新的消息来处理。因此当我们使用 QoS 1 时,消息的重复在协议层面上是无法避免的。
-
QoS 2
消息仅传送一次。设计了重发和重复消息发现机制,保证消息到达对方并且严格只到达一次。
- 对于不能忍受消息丢失,且不希望收到重复的消息,数据完整性与及时性要求较高的场景,可以选择 QoS 2。
- QoS 2 解决了 QoS 0、1 消息可能丢失或者重复的问题,但相应地,它也带来了最复杂的交互流程和最高的开销。每一次的 QoS 2 消息投递,都要求发送方与接收方进行至少两次请求/响应流程。
- 首先,发送方存储并发送 QoS 为 2 的 PUBLISH 报文以启动一次 QoS 2 消息的传输,然后等待接收方回复 PUBREC 报文。这一部分与 QoS 1 基本一致,只是响应报文从 PUBACK 变成了 PUBREC。
- 当发送方收到 PUBREC 报文,即可确认对端已经收到了 PUBLISH 报文,发送方将不再需要重传这个报文,并且也不能再重传这个报文。所以此时发送方可以删除本地存储的 PUBLISH 报文,然后发送一个 PUBREL 报文,通知对端自己准备将本次使用的 Packet ID 标记为可用了。与 PUBLISH 报文一样,我们需要确保 PUBREL 报文到达对端,所以也需要一个响应报文,并且这个 PUBREL 报文需要被存储下来以便后续重传。
- 当接收方收到 PUBREL 报文,也可以确认在这一次的传输流程中不会再有重传的 PUBLISH 报文到达,因此回复 PUBCOMP 报文表示自己也准备好将当前的 Packet ID 用于新的消息了。
- 当发送方收到 PUBCOMP 报文,这一次的 QoS 2 消息传输就算正式完成了。在这之后,发送方可以再次使用当前的 Packet ID 发送新的消息,而接收方再次收到使用这个 Packet ID 的 PUBLISH 报文时,也会将它视为一个全新的消息。
- 当我们使用 QoS 1 消息时,对接收方来说,回复完 PUBACK 这个响应报文以后 Packet ID 就重新可用了,也不管响应是否确实已经到达了发送方。所以就无法得知之后到达的,携带了相同 Packet ID 的 PUBLISH 报文,到底是发送方因为没有收到响应而重传的,还是发送方因为收到了响应所以重新使用了这个 Packet ID 发送了一个全新的消息。
- QoS 2 中增加的 PUBREL 流程,正是提供了帮助通信双方协商 Packet ID 何时可以重用的能力。
- QoS 2 规定,发送方只有在收到 PUBREC 报文之前可以重传 PUBLISH 报文。一旦收到 PUBREC 报文并发出 PUBREL 报文,发送方就进入了 Packet ID 释放流程,不可以再使用当前 Packet ID 重传 PUBLISH 报文。同时,在收到对端回复的 PUBCOMP 报文确认双方都完成 Packet ID 释放之前,也不可以使用当前 Packet ID 发送新的消息。
Persistent Session
使用场景
- 客户端希望即使在网络断连的情况下,也必须从特定的主题下获取到所有消息。你想让MQTT服务端为此客户端暂时存储这些消息,并在客户端重新恢复连接的时候分发这些消息。
- 客户端资源有限。你希望MQTT服务端保存该客户端的订阅信息,并且可以迅速地恢复被中断的通信。
- 客户端需要在重连之后,继续所有的QoS 1和QoS 2级别的PUBLISH消息。
在持久会话中,代理存储以下信息(即使客户端离线)。当客户端重新连接时,信息立即可用。
- 会话的存在(即使没有订阅)。
- 客户端的所有订阅。
- 客户端尚未确认的服务质量 (QoS) 1 或 2 流中的所有消息。
- 客户端在离线时错过的所有新 QoS 1 或 2 消息。
- 从客户端收到的所有尚未完全确认的 QoS 2 消息。
当客户端连接到代理时,它可以请求持久会话。客户端使用cleanSession标志告诉代理它需要什么样的会话: - 当 clean session 标志设置为 true 时,客户端不需要持久会话。如果客户端因任何原因断开连接,则从先前持久会话排队的所有信息和消息都将丢失。
- 当 clean session 标志设置为 false 时,代理会为客户端创建一个持久性会话。所有信息和消息都将保留到客户端下次请求干净会话时为止。如果 clean session 标志设置为 false 并且代理已经有一个可用于客户端的会话,它会使用现有会话并将先前排队的消息传递给客户端。
3.1 会话和消息过期
MQTT5中将 Clean Session 标志拆分为 Clean Start 标志(指示会话应在不使用现有会话的情况下开始)和 Session Expiry 间隔(指示断开连接后会话保留多长时间)。会话到期间隔可以在断开连接时修改。
将 Clean Start 设置为 1 并将 Session Expiry Interval 设置为 0 等同于 MQTT v3.1.1 中将 Clean Session 设置为 1。
Clean Start
仅用于指示服务端在连接时应该尝试恢复之前的会话还是直接创建全新的会话
- Clean Start=1:客户端和服务端必须丢弃任何已存在的会话,并开始一个新的会话
- Clean Start=0:
- 存在一个关联此客户端标识符的会话,服务端必须基于此会话的状态恢复与客户端的通信
- 不存在任何关联此客户端标识符的会话,服务端必须创建一个新的会话
3.1.1 Session Expiry Interval(会话过期)
- 以秒为单位,如果 Session Expiry Interval 设置为 0 或者未指定,会话将在网络连接关闭时结束。
- 如果 Session Expiry Interval 为 0xFFFFFFFF ,则会话永不过期。
MQTT v5.0 支持客户端在断开连接时重新指定 Seesion Expiry Interval,如果网络连接关闭时(DISCONNECT 报文中的 Session Expiry Interval 可以覆盖 CONNECT 报文中的设置) Session Expiry Interval 大于0,则客户端与服务端必须存储会话状态 。这样我们可以非常容易地满足类似客户端网络连接异常断开时会话状态被服务器保留,客户端正常下线时会话则随着连接关闭而结束的场景
3.1.2 消息过期
客户端可以为每条 PUBLISH 消息单独设置消息过期时间间隔(以秒为单位)。此间隔定义了代理为当前未连接的任何匹配订阅者存储 PUBLISH 消息的时间段。当没有设置消息过期时间间隔时,代理必须为匹配的订阅者无限期地存储消息。当在PUBLISH 消息上设置retained=true选项时,此间隔还定义消息在主题上保留多长时间。
3.2 用户属性
用户属性(User Properties)其实是一种自定义属性,允许用户向 MQTT 消息添加自己的元数据,传输额外的自定义信息以扩充更多应用场景。 它由一个用户自定义的 UTF-8 的键/值对数组组成,并在消息属性字段中配置,只要不超过最大的消息大小,可以使用无限数量的用户属性来向 MQTT 消息添加元数据,并在发布者、MQTT 服务器和订阅者之间传递信息。
连接客户端的用户属性
- 当客户端与 MQTT 服务器发起连接时,服务器可以预先定义好一些需要并且可以使用到的元数据信息,即用户属性,当连接成功后,MQTT 服务可以拿到连接发送过来的相关信息进行使用,因此连接客户端的用户属性依赖于 MQTT 服务器。
消息发布的用户属性
- 消息发布时的用户属性可能是较为常用的,因为它们可以在客户端与客户端之间进行元数据信息传递。比如可以在发布时添加一些常见的信息:消息编号,时间戳,文件,客户端信息和路由信息等属性。
用户属性的使用
- 文件传输
- MQTT 5 的用户属性,可扩展为使用其进行文件传输,而不是像之前的 MQTT 3 中将数据放到消息体的 Payload 中,用户属性使用键值对的方式。这也意味着文件可以保持为二进制,因为文件的元数据在用户属性中。
- 资源解析
- 当客户端连接到 MQTT 服务器后,不同的客户端、供应商平台或系统存在着不同的方式传递消息数据,消息数据的格式可能都存在着一些结构差异。还有一些客户端是分布在不同的地域下。比如:地域 A 的设备发送的消息格式是 JSON 的,地域 B 的设备发送的是 XML 的,此时服务器接收到消息后可能需要一一进行判断和对比,找到合适的解析器来进行数据解析。
- IoT 部署通常很复杂,并且存在独立的系统可能会导致难以查明特定消息的来源或多层消息流失败的原因。为了提高效率和减少计算负载,我们可以利用用户属性功能来添加数据格式信息和地域信息,当服务器接收到消息后,可以使用用户属性中提供的元数据来进行数据解析操作。并且当区域 A 的客户端订阅接收到来自区域 B 的客户端消息时,也能快速的清楚特定的消息的来自于哪个区域等,从而使的消息具有了可追溯性。
- 消息路由
- 我们还可以使用用户属性来做应用层级别的路由。如上所述,存在着不同的系统和平台,每个区域存在着不同的设备,多个系统可能收到同一个设备的消息,有些系统需要将数据进行实时的展示,另一个系统可能将这些数据进行时序存储。因此 MQTT 服务器可以通过上报消息中配置的用户属性来确定将消息分发到存储消息的系统还是展示数据的系统。
3.3 更多的原因码
- MQTT v3.1.1 协议只有 10 种返回码,这些返回码所能表示的含义很少,且相同的返回码的值在不同的报文中可以有不同的含义。
- MQTT v5.0 协议将返回码改名成了原因码,增加了用于表示更多类型的错误的原因码。
3.4 主题别名
主题别名(Topic Alias)是 MQTT v5.0 中新加入的与主题名(topic)相关的特性。它允许用户将主题长度较长且常用的主题名缩减为一个双字节整数来降低发布消息时的带宽消耗。
概念
- 它是一个双字节整数,并将作为属性字段,编码在PUBLISH报文中可变报头部分。并且在实际应用中,将受到CONNECT报文和CONNACK报文中“主题别名最大长度”属性的限制。只要不超过该限制,任何主题名,都可以使用此特性缩减为编码长度2字节的整数。
- 该值由客户端和服务端各自维护,且生命周期和作用范围仅限于当前连接。连接断开后需要再次使用主题别名需要重新建立主题别名<=>主题名映射关系。
- 在使用 MQTT v3 协议时。如果客户端在某次连接中需要发布大量相同主题的消息,那么在每一条PUBLISH报文中写入相同的主题名,就造成了客户端和服务端之间带宽资源的浪费。同时对于服务端而言,每次对相同主题名的 UTF-8 字符串进行解析,都是对计算资源的浪费。
- 除去第一次发布消息外,之后的每个PUBLISH报文,都需要将“主题名”这个已经传递过的信息再次通过网络传输。即便抛开客户端和服务端之间额外的带宽消耗不言,对服务端来说,面对成千上万的传感器发布的大量消息,对每个客户端的每条消息,都要将同样的主题名字符串进行解析,这将造成了计算资源的浪费。
- 此时使用 MQTT v5 中的主题别名特性,就可以有效降低资源消耗。当客户端或服务端发布频率较高,且主题名长度较大的情景下,使用主题别名可以将每条消息中主题名的带宽消耗缩减为 2 字节,同时因为计算机处理整数的效率高于处理字符串的效率,对于客户端或服务端在报文解析时消耗的计算资源也有了一定的节约。
主题别名最大值(Topic Alias Maximum)
- 在 MQTT 客户端和服务端使用主题别名进行发布消息前,需要对可以使用的最大主题别名长度进行约定。这部份信息交换将在CONNECT报文和CONNACK报文中完成.
- 客户端的CONNECT报文中"主题别名最大值"指示了本客户端在此次连接中服务端可以使用的最大主题别名数量;同样地,服务端发送的CONNACK报文中,也通过此值表明了当前连接中对端(客户端)可以使用的最大主题别名数量。
3.5 流量控制
- 通常服务端的资源都是固定且有限的,而客户端的流量则可能是随时随地变化的。正常业务(用户集中访问、设备大量重启)、被恶意攻击、网络波动,都会导致流量出现激增,如果服务端没有对其进行任何限制,就会导致负载迅速上升,进而导致响应速度下降,影响其他业务,甚至导致系统瘫痪。
- 在 MQTT v5 中,发送端会有一个初始的发送配额,每当它发送一个 QoS 大于 0 的 PUBLISH 报文,发送配额就相应减一,而每当收到一个响应报文,发送配额就会加一。如果接收端没有及时响应,导致发送端的发送配额减为 0,发送端应当停止发送所有 QoS 大于 0 的 PUBLISH 报文直至发送配额恢复。我们可以将其视为变种的令牌桶算法,它们之间的区别仅仅是增加配额的方式从以固定速率增加变成了按实际收到响应报文的速率增加。
Receive Maximum
- 为了支持流量控制,MQTT v5 新增了一个 Receive Maximum 属性,表示客户端或服务端愿意同时处理的 QoS 为 1 和 2 的 PUBLISH 报文最大数量,即对端可以使用的最大发送配额。如果接收端已收到但未发送响应的 QoS 大于 0 的 PUBLISH 报文数量超过 Receive Maximum 的值,接收端将断开连接避免受到更严重的影响。
为什么没有Qos 0
- MQTT v5 的流量控制机制完全依赖响应报文,这就导致它的流量控制只能局限在 QoS 1,2 消息中。
- 当发送配额减为 0 时,发送端可以选择继续发送 QoS 为 0 的 PUBLISH 报文,也可以选择暂停发送。其中暂停发送的行为逻辑是,如果 QoS 1,2 的 PUBLISH 报文的应答速度变慢,通常意味着接收端的消费能力已经下降,继续发送 QoS 0 消息只会令情况变得更糟。
3.6 共享订阅
共享订阅是 MQTT 5.0 协议引入的新特性,相当于是订阅端的负载均衡功能。
概念
我们知道一般的非共享订阅的消息发布流程是这样的:
-
在这种结构下,如果订阅节点发生故障,就会导致发布者的消息丢失(QoS 0)或者堆积在 Server 中(QoS 1, 2)。一般情况下,解决这个问题的办法都是直接增加订阅节点,但这样又产生了大量的重复消息,不仅浪费性能,在某些业务场景下,订阅节点还需要自行去重,进一步增加了业务的复杂度。
-
其次,当发布者的生产能力较强时,可能会出现订阅者的消费能力无法及时跟上的情况,此时只能由订阅者自行实现负载均衡来解决,又一次增加了用户的开发成本。
-
现在,在 MQTT 5.0 协议中,可以通过共享订阅特性解决上面提到的问题。当使用共享订阅时,消息的流向就会变为:
-
同非共享订阅一样,共享订阅包含一个主题过滤器和订阅选项,唯一的区别在于共享订阅的主题过滤器格式必须是$share/{ShareName}/{filter} 这种形式。这几个的字段的含义分别是:
- $share 前缀表明这将是一个共享订阅
- {ShareName} 是一个不包含 “/”, “+” 以及 “#” 的字符串。订阅会话通过使用相同的 {ShareName} 表示共享同一个订阅,匹配该订阅的消息每次只会发布给其中一个会话
- {filter} 即非共享订阅中的主题过滤器
通配符
-
单层通配符:‘+’ 是只能用于单个主题层级匹配的通配符。在主题过滤器的任意层级都可以使用单层通配符, 包括第一个和最后一个层级。 然而它必须占据过滤器的整个层级 。 可以在主题过滤器中的多个层级中使用它, 也可以和多层通配符一起使用。
-
多层通配符: ‘#’ 是用于匹配主题中任意层级的通配符。 多层通配符表示它的父级和任意数量的子层级。 多层通配符必须位于它自己的层级或者跟在主题层级分隔符后面。 不管哪种情况, 它都必须是主题过滤器的最后一个字符。
-
需要注意的是,如果服务端正在向其选中的订阅端发送 QoS 2 消息,并且在分发完成之前网络中断,服务端会在订阅端重新连接时继续完成该消息的分发。如果订阅端的会话在其重连之前终止,服务端将丢弃该消息而不尝试发送给其他订阅端。
-
如果是 QoS 1 消息,服务端可以等订阅端重新连接之后继续完成分发,也可以在订阅端断开连接时就立即尝试将消息分发给其他订阅端,MQTT 协议没有强制规定,因此需要视服务器的具体实现而定。但如果在等待订阅端重连期间其会话终止,服务端则会将消息尝试发送给其他订阅端。
3.7 增强认证
在物联网的应用场景中,安全设计是非常重要的一个环节,敏感数据泄露或是边缘设备被非法控制等事故都是不可接受的,但是相比于其他应用场景,物联网项目还存在着以下局限:
- 安全性与高性能之间不可以兼顾;
- 加密算法需要更多的算力,而物联网设备的性能往往非常有限;
- 物联网的网络条件常常要比家庭或者办公室的网络条件差许多。
为了解决上述问题,MQTT 协议 提供了简单认证和增强认证,方便在应用层验证设备。
3.7.1 简单认证
- MQTT CONNECT 报文使用用户名和密码支持基本的网络连接认证,这个方法被称为简单认证。该方法也可以被用来承载其他形式的认证,例如把密码作为令牌(Token)传递。
- 服务器在收到 CONNECT 报文后,可以通过其包含的用户名和密码来验证客户端的合法性,保障业务的安全。
- 相比于增强认证,简单认证对于客户端和服务器的算力占用都很低,对于安全性要求不是那么高,计算资源紧张的业务,可以使用简单认证。
- 但是,在基于用户名和密码这种简单认证模型的协议中,客户端和服务器都知道一个用户名对应一个密码。在不对信道进行加密的前提下,无论是直接使用明文传输用户名和密码,还是给密码加个哈希的方法都很容易被攻击。
3.7.2 增强认证
- 基于更强的安全性考虑,MQTT v5 增加了新特性 增强认证,增强认证包含质询/响应风格的认证,可以实现对客户端和服务器的双向认证,服务器可以验证连接的客户端是否是真正的客户端,客户端也可以验证连接的服务器是否是真正的服务器,从而提供了更高的安全性。
- 增强认证依赖于认证方法和认证数据来完成整个认证过程,在增强认证中,认证方法通常为 SASL( Simple Authentication and Security Layer ) 机制,使用一个注册过的名称便于信息交换。但是,认证方法不限于使用已注册的 SASL 机制,服务器和客户端可以约定使用任何质询 / 响应风格的认证。
认证方法
- 认证方法是一个 UTF-8 的字符串,用于指定身份验证方式,客户端和服务器需要同时支持指定的认证方法。客户端通过在 CONNECT 报文中添加认证方法字段来启动增强认证,增强认证过程中客户端和服务器交换的报文都需要包含认证方法字段,并且认证方法必须与 CONNECT 报文保持一致。
认证数据
- 认证数据是二进制信息,用于传输加密机密或协议步骤的多次迭代。认证数据的内容高度依赖于认证方法的具体实现。
重新认证
- 增强认证完成之后,客户端可以在任意时间通过发送 AUTH 报文发起重新认证,重新认证开始后,同增强认证一样,客户端与服务器通过交换 AUTH 报文来交换认证数据,直到服务器向客户端发送原因码为 0x00( 成功) 的 AUTH 报文表示重新认证成功。需要注意的是,重新认证的认证方法必须与增强认证一致。
- 在重新认证的过程中,客户端和服务器的其他报文流可以继续使用之前的认证。
3.8 订阅标识符
客户端可以在订阅时指定一个订阅标识符,服务端将在订阅成功创建或修改时建立并存储该订阅与订阅标识符的映射关系。当有匹配该订阅的 PUBLISH 报文要转发给此客户端时,服务端会将与该订阅关联的订阅标识符随 PUBLISH 报文一并返回给客户端。
因此,客户端可以建立订阅标识符与消息处理程序的映射,以在收到 PUBLISH 报文时直接通过订阅标识符将消息定向至对应的消息处理程序,这会远远快于通过主题匹配来查找消息处理程序的速度。
No Local
- 在 MQTT v3.1.1 中,如果你订阅了自己发布消息的主题,那么你将收到自己发布的所有消息。
- 而在 MQTT v5 中,如果你在订阅时将此选项设置为 1,那么服务端将不会向你转发你自己发布的消息。
Retain As Publish
- 这一选项用来指定服务端向客户端转发消息时是否要保留其中的 RETAIN 标识,注意这一选项不会影响保留消息中的 RETAIN 标识。
- 因此当 Retain As Publish 选项被设置为 0 时,客户端直接依靠消息中的 RETAIN 标识来区分这是一个正常的转发消息还是一个保留消息,而不是去判断消息是否是自己订阅后收到的第一个消息(转发消息甚至可能会先于保留消息被发送,视不同 Broker 的具体实现而定)。
Retain Handling
这一选项用来指定订阅建立时服务端是否向客户端发送保留消息:
- Retain Handling 等于 0,只要客户端订阅成功,服务端就发送保留消息。
- Retain Handling 等于 1,客户端订阅成功且该订阅此前不存在,服务端才发送保留消息。毕竟有些时候客户端重新发起订阅可能只是为了改变一下 QoS,并不意味着它想再次接收保留消息。
- Retain Handling 等于 2,即便客户订阅成功,服务端也不会发送保留消息。
3.9 有效载荷标识
在 MQTT 5.0 的所有报文类型中,该属性只存在于 PUBLISH 报文和 CONNECT 报文的遗嘱属性中。
- 有效载荷标识只占据一个字节大小,它只有 0(0x00) 和 1(0x01) 两个值。
- MQTT CONNECT 报文中,当遗嘱属性的有效载荷标识的值为 0 时,意味着遗嘱消息是未确定的字节,当该属性值为 1 时,意味着遗嘱消息是 UTF-8 编码的字符数据,遗嘱载荷(Will Payload)中的数据必须符合标准 UTF-8 的定义。
- MQTT PUBLISH 报文中,当 PUBLISH 属性的有效载荷标识的值为 0 时,意味着 PUBLISH 消息是未确定的字节,当该属性值为 1 时,意味着 - PUBLISH 报文的有效载荷是 UTF-8 编码的字符数据,PUBLISH 报文载荷(Payload)中的数据必须符合标准 UTF-8 的定义。
内容类型
- 在 MQTT 5.0 的所有报文类型中,该属性同样只存在于 PUBLISH 报文和 CONNECT 报文的遗嘱属性中。该属性存放的是 UTF-8 编码的字符串,用于描述遗嘱消息或 PUBLISH 消息的内容。
- 它是由收发消息的应用程序决定的。在消息转发过程中, 内容类型不能被篡改。
- 内容类型的一个比较典型的应用就是存放 MIME 类型,比如 text/plain 表示文本文件,audio/aac 表示音频文件。
3.10 请求响应
-
在 MQTT 中客户端可以向指定主题发布消息,也可以订阅指定主题以接收感兴趣的消息。在明确有人订阅的情况下,大于 0 的 QoS 可以保证消息送达至订阅端。但如果结合一些业务场景,即不仅仅是将消息投递至订阅端,可能需要订阅端触发一些行为并返回结果,又或者是需要向订阅端请求一些信息,发布订阅模式下的实现就会稍显笨重,通信双方需要事先协商好请求主题和响应主题。
-
如果同一个请求主题存在多个请求方,为了将响应正确地返回给请求方,需要多个不同的响应主题,最常见的办法就是在 Payload 首部或是其他位置插入客户端标识符(Client ID)等能够唯一标识该请求客户端的字段,响应方在收到请求后按照事先约定的规则提取这些字段以及真正的 Payload,并将这些字段用于构造响应主题。
-
期望请求接收方只需要关注怎么处理请求即可,而不用花费额外的精力考虑怎么将响应正确返回给请求方。MQTT 5.0 新增了 响应主题(Response Topic) 属性
- MQTT 客户端(请求方)向请求主题发布包含 响应主题 属性的请求消息。
- 假如有其他 MQTT 客户端(响应方)订阅了与请求消息发布时使用的主题名相匹配的主题过滤器,那么将收到该请求消息。
- 响应方根据请求消息采取适当的操作,然后向该 响应主题 属性指定的主题发布响应消息。
-
与 HTTP 的请求响应模式不同,MQTT 的请求响应是异步的,这带来了一个问题,即响应消息与请求消息如何关联。最常用的办法就是在请求消息中携带一个特征字段,响应方在响应时将收到的字段原封不动地返回,请求方在收到响应消息时就可以根据其中的特征字段来匹配相应的请求。很显然 MQTT 也是这么考虑的,所以为 PUBLISH 报文新增了一个 对比数据(Correlation Data) 属性。
-
可能存在多个请求方同时发起请求的情况,为避免不同请求方之间的冲突,请求方客户端使用的响应主题最好对于该客户端是唯一的。由于请求方和响应方通常都需要对这些主题进行授权,因此使用随机主题名称将会对授权造成挑战。
-
为了解决此问题,MQTT 5.0 在 CONNACK 报文中定义了一个名为响应信息的属性。服务端可以使用此属性指导客户端如何选择使用的响应主题。此机制对于服务端和客户端都是可选的。连接时,客户端通过设置 CONNECT 报文中的请求响应信息属性来请求服务端发送响应信息。这会导致服务端在 CONNACK 报文中插入响应信息属性,请求方可以使用响应信息来构建响应主题。
使用建议
- 由于发布订阅模式本身的一些局限性,使用大于 0 的 QoS 也只能保证消息到达了对端而不是订阅端,如果发布消息时订阅端还未完成订阅,那么消息就会丢失,但发布方却无法得知。因此,对于一些投递要求比较严格的消息,可以通过请求响应来确认消息是否到达订阅端。
- 某些数据上报类的应用,当你感觉上报时间间隔设置得太长太短都不合适时,也许你可以尝试改成通过请求响应主动请求数据。但需要注意,如果请求方过多,导致数据实际上报频率大大超过原先的话,反而得不偿失,所以需要根据实际场景进行考量。
3.11 其他功能
3.11.1 遗嘱消息延迟时间
遗嘱消息是MQTT为那些可能出现意外断线的设备提供的将遗嘱优雅地发送给第三方的能力。意外断线包括但不限于:
- 因网络故障或网络波动,设备在保持连接周期内未能通讯,连接被服务端关闭
- 设备意外掉电
- 设备尝试进行不被允许的操作而被服务端关闭连接,例如订阅自身权限以外的主题等添加指定连接结束和发送遗嘱消息之间的延迟的功能。这样做的目的是,如果重新建立与会话的连接,则不会发送遗嘱消息。这允许在不通知其他人的情况下短暂中断连接。
3.11.2 自动分配 Client ID
- 通过 CONNACK 报文中的 Assigned Client Identifier 属性,MQTT 5.0 还提供了一个非常便捷的功能,即允许由 Broker 统一为 Client 分配 Client ID,而不是由 Client 自行指定,毕竟提前为 Client 分配一个全局唯一的 Client ID 在某些场景下不是一件容易的事情。
- 这个全新特性的使用也非常简单,只要 Client 在连接时提供一个零字节的 Client ID,Broker 就会在响应的 CONNACK 报文中携带 Assigned Client Identifier 属性,该属性的值就是自动分配的 Client ID。Client 可以一直持有和使用这个 Client ID,直到会话过期。
3.11.3 服务端断开
允许服务端发送DISCONNECT报文,以指示连接被关闭的原因。
3.11.4 服务端参考
允许服务端指定备用服务端
3.11.5 可选的服务端功能
- 考虑到不是所有 MQTT Broker 都是完整实现,可能无法提供完整的 MQTT 5.0 功能,因此 MQTT 5.0 还支持了可选的服务端功能。
- CONNACK 报文中的 Retain Available 属性可用于声明是否支持保留消息
- Wildcard Subscription Available 属性可用于声明是否支持通配符订阅
- Subscription Identifier Available 属性可用于声明是否支持订阅标志符
- Shared Subscription Available 属性可用于声明是否支持共享订阅
附录:
最后附上mqtt5.0 OASIS pdf说明
https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.pdf