本章将从MQTT协议连接的建立开始,逐一讲解MQTT 3.1.1的架构和通信细节。
Client在可以发布和订阅消息之前,必须先连接到Broker。Client建立到Broker的连接的流程如图4-1所示。
图4-1 Client建立到Broker的连接的流程
1)Client向Broker发送一个CONNECT数据包。
2)Broker在收到Client的CONNECT数据包后,如果允许Client接入,则回复一个CONNACK包,该CONNACK包的返回码为0,表示MQTT协议连接建立成功;如果不允许Client接入,也回复一个CONNACK包,该CONNACK包的返回码为一个非0的值,用来标识接入失败的原因,然后断开底层的TCP连接。
CONNECT数据包的格式如下。
CONNECT数据包的固定头格式如图4-2所示。
图4-2 CONNECT数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为1,代表CONNECT数据包。
可变头由4部分组成,依次为:协议名称、协议版本、连接标识和Keep Alive。
1)协议名称是一个UTF-8编码字符串。在MQTT协议数据包中,字符串会有2个字节的前缀,用于标识字符串的长度。
协议名称的值固定为“MQTT”,加上前缀一共6个字节,如图4-3所示。
图4-3 协议名称字段
如果协议名称不正确,Broker会断开与Client的连接。
2)协议版本长度为1个字节,是一个无符号整数,MQTT 3.1.1的版本号为4,如图4-4所示。
图4-4 协议版本号字段
3)连接标识长度为1个字节,字节中不同的位用于标识不同的连接选项,如图4-5所示。
图4-5 连接标识字段
除保留位外,每一个标识位的含义如下所示。
●用户名标识(User Name Flag):标识消息体中是否有用户名字段,长度为1位,值为0或1。
●密码标识(Password Flag):标识消息体中是否有密码字段,长度为1位,值为0或1。
●遗愿消息Retain(Will Retain)标识:标识遗愿消息是不是Retain消息,长度为1位,值为0或1。
●遗愿消息QoS(Will QoS)标识:标识遗愿消息的QoS,长度为2位,值为0、1或2。
●遗愿标识(Will Flag):标识是否使用遗愿消息,长度为1位,值为0或1。
●会话清除(Clean Session)标识:标识Client是否建立一个持久会话,长度为1位,值为0或1。当会话清除标识设为0时,代表Client希望建立一个持久会话的连接,Broker将存储该Client订阅的主题和未接收的消息,否则Broker不会存储这些数据,并在建立连接时清除这个Client之前存在的持久会话所保存的数据。
4)可变头的最后2个字节代表连接的Keep Alive,即连接保活字段,如图4-6所示。
图4-6 连接保活字段
Keep Alive代表一个单位为秒的时间间隔,Client和Broker在这个时间间隔之内至少要有一次消息交互,否则Client和Broker会认为它们之间的连接已经断开,具体会在4.5节中进行详细讲解。
CONNECT数据包的消息体依次由5个字段组成:客户端标识符、遗愿主题、遗愿消息、用户名和密码。除了客户端标识符外,其他4个字段都是可选的,由可变头里对应的连接标识来决定是否包含在消息体中。
这些字段有一个2个字节的前缀,用来标识字段的长度,如图4-7所示。
图4-7 MQTT协议的变长字段格式
1)客户端标识符(Client Identif ier):这是用来标识Client身份的字段,在MQTT 3.1.1中,该字段的长度是1~23个字节,而且只能包含数字和26个英文字母(包括大小写),Broker通过这个字段来区分不同的Client。连接时,Client应该保证它的Identif ier是唯一的,通常我们可以使用UUID、唯一的设备硬件标识或者Android设备的DEVICE_ID等作为客户端标识符的取值来源。
MQTT协议中要求Client连接时必须带上客户端标识符,但也允许Broker在实现时接收客户端标识符为空的CONNECT数据包,这时Broker会为Client分配一个内部唯一的Identif ier。如果你需要使用持久会话,那就必须自己为Client设定一个唯一的Identif ier。
2)遗愿主题(Will Topic):如果可变头中的遗愿标识为1,那么消息体中将包含遗愿主题。当Client非正常地中断连接时,Broker将向指定的遗愿主题发布遗愿消息。
3)遗愿消息(Will Message):如果可变头中的遗愿标识为1,那么消息体中将包含遗愿消息。当Client非正常地中断连接时,Broker将向指定的遗愿主题发布由该字段指定的内容。
4)用户名(Username):如果可变头中的用户名标识为1,那么消息体中将包含用户名字段,Broker可以使用用户名和密码对接入的Client进行验证,只允许已授权的Client接入。注意,不同的Client需要使用不同的客户端标识符,但它们可以使用同样的用户名和密码进行连接。
5)密码(Password):如果可变头中的密码标识为1,那么消息体中将包含密码字段。
当Broker收到Client的CONNECT数据包后,将检查并校验CONNECT数据包的内容,然后给Client回复一个CONNACK数据包。
CONNACK数据包的格式如下所示。
CONNACK数据包的固定头格式如图4-8所示。
图4-8 CONNACK数据包的固定头格式
当固定头中的MQTT数据包的类型字段值为2时,则代表该数据包是CONNACK数据包。CONNACK数据包剩余长度的值固定为2。
CONNACK数据包的可变头为2个字节,由连接确认标识和连接返回码组成,如图4-9所示。
图4-9 CONNACK数据包的可变头格式
1)连接确认标识:连接确认标识的前7位都是保留位,必须设为0,最后一位是会话存在标识(Session Present Flag),值为0或1。当Client在连接时设置Clean Session=1,则CONNACK中的会话存在标识始终为0;当Client在连接时设置Clean Session=0,那就有两种情况——如果Broker保存了这个Client之前留下的持久会话,那么CONNACK中的会话存在标识值为1;如果Broker没有保存该Client的任何会话数据,那么CONNACK中的会话存在标识值为0。
2)连接返回码(Connect Return Code):用于标识Client与Broker的连接是否建立成功。
连接返回码及对应状态如表4-1所示。
表4-1 连接返回码及对应状态
这里重点讲一下返回码4和返回码5。返回码4在MQTT协议中的含义是用户名(Username)或密码(Password)的格式不正确,但是在大部分的Broker实现中,在使用错误的用户名或密码时,得到的返回码也是4。所以,这里我们认为返回码4代表错误的用户名或密码。返回码5一般在Broker不使用用户名和密码而使用IP地址或者客户端标识符进行验证的时候使用,用来标识Client没有通过验证。
返回码2代表客户端标识符格式不规范,比如长度超过23个字符、包含了不允许的字符等(部分Broker的实现在协议标准上做了扩展,比如允许超过23个字符的客户端标识符等)。
CONNACK数据包没有消息体。
当Client向Broker发送CONNECT数据包并获得返回码为0的CONNACK包后,就代表连接建立成功,可以发布与订阅消息了。
接下来我们看一下MQTT协议的连接是如何关闭的。MQTT协议的连接关闭可以由Client或Broker二者中的任意一方发起。
Client主动关闭连接的流程非常简单,只需要向Broker发送一个DISCONNECT数据包就可以了。
DISCONNECT数据包的固定头格式如图4-10所示。
图4-10 DISCONNECT数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为14,代表该数据包为DISCONNECT数据包。DISCONNECT的数据包剩余长度的值固定为0。
DISCONNECT数据包没有可变头和消息体。
在Client发送完DISCONNECT数据包之后,就可以关闭底层的TCP连接了,不需要等待Broker的回复,Broker也不会回复DISCONNECT数据包。
在这里,读者可能会有一个疑问,为什么需要在关闭TCP连接之前,发送一个与Broker没有交互的DISCONNECT数据包,而不是直接关闭底层的TCP连接?
这里涉及MQTT协议的一个特性,即Broker需要判断Client是否正常地断开连接。当Broker收到Client的DISCONNECT数据包时,会认为Client是正常地断开连接,那么它会丢弃当前连接指定的遗愿消息。如果Broker检测到Client的TCP连接丢失,但又没有收到DISCONNECT数据包,那么它会认为Client是非正常地断开连接,接着会向在连接的时候指定的遗愿主题发布遗愿消息。
MQTT协议规定Broker在没有收到Client的DISCONNECT数据包之前都应该保持和Client的连接,只有Broker在Keep Alive的时间间隔里没有收到Client的任何MQTT协议数据包时才会主动关闭连接。一些Broker在MQTT协议上做了一些拓展,支持Client的连接管理,可以主动断开和某个Client的连接。
Broker主动关闭连接之前不需要向Client发送任何MQTT协议数据包,直接关闭底层的TCP连接就可以。
接下来,我们将用代码来展示各种情况下MQTT协议连接的建立及断开。
在这里,我们使用Node.js的MQTT库,请确保已安装Node.js,并通过npm install mqtt--save安装了MQTT库。
这里使用一个公共的Broker:mqtt.eclipse.org。
首先引用MQTT库。
然后建立连接。
这里通过clientId选项指定客户端标识符,并通过Clean选项设定会话清除标识为false,代表我们要建立一个持久会话的连接。
接下来通过捕获connect事件将CONNACK数据包中的返回码和会话存在标识打印出来,然后断开连接。
完整的代码persistent_connection.js如下所示。
在终端上运行node persistent_connection.js会得到以下输出。
连接成功,因为这是客户端标识符为“mqtt_sample_id_1”的Client第一次建立连接,所以sessionPresent为false。
再次运行node persistent_connection.js,sessionPresent变为true。
这是因为之前已经创建了一个持久会话,所以再使用同样的客户端标识符进行连接时,得到的SessionPresent为true,表示会话已经存在了。
我们只需要将clean选项设为true,就可以建立一个非持久会话的连接了。完整的代码non_persistent_connetion.js如下所示。
第4行代码将clean设为true。
我们在终端上运行node persistent_connection.js会得到以下输出。
无论运行多少次,sessionPresent都会为false。
接下来看一下如果两个Client使用相同的客户端标识符会发生什么事情。我们把代码稍微调整下,在连接成功时保持连接,然后捕获offline事件,在Client的连接被关闭时打印出来。
完整的代码identical.js如下所示。
从第10行代码开始,捕获Client的离线事件,并输出。
然后打开两个终端,分别运行node identical.js,会看到在两个终端上不停地输出以下内容。
在MQTT协议中,两个Client使用相同的客户端标识符进行连接时,如果第二个Client连接成功,Broker会关闭与第一个Client的连接。
由于我们使用的MQTT库实现了断线重连功能,因此当连接被Broker关闭时,Client会尝试重新连接,结果就是这两个Client交替地把对方顶下线,导致我们看到上面所示的输出。因此,在实际应用中,一定要保证每一个设备使用的客户端标识符都是唯一的。
如果你观察到一个Client不停地上线和下线,那么就很有可能是由于客户端标识符冲突造成的。
在本节中,我们学习了MQTT连接关闭的过程,并且学习了连接建立和关闭的相关代码。在4.2节,我们将学习订阅与发布的概念,进而实现消息在Client之间的传输。
上文介绍了MQTT基于订阅与发布的消息模型,MQTT协议的订阅与发布是基于主题的。一个典型的MQTT消息发送与接收的流程如图4-11所示。
1)ClientA连接到Broker。
2)ClientB连接到Broker,并订阅主题Topic1。
3)ClientA给Broker发送一个PUBLISH数据包,主题为Topic1。
4)Broker收到ClientA的消息,发现ClientB订阅了Topic1,然后通过发送PUBLISH数据包的方式将消息转发到ClientB。
5)ClientB从Broker接收到该消息。
图4-11 MQTT消息的发送与接收流程
和传统的队列有点不同,如果ClientB在ClientA发布消息之后再订阅Topic1,那么ClientB就不会收到该消息。
MQTT协议通过订阅与发布模型对消息的发布者和订阅者进行解耦,发布者在发布消息时不需要订阅方也能连接到Broker,只要订阅方之前订阅过相应主题,那么它在连接到Broker之后就可以收到发布方在它离线期间发布的消息。为了方便起见,在本书中我们称这种消息为离线消息。
要接收离线消息,需要Client使用持久会话,且发布时消息的QoS不小于1。
在继续学习前,我们有必要搞清楚两组概念:发布者(Publisher)和订阅者(Subscriber),发送方(Sender)和接收方(Receiver)。只有弄清楚这两组概念,我们才能更好地理解订阅与发布的流程以及QoS的概念。
Publisher和Subscriber是相对于主题来说的身份,如果一个Client向某个主题发布消息,那么它就是Publisher;如果一个Client订阅了某个主题,那么它就是Subscriber。在上面的例子中,ClientA是Publisher,ClientB是Subscriber。
Sender和Receiver是相对于消息传输方向来说的身份,仍然用前面的例子做解释。
●当ClientA发布消息时,它给Broker发送一条消息,那么ClientA是Sender,Broker是Receiver。
●当Broker转发消息给ClientB时,Broker是Sender,ClientB则是Receiver。
Publisher/Subscriber、Sender/Receiver这两组概念最大的区别是Publisher和Subscriber只可能是Client,而Sender/Receiver有可能是Client,也有可能是Broker。
解释清楚这两组不同的概念之后,我们来看一下PUBLISH数据包。
PUBLISH数据包用于在Sender和Receiver之间传输消息数据,也就是说,当Publisher要向某个主题发布一条消息的时候,Publisher会向Broker发送一个PUBLISH数据包;当Broker要将一条消息转发给订阅了某个主题的Subscriber时,Broker也会向Subscriber发送一个PUBLISH数据包。PUBLISH数据包的格式如下所示。
PUBLISH数据包的固定头格式如图4-12所示。
图4-12 PUBLISH数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为3,代表该数据包是PUBLISH数据包。PUBLISH数据包固定头中的标识位(Flag)中有如下3个字段。
●消息重复标识(DUP Flag):长度为1位,值为0或1。当DUP Flag=1时,代表该消息是一条重发消息,因为Receiver没有确认收到之前的消息。这个标识只在QoS大于0的消息中使用。
●QoS:长度为2位,值为0、1、2,代表PUBLISH消息的服务质量级别。
●Retain标识(Retain Flag):长度为1位,值为0或1。当Retain标识在从Client发送到Broker的PUBLISH数据包中被设为1时,Broker应该保存该消息,并且之后有任何新的Subscriber订阅PUBLISH数据包中指定的主题时,都会先收到该消息,这种消息也被称为Retained消息;当Retain标识在从Broker发送到Client的PUBLISH数据包中被设为1时,代表该消息是一条Retained消息。
PUBLISH数据包的可变头由两个字段组成——主题名和包标识符(Packet Identif ier)。其中,包标识符只会在QoS1和QoS2的PUBLISH数据包里出现,具体在4.3节详细讲解。
主题名是一个UTF-8编码的字符串,它由两个前缀字节来辨识字符串的长度,如图4-13所示。
图4-13 主题名字段
由于只有2个字节标识主题名长度,所以主题名的最大长度为65535字节。
虽然主题名可以是长度为1~65535范围内的任意字符串(可以包含空格),但是在实际项目中,我们最好还是遵循以下命名规则。
●主题名称应该包含层级,不同的层级用“/”划分,比如,2楼201房间的温度感应器可以用主题“home/2ndf loor/201/temperature”表示。
●主题名称开头不要使用“/”,例如:“/home/2ndf loor/201/temperature”。
●不要在主题中使用空格。
●只使用ASCII字符。
●主题名称在可读的前提下尽量短一些。
●主题名称对大小写是敏感的,“Home”和“home”是两个不同的主题。
●可以将设备的唯一标识加到主题中,比如:“warehouse/shelf/shelf1_ID/status”。
●主题尽量精确,不要使用泛用的主题,例如在201房间中有3个传感器,温度传感器、亮度传感器和湿度传感器,那么你应该使用3个主题名称,如“home/2ndf loor/201/temperature”“home/2ndf loor/201/brightness”和“home/2ndf loor/201/humidity”,而不是让这3个传感器都使用“home/2ndf loor/201”这个主题名。
●以“$”开头的主题属于Broker预留的系统主题,通常用于发布Broker的内部统计信息,比如“$SYS/broker/clients/connected”。应用程序不要使用“$”开头的主题收发数据。
PUBLISH数据包的消息体就是该数据包要发送的数据,它可以是任意格式的数据,比如二进制数据、文本、JSON等。具体数据格式由应用程序定义。在实际生产中,我们可以使用JSON、Protocol Buffer等格式对数据进行编码。
消息体中数据的长度可以由固定头中的数据包剩余长度减去可变头的长度得到。
接下来写一小段代码,目的是向一个主题发布一条QoS为1的使用JSON编码的数据,然后退出。代码如下。
第11行代码表示向主题“home/2ndf loor/201/temperature”发送一条QoS为1的消息,消息的内容是格式为JSON的字符串。
运行“node publisher.js”,会得到以下输出。
ClientB想要接收ClientA发布到某个主题的消息,就必须先向Broker订阅这个主题。订阅一个主题的流程如图4-14所示。
图4-14 Client的订阅流程
1)Client向Broker发送一个SUBSCRIBE数据包,其中包含Client想要订阅的主题以及其他参数。
2)Broker收到SUBSCRIBE数据包后,向Client发送一个SUBACK数据包作为应答。
接下来我们看一下数据包的具体内容。
(1)固定头
SUBSCRIBE数据包的固定头格式如图4-15所示。
图4-15 SUBSCRIBE数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为8,代表该数据包是SUBSCRIBE数据包。
(2)可变头
SUBSCRIBE数据包的可变头只包含一个2字节的包标识符,用来唯一标识一个数据包。数据包标识只需要保证在从Sender到Receiver的一次消息交互中唯一即可。SUBSCRIBE数据包的可变头的包标识符格式如图4-16所示。
图4-16 SUBSCRIBE数据包的可变头的包标识符格式
(3)消息体
SUBSCRIBE数据包中的消息体由Client要订阅的主题列表构成。和PUBLISH数据包的主题名不同,SUBSCRIBE数据包中的主题名可以包含通配符,通配符包括单层通配符“+”和多层通配符“#”。使用包含通配符的主题名可以订阅满足匹配条件的所有主题。为了和PUBLISH数据包中的主题名进行区分,我们称SUBSCRIBE数据包中的主题名为主题过滤器(Topic Filter)。
单层通配符“+”:如之前所述,MQTT协议的主题名是具有层级概念的,不同的层级间用“/”分割,“+”可以用来指代任意一个层级。例如:“home/2ndfloor/+/temperature”可匹配:home/2ndfloor/201/temperature、home/2ndfloor/202/temperature,不可匹配home/2ndfloor/201/livingroom/temperature、home/3ndfloor/301/temperature。
多层通配符“#”:“#”和“+”的区别在于,“#”可以用来指定任意多个层级,但是“#”必须是主题过滤器的最后一个字符,同时必须跟在“/”后面,除非主题过滤器只包含“#”这一个字符。例如:“home/2ndf loor/#”可匹配home/2ndf loor、home/2ndf loor/201、home/2ndf loor/201/temperature、home/2ndf loor/202/temperature、home/2ndf loor/201/livingroom/temperature,不可匹配home/3ndf loor/301/temperature。
“#”是一个合法的主题过滤器,代表所有的主题;而“home#”不是一个合法的主题过滤器,因为“#”号需要跟在“/”后面。
每一个主题过滤器必须是一个UTF-8编码的字符串,在这个字符串后面紧跟着1个字节,用于描述订阅该主题的QoS。主题过滤器的格式如图4-17所示。
图4-17 主题过滤器格式
QoS的最后2位用于标识QoS值,值为0、1或2。
消息体的主题列表按照上面的格式依次拼接即可。
为了确认每一次的订阅,Broker在收到SUBSCRIBE数据包后都会回复一个SUBACK数据包作为应答。
(1)固定头
SUBACK数据包的固定头格式如图4-18所示。
图4-18 SUBACK数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为9,代表该数据包是SUBACK数据包。
(2)可变头
SUBACK数据包的可变头只包含一个2字节的包标识符,其格式如图4-19所示。
图4-19 SUBACK数据包的可变头格式
(3)消息体
SUBACK数据包的消息体包含一组返回码,返回码的数量和顺序与SUBSCRIBE数据包的订阅列表对应,用于标识订阅类别中每一个订阅项的订阅结果。
SUBACK数据包中每一个返回码为一个字节,如图4-20所示。
图4-20 返回码字段
返回码列表按照图4-20所示的格式依次拼接而成。
返回码的值及其对应含义如表4-2所示。
表4-2 返回码的值及其对应含义
返回码0、1、2代表订阅成功,同时Broker授予Subscriber不同的QoS等级,这个等级可能会与Subscriber在SUBSCRIBE数据包中要求的不一样。
返回码128代表订阅失败,比如Client没有权限订阅某个主题,或者要求订阅的主题格式不正确等。
接下来,我们试着写一下订阅并处理消息的代码。订阅主题为4.2.2节中代码实现的publisher.js,然后通过捕获message事件获取接收的消息并输出。
通常,在建立和Broker的连接后我们就可以开始订阅了,但这里有一个小小的优化,如果你建立的是持久会话的连接,那么Broker有可能已经保存了之前连接时订阅的主题,这样就没必要再发起SUBSCRIBE请求了。这个小优化在网络带宽或者设备处理能力较差时尤为重要。
完整的代码subscriber.js如下。
第9行代码通过判断CONNACK的sessionPresent标识,来决定是否发起订阅,如果会话已经存在,则不再发起订阅。
第11行代码指定订阅主题“home/2ndf loor/201/temperature”,订阅的QoS等级为1。
在终端上运行“node subscriber.js”会得到以下输出。
第一次运行上述代码的时候,Broker上面没有保存这个Client的会话,所以需要进行订阅,现在按下组合键“Ctrl+C”以终止运行这段代码,然后重新运行,因为Broker上已经保存了这个Client的会话,不需要再订阅,所以我们也不会看到订阅相关的输出。
在4.2.2节中,我们运行过publisher.js,向“home/2ndf loor/201/temperature”这个主题发布过一个消息,但是这发生在subscriber.js订阅该主题之前,所以现在Subscriber不会收到任何消息,我们需要再运行一次publish.js,然后在运行subscriber.js的终端上会得到如下输出。
这样,我们就通过MQTT协议完成了一次点对点的消息传递,同时也验证了建立持久会话连接之后,Broker会保存Client的订阅信息。
Subscriber也可以取消对某些主题的订阅。取消订阅流程如图4-21所示。
图4-21 取消订阅流程
1)Client向Broker发送一个UNSUBSCRIBE数据包,其中包含Client想要取消订阅的主题。
2)Broker收到UNSUBSCRIBE数据包后,向Client发送一个UNSUBACK数据包作为应答。
接下来看一下数据包的具体内容。
(1)固定头
UNSUBSCRIBE数据包的固定头格式如图4-22所示。
图4-22 UNSUBSCRIBE数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为10,代表该数据包是UNSUBSCRIBE数据包。
(2)可变头
UNSUBSCRIBE数据包的可变头只包含一个2字节的包标识符,其格式如图4-23所示。
图4-23 UNSUBSCRIBE数据包的可变头中的包标识符格式
(3)消息体
UNSUBSCRIBE数据包的消息体包含要取消的主题过滤器(Topic Filter)列表,这些主题过滤器的规则和SUBSCRIBE数据包中的规则是一样的,不过不再包含QoS字段,其格式如图4-24所示。
图4-24 UNSUBSCRIBE数据包的消息体格式
UNSUBSCRIBE数据包的消息体的主题列表按照图4-24所示的格式依次拼接而成。
和订阅时不同,取消订阅时,主题名中的通配符并不起通配作用。取消订阅的主题名必须每个字符都和订阅时指定的主题名相同,这样才能被取消。例如,订阅主题名为“home/2ndf loor/201/temperature”,取消订阅名为“home/+/201/temperature”,这样并不会取消之前的订阅。
同理,订阅的时候使用了通配符,取消订阅的时候也必须使用完全一样的主题名。例如,订阅主题名为“home/+/201/temperature”,取消订阅名为“home/+/201/temperature”,这样才能取消之前的订阅。
Broker在收到UNSUBSCRIBE数据包后,会回复给Client一个UNSUBACK数据包作为响应。
(1)固定头
UNSUBACK数据包的固定头格式如图4-25所示。
图4-25 UNSUBACK数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为11,代表该数据包是UNSUBACK数据包。UNSUBACK数据包中的固定头的数据包剩余长度字段的值固定为2。
(2)可变头
UNSUBACK数据包的可变头只包含一个2字节的包标识符,其格式如图4-26所示。
图4-26 UNSUBACK可变头的包标识符格式
(3)消息体
UNSUBACK数据包没有消息体。
下面要完成的代码很简单,只需要在建立连接后取消之前订阅的主题。
完整的代码unsubscribe.js如下所示。
在终端上运行“node unsubscribe.js”,会得到以下输出。
这里取消了对“home/2ndfloor/201/temperature”的订阅,所以再次运行subscriber.js和publisher.js的时候,在运行subscribe.js的终端上就不会再有“home/2ndfloor/201/temperature”的输出信息了。如何使subscriber.js重新订阅这个主题呢?读者可以参考上文进行思考,然后自己动手实现。
在本节中,我们学习了MQTT协议发布、订阅消息的模型及其特性,并第一次实现了消息的点对点传输。接下来,我们将学习MQTT协议中一个非常重要的特性——QoS等级。
前文多次提到了QoS,CONNECT数据包、PUBLISH数据包、SUBSCRIBE数据包中都有QoS标识。那么MQTT协议提供的QoS是什么呢?
作为最初用来在网络带宽窄、信号不稳定的环境下传输数据的协议,MQTT协议设计了一套保证消息稳定传输的机制,包括消息应答、存储和重传。在这套机制下,MQTT协议还提供了3种不同等级的QoS。
●QoS0:At most once,至多一次。
●QoS1:At least once,至少一次。
●QoS2:Exactly once,确保只有一次。
这三个等级都是什么意思呢?QoS是消息的发送方(Sender)和接收方(Receiver)之间达成的一个协议。
●QoS0:表示Sender发送一条消息,Receiver最多能收到一次,也就是说Sender尽力向Receiver发送消息,如果发送失败,则放弃。
●QoS1:表示Sender发送一条消息,Receiver至少能收到一次,也就是说Sender向Receiver发送消息,如果发送失败,Sender会继续重试,直到Receiver收到消息为止,但是因为重传,Receiver可能会收到重复的消息。
●QoS2:表示Sender发送一条消息,Receiver确保能收到且只收到一次,也就是说Sender尽力向Receiver发送消息,如果发送失败,会继续重试,直到Receiver收到消息为止,同时保证Receiver不会因为消息重传而收到重复的消息。
注意,QoS是Sender和Receiver之间达成的协议,不是Publisher和Subscriber之间达成的协议。也就是说,Publisher发布一条QoS1等级的消息,只能保证Broker至少收到一次;而对应的Subscriber能否至少收到一次这条消息,还要取决于Subscriber在订阅的时候和Broker协商的QoS等级。
接下来,我们看一下QoS0、QoS1和QoS2的机制,并讨论一下什么是QoS降级。
QoS0是最简单的一个QoS等级。在QoS0等级下,Sender和Receiver之间的消息传递流程如图4-27所示。
图4-27 QoS0等级下Sender和Receiver之间的消息传递流程
Sender向Receiver发送一个包含消息数据的PUBLISH数据包,然后不管结果如何,丢弃已发送的PUBLISH数据包,这样一条消息即可完成发送。
QoS1要保证消息至少到达Receiver一次,所以这里有一个应答机制。在QoS1等级下,Sender和Receiver之间的消息传递流程如图4-28所示。
图4-28 QoS1等级下Sender和Receiver之间的消息传递流程
1)Sender向Receiver发送一个带有消息数据的PUBLISH数据包,并在本地保存这个PUBLISH数据包。
2)Receiver在收到PUBLISH数据包后,向Sender发送一个PUBACK数据包,PUBACK数据包没有消息体,只在可变头中有一个包标识符,和它收到的PUBLISH数据包中的包标识符一致。
3)Sender在收到PUBACK数据包之后,根据PUBACK数据包中的包标识符找到本地保存的PUBLISH数据包,然后丢弃,这样一次消息发送就完成了。
4)如果Sender在一段时间内没有收到PUBLISH数据包对应的PUBACK数据包,那么它会将PUBLISH数据包中的DUP标识设为1(表示重新发送PUBLISH数据包),然后重新发送该PUBLISH数据包。重复这个流程,直到Sender收到PUBACK数据包,然后执行第3步。
当QoS为1或2时,PUBLISH数据包的可变头中将包含包标识符字段。包标识符长度为2字节,其格式如图4-29所示。
图4-29 PUBLISH数据包的包标识符格式
包标识符用来唯一标识一个MQTT协议数据包,但它并不要求全局唯一,只要保证在一次完整的消息传递过程中是唯一的就可以。
(1)固定头
PUBACK数据包的固定头格式如图4-30所示。
图4-30 PUBACK数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为4,代表该数据包是PUBACK数据包。PUBACK数据包剩余长度字段的值固定为2。
(2)可变头
PUBACK数据包的可变头只包含一个2字节的包标识符,如图4-31所示。
图4-31 PUBACK数据包的可变头中的包标识符格式
(3)消息体
PUBACK数据包没有消息体。
QoS0和QoS1是相对简单的QoS等级,QoS2不仅要确保Receiver能收到Sender发送的消息,还要确保消息不重复。所以,它的重传和应答机制要更复杂,同时开销也是最大的。
在QoS2等级下,Sender和Receiver之间的消息传递流程如图4-32所示。
图4-32 QoS2等级下Sender和Receiver之间的消息传递流程
QoS2使用2套请求/应答流程(一个4段的握手)来确保Receiver收到来自Sender的消息,且不重复。
1)Sender发送QoS值为2的PUBLISH数据包,假设该数据包中包标识符为P,并在本地保存该PUBLISH数据包。
2)Receiver收到PUBLISH数据包后,在本地保存PUBLISH数据包的包标识符为P,并回复Sender一个PUBREC数据包。PUBREC数据包的可变头中的包标识符为P,没有消息体。
3)当Sender收到PUBREC数据包后,它就可以安全地丢掉初始的包标识符为P的PUBLISH数据包,同时保存PUBREC数据包,并回复Receiver一个PUBREL数据包。PUBREL数据包的可变头中的包标识符为P,没有消息体;如果Sender在一定时间内没有收到PUBREC数据包,它会把PUBLISH数据包的DUP标识设为1,并重新发送该PUBLISH数据包。
4)当Receiver收到PUBREL数据包时,它可以丢弃掉保存的包标识符为P的PUBLISH数据包,并回复Sender一个PUBCOMP数据包。PUBCOMP数据包的可变头中的包标识符为P,没有消息体。
5)当Sender收到PUBCOMP数据包,它会认为数据包传输已完成,并丢掉对应的PUBREC数据包。如果Sender在一定时间内没有收到PUBCOMP数据包,则会重新发送PUBREL数据包。
我们可以看到,在QoS2中,想要完成一次消息的传递,Sender和Receiver之间至少要发送4个数据包,所以说,QoS2是最安全,也是最慢的一种QoS等级。
(1)固定头
PUBREC数据包的固定头格式如图4-33所示。
图4-33 PUBREC数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为5,代表该数据包是PUBREC数据包。PUBREC数据包剩余长度字段的值固定为2。
(2)可变头
PUBREC数据包的可变头只包含一个2字节的包标识符,其格式如图4-34所示。
图4-34 PUBREC数据包可变头中的包标识符格式
(3)消息体
PUBREC数据包没有消息体。
(1)固定头
PUBREL数据包的固定头格式如图4-35所示。
图4-35 PUBREL数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为6,代表该数据包是PUBREL数据包。PUBREL数据包剩余长度字段的值固定为2。
(2)可变头
PUBREL数据包的可变头只包含一个2字节的包标识符,其格式如图4-36所示。
图4-36 PUBREL数据包的可变头中的包标识符格式
(3)消息体
PUBREL数据包没有消息体。
(1)固定头
PUBCOM数据包的固定头格式如图4-37所示。
图4-37 PUBCOM数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为7,代表该数据包是PUBCOM数据包。PUBCOM数据包剩余长度字段的值固定为2。
(2)可变头
PUBCOM数据包的可变头只包含一个2字节的包标识符,其格式如图4-38所示。
图4-38 PUBCOM数据包的可变头中的包标识符格式
(3)消息体
PUBCOM数据包没有消息体。
本节会实现一个发布端和一个订阅端,它们可以通过命令行参数来指定发布和订阅的QoS,同时,通过捕获packetsend和packetreceive事件,将发送和接收到的MQTT协议数据包的类型打印出来。
发布端的代码publish_with_qos.js如下所示。
第10~12行代码捕获packetsend事件,然后将发送的MQTT协议数据包的类型打印出来。
第13~15行代码捕获packetreceive事件,然后将收到的MQTT协议数据包的类型打印出来。
第16行代码使用命令行中指定的QoS来发布消息。
订阅端的代码subscribe_with_qos.js如下所示。
第11行代码使用命令行中指定的QoS进行订阅。
第12~14行代码捕获packetsend事件,然后将发送的MQTT协议数据包的类型打印出来。
第15~17行代码捕获packetreceive事件,然后将收到的MQTT协议数据包的类型打印出来。
在subscribe_with_qos.js中,Client每次连接到Broker后都会按照参数指定的QoS重新订阅主题,订阅成功以后才开始捕获接收和发送的数据包,所以Client从连接后到重新订阅前收到的离线消息都不会被打印出来。
我们可以通过node publish_with_qos.js--qos=xxx和node subscribe_with_qos.js--qos=xxx来运行这两段代码。
接下来,用不同的参数组合来运行这两段代码,看看输出分别是什么。
需要先运行subscribe_with_qos.js再运行publish_with_qos.js,确保接收到的消息可以打印出来。
运行node publish_with_qos.js--qos=0,输出为:
运行node subscribe_with_qos.js--qos=0,输出为:
Publisher到Broker,Broker到Subscriber,使用的都是QoS0。
运行node publish_with_qos.js--qos=1,输出为:
运行node subscribe_with_qos.js--qos=1,输出为:
Publisher到Broker,Broker到Subscriber,使用的都是QoS1。
运行node publish_with_qos.js--qos=0,输出为:
运行node subscribe_with_qos.js--qos=1,输出为:
这里就有点奇怪了,很明显Broker到Subscriber使用的是QoS0,和Subscriber订阅时指定的QoS不一样。原因会在4.3.6节详细解释,这里暂不展开。
运行node publish_with_qos.js--qos=1,输出为:
运行node subscribe_with_qos.js--qos=0,输出为:
和设定的一样,Publisher到Broker使用的是QoS1,Broker到Subscriber使用的是QoS0。Publisher使用QoS1发布消息,但是消息到Subscriber却是QoS0。也就是说,Subscriber有可能无法收到消息,这种现象被称为QoS降级(QoS Degrade)。
运行node publish_with_qos.js--qos=2,输出为:
运行node subscribe_with_qos.js--qos=2,输出为:
可以看到,Publisher到Broker,Broker到Subscriber,使用的都是QoS2。
在上节的代码实践中,我们已经发现了在某些情况下,Broker在发送消息到Subscriber时使用的QoS和Subscriber在订阅主题时指定的QoS不一样。
这里有一个很重要的计算方法:在MQTT协议中,从Broker传递到Subscriber的实际的QoS等级等于Publisher发布消息时指定的QoS等级和Subscriber在订阅时与Broker协商的QoS等级中最小的那一个。
Actual Subscribe QoS=MIN(Publish QoS,Subscribe QoS)
这也就解释了在“publish qos=0,subscribe qos=1”的情况下Subscriber的实际QoS为0,以及“publish qos=1,subscribe qos=0”时出现QoS降级的原因。
同样,如果“publish qos=1,subscribe qos=2”或者“publish qos=2,subscribe qos=1”,那么实际Subscriber接收到的QoS等级仍然为1。
理解了实际的QoS的计算方法,你才能更好地设计系统中Publisher和Subscriber使用的QoS。例如,若你希望Subscriber至少收到一次Publisher的消息,那么你要确保Publisher和Subscriber都使用不小于1的QoS。
如果Client想接收离线消息,就必须在连接到Broker的时候指定使用持久会话(Clean Session=0),这样Broker才会存储Client在离线期间没有确认接收的QoS大于1的消息。
以下情况可以选择QoS0:
●Client和Broker之间的网络连接非常稳定,例如一个通过有线网络连接到Broker的用于测试的Client。
●可以接受丢失部分消息,比如一个传感器以非常短的间隔发布状态数据,那丢失一些数据也可以接受。
●不需要离线消息。
以下情况可以选择QoS1:
●应用需要接收所有的消息,而且可以接收并处理重复的消息。
●无法接受QoS2带来的额外开销,QoS1发送消息的速度比QoS2快很多。
以下情况可以选择QoS2:
●应用必须接收所有的消息,而且应用在重复的消息下无法正常工作,同时你可以接受QoS2带来的额外开销。
实际上,QoS1是应用最广泛的QoS等级。QoS1表示发送消息的速度很快,而且能够保证消息的可靠性。虽然使用QoS1等级可能会收到重复的消息,但是在应用程序中处理重复消息通常并不是件难事。在6.1节中,我们会看到如何在应用程序中对消息进行去重。
至此,我们学习了MQTT协议在3种不同的QoS等级下的消息传递流程,并用代码进行了验证。同时,我们也讨论了如何根据实际的应用场景来选择不同的QoS等级。在4.4节,我们将学习MQTT协议的另外两个特性——Retained消息和LWT。
本节我们将学习MQTT的Retained消息和LWT。
让我们来考虑一下这个场景。你有一个温度传感器,它每3个小时向一个主题发布当前温度。那么问题来了,有一个新的订阅者在它刚刚发布了当前温度之后订阅了这个主题,那么这个订阅端什么时候才能收到温度消息呢?
没错,和你想的一样,它必须等到3个小时以后,温度传感器再次发布消息的时候才能收到。在这之前,这个新的订阅者对传感器的温度数据一无所知。
那该怎么解决这个问题呢?
这时候就轮到Retained消息出场了。Retained消息是指在PUBLISH数据包中将Retain标识设为1的消息,Broker收到这样的PUBLISH数据包以后,将为该主题保存这个消息,当一个新的订阅者订阅该主题时,Broker会马上将这个消息发送给订阅者。
Retained消息有如下特点:
●一个Topic只能有一条Retained消息,发布新的Retained消息将覆盖旧的Retained消息。
●如果订阅者使用通配符订阅主题,那他会收到所有匹配主题的Retained消息。
●只有新的订阅者才会收到Retained消息,如果订阅者重复订阅一个主题,那么在每次订阅的时候都会被当作新的订阅者,然后收到Retained消息。
当Retained消息发送到订阅者时,PUBLISH数据包中的Retain标识仍然是1。订阅者可以判断这个消息是不是Retained消息,进而做出相应的处理。
Retained消息和持久会话没有任何关系。Retained消息是Broker为每一个主题单独存储的,而持久会话是Broker为每一个Client单独存储的。
如果你想删除某个主题的Retained消息,只要向这个主题发布一个Payload长度为0的Retained消息就可以了。
现在,要解决本节开头提到的那个问题就很简单了,温度传感器每3个小时向相应的主题发布包含当前温度的Retained消息,那么无论新的订阅者什么时候订阅这个主题,他都能收到温度传感器上一次发布的数据。
下面编写一个发布Retained消息的发布端和一个接收消息的订阅端,订阅端在接收消息的时候将消息的Retain标识和内容打印出来。
发布端的代码publish_retained.js如下所示。
第9行代码在发布时指定Retain标识为1。
订阅端的代码subscribe_retained.js如下所示。
第9行代码判断了CONNACK数据包中的会话存在标识,只有在第一次建立会话的时候才进行订阅,重复多次运行订阅端代码也只会触发一次订阅。
第28行代码是在收到消息的时候打印出消息的Retain标识。
我们首先运行publish_retained.js,再运行subscribe_retained.js,会得到如下输出:
当订阅端第一次订阅该主题的时候,Broker会将为该主题保存的Retained消息转发给订阅端,所以在Publisher发布之后Subscriber再订阅主题也能收到Retained消息。
然后我们再运行一次publish_retained.js,运行subscribe_retained.js的终端会有如下输出:
由于此时订阅端已经订阅了该主题,Broker收到Retained消息以后,只保存该消息,然后按照正常的转发逻辑转发给订阅端,因此对于订阅端来说,这只是一个普通的MQTT PUBLISH数据包,所以Retain标识为0。
接着按下组合键Ctrl+C,关闭subscribe_retained.js,重新运行,此时因为会话已经存在,订阅端不会重新订阅这个主题,终端不会有任何输出。由此可见,Retained消息只对新订阅的订阅者有效。
LWT全称为Last Will and Testament,也就是我们在连接Broker时提到的遗愿,包括遗愿主题、遗愿QoS、遗愿消息等。
顾名思义,当Broker检测到Client非正常地断开连接时,就会向Client的遗愿主题中发布一条消息。遗愿的相关设置是在建立连接时,在CONNECT数据包里面指定的。
●Will Flag:是否使用LWT。
●Will Topic:遗愿主题名,不可使用通配符。
●Will QoS:发布遗愿消息时使用的QoS等级。
●Will Retain:遗愿消息的Retain标识。
●Will Message:遗愿消息内容。
Broker会在出现以下情况时认为Client是非正常断开连接的:
1)Broker检测到底层I/O异常。
2)Client未能在Keep Alive的间隔内和Broker进行消息交互。
3)Client在关闭底层TCP连接前没有发送DISCONNECT数据包。
4)Broker因为协议错误关闭了和Client的连接,比如Client发送了一个格式错误的MQTT协议数据包。
如果Client通过发布DISCONNECT数据包断开连接,这属于正常断开连接,不会触发LWT的机制。同时,Broker还会丢掉这个Client在连接时指定的LWT参数。
通常,如果我们关心设备,比如传感器的连接状态,则可以使用LW T。在接下来的代码实践中,我们会使用Retained消息和LWT实现对一个Client的连接状态监控。
实现Client连接状态监控的原理很简单:
1)Client在连接时指定遗愿主题名为client/status,遗愿消息内容为off line,遗愿消息的Retain标识为1。
2)Client在连接成功后向同一个主题client/status发布一个内容为online的Retained消息。
那么,订阅者在任何时候订阅“client/status”,都会获取Client当前的连接状态。
client.js的代码如下所示。
代码的第5~9行对Client的LWT进行了设置。
在第15行,Client在连接到Broker后会向指定的主题发布一条消息。
用于监控Client连接状态的monitor.js代码如下所示。
首先运行client.js,然后运行monitor.js,我们会得到以下输出:
在运行client.js的终端上,按下组合键“Ctrl+C”终止client.js,之后在运行monitor.js的终端上会得到以下输出:
重新运行client.js,在运行monitor.js的终端上会得到以下输出:
按下组合键“Ctrl+C”终止monitor.js,然后重新运行monitor.js,会得到以下输出:
这样,我们就完美地监控了Client的连接状态。
本节我们学习了Retained消息和LWT,并利用这两个特性完成了对Client连接状态的监控。接下来,我们将学习Keep Alive和在移动端的连接保活。
在生产环境下,特别是在物联网这种无人值守的设备比较多的情况下,我们都希望设备能够自动从错误中恢复过来,比如在网络故障恢复以后,设备能够自动重新连接Broker。本节我们就来学习MQTT协议的Keep Alive机制,以及连接保活的方式。
在4.4节中,我们提到Broker需要知道Client是否非正常地断开了和它的连接,以发送遗愿消息。实际上,Client也需要能够很快地检测到它和Broker的连接断开,以便重新连接。
MQTT协议是基于TCP的一个应用层协议,理论上TCP在连接断开时会通知上层应用,但是TCP有一个半打开连接(Half-open Connection)的问题。这里不会深入分析TCP,需要记住的是,在这种状态下,一端的TCP连接已经失效,但是另外一端并不知情,它认为连接依然是打开的,需要很长时间才能感知到对端连接已经断开,这种情况在使用移动网络或者卫星网络的时候尤为常见。
仅仅依赖TCP层的连接状态监测是不够的,于是MQTT协议设计了一套Keep Alive机制。回忆一下,在建立连接的时候,我们可以传递一个Keep Alive参数,它的单位为秒。MQTT协议约定:在1.5倍Keep Alive的时间间隔内,如果Broker没有收到来自Client的任何数据包,那么Broker会认为它和Client之间的连接已经断开;同样,在这段时间间隔内,如果Client没有收到来自Broker的任何数据包,那么Client会认为它和Broker之间的连接已经断开。
MQTT协议中设计了一对PINGREQ/PINGRESP数据包,当Broker和Client之间没有任何数据包传输时,我们可以通过PINGREQ/PINGRESP数据包满足Keep Alive的约定和连接状态的监测。
当Client在一个Keep Alive时间间隔内没有向Broker发送任何数据包,比如PUBLISH数据包和SUBSCRIBE数据包时,它应该向Broker发送PINGREQ数据包。PINGREQ数据包的格式如下所示。
(1)固定头
PINGREQ数据包的固定头格式如图4-39所示。
图4-39 PINGREQ数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为12,代表该数据包是PINGREQ数据包。PINGREQ数据包剩余长度字段的值固定为0。
(2)可变头
PINGREQ数据包没有可变头。
(3)消息体
PINGREQ数据包没有消息体。
当Broker收到来自Client的PINGREQ数据包时,它应该回复Client一个PINGRESP数据包。PINGRESP数据包的格式如下所示。
(1)固定头
PINGRESP数据包的固定头格式如图4-40所示。
图4-40 PINGRESP数据包的固定头格式
固定头中的MQTT协议数据包类型字段的值为13,代表该数据包是PINGRESP数据包。PINGRESP数据包剩余长度字段的值固定为0。
(2)可变头
PINGRESP数据包没有可变头。
(3)消息体
PINGRESP数据包没有消息体。
Keep Alive机制还有以下几点需要注意:
1)如果在一个Keep Alive时间间隔内,Client和Broker有过数据包传输,比如PUBLISH数据包,那Client就没有必要再使用PINGREQ数据包了,在网络资源比较紧张的情况下这点很重要。
2)Keep Alive的值是由Client指定的,不同的Client可以指定不同的值。
3)Keep Alive的最大值为18个小时12分15秒。
4)Keep Alive的值如果被设置为0,则代表不使用Keep Alive机制。
首先我们编写一段简单的Client代码,它会把发送和接收到的MQTT协议数据包的类别打印出来。
完整的代码keepalive.js如下所示。
代码第6行把Keep Alive的值设为5秒。
运行keepalive.js,我们会得到以下输出:
可以看到,每隔5秒就会有一个PINGREQ/PINGRESP数据包的交互。
然后再编写一段Client代码,这个Client每隔4秒发布一条消息,完整的代码keepalive_with_publish.js如下所示。
代码的第6行把Keep Alive的值设为5秒。
代码的第18~20行,设置了定时器,每隔4秒发布一次。
运行Keepalive_with_publish.js,会得到以下输出:
正如之前所讲的那样,如果在一个Keep Alive时间间隔内,Client和Broker之间传输过数据包,那么就不会触发PINGREQ/PINGRESP数据包。
Client的连接保活逻辑很简单,在检测到连接断开时再重新进行连接就可以了。大多数语言的MQTT Client都支持这个功能,并默认打开。不过如果是移动设备,比如在Android系统或者iOS系统的智能手机上使用MQTT Client,那情况就有所不同了。通常在移动端使用MQTT协议的时候会碰到一个问题:App被切入后台后,怎样才能保持与MQTT协议的连接并继续接收消息呢?接下来,我们就通过Android系统和iOS系统分别来讲一下。
在Android系统上,我们可以在一个Service中创建和保持MQTT协议连接,这样即使App被切入后台,这个Service还在运行,MQTT协议的连接还存在,就能接收消息。参考代码如下所示。
接收到MQTT消息后,我们可以通过一些方式,比如广播通知App处理这些消息。
iOS系统的连接保活机制与Android系统的不同,在App被切入后台时,你没有办法在后台运行App的任何代码,所以无法通过MQTT协议的连接来获取消息。当然,iOS系统提供了几种可以在后台运行的方式,比如Download、Audio等,但如果你的App假借这些方式运行后台程序,是过不了审核的,所以这里只讨论正常情况。
在iOS系统中的App切入后台后,正确接收MQTT协议消息的方式是:
1)Publisher发布一条或多条消息。
2)Publisher通过某种渠道(比如HTTP API)告知App的应用服务器,然后服务器通过系统的APNs向对应的Subscriber推送一条消息。
3)用户点击推送,App进入前台。
4)App重新建立和Broker的连接。
5)App收到Publisher刚刚发送的一条或多条消息。
App端的代码如下所示。
实际上,当下国内主流的Android系统都有后台清理功能,App被切入后台后,它的服务,即使是前台服务(Foreground Service)也会很快地被杀掉,除非App被厂商或者用户加入白名单。所以在Android系统上最好还是利用厂商的推送通道,比如华为推送、小米推送等,即在App被切入后台时采用和iOS系统上一样的机制来接收MQTT协议的消息。
至此,MQTT 3.1.1协议的主要特性就介绍完了,所有的代码可以在https://github.com/suf ish/mqtt-sample中找到。下一章将详细讲解MQTT 5.0的相关内容。