Skip to content

Commit

Permalink
feat: new post mqtt 4
Browse files Browse the repository at this point in the history
  • Loading branch information
xizhibei committed May 2, 2024
1 parent 543d98d commit 325655f
Showing 1 changed file with 132 additions and 0 deletions.
132 changes: 132 additions & 0 deletions source/_posts/mqtt-4-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
title: 【MQTT 系列】(四)v3.1.1 特性
tags:
- MQTT
date: 2024-05-02 20:44:51
---


[上次的文章](/2021/12/11/mqtt-3-sub-pub-and-topics/)中(似乎有那么亿点久了 :P),我们说了 MQTT 的发布订阅相关的功能,这次我们直接来说它的特性。

<!-- more -->

### 服务质量 QoS
QoS 有三个级别,分别用 0、1、2 来表示,代表的意义如下:

- **QoS 0**: 最多一次,这是最简单的级别,但是消息可能会丢失,可以理解为 UDP 数据包,只管发送,不管到达,性能也是最好的,比较适用于周期性的传感器数据上报;
- **QoS 1**: 至少一次,这意味着消息至少会被发送一次,及时在网络不稳定的情况下也能保证消息的到达,性能稍差,比较适用于不在意重复(接收端做了重复数据的处理),但却在意数据需要确保送达的场景;
- **QoS 2**:刚好一次,这是最高的级别,保证消息既不会丢失,也不会重复,显然这种方式的性能是最差的,比较适用于对消息的重复性有严格要求的场景,比如航空航天等;

在常见的家庭、办公室以及工业物联网场景中,我们常用的只有 QoS 0 跟 1 级别,QoS 2 这个级别由于性能差,用的比较少,而且对于数据比较重要的场景,我们也完全可以在接收端进行重复数据的处理即可,性能高了很多。并且,我们也完全用 QoS 0 在应用层实现自己的消息重发机制来实现消息的不丢失。

另外,除了 QoS 0,其它两个级别都需要用到客户端的存储来进行消息的重发机制,因此如果本地有多个客户端实例的情况下,需要注意给它们分配不同的存储区域,不然会导致冲突以及错误。

比如在 [paho.mqtt.golang](https://github.com/eclipse/paho.mqtt.golang) 中,它默认使用 [MemoryStore](https://github.com/eclipse/paho.mqtt.golang/blob/fe38f8024a1a2edb07fec9906f5a4389cd1262b6/memstore.go) 来作为 `Store` 使用,但是如果你用了它的 [FileStore](https://github.com/eclipse/paho.mqtt.golang/blob/fe38f8024a1a2edb07fec9906f5a4389cd1262b6/filestore.go),我们需要制定不同的文件夹路径来解决这个问题。

### 保持会话 Persistent Session

保持会话是指客户端在断开连接之后,可以恢复到之前的会话状态,而不是丢失所有未处理的消息。显然,这对于需要长期连接到网络的物联网设备而言非常重要,它们所处的环境可能非常糟糕,需要在网络中断后恢复。保持会话就允许设备可以在重新连接时从上次断开的位置重新开始,防止处理重复的数据以及丢失数据。

跟这个特性相关的配置就是 `CleanSession`,具体的原理也很容易理解, Broker 会在客户端连接的时候就确认客户端是不是需要保持会话,即是不是设置了 `CleanSession=false`,需要保持会话的时候, Broker 会存储以下信息:

1. 会话本身的信息,包含一些连接时候的参数;
2. 客户端的订阅信息;
3. 消息:
1. QoS 1&2 未被客户端确认的消息;
2. QoS 1&2 离线时候的新消息;
3. QoS 2 的未完成确认的消息;

同时,客户端也会存储 QoS 0 之外的消息:
1. QoS 1&2 未被 Broker 确认的消息;
2. QoS 2 的未完成确认的消息;

显然,由于 [paho.mqtt.golang](https://github.com/eclipse/paho.mqtt.golang) 默认使用 `MemoryStore`,这里你需要特别注意下,改成 `FileStore`

这里需要注意的地方:

- 离线期间的数据可能会比较多(根据你的业务来计算),那么在客户端恢复连接的时候,可能会有大量的数据需要处理,需要考虑设备的性能进行接收的限制。或者,如果你不想处理离线期间的消息,即能接受它们的丢失,那么设置 `CleanSession=true` 会更适合你;
- 出了发送的 QoS,还有接收的 QoS,而这时候如果两者不一样,就会出现降级,即按低的 QoS 来处理;

### 保留消息 Retained Message

保留消息是经常被误解的功能,也经常被滥用,以为这个是用来存储消息的功能,实际上并不是,它的一个重要属性便是,每个 Topic 只能保存最新的一个保留消息。

保留消息是在发送的时候设置,用来告诉 Broker 当前这个消息是否需要保留,我们可以从代码中看到,`retained` 是发送的第三个参数,往往我们会设置它为 `false`,在需要的时候会设置它为 `true`

```go
type Client interface {
// ...

Publish(topic string, qos byte, retained bool, payload interface{}) Token

// ...
}
```

保留消息最常见的使用场景包括:

- **设备状态更新**:例如,一个智能家居系统中的灯光控制器发布一个状态更新消息,所有订阅了这个主题的客户端(如手机应用或其他控制器)都能立即知道灯光的当前状态,当然还有个最重要的状态也可以使用:设备的上下线状态;
- **通知系统**:在一个通知系统中,当有新通知发布时,保留消息可以确保所有在线的用户都能立即看到最新的通知,而无需等待下一次心跳检查或订阅更新;

需要注意的地方:

- 跟使用 QoS 1&2 需要注意离线期间的消息一样,订阅 Retained 相关的主题时,也需要注意,如果你订阅了一大堆的主题都有 Retained 消息,那么在订阅成功的那一刻,就会有大量的消息从 Broker 发送过来(这点也跟 Broker 相关,它们往往也会限制已订阅主题的 Retained 消息数量);
- 如果想要删除 Retained 消息,给相同主题发送一个空的消息即可,一般来说除非你发送错了,比如把一个消息误标记成 Reatained 消息了,大多数情况下你并不需要删除,因为后一个消息总是会覆盖前一个消息;


### 遗嘱消息 Last Will and Testament

遗嘱消息,顾名思义就是客户端下线之后留下的消息。简单来说就是客户端设置的,能够让 Broker 在检测到客户端断连时,向指定的 Topic 发送指定消息的一种功能。它最适合的应用场景就是客户端断连时,给相关的 Topic 发送一个下线消息,而其它客户只要订阅这个 Topic 便能及时获取其它客户端的下线通知。

[paho.mqtt.golang](https://github.com/eclipse/paho.mqtt.golang) 中涉及到的几个连接配置参数如下,就如同人留下遗嘱的时候是生前,MQTT 协议中,留遗嘱是在连接的时候设置:

```go
WillEnabled bool
WillTopic string
WillPayload []byte
WillQos byte
WillRetained bool
```

可以看到,它需要的参数跟正常的消息一样,如果配合 Retained,就能简单实现设备的上下线通知消息了。

### 保活协议以及客户端接管 KeepAlive Client Takeover

这其实是为了解决 TCP 的「半开」问题,也就是理论上 TCP 本身虽然有断开的通知机制,但是实际情况下,还是会出现一方断开,却不通知另一方的情况出现,MQTT 在之前往往是手机或者卫星连接时出现,而如今更多的情况是物联网设备断电的情况下出现。

因此 MQTT 协议就有 `KeepAlive` 选项,于是客户端需要跟 Broker 协商一个心跳周期,来检测对方是否在线,在这个周期内,如果客户端跟 Broker 之间有消息交换,那么心跳包没必要发送,但是一旦在这个周期内,客户端没有其它消息交换,客户端就必须发送一个心跳包来告诉 Broker 自己仍然在线。对应的,如果 Broker 在一个半的心痛周期内没有收到心跳包,那么就可以认为客户端已经离线,需要主动断开。同样的,如果客户端没有在一个合理的时间范围内(即 `PingTimeout`)收到 Broker 的心跳回复包,那么也需要主动断开连接。

不知道你有没有意识到一个问题,那就是如果 Broker 在没有断开客户端连接的情况下,比如心跳周期很长,但是 TCP 连接已经处于半开了,但是客户端却已经在重连了,如果这时候客户端重连成功,是不是意味着会出现客户端跟 Broker 产生多个 TCP 连接?但现实却不会出现这个问题,因为 MQTT 协议中,需要设置 ClientId,同一个 ClientId 跟 Broker 最多只能保持一个连接,后一个连接会接管前一个连接,即把前一个连接踢下线,这就是所谓的客户端接管。

[paho.mqtt.golang](https://github.com/eclipse/paho.mqtt.golang) 涉及到的几个参数如下:

```go
ClientID string
KeepAlive int64
PingTimeout time.Duration
```

从它的[源码](https://github.com/eclipse/paho.mqtt.golang/blob/fe38f8024a1a2edb07fec9906f5a4389cd1262b6/options.go#L134)可以看到,默认的心跳周期是 30 秒,而心跳的超时时间为 10 秒。

### 总结

总的来说,MQTT 协议的这些特性使其能够高效地支持各种物联网应用场景,包括资源受限的远程设备、不可靠网络环境、实时数据分发等。了解和正确使用这些特性,有助于构建更加可靠和高效的物联网系统。

同时,我也相信,了解了协议细节的你,能够在使用到这个协议时,更加专业,少犯一些低级错误:

- 实现客户端的时候:
* 把 Retained 认为是服务端需要保留的数据;
* 忽略场景,将所有发送的消息的 QoS 设置为 2;
- 在对接客户端的时候:
* 要求对方实现应用层的心跳协议;
* 责怪对方请求频率太高了(这里给你留个课后作业,为什么?);

我们这次只说明了 MQTT v3.1.1 的特性,事实上 MQTT 还有 v5 特性,我会在下次再继续讲解。

### Refs

- [What is MQTT Quality of Service (QoS) 0,1, & 2? – MQTT Essentials: Part 6](https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels/)
- [Understanding Persistent Sessions and Clean Sessions – MQTT Essentials: Part 7](https://www.hivemq.com/blog/mqtt-essentials-part-7-persistent-session-queuing-messages/)
- [What are Retained Messages in MQTT? – MQTT Essentials: Part 8](https://www.hivemq.com/blog/mqtt-essentials-part-8-retained-messages/)
- [What is MQTT Last Will and Testament (LWT)? – MQTT Essentials: Part 9](https://www.hivemq.com/blog/mqtt-essentials-part-9-last-will-and-testament/)
- [What Is MQTT Keep Alive and Client Take-Over? – MQTT Essentials Part 10](https://www.hivemq.com/blog/mqtt-essentials-part-10-alive-client-take-over/)

0 comments on commit 325655f

Please sign in to comment.