【网络】MQTT协议基础

【网络】MQTT协议基础

本文介绍了mqtt协议相关内容,背景知识和协议规范

背景

MQTT是一个客户端服务端架构的发布/订阅模式的消息传输协议。它的设计思想是轻巧、开放、简单、规范,易于实现。这些特点使得它对很多场景来说都是很好的选择,特别是对于受限的环境如机器与机器的通信(M2M)以及物联网环境(IoT)。 ——MQTT协议规范中文版

MQTT协议最初版本是在1999年建立的。该协议的发明人是的Andy Stanford-Clark和Arlen Nipper。

MQTT 的诞生,是在一个利用卫星通讯监控输油管道的项目。

所以它最初是面对 低带宽、高延迟或不可靠的网络 而设计的。虽然历经几十年的更新和变化,以上这些特点仍然是MQTT协议的核心特点。但是与最初不同的是,MQTT协议已经从嵌入式系统应用拓展到开放的物联网(IoT)领域。

版本

MQTT协议有三个版本,分别是:

  • MQTT v3.1
  • MQTT v3.1.1
  • MQTT v5.0

目前客户端很多主流的MQTT库都是基于MQTT v3.1.1版本。而且后续的5.0是完全兼容3.1.1版本的,所以优先学习这个3.1.1版本。

基本通信介绍

和其他网络通信流程一致,MQTT通信中的角色也是分为服务端和客户端。

cs

  • 服务端是MQTT信息传输的枢纽,负责将MQTT客户端发送来的信息传递给MQTT客户端。MQTT服务端还负责管理MQTT客户端。确保客户端之间的通讯顺畅,保证MQTT消息得以正确接收和准确投递。
  • 客户端可以往服务端推送消息和获取消息。

MQTT协议的通信架构是独特的 订阅/发布机制 ,不像HTTP那种 请求/响应机制 ,从服务端获取数据需要客户端主动发起请求,服务端才能返回数据。

MQTT的通信机制更像是Android里的广播,客户端可以订阅(Subscribe)某个主题,当服务端发现这个主题被某一个客户端发布(Publish)更新时,就会将消息推送给订阅了该主题的客户端。

订阅发布机制

MQTT服务端在管理MQTT信息通讯时,就是使用一个个的“主题”来控制的。

比如车载云平台开发时,就会有一个 车辆状态 的主题,当车辆状态发生变化时,就会往这个 车辆状态 主题发送消息。具体的,当汽车的胎压传感器有数据上报时,就会往“胎压传感器”这个主题发送消息。手机客户端,订阅了这个胎压主题,就可以收到胎压传感器上报到服务器,而后分发下来的消息。

mqtt

同时,手机端可以往 座椅加热 这个主题发布消息,打开开关。服务器收到消息后,就会下发到订阅这个主题的汽车客户端,车辆控制器收到后就可以远程打开座椅加热。在冬天,司机上车之前就可以提前打开座椅加热,上车之后就不会被冻了。

mqtt

订阅发布的特性

从以上实例我们可以看到,MQTT通讯的核心枢纽是MQTT服务端。有了服务端对MQTT信息的接收、储存、处理和发送,客户端在发布和订阅信息时,可以相互独立,且在空间上可以分离,时间上可以异步。

相互可独立

MQTT客户端是一个个独立的个体。它们无需了解彼此的存在,依然可以实现信息交流。客户端本身可以完全不知道有多少个MQTT客户端订阅了这一主题。而订阅了同一主题的客户端也完全不知道彼此的存在。大家只要订阅了同一个主题,MQTT服务端就会在每次收到新信息时,将信息发送给订阅的客户端。

空间可分离

空间分离相对容易理解,MQTT客户端在通讯必要条件是连接到了同一个MQTT通讯网络。这个网络可以是互联网或者局域网。只要客户端联网,无论他们远在天边还是近在眼前,都可以实现彼此间的通讯交流。

时间可异步

MQTT客户端在发送和接收信息时无需同步。这一特点对物联网设备尤为重要。有时物联网设备会发生意外离线的情况。当我们的汽车在行驶过程中,可能会突然进入隧道,这时汽车可能会断开与MQTT服务端的连接。假设在此时我们的手机客户端向汽车客户端所订阅的“空调温度”主题发布了信息,而汽车恰恰不在线。这时,MQTT服务端可以将“空调温度”主题的新信息保存,待汽车再次上线后,服务端再将“空调温度”信息推送给汽车。

主题Topic

MQTT协议中,消息的传输是通过主题来实现的。主题是一个字符串,用来标识消息的类型。当客户端订阅了一个主题时,服务端会将该主题的信息发送给客户端。当客户端发布了一个主题时,服务端会将该主题的信息发送给所有订阅了该主题的客户端。

主题格式

MQTT主题是一个字符串,用来标识消息的类型。主题可以包含多个单词,单词之间使用斜杠(/)分隔。同时,主题也可以分级,例如:

  • motor_speed
  • motor_speed/left
  • motor_speed/right

这些主题可以用来标识不同类型的消息。例如,“motor_speed”主题可以用来标识电机的速度信息,而“motor_speed/left”主题可以用来标识左电机的速度信息。

定义主题时,需要注意以下几点:

  • 主题是区分大小写的。如上列表中的主题 motor_speed和Motor_speed是两个完全不同的主题。
  • 主题可以使用空格 ,如以上列表中的current time,虽然有空格分隔current和time这两个词,但这实际是一个MQTT主题。不过,虽然我们可以使用空格,但是笔者强烈建议您不要在主题中使用空格。我们在开发时一不小心,可能就会漏掉空格,这将造成不必要的麻烦。
  • 大部分MQTT服务端是 不支持中文主题 的,所以我们应使用英文字符或ASCII字符来作为MQTT主题。

主题通配符

当客户端订阅主题时,可以使用通配符同时订阅多个主题。通配符只能在订阅主题时使用,下面我们将介绍两种通配符:单级通配符和多级通配符。

单级通配符: +

顾名思义,单级通配符可以代替一个主题级别。 以下为含有单极通配符的主题示例。

home/sensor/+/temperature

当客户端订阅了以上主题后,它将会收到以下主题的信息内容:

home/sensor/kitchen/temperature
home/sensor/bedroom/temperature

我们可以看到,在home后面的级别中,由于客户端订阅的主题使用了+ 单级通配符,因此无论home级别后面的内容是什么,客户端都能收到这些主题的信息。

相反,客户端将无法收到以下主题的信息。

home/sensor/bedroom/brightness
office/sensor/bedroom//temperature
home/screen/livingroom/temperature

以上主题的红色部分都是客户端无法收到信息的原因。这些红色的部分都是与客户端订阅的主题“home/sensor/+/temperature”不相符的部分。

多级通配符 #

单级通配符仅可代替一个主题级别,而多级通配符”#”可以涵盖任意数量的主题级别。如下示例所示, 多级通配符必须是主题中的最后一个字符。

home/sensor/#

当客户端订阅了以上含有”#”的主题后,可以收到以下主题的信息。

home/sensor/kitchen/temperature
home/sensor/bedroom/brightness
home/sensor/data

多级通配符可以代替多级主题信息,因此无论”home/sensor”后面有一级还是多级主题,都可以被订阅了”home/sensor/#”的客户端接收到。

注意事项

以$开始的主题

$ 开始的主题是 MQTT服务端系统保留的特殊主题 ,我们不能随意订阅或者向其发布信息。以下是此类主题的示例:

$SYS/broker/clients/connected
$SYS/broker/clients/disconnected
$SYS/broker/clients/total
$SYS/broker/messages/sent
$SYS/broker/uptime

类似的主题还有很多。不过请记住一点,以$符号开头的主题是系统保留的特殊主题,我们不能随意订阅或者向其发布信息

不要用 “/” 作为主题开头

MQTT允许使用“/”作为主题的开头,例如/home/sensor/data。但是这将这么做毫无意义,而且会额外产生一个没有用处的主题级别。所以我们应避免使用/作为主题的开头。

连接服务器

分为两步,第一步是客户端发送连接请求,第二步是服务端发送连接确认。

一、客户端连接请求

首先MQTT客户端将会向服务端发送连接请求。该请求实际上是一个包含有连接请求信息的数据包。这个数据包的官方名称为CONNECT。

connect

有三个必选参数:

  • 客户端ID – 客户端的唯一标识。
  • cleansession – 是否清除该客户端之前的会话。
  • keepalive – 心跳时间,用于确认连接状态。

clientId

ClientId 是MQTT客户端的标识。MQTT服务端用该标识来识别客户端。因此ClientId必须是独立的。如果两个MQTT客户端使用相同ClientId标识,服务端会把它们当成同一个客户端来处理。

通常ClientId是由一串字符所构成的,如上图所示,此示例中的clientID是“client-1”。在车载端,clientID是由车载设备的唯一标识所构成的,通常为内部项目号加上车架号,也就是俗称的 VIN 码。

cleanSession

如果 cleanSession 被设置为“true”。那么服务端不需要客户端确认收到报文,也不会保存任何报文。在这种情况下,即使客户端错过了服务端发来的报文,也没办法让服务端再次发送报文。

cleanSession 设置为”false”。那么服务端就知道,后续通讯中,客户端可能会要求我保存没有收到的报文。

如果某个客户端用于收发非常重要的信息(比如前文示例中汽车自动驾驶系统),那么该客户端在连接服务端时,应该将 cleanSession 设置为 false 。这样才能让服务端保存那些没有得到客户端接收确认的信息。以便服务端再次尝试将这些重要信息再次发送给客户端。请注意,如果需要服务端保存重要报文,光设置cleanSession 为false是不够的,还需要传递的MQTT信息QoS级别大于0。

相反的,如果某个客户端用于收发不重要的信息(比如前文示例中车载音乐系统)那么该客户端在连接服务端时,应该将cleanSession设置为 true

keepalive

keepalive是一个心跳时间。该时间用来检测客户端是否在线。如果客户端超过所设置的keepalive时间后,仍然没有收到服务端的任何报文,那么服务端就会认为客户端已经掉线。此时服务端会断开与客户端的连接。

其他参数

除了这三个必选的参数,同时还有几个可选参数

  • 用户名 – 连接到MQTT服务端的用户名。
  • 密码 – 连接到MQTT服务端的密码。
  • 遗嘱消息 – 当客户端意外断开连接时,发布遗嘱消息。
  • 遗嘱主题 – 当客户端意外断开连接时,会像该遗嘱主题发布遗嘱消息。
  • 遗嘱QoS – 当客户端意外断开连接时,遗嘱消息的服务质量。

用户密码认证

username(用户名)和password(密码)是可选的CONNECT信息。也就是说,有些服务端开启了客户端用户密码认证,这种服务端需要客户端在连接时正确提供认证信息才能连接。当然,那些没有开启用户密码认证的服务端无需客户端提供用户名和密码认证信息。

有些公用MQTT服务端也利用此信息来识别客户端属于哪一个用户,从而对客户端进行管理。比如用户可以拥有私人主题,这些主题只有该用户可以发布和订阅。对于私人主题,服务端就可以利用客户端连接时的用户名和密码来判断该客户端是否有发布订阅该用户私人主题的权限。

二、服务端连接确认

MQTT服务端收到客户端连接请求后,会向客户端发送连接确认。同样的,该确认也是一个数据包。这个数据包官方名称为CONNACK。

connack

returnCode

这里可以类比HTTP的状态码。当服务端收到了客户端的连接请求后,会向客户端发送returnCode(连接返回码),用以说明连接情况。如果客户端与服务端成功连接,则返回数字 “0” 。如果未能成功连接,连接返回码将会是一个非零的数值,具体这个数值的含义如下:

  • 0——成功连接
  • 1——连接被服务端拒绝,原因是不支持客户端的MOTT协议版本
  • 2——连接被服务端拒绝,原因是不支持客户端标识符的编码可能造成此原因的是客户端标识符编码是UTF-8,但是服务端不允许使用此编码,
  • 3——连接被服务端拒绝,原因是服务端不可用。即,网络连接已经建立,但MQTT服务不可用。
  • 4——连接被服务端拒绝,原因是用户名或密码无效。
  • 5——连接被服务端拒绝,原因是客户端未被授权连接到此服务。

sessionPresent

sessionPresent是一个布尔值。如果该值为“true”,则表示服务端已经保存了与客户端的会话。如果该值为“false”,则表示服务端没有保存与客户端的会话。

当重要客户端连接服务端时, 服务端可能保存着没有得到确认的报文 。如果是这样的话,那么客户端在连接服务端时,就会通过sessionPresent来了解服务端是否有之前未能确认的信息。

QoS服务质量

MQTT协议有三种服务质量级别:

QoS = 0 – 最多发一次 QoS = 1 – 最少发一次 QoS = 2 – 保证收一次

以上三种不同的服务质量级别意味着不同的MQTT传输流程。对于较为重要的MQTT消息,我们通常会选择QoS>0的服务级别(即QoS 为1或2)。

QoS=0

0是服务质量QoS的最低级别。当QoS为0级时,MQTT协议并不保证所有信息都能得以传输。也就是说,QoS=0的情况下,MQTT服务端和客户端不会对消息传输是否成功进行确认和检查。消息能否成功传输全看网络环境是否稳定。

也就是说,在QoS为0时。发送端一旦发送完消息后,就完成任务了。发送端不会检查发出的消息能否被正确接收到。

QoS=1

QoS=1是服务质量QoS的中间级别。当QoS为1级时,MQTT协议会保证消息至少被传输一次。

当QoS=1时,MQTT服务端和客户端会对消息传输进行确认。也就是说,当MQTT服务端收到了客户端的消息后,会向客户端发送一个PUBACK(发布确认)报文。客户端收到PUBACK报文后,就知道消息已经被服务端成功接收。 保证收到一次消息的兜底机制,救赎客户端发送后会启动一个计时器,当倒计时结束还没有收到服务端反馈时,就再次发送。所以QoS为1时,有重复发送的可能。

QoS=2

MQTT服务质量最高级是2级,即QoS = 2。当MQTT服务质量为2级时,MQTT协议可以确保接收端只接收一次消息。这个最高质量的通信,是通过 两阶段确认,共四次握手 来实现的。

流程:

  1. 发送端发送QoS=2的 PUBLISH 报文给接收端,这条报文会附带一个 packetId 数据包标识符,用于识别单次通信并作为去重的依据;
  2. 接收端收到QoS为2的消息后,会把此报文进行临时存储,并立即返回 PUBREC (Publish Received) 报文作为应答。后面如果再次收到同一个 packetId 的消息时,会主动去重;
  3. 发送端收到接收端反馈的 PUBREC 报文后,可以确定接收方已经收到了第一次的消息,就不会再重复发送PUBLISH消息。然后,发送端会再次发送一条 PUBREL(Publish Release) 释放报文作为应答;
  4. 当接收端收到PUBREL报文后,会应答发送端一条 PUBCOMP(Publish Complete) 报文。至此,一次QoS=2的MQTT消息传输就结束了。

发布,订阅,取消订阅

  • PUBLISH – 发布信息
  • SUBSCRIBE – 订阅主题
  • SUBACK – 订阅确认
  • UNSUBSCRIBE – 取消订阅

一、PUBLISH发布信息

MQTT客户端可以向MQTT服务端发布信息。发布信息的数据包的官方名称为PUBLISH。

publish

MQTT 中的 PUBLISH 消息具有多个决定其行为的属性,包括数据包标识符、主题名称、服务质量、保留标志、有效负载和 DUP 标志。让我们逐一了解一下。

topicName – 主题名

主题名用于识别此信息应发布到哪一个主题。

qos – 服务质量

QoS(Quality of Service)表示MQTT消息的服务质量等级。QoS有三个级别:0、1和2。QoS决定MQTT通讯有什么样的服务保证。

packetId – 数据包标识符

报文标识符可用于对MQTT报文进行标识。不同的MQTT报文所拥有的标识符不同。MQTT设备可以通过该标识符对MQTT报文进行甄别和管理。请注意:报文标识符的内容与QoS级别有密不可分的关系。只有QoS级别大于0时,报文标识符才是非零数值。如果QoS等于0,报文标识符为0。

retainFlag – 保留标志

在默认情况下,当客户端订阅了某一主题后,并不会马上接收到该主题的信息。只有在客户端订阅该主题后,服务端接收到该主题的新信息时,服务端才会将最新接收到的该主题信息推送给客户端。

但是在有些情况下,我们需要客户端在订阅了某一主题后马上接收到一条该主题的信息。这时候就需要用到保留标志这一信息。

在需要发布保留消息时,MQTT设备需要将PUBLISH报文中retainFlag设置为true(如上图所示)。

当然,如果要发布非保留消息,那么PUBLISH报文中retainFlag设置为false。

更新保留消息

每一个主题只能有一个“保留消息”,如果客户端想要更新“保留消息”,就需要向该主题发送一条新的“保留消息”,这样服务端会将新的“保留消息”覆盖旧的“保留消息”。当有客户端订阅该主题时,服务端就会将最新的“保留消息”发送给订阅客户端了。

删除保留消息的方法

如果要删除主题的“保留消息”,可以通过向该主题发布一条空的“保留消息”,也就是发送一条0字节payload的“保留消息”

payload – 有效负载

有效載荷是我们希望通过MQTT所发送的实际内容。我们可以使用MQTT协议发送文本,图像等格式的内容。这些内容都是通过有效載荷所发送的。

dupFlag – 重复标志

当MQTT报文的接收方没有及时确认收到报文时,发送方会重复发送MQTT报文。在重复发送MQTT报文时,发送方会将此“重发标志”设置为true。请注意,重发标志只在QoS级别大于0时使用。

二、SUBSCRIBE订阅主题

客户端要想订阅主题,首先要向服务端发送主题订阅请求。客户端是通过向服务端发送SUBSCRIBE报文来实现这一请求的。

subscribe

该报文包含有一系列“订阅主题名”。请留意,一个SUBSCRIBE报文可以包含 有单个或者多个 订阅主题名。可以同时订阅一个或者多个主题。

在以上PUBLISH报文讲解中,我们曾经提到过QoS(服务质量等级)这一概念。同样的,客户端在订阅主题时也可以明确QoS。服务端会根据SUBSCRIBE中的QoS来提供相应的服务保证。

另外每一个SUBSCRIBE报文同样包含有“报文标识符”。

三、SUBACK订阅确认

服务端接收到客户端的订阅报文后,会向客户端发送SUBACK报文确认订阅。SUBACK报文包含有“订阅返回码”和“报文标识符”这两个信息。

suback

客户端可通过一个SUBSCRIBE报文发送多个主题的订阅请求。服务端会针对SUBSCRIBE报文中的所有订阅主题来 逐一回复给客户端一个返回码

返回码含义:

返回码Return Code Response
0订阅成功 – QoS 0
1订阅成功- QoS 1
2订阅成功- QoS 2
128订阅失败

四、UNSUBSCRIBE取消订阅

当客户端要取消订阅某主题时,可通过向服务端发送UNSUBSCRIBE – 取消订阅报文来实现。

unsubscribe

UNSUBSCRIBE报文包含两个重要信息,第一个是取消订阅的主题名称。同一个UNSUBSCRIBE报文可以同时包含多个取消订阅的主题名称。另外,UNSUBSCRIBE报文也包含“报文标识符”,MQTT设备可以通过该标识符对报文进行管理。

当服务端接收到UNSUBSCRIBE报文后,会向客户端发送取消订阅确认报文 – UNSUBACK报文。该报文含有客户端所发送的“取消订阅报文标识符”。

客户端接收到UNSUBACK报文后就可以确认取消主题订阅已经成功完成了。

心跳机制

客户端并不经常发送消息给服务端。对于这种客户端,服务端可以使用类似心跳检测的方法,来判断客户端是否在线。

这个心跳机制不仅可以用于服务端判断客户端是否保持连接,也可以用于客户端判断自己与服务端是否保持连接。如果客户端在发送心跳请求(PINGREQ)后,没有收到服务端的心跳响应(PINGRESP),那么客户端就会认为自己与服务端的连接已经被断开了。

keepalive

客户端发送心跳请求的时间间隔就是连接时CONNECT报文中keepalive的值。

双方对齐了心跳间隔之后,不是每次都必须要发送消息才可以确认连接在线。

如果客户端在心跳时间间隔内 发布了业务消息给服务端 ,那么服务端不需要客户端发送心跳请求也可以确定该客户端肯定在线。

当客户端在心跳间隔内没有新消息发送给服务端,这时客户端才会主动发送一个心跳请求消息给服务端。以表明自己仍让在线。

在实际运行中,如果服务端没有在1.5倍心跳时间间隔内收到客户端发布消息(PUBLISH)或发来心跳请求(PINGREQ),那么服务端就会认为这个客户端已经掉线。

遗嘱消息

在CONNECT报文里,同样有一条遗嘱消息。客户端可以提前设置好一条遗嘱消息,当客户端异常掉线之后,由服务端将这条遗嘱消息发送给其他订阅了这条遗嘱主题的客户端。如果是正常断开,则不会发送。

正常断联

当客户端正常断开连接时,会向服务端发送DISCONNECT报文,服务端接收到该报文后,就知道,客户端是正常断开连接,而并非意外断开连接。

意外断联

然而,当服务端在没有收到DISCONNECT报文的情况下,发现客户端“心跳”停止了,这时服务端就知道客户端是意外断线了。

回顾一下CONNNECT报文:

connect

有关遗嘱消息的参数含义如下:

lastWillTopic – 遗嘱主题

遗嘱消息也有主题和正文内容。lastWillTopic的作用正是告知服务端,本客户端的遗嘱主题是什么。只有那些订阅了这一遗嘱主题的客户端才会收到本客户端的遗嘱消息。

lastWillMessage – 遗嘱消息

遗嘱消息定义了遗嘱消息内容。在本示例中,那些订阅了主题”hans/will”的客户端会在客户端意外断线时,收到服务端发布的“unexpected exit”。

lastWillQoS – 遗嘱QoS

对于遗嘱消息来说,同样可以使用服务质量来控制遗嘱消息的传递和接收。这里的服务质量与普通MQTT消息的服务质量是一样的概念。

lastWillRetain – 遗嘱保留

遗嘱消息也可以设置为保留消息,服务端会根据此处内容,对遗嘱消息进行相应的保留与否处理