目录
4、MOTT Last Will and Testament(遗嘱)
5、Keep Alive(长连接)and Client Take-Over(接收)
服务质量(QoS)级别是消息发送方和消息接收方之间的协议,它定义了特定的消息如何进行交付。MQTT 有三个 QoS 级别:
在 MQTT 中讨论 QoS 时,需要考虑消息传递的两个方面:
客户端将消息发布到 Broker 时会定义消息的 QoS 级别。Broker 将消息传输到订阅客户端时会使用每个订阅客户端在订阅过程中定义的 QoS 级别。如果订阅客户端定义的 QoS 级别低于发布客户端,Broker 将以较低的服务质量传输消息到订阅客户端。
QoS 是 MQTT 协议的一个关键特性。QoS 使客户端能够选择与其网络可靠性和应用程序逻辑相匹配的服务级别。因为 MQTT 控制消息的重传并保证交付(即使底层传输不可靠),所以 QoS 使 MQTT 在不可靠网络中的通信变得更加容易。// 定义消息传输的可靠性
QoS 0 - at most once
最小的 QoS 级别为 0。这个服务级别保证了最好的消息传递。但不保证交付质量。消息接收方不会确认收到的消息,消息发送方也会不存储和重传消息。所以,QoS 级别为 0 的情况通常被称为 “发后不理” ,该级别提供与底层 TCP 协议相同的交付保证。// 发后不理
QoS 1 - at least once
QoS 级别为 1 ,该级别保证消息至少能有一次被传递到接收者。发送方会存储消息,直到获得接收方的确认为止。该级别下,一条消息可能会被发送或传递多次。// 不保证消息不重复
发送方使用每个包中的包标识符(ID)将 PUBLISH 包与相应的 PUBACK 包进行匹配。如果发送方在规定的时间内没有收到 PUBACK 报文,那么会重新发送 PUBLISH 报文。例如,如果接收方是 Broker,那么 Broker 会将消息发送到所有订阅的客户端,然后用 PUBACK 包进行应答。// 这里需要注意,Broker 会将消息发送到所有订阅的客户端,然后才应答
如果发布客户端再次发送消息,它会携带一个重复(DUP)标志。在 QoS 1 级别中,这个 DUP 标志不会被 Broker 或客户端处理。消息的接收者会发送 PUBACK 进行消息确认,而不会考虑 DUP 标志。// 接收端不会对消息进行去重处理
QoS 2 - exactly once
QoS 2 是 MQTT 中最高级别的质量服务。此级别保证每个消息仅被接收方接收一次。QoS 2 是所有服务级别中消息交付最安全、处理速度最慢的级别。该级别要求发送方和接收方之间提供至少两次的请求和响应交互(四次握手)。发送方和接收方使用第一次生成的 PUBLISH 消息标识符来处理消息的传递。// 每条消息仅处理一次,处理重复消息
当接收方从发送方收到 QoS 2 级别的 PUBLISH 报文时,将会对 PUBLISH 消息进行相应的处理,并返回一个该 PUBLISH 报文的 PUBREC 包对消息进行确认。如果发送方没有从接收方得到 PUBREC 包,它将再次发送带有重复(DUP)标志的 PUBLISH 包,直到它收到确认包为止。
一旦发送方从接收方收到 PUBREC 包,发送方就可以安全地丢弃原来的 PUBLISH 包。然后发送方存储来自接收方的 PUBREC 包,并用 PUBREL 包进行响应。
接收方收到 PUBREL 包后,就可以丢弃该消息的所有存储的状态,并用一个 PUBCOMP 包进行应答(发送方收到 PUBCOMP 包时也是如此)。在接收方完成处理并将 PUBCOMP 包发送回发送方之前,接收方会存储原始 PUBLISH 包的包标识符的引用。这样做可以避免重复处理消息。发送方收到 PUBCOMP 包后,发布消息的包标识符就可以重用了。// 因为消息传递完成后,原来的标识符已经没有什么用了。
当 QoS 2 级别的消息传输完成时,双方都能确定消息被传递了。
如果数据包在途中丢失,发送方有责任在规定的时间内重新发送消息。接收方有相应地责任响应每条收到的消息。
每个客户端的包标识符都是惟一的
MQTT 用于 QoS 1 和 QoS 2 的包标识符在客户端和 Broker 的一次交互中是惟一的。但这个标识符在所有的客户端之间并不是唯一的。因为,一旦一次交互完成,包标识符就可以重用了。包标识符可以重用是该标识不需要超过 65535 的原因。因为客户端在没有完成交互的情况下发送超过这个数量的消息是不现实的。
选择 QoS 0 - at most once 的情况:
选择 QoS 1 - at least once 的情况:
选择 QoS 2 - exactly once 的情况:
适用于应用程序对所有消息要求只能接收一次的情况。如果重复的交付会损害应用程序或者订阅的客户端,那么通常会有这种要求。需要注意 QoS 2 级别的开销,因为 QoS 2 级别的交互需要更多的时间来完成。
所有以 QoS 1 或 2 级别发送的消息都会对断连的客户端进行排队,直到客户端再次重连为止。但是,这种排队机制只有在客户端使用持久会话的情况下才可能实现。// 只有服务质量为1 或者 2 ,且客户端具备持久会话的情况下才能进行排队。
如果要从 MQTT Broker 接收消息,客户端需要连接到 Broker 并创建相关的主题的订阅。如果客户端和 Broker 之间的连接在非持久会话期间中断,那么这些订阅信息将丢失,客户端在重新连接时需要再次订阅。对于资源有限的客户端来说,每次连接中断都需要重新订阅是一种负担。为了避免这个问题,客户端可以在连接到代理时请求使用持久会话。持久会话会保存 Broker 与客户端相关的所有信息。// 持久会话用来减轻重连负担
在持久会话中,Broker 会存储以下信息(即使客户端断连)。当客户端重新连接时,这些信息立即可用。
当客户端连接到 Broker 时,它可以创建持久会话。客户端使用 Clean Session 标志来告诉 Broker 它需要什么样的会话:
从 MQTT 3.1.1 开始,来自 Broker 的 CONNACK 消息会包含当前会话的标志。这个标志告诉客户端之前建立的会话在 Broker 上是否仍然可用。
客户端的持久会话
跟 Broker 一样,每个 MQTT 客户端也必须存储持久会话。当客户端请求服务器保存会话数据时,客户端负责存储以下信息:// 持久会话中客户端和 Broker 都会存储信息
使用持久会话的情况(Persistent Session):
无需使用持久会话的情况(Clean session):
有人经常会问 Broker 会将会话存储多长时间。简单的答案是:Broker 存储会话,直到客户端恢复连接并接收消息。但是,如果客户端长时间不连接会发生什么情况呢?通常情况下,操作系统的内存是消息存储的主要限制因素。对于这种情况,目前并没有标准答案。解决问题的合适方案取决于你的应用程序。// 正常情况下,如果没有被消费就会一直存储
保留消息也是一条普通的 MQTT 消息,该消息的保留标志(retained flag)被设置为 true 。Broker 会存储最后一条保留消息以及该主题对应的 QoS 级别。如果客户端订阅了该保留消息的主题,那么客户端会立即收到该保留消息。Broker 只会为每个主题存储一条保留消息。// 保留消息一般和遗嘱消息结合使用
如果订阅的客户端在订阅主题时使用了通配符,那么即使与保留消息的主题不完全匹配,该客户端也会收到这些保留消息。比如,客户端 A 向 myhome/livingroom/temperature 主题发布了一条保留消息。过了一会儿,客户端 B 订阅了主题 myhome/# ,那么客户端 B 在订阅主题后,将收到客户端 A 发布的保留消息。客户端 B (订阅客户端) 可以识别保留消息,因为 Broker 在发送保留消息时保留标志会设置为 true 。客户端 B 可以决定如何处理保留消息。
保留消息可以帮助新订阅的客户端在订阅主题后立即获得主题状态的变更情况。同时保留消息还为等待发布客户端发送下一个数据消除了等待的时间。// 不用等客户端发送数据就知道发送客户端的状态
换句话说,主题的保留消息是最后一条已知的正确数据。保留消息不一定是最后一个数据,但是它必须是保留标志设置为 true 的最后一个数据。
需要注意的是,保留消息与持久会话没有任何的关系。Broker 在存储了保留消息之后,只有一种方式可以删除它。// 进行覆盖操作
发送一条保留消息
从开发人员的角度来看,发送一条保留消息非常的简单且非常直接。只需要在 MQTT 发布消息时,将消息的保留标志设置为 true。通常情况下,客户端代码提供了设置此标志的简单方法。
删除一条保留消息
删除主题的保留消息非常简单:向希望删除之前保留消息的主题上,发送一条零字节负载的保留消息(空消息)。Broker 删除保留消息后,新的订阅者将不会再获得该主题的保留消息。一般情况下,保留消息不需要删除,因为旧的保留消息会被新的保留消息覆盖。// 其实发送零字节的保留消息也是一种覆盖操作,消息没有内容,就算接收了也无任何影响。
当希望新连接的订阅者能立即接收消息时(而不需要等待客户端发布下一条消息),保留消息是有意义的。该机制对于判断各个主题上的组件或设备的状态更新非常有帮助。例如,设备 1 的状态存储在主题 “myhome/devices/device1/status” 上。当使用保留消息时,该主题的新订阅者在订阅后会立即获得设备 1 的状态(在线 / 离线)。对于按时间间隔、温度、GPS坐标或其他数据发送数据的客户端也是如此。如果没有保留消息,新的订阅者将在发布间隔之间不清楚发布者是否还在正常发数据,所以清楚主题数据的更新情况。当使用保留消息时,有助于立即向连接的客户端提供最后一条正确的数据,从而让新订阅者能获取到主题的最新情况。// 可以是保留该主题中的最新的消息,设备的最新状态等。
在 MQTT 中,如果有一个客户端异常离线,可以使用遗嘱(LWT)来通知其他客户端,每个客户端在连接到 Broker 时都可以指定它的遗嘱消息。遗嘱消息是一个普通的 MQTT 消息,该消息有主题、有保留消息标志、QoS 级别和有效负载。Broker 会一直存储 LWT 消息,直到检测到该客户端已经异常离线。当检测到客户端异常离线时,Broker 会把该客户端的遗嘱消息发送给订阅了此遗嘱消息主题的所有客户端。如果客户端使用正确的 DISCONNECT 消息正常的断开连接,那么 Broker 将丢弃该客户端存储的 LWT 消息。
当客户端的离线时,LWT 可以帮助实现各种策略(或者至少可以通知其他客户端该离线状态)。
客户端可以在 CONNECT 消息中指定 LWT 消息,CONNECT 消息是客户端向 Broker 发起连接请求时的消息。
根据 MQTT 3.1.1 规范,Broker 必须在以下情况下分发客户端的 LWT 消息:
当订阅中的一个客户端异常离线时,LWT 消息可以用来通知其他订阅的客户端该订阅者的异常离线状态。在实际场景中,LWT 消息经常与保留消息结合使用,用来存储特定主题的客户端的状态。例如,client-1 首先向 Broker 发送一条 CONNECT 消息,其中包含一个 lastWillMessage,该 lastWillMessage 的有效负载为 “Offline”,lastWillRetain 标志设置为 true,lastWillTopic 设置为 client1/status。接下来,client-1 向相同的主题(client1/status)发送一条带有有效负载 “Online” 的 PUBLISH 消息,并将保留标志(retained flag)设置为 true。此时,只要 client-1 一直保持连接,新订阅到 client1/status 主题的客户端就会收到 “Online” 这条保留消息。如果 client-1 异常离线,Broker 会发布将带有有效负载 “Offline” 的 LWT 消息,把此消息作为新的保留消息。在 client-1 离线时,订阅主题的客户端将从 Broker 接收到 LWT 保留消息(“Offline”)。这种消息保留模式可以使订阅特定主题的其他客户端能获取到 client-1 当前状态的最新信息。// LWT + 保留消息模式可以使订阅者能获取到发布者的最新状态,判断发布者是否在线,针对不同状态来实现不同的逻辑。
MQTT 基于 TCP 协议。该协议确保数据包以一种 “可靠、有序和纠错” 的方式在互联网上传输。但是,有时候通信方之间的传输并不同步。比如,如果通信中的一方崩溃或者传输发生错误等情况。在 TCP 中,这种不完全连接的状态称为半开放连接。需要指出的是,在半开放连接中,通信正常的一方将继续工作,而不会被通知另一方已经发生故障。此时,正常的一方仍然尝试继续发送消息,并等待对方的确认。
MQTT 包含一个 Keep Alive 函数,该函数为半开放连接问题提供了一种解决方法。(或者至少可以评估连接是否仍然有效)。
Keep Alive 用来确保 Broker 和客户端之间的连接仍然是打开的,并且 Broker 和客户端都知道连接处于打开状态。当客户端与 Broker 建立连接时,客户端将在规定的时间内与 Broker 进行通信(以秒为单位)。该时间间隔规定了 Broker 和客户端之间不能相互通信的最大时间长度。
MQTT规范规定如下:
Keep Alive…是客户端完成发送一个控制包到开始发送下一个控制包之间允许经过的最大时间间隔。客户端有责任确保发送控制数据包之间的间隔不超过 Keep Alive 值。在没有发送任何其他控制包的情况下,客户端必须发送一个 PINGREQ 包。
只要 Broker 和客户端之间频繁地交换消息并且不超过 Keep Alive 的时间,就不需要发送额外的消息来确定连接是否仍处于打开状态。
如果客户端在 Keep Alive 期间没有发送消息,它必须向 Broker 发送一个 PINGREQ 包,以确认连接是可用的,并且确保 Broker 此时也仍然可用。
如果客户端在规定的时间内没有发送消息或 PINGREQ 包,那么 Broker 必须在1.5倍的 Keep Alive 时间间隔内与客户端断开连接。同样,如果客户端在规定的时间内没有收到来自 Broker 的响应,那么客户端也关闭此连接。// 该方式不会造成另一方无限的等待
Keep Alive Flow 数据流
Keep Alive 特性使用以下两个报文:
PINGREQ
PINGREQ 由客户端发送,并提示 Broker 客户端仍然存在。如果客户端没有发送任何其他类型的包(例如 PUBLISH 或 SUBSCRIBE 包),那么客户端必须向代理发送一个 PINGREQ 包。客户端可以在任何时候发送一个 PINGREQ 包来确认网络连接是否仍然存在。PINGREQ 报文不包含有效载荷(无有效数据)。// 心跳机制,询问
PINGRESP
当 Broker 接收到一个 PINGREQ 包时,Broker 必须用一个 PINGRESP 包来回复客户端,告诉客户端它仍然可用。PINGRESP 包也不包含有效负载(也没有有效数据)。// 心跳机制,回答
这些情况需要注意:
通常,断开连接的客户端会尝试着与 Broker 重新建立连接。如果此时 Broker 仍然为客户端之前的连接提供半开放的连接(MQTT Broker 仍然会认为客户端在线),那么重新连接后会执行客户端接管(Client Take-Over)。然后 Broker 会关闭同一客户端的前一个连接(由客户端的标识符确定),并且与该客户端建立一个新的连接。这种机制可以确保半开放的连接不会阻止离线的客户端重新建立连接。