## 一、MQTT协议简介
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议 上,由IBM在1999年发布。
![img.png](img.png)
MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
### 相较于HTTP
在传统互联网应用中,HTTP被广泛应用。通常情况下,客户端的设备配置、网络环境都比较可控,另外请求通常会基于各种业务传递大量的数据。
但是对于物联网来说,网络不稳定、设备能力不够,是重点考虑的因素。再者物联网传输的消息大小远小于传统互联网的业务数据,如果每次发送数据都来一次连接/断开TCP,发送的数据越多,数据总通信量也就越大。
再有就是在灵活性上HTTP的请求-响应模式也不如MQTT的发布/订阅模式。必须由设备主动向服务器发送数据,而服务器难以主动向设备推送数据。对于单单的数据采集等场景还勉强适用,但是对于频繁的操控场景,只能推过设备定期主动拉取的的方式,实现成本和实时性都大打折扣。
## 二、MQTT协议设计规范
由于物联网的环境是非常特别的,所以MQTT遵循以下设计原则:
- 精简,不添加可有可无的功能;
- 发布/订阅(Pub/Sub)模式,方便消息在传感器之间传递;
- 允许用户动态创建主题,零运维成本;
- 把传输量降到最低以提高传输效率;
- 把低带宽、高延迟、不稳定的网络等因素考虑在内;
- 支持连续的会话控制;
- 理解客户端计算能力可能很低;
- 提供服务质量管理;
- 假设数据不可知,不强求传输数据的类型与格式,保持灵活性。
> 发明人当时正在开发一个利用卫星通讯监控输油管道的项目。为了实现这个项目要求,他们需要开发一种用于嵌入式设备的通讯协议。 从诞生之初就是专为低带宽、高延迟或不可靠的网络而设计的。虽然历经几十年的更新和变化,以上这些特点仍然是MQTT协议的核心特点。但是与最初不同的是,MQTT协议已经从嵌入式系统应用拓展到开放的物联网(IoT)领域。
### 版本
目前MQTT主流版本有两个,分别是`MQTT3.1.1`和`MQTT5`。`MQTT3.1.1`是在2014年10月发布的,而`MQTT5`是在2019年3月发布的。 MQTT5是在 `MQTT3.1.1` 的基础上进行了升级,添加了更多的功能、完善 MQTT 协议。因此MQTT5是完全兼容`MQTT3.1.1`的。
## 三、MQTT与消息队列MQ的区别
MQTT 并不是消息队列,尽管两者的很多行为和特性非常接近,比如都采用发布订阅模式等,但是他们面向的场景有着显著的不同。
- 消息队列主要用于服务端应用之间的消息存储与转发,这类场景往往数据量大但接入量少。
- MQTT 面向的是 IoT 领域和移动互联网领域,这类场景的侧重点是海量的设备接入、管理与消息传输。
在实际的场景中,两者往往被结合起来使用,譬如先由 MQTT Broker 接收物联网设备上传的数据,然后通过消息队列MQ将这些数据转发到具体应用进行处理。
![img_1.png](img_1.png)
## 四、MQTT协议对于车联网
在车联网中有一个重要角色:TSP(Telematics Service Provider)汽车远程服务提供商。TSP 上接汽车、车载设备制造商、网络运营商,下接内容提供商。
既然身处核心地位,自然少不了与其他环节的各种交互,比如说云平台与车机端的消息接入。
而 MQTT 是基于发布/订阅模式的物联网通信协议,具有简单易实现、支持 QoS、报文小等特点,在车联网场景中,MQTT 能够胜任海量车机系统灵活、快速、安全地接入,并保证复杂网络环境下消息实时性、可靠性。主要优势:
1. 开放消息协议,简单易实现。市场上有大量成熟的软件库与硬件模组,可以有效降低车机接入难度和使用成本;
2. 提供灵活的发布订阅和主题设计,能够通过海量的 Topic 进行消息通信,应对各类车联网业务;
3. Payload 格式灵活,报文结构紧凑,可以灵活承载各类业务数据并有效减少车机网络流量;
4. 提供三个可选的 QoS 等级,能够适应车机设备不同的网络环境;
5. 提供在线状态感知与会话保持能力,方便管理车机在线状态并进行离线消息保留。
## 五、MQTT协议特性介绍
### 1. 客户端与服务器
实现MQTT协议需要客户端和服务器端通讯完成。在通讯过程中,MQTT协议中有三种身份 :发布者(Publish) 、代理(Broker)(服务器) 、订阅者(Subscribe) 。
其中,消息的发布者和订阅者都是客户端,消息代理是服务器。
#### 客户端
MQTT客户端可以向服务端发布信息,也可以从服务端收取信息。
我们把客户端发送信息的行为称为发布信息。而客户端要想从服务端收取信息,则首先要向服务端订阅信息。订阅信息这一操作很像我们在视频网站订阅某一类型电视剧。当这部电视剧上新后,视频网站会向订阅了该剧的用户发送信息,告诉他们有新剧上线了。
1. 发布其他客户端可能会订阅的信息
2. 订阅其它客户端发布的消息
3. 退订或删除应用程序的消息
4. 断开与服务器连接
#### 服务器
MQTT 服务端通常是一台服务器,也称为“消息代理”(Broker)。位于消息发布者和订阅者之间,它是MQTT信息传输的枢纽,负责将MQTT客户端发送来的信息传递给MQTT客户端。
MQTT 服务端还负责管理 MQTT 客户端。确保客户端之间的通讯顺畅,保证 MQTT 消息得以正确接收和准确投递。
1. 接受来自客户的网络连接
2. 接受客户发布的应用信息
3. 处理来自客户端的订阅和退订请求
4. 向订阅的客户转发应用程序消息
### 2.主题
刚刚我们在讲解 MQTT 客户端订阅信息时,使用了用户在视频网站订阅电视剧这个例子。在 MQTT 通讯中,客户端所订阅的肯定不是一部部电视剧,而是一个个主题。MQTT 服务端在管理 MQTT 信息通讯时,就是使用主题来控制的。
![img_2.png](img_2.png)
再来举个例子:
> ```text
> 在以上图示中一共有三个MQTT客户端:
>
> 它们分别是汽车,手机和电脑。
>
> 假设我们需要利用手机和电脑获取汽车的速度,
> 那么我们首先要利用电脑和手机向MQTT服务器订阅主题“汽车速度”。
>
> 接下来,当汽车客户端向服务端的“汽车速度”主题发布
> 信息后,服务端就会首先检查以下都有哪些客户端
> 订阅了“汽车速度”这一主题的信息。
>
> 当它发现订阅了该主题的客户端有一个手机和一个电脑,
> 于是服务端就会将刚刚收到的“汽车速度”信息转发给订阅了
> 该主题的手机和电脑客户端。
>
> 在以上实例中,汽车是“汽车速度”主题的发布者,
> 而手机和电脑则是该主题的订阅者。
> ```
另外,**消息发布者同时也可以是订阅者** ,看图:
![img_3.png](img_3.png)
### **3. MQTT 发布/订阅 特性**
从上述列子可以看出,MQTT通讯的核心枢纽是MQTT服务端。有了服务端对MQTT信息的接收、储存、处理和发送,客户端在发布和订阅信息时,可以相互独立,且在空间上可以分离,时间上可以异步。
- 相互独立
MQTT客户端是一个个独立的个体。它们无需了解彼此的存在,依然可以实现信息交流。
比如以上实例中汽车客户端在发布“汽车速度”信息时,汽车客户端本身可以完全不知道有多少个MQTT客户端订阅了“汽车速度”这一主题。而订阅了“汽车速度”主题的手机和电脑客户端也完全不知道彼此的存在。
大家只要订阅了“汽车速度”主题,MQTT服务端就会在每次收到新信息时,将信息发送给订阅了“汽车速度”主题的客户端。
- 空间可分离
MQTT客户端在通讯时必要条件是连接到了同一个MQTT通讯网络。这个网络可以是互联网或者局域网。只要客户端联网,无论在哪里,都可以实现彼此间的通讯交流。
- 时间可异步
MQTT客户端在发送和接收信息时无需同步。这一特点对物联网设备尤为重要。有时物联网设备会发生意外离线的情况。比如当我们的汽车在行驶过程中,可能会突然进入隧道,这时汽车可能会断开与MQTT服务端的连接。
假设在此时我们的手机客户端向汽车客户端所订阅的“空调温度”主题发布了信息,而汽车恰恰不在线。这时,MQTT服务端可以将“空调温度”主题的新信息保存,待汽车再次上线后,服务端再将“空调温度”信息推送给汽车。
### **4. 会话**
从客户端向服务端发起 MQTT 连接请求开始,到连接中断,直到会话过期为止的消息收发序列称之为会话。
因此,会话可能仅持续一个网络连接,如果客户端能在会话过期之前重新建立了连接的话,会话也可能跨越多个网络连接存在。
**会话状态的使用**
如果客户端因为网络波动等原因导致连接短暂中断,但在会话过期前重新与服务端建立了连接,那么就可以沿用上次连接建立的订阅关系,不需要重新订阅一遍。
在低带宽、不稳定的网络场景下,网络中断可能会发生得很频繁,保存会话状态的方式避免了每次连接都需要重新订阅,降低了重连时客户端和服务端的资源消耗。
服务端在客户端脱机期间为其保留未完成确认的以及后续到达的消息,客户端重新连接时再一并转发,既可以避免消息丢失,也能够降低某些场景下用户对网络变化的感知度。
### **5. QoS(服务质量)**
一个物联网系统中有些信息非常重要,我们需要确保这类重要信息可以准确无误的发送和接收。如果有些信息在传输中丢失但是不会影响系统的运行,则相对不那么重要。
MQTT服务质量(Quality of Service 缩写 QoS)正是用于告知物联网系统,哪些信息是重要信息需要准确无误的传输,而哪些信息不那么重要,即使丢失也没有问题。
MQTT 设计了 3 个 QoS 等级:
- `QoS 0` :消息最多发 1 次,占用的网络资源最低。
发送端一旦发送完消息后,就完成任务了。发送端不会检查发出的消息能否被正确接收到。
在网络环境稳定的情况下,信息传输一般是不会出现问题的。但是在环境不稳定的情况下,可能会在传输过程中出现MQTT消息丢失的情况,所以适用于传输重要性较低的信息。
- `QoS 1` :消息最少发 1 次,但是有可能出现接收端反复接收同一消息的情况。
发送端在消息发送完成后,会检查接收端是否已经成功接收到了消息。
发送端将消息发送给接收端后,会等待接收端的确认。接收端成功接收消息后,会发送一条确认报文`PUBACK`给发送端。如果发送端收到了这条`PUBACK`确认报文,那么它就知道消息已经成功接收。
假如过了一段时间后,发送端没有收到`PUBACK`报文,那么发送端会再次发送消息,然后再次等待接收端的PUBACK确认报文。因此,发送端在没有收到接收端的`PUBACK`确认报文以前,会重复发送同一条消息。
当发送端重复发送一条消息时,PUBLISH报文中的`dupFlag`会被设置为`True`,为了告诉接收端,此消息为重复发送的消息。
![img_4.png](img_4.png)
- QoS 2 :消息保证收 1 次。
MQTT协议可以确保接收端只接收一次消息。但是收发过程相对更加复杂,发送端需要接收端进行两次消息确认。因此,`QoS 2`最安全的服务级别,也是最慢的服务级别。此类服务等级适用于重要消息传输。
![img_5.png](img_5.png)
1. 发送端发送QoS 2的PUBLISH报文给接收端(给你发消息啦)
2. 接收端回复PUBREC确认报文给发送端(我收到啦)
3. 发送端收到PUBREC报文后,会把此报文进行存储,并且返回PUBREL报文作为应答(本次发送可以结束啦)
4. 接收端收到PUBREL报文后,会应答发送端一条PUBCOMP报文(可以,结束吧)
> Tips:由于QoS1和QoS2都能确保客户端接收到消息,但是QoS1所占用的资源较QoS2占用资源更小。因此建议使用QoS1来实现网络资源较为珍贵的环境下传输重要信息。
**服务质量降级**
对于发布和订阅消息的客户端,服务端会主动采用较低级别的QoS来实现消息传输。
场景1:
假如`客户端A`发布到`主题1`的消息是采用`QoS = 2`,然而`客户端B`订阅`主题1`采用`QoS = 1`。
![img_6.png](img_6.png)
在这种情况下,服务端会使用较低级别来提供服务。
虽然`A`发送到`主题1`的消息采用`QoS为2`,但是由于`B`在订阅`主题1`时采用的`QoS为1`,所以`服务端`发送`主题1`的消息给`B`时,采用的`QoS为1`。
场景2:
假如`客户端A`发布`主题1`消息时使用`QoS为0`,而`客户端B`订阅`主题1`消息时使用`QoS为1。
![img_7.png](img_7.png)
虽然`客户端B`订阅`主题1`消息时`QoS为1`,但是由于`客户端A`发送`主题1`消息时`QoS为0`,所以`服务端`发送消息给`B`的`QoS为0`。
### **6. 保留消息**
有个这样的场景:
```text
一套智能家居物联网系统,有一个检测室温的
MQTT客户端,每整点时把当前室温
向MQTT服务端发布。
系统中还有另一个客户端用于显示温度信息,
客户端一启动就会订阅室温主题。
正常情况下:假如上午7:00,室温检测客户端
将最新的室温消息发布到了服务端,那么订阅了
室温消息的显示客户端也就马上获取到室温消息
并且显示在屏幕上。
但是,7点10分的时候,显示客户端的插头被
碰掉了,手动恢复通电后,客户端启动后会立刻
订阅室温主题。
```
问题来了,室温客户端每到整点才发布一次温度信息。上一次发布时间是`7:00`,下一次发布时间是`8:00`。所以,尽管显示客户端订阅了室温主题,它还要等到`8:00`钟才能收到最新室温消息。在`8:00`前的几十分钟里,显示客户端无法获知当前室温信息。
为了避免以上情况出现,可以让室温测量客户端在每次向室温主题发布消息时都使用`保留消息` 这一模式将温度信息发布到服务端。这样无论显示客户端在任何时间订阅室温主题,都会马上收到该主题中的`保留消息` 。
### **7. 心跳机制**
客户端在没有向服务端发送信息时,可以定时向服务端发送一条消息。这条用于心跳机制的消息也被称作心跳请求(PINGREQ)。
心跳请求的作用正是**用于告知服务端,当前客户端依然在线 **。
服务端在收到客户端的心跳请求后,会回复一条消息。这条回复消息被称作心跳响应(PINGRESP)。
**心跳时间间隔**
我们在开发客户端时,可以对其进行设置。
设置好心跳时间间隔后,客户端就知道多久要发送一条心跳请求给服务端。当客户端连接服务端时,会将心跳时间间隔信息放入`CONNECT`报文中的`keepAlive`。
![img_8.png](img_8.png)
图中`keepAlive`数值为`60`。这就意味着,客户端的心跳间隔时间是`60秒`。
- 客户端在心跳间隔时间内,如果有消息发布,那就直接发布消息而不发布心跳请求。
- 客户端没有消息发布,那么它就会发布一条心跳请求给服务端。
**客户端掉线**
另外,在实际运行中,如果服务端没有在`1.5倍心跳时间间隔内`收到客户端发布消息(PUBLISH)或发来心跳请求(PINGREQ),那么服务端就会认为这个`客户端已经掉线`。
小结一下,心跳机制可以让服务端随时掌握客户端连接情况。当客户端“心跳”正常时,服务端即知道客户端仍然`在线(活着)`。当心跳一旦停止,服务端就会发现该客户端已经`断线(死亡)`。
### **8. 遗嘱机制**
为了让客户端可以更好的发挥作用,便于服务端管理,MQTT 协议允许客户端在“活着”的时候就写好遗嘱,这样一旦客户端`意外断线` ,服务端就可以将客户端的遗嘱公之于众。
> 注意:客户端的遗嘱只在意外断线时才会发布,如果客户端正常的断开了与服务端的连接,这个遗嘱机制是不会启动的,服务端也不会将客户端的遗嘱公布。
意外断线包括但不限于:
- 因网络故障或网络波动,设备在保持连接周期内未能通讯,连接被服务端关闭
- 设备意外掉电
- 设备尝试进行不被允许的操作而被服务端关闭连接,例如订阅自身权限以外的主题等
### **9. 数据包结构**
整体由3个部分组成:
- `固定报头 Fixed header` :存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识;
- `可变报头 Variable header` :存在于部分MQTT数据包中,数据包类型决定了可变头是否
整体 MQTT 的消息格式如下图所示:
![img_9.png](img_9.png)
数据包里目前不进行展开,后面进一步需要接触后,另起篇幅介绍。