一、MQTT 通信基本原理
MQTT
是一种基于
客户端
-
服务端
架构的消息传输协议,所以在
MQTT
协议通信中,有两个最为重要的角色,它们便是服务端
和
客户端
。
举例:若开发板向“芯片温度”这一主题发布消息,那么服务端接收到消息后会将消息转发给订阅了“芯片温度”的所有客户端。
服务端
MQTT
服务端通常是一台服务器(
broker
),它是
MQTT
信息传输的枢纽,负责将
MQTT
客户端发送来的信息传递给 MQTT
客户端;
MQTT
服务端还负责管理
MQTT
客户端,以确保客户端之间的通讯顺畅, 保证 MQTT
信息得以正确接收和准确投递。
客户端
MQTT
客户端可以向服务端发布信息,也可以从服务端收取信息;我们把客户端发送信息的行为称为 “发布”信息。而客户端要想从服务端收取信息,则首先要向服务端“订阅”信息。“订阅”信息这一操作很像我们在使用微信时“关注”了某个公众号,当公众号的作者发布新的文章时,微信官方会向关注了该公众号的所有用户发送信息,告诉他们有新文章更新了,以便用户查看。
MQTT 主题
客户端想要从服务器获取信息,首先需要订阅信息,那客户端如何订阅信息呢?这里我们要引入“主题(Topic
)”的概念,“主题”在
MQTT
通信中是一个非常重要的概念,客户端发布信息以及订阅信息都是围绕“主题”来进行的,并且 MQTT
服务端在管理
MQTT
信息时,也是使用“主题”来控制的。
客户端发布消息时需要为消息指定一个“主题”,表示将消息发布到该主题;而对于订阅消息的客户端来说,可通过订阅“主题”来订阅消息,这样当其它客户端或自己(当前客户端)向该主题发布消息时,MQTT 服务端就会将该主题的信息发送给该主题的订阅者(客户端)。
值得注意的是,
MQTT
客户端在通信时,角色往往不是单一的,一个客户端既可以作为信息发布者也可以同时作为信息订阅者。如下图所示:
MQTT 发布/订阅特性
MQTT
通信的核心枢纽是
MQTT
服务端,它负责将
MQTT
客户端发送来的 信息传递给 MQTT
客户端,还负责管理
MQTT
客户端,以确保客户端之间的通讯顺畅,保证
MQTT
信息得以正确接收和准确投递。
⚫
客户端相互独立:
MQTT
客户端是一个个独立的个体,它们无需了解彼此的存在,依然可以实现 信息交流。
⚫
时间上可异步:
MQTT
客户端在发送和接收信息时无需同步。这一特点对物联网设备尤为重要, 前面我们也介绍了,MQTT
从诞生之初就是专为低带宽、高延迟或不可靠的网络而设计的,高延 迟和不可靠网络必然就会导致时间上的异步;物联网设备在运行过程中发生意外掉线是非常正常 的情况,我们使用上面的实例二的场景来作说明,当开发板在运行过程中,可能会由于突然断电 (假设开发板是通过电源适配器供电的)导致掉线,这时开发板会断开与 MQTT
服务端的连接。 假设此时我们的手机客户端向开发板客户端所订阅的“LED
控制”主题发布了信息,而开发板恰 恰不在线,这时,MQTT
服务端可以将“
LED
控制”主题的新信息保存,待开发板客户端再次上线后,服务端再将“LED
控制”信息推送给开发板。所以这就必然导致了,手机发送信息与开发板接收信息在时间上是异步的。
二、连接MQTT服务端
MQTT
客户端之间想要实现通信,必须要通过
MQTT
服务端。所以,客户端无论是发布信息还是订阅信息都必须先连接到服务端。下面我们来看一下,客户端连接服务端的详细过程。
MQTT 客户端连接服务端总共包含了两个步骤:
①、首先客户端需要向服务端发送连接请求,这个连接请求实际上就是向服务端发送一个
CONNECT报文,也就是发送了一个 CONNECT
数据包。
②、
MQTT
服务端收到连接请求后,会向客户端发送连接确认。连接确认实际上是向客户端发送一个CONNACK 报文,也就是
CONNACK
数据包。
CONNECT 报文
在上面的描述中我们看到,
MQTT
客户端要想连接服务端,首先要向服务端发送
CONNECT
报文。如 果此 CONNECT
报文的格式或内容不符合
MQTT
规范,则服务器会拒绝客户端的连接请求。
一个CONNECK报文内容举例如下:
所谓报文就是一个数据包,
MQTT
报文组成分为三个部分:固定头(
Fixed header
)、可变头(
Variable header)以及有效载荷(
Payload
,消息体)。这里我们简单地介绍一下:
⚫
固定头(
Fixed header
):
存在于所有
MQTT
报文中,固定头中有报文类型标识,可用于识别是哪 种 MQTT
报文,譬如该报文是
CONNECT
报文还是
CONNACK
报文,亦或是其它类型报文。
⚫
可变头(
Variable header
):
存在于部分类型的
MQTT
报文中,报文的类型决定了可变头是否存 在及其具体的内容。
⚫
消息体(
Payload
):
存在于部分类型的
MQTT
报文中,
payload
就是消息载体的意思。
在上图中的报文内容有:
clientId--
客户端
id
clientId
是
MQTT
客户端的标识,也就是
MQTT
客户端的名字,
MQTT
服务端可通过
clientId
来区分不同的客户端,MQTT
服务端用该标识来识别客户端。
keepAlive--
心跳时间间隔
keepAlive 其实是指定了心跳时间间隔,也就是客户端向服务端发送心跳包的时间间隔。譬如 keepAlive=60
,表示告诉服务端,客户端将会每隔 60
秒左右向服务端发送心跳包。
cleanSession--
清除会话
cleanSession
设置为
1
,表示此次连接将创建一个新的临时会话,在客户端断开后,这个会话会自动销毁。而 cleanSession
设置为
0
,表示创建一个持久性会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。
CONNACK 报文
returnCode--
连接返回码
当服务端收到了客户端的连接请求后,会向客户端发送
returnCode(
连接返回码
)
,用来说明连接情况。 如果客户端与服务端成功连接,则返回数字“0
”。如果未能成功连接,返回码将会是一个非零的数字。
sessionPresent
CONNACK
报文的
sessionPresent
与
CONNECT
报文的
cleanSession
相互配合。其作用是客户端发送连接请求时,服务端告知客户端有没有保存会话状态。这个被服务端保存的会话状态是来自于上一 次客户端连接时,譬如离线消息以及上一次连接时客户端所订阅的主题。
三、断开连接
当
MQTT
客户端连接到服务端之后,在后续的通信过程中,如果客户端想要断开与服务端的连接,此 时客户端可以主动向服务端发送一个 DISCONNECT
报文来断开与服务端的连接,如下图所示:
四、发布消息、订阅主题与取消订阅主题
当客户端连接到服务端之后,便可以发布消息或订阅主题了。
PUBLISH–发布消息
当客户端连接到服务端之后,就可以向服务端发布消息了,每条发布的消息必须指定一个“主题”,表示向某主题发布消息;MQTT
服务端可以通过主题来确定将消息转发给哪些客户端(订阅了该主题的客户端)。
MQTT 客户端向服务端发布消息其实就是向服务端发送一个 PUBLISH
报文,服务端收到客户端发送过来的 PUBLISH
报文之后,会向发送方回复一个报文。根据
QoS
的不同,回复的报文类型也是不同的,并且 整个发布消息的过程也将会有所区别;譬如对于 QoS=1
时,客户端向服务端发送
PUBLISH
报文,服务端 收到 PUBLISH
报文之后会向发送方回复
PUBACK
报文;而对于
QoS=2
的情况,将会更加复杂。
下图是
PUBLISH
报文包含的信息:
packetId--
报文标识符
报文标识符可用于对
MQTT
报文进行标识(识别不同的报文)。不同的
MQTT
报文所拥有的标识符不同。MQTT
设备可以通过该标识符对
MQTT
报文进行甄别和管理,
MQTT
协议内部使用的标识符。请注意:
报文标识符的内容与
QoS
级别有密不可分的关系。只有
QoS
级别大于
0
时,报文标识符才是非零数值。如果 QoS
等于
0
,报文标识符为
0
topicName--
主题名字
这个就是发布消息时对应的主题的名字,这是一个字符串,譬如上图中
topicName=
“
myTopic
”,表示会将消息发布到“myTopic
”这个主题。
payload--
有效载荷
有效载荷是我们希望通过
MQTT
所发送的实际内容。我们可以使用
MQTT
协议发送字符串文本,图像等格式的内容。这些内容都是通过有效载荷所发送的。
qos--
服务质量等级
QoS
(
Quality of Service
)表示
MQTT
消息的服务质量等级。
QoS
有三个级别:
0
、
1
和
2
,
QoS
决定 MQTT 通信有什么样的服务保证。
retain--
保留标志
在默认情况下,当客户端订阅了某一主题后,并不会马上接收到该主题的信息。因为客户端订阅该主题 之后,并没有其它客户端向该主题发布消息;只有在客户端订阅该主题后,服务端接收到该主题的新消息时,服务端才会将最新接收到的该主题消息推送给客户端。
但是在有些情况下,我们需要客户端在订阅了某一主题后马上接收到一条该主题的信息。这时候就需要用到保留标志这一信息。
dup--
重发标志
dup
标志指示此消息是否重复。
当
MQTT
报文的接收方没有及时向报文发送发回复
确认收到报文
时,发送方会以为对方没有收到信息,会再次重复发送 MQTT
报文(譬如客户端向服务端发送
PUBLISH
报文,服务端收到
PUBLISH
报文之后需要向客户端回复一个 PUBACK
报文,如果客户端没收到
PUBACK
报文,则会认为服务端可能没接收到自己发送的报文,将会再次发送 PUBLISH
报文)。在重复发送
MQTT 报文时,发送方会将此“dup--重发标志”设置为 true
。请注意,重发标志只在
QoS
级别大于
0
时使用。
SUBSCRIBE--订阅主题
客户端要想接收消息,首先要订阅该消息的主题。这样,当有客户端向该主题发布消息后,订阅了该主题的客户端就能接收到消息了。
客户端要想订阅主题,首先要向服务端发送主题订阅请求。客户端是通过向服务端发送
SUBSCRIBE
报文来实现这一请求的。该报文包含有一系列“订阅主题名”。请留意,一个 SUBSCRIBE
报文可以包含有单个或者多个订阅主题名。也就是说,一个 SUBSCRIBE 报文可以用于订阅一个或者多个主题。服务端会根据
SUBSCRIBE
中的
QoS
来提供相应的服务保证。
SUBACK
报文包含有“订阅返回码”和“报文标识符”这两个信息。
returnCode--
订阅返回码
客户端向服务端发送订阅请求后,服务端会给客户端返回一个订阅返回码。在之前的讲解中我们说过,客户端可通过一个 SUBSCRIBE
报文发送多个主题的订阅请求。服务端会针对 SUBSCRIBE
报文中的所有订阅主题来逐一回复给客户端一个返回码。这个返回码的作用是告知客户端是否成功订阅了主题。
UNSUBSCRIBE--取消订阅主题
客户端订阅了某一主题之后,可以随时取消订阅,
MQTT
协议提供了这样的操作。
客户端通过向服务端发送一个
UNSUBSCRIBE
报文来取消订阅主题,当服务端接收到
UNSUBSCRIBE报文后,会向发送发回复一个 UNSUBACK
报文(取消订阅确认报文),如下图所示:
五、主题的进阶
主题的基本形式就是一个字符串,譬如:
"myTopic"
、
"currentTemp"
、
"LEDControl"等,但是有几个点需要大家注意一下:
1、主题形式
⚫
主题是区分大小写的。
所以
"LEDControl"
和
"ledControl"
是两个不同的主题。
⚫
主题可以使用空格。
譬如
"LED Control"
,虽然主题允许使用空格,但是笔者建议大家尽量不要使用空格。
⚫
不要使用中文主题。
虽然有些
MQTT
服务器支持中文主题,但是绝大部分
MQTT
服务器是不支持中文主题的,所以大家不要使用中文主题,而是使用 ASCII
字符来作为
MQTT
主题。
2、主题分级
MQTT
主题可以是一个简单的字符串,譬如:
"myTopic"
、
"currentTemp"
、
"LEDControl"
,事实上,
MQTT协议为了更好的对主题进行管理和分类,支持主题分级,对主题进行分级处理,各个级别之间使用" / "
符号进行分隔。如下所示:
"home/sensor/led/brightness"
在以上示例中一共有四级主题,分别是第
1
级
home
、第
2
级
sensor
、第三级
led
、第
4
级
brightness
。 主题的每一级至少需要一个字符;而只有一个简单字符串的主题,如"myTopic"
、
"currentTemp"、 "LEDControl",这些都是单一级别的主题。需要注意的是,主题名称不要使用
" / "
开头。
3、主题通配符
当客户端订阅主题时,可以使用通配符同时订阅多个主题。通配符只能在订阅主题时使用,分为单级通配符和多级通配符。
4、主题应用注意事项
以
$
开头的主题
以
$
号开头的主题是
MQTT
服务端系统保留的特殊主题,客户端不可随意订阅或向其发布信息
不要使用“
/
”作为主题开头
尽量不要使用“
/
”作为主题的开头,这样做没有什么意义,而且额外产生一个没有用处的主题级别。
主题中不要使用空格
虽然,
MQTT
支持在主题中使用空格,但是我们应该尽量避免使用空格。
保持主题简洁明了
MQTT
是一种轻量级的通讯协议,它常用于网络带宽受限的环境,因此我们应尽量让主题简洁明了,从而让设备间交互的内容更加简洁,以更好的适应网络带宽受限的环境。
主题中尽量使用
ASCII
字符
虽然有些
MQTT
设备支持
UTF-8
字符作为
MQTT
主题,建议在主题中尽量使用
ASCII
字符。