本文首发
(1)发布其他客户端可能会订阅的信息。
(2)订阅其它客户端发布的消息。
(3)退订或删除应用程序的消息。
(4)断开与服务器连接。
MQTT服务器也称为“消息代理”(Broker),位于消息发布者和订阅者之间,它可以:
(1)接受来自客户的网络连接;
(2)接受客户发布的应用信息;
(3)处理来自客户端的订阅和退订请求;
(4)向订阅的客户转发应用程序消息。
5MQTT协议中核心概念
(1)会话(Session)与订阅(Subscription)
每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。订阅包含主题筛选器(TopicFilter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。
(2)主题名(TopicName)和主题筛选器(TopicFilter)
主题名是消息的标签,主题筛选器是订阅者指定的标签通配符表达式。一个主题筛选器可以匹配到多个主题名称,服务器会将订阅所匹配到标签下的所有消息发送给订阅者。MQTT的主题名有层级结构,主题筛选器支持通配符+和#:
MQTT的主题是不要预先创建的,发布者发送消息到某个主题、或者订阅者订阅某个主题的时候,Broker就会自动创建这个主题。
(3)消息
MQTT协议消息数据包由固定头、可变头、消息体三部分组成:
固定头(Fixedheader),共2个字节,第一个字节的高4位定义了此消息的类型,第一个字节的低4位根据消息的类型,含有不同的含义。第二个字节声明接下来的可变头及负载的数据长度,固定头结构如下:
其中第一个字节的4-7bit为数据包消息类型,有14种:
可变头(Variableheader),存在于部分MQTT数据包中,数据包类型决定了可变头是否存在及其具体内容。可变头位于固定头和负载之间,其内容因数据包类型而不同,常用于包的标识:
消息体(Payload),存在于部分MQTT数据包中,表示客户端收到的具体内容,包括CONNECT、SUBSCRIBE、SUBACK、UNSUBSCRIBE四种类型的消息。PUBLISH消息体内容可选,其他类型都没有消息体。
MQTT中有两种特殊的消息:Retained消息和LWT遗嘱消息,分别介绍如下:
Retained消息是指在PUBLISH数据包中Retain标识设为1的消息,Broker收到这样的PUBLISH包以后,将保存这个消息,当有一个新的订阅者订阅相应主题的时候,Broker会马上将这个消息发送给订阅者。Retain消息用于解决新的订阅开始时能够接收订阅之前的消息,有以下一些特点:
一个Topic只能有1条Retained消息,发布新的Retained消息将覆盖老的Retained消息;
如果订阅者使用通配符订阅主题,它会收到所有匹配的主题上的Retained消息;
只有新的订阅者才会收到Retained消息,如果订阅者重复订阅一个主题,也会被当做新的订阅者,然后收到Retained消息;
Retained消息发送到订阅者时,消息的Retain标识仍然是1,订阅者可以判断这个消息是否是Retained消息,以做相应的处理。
Retained消息和持久性会话没有任何关系,Retained消息是Broker为每一个Topic单独存储的,而持久性会话是Broker为每一个Client单独存储的。
遗嘱常用于获取设备的连接状态。当Client非正常断开连接,将发送遗嘱消息给订阅者,Broker在以下情况下认为Client是非正常断开连接的:
Broker检测到底层的I/O异常;
Client未能在KeepAlive的间隔内和Broker之间有消息交互;
Client在关闭底层TCP连接前没有发送DISCONNECT数据包;
Broker因为协议错误关闭和Client的连接,比如Client发送了一个格式错误的MQTT数据包。
如果Client通过发布DISCONNECT数据包断开连接,这个属于正常断开连接,不会触发LWT的机制,同时,Broker还会丢弃掉这个Client在连接时指定的LWT参数。
(4)服务质量QoS
QoS在设置上由2位的二进制控制,且值不允许为3(0x11):
要注意的是,QoS是Sender和Receiver之间达成的协议,不是Publisher和Subscriber之间达成的协议。比如Publisher发布一条QoS1的消息,只能保证Broker能至少收到一次这个消息;至于对应的Subscriber能否至少收到一次这个消息,还要取决于Subscriber在Subscribe的时候和Broker协商的QoS等级。
另外,在MQTT协议中,从Broker到Subscriber这段消息传递的实际QoS等于"Publisher发布消息时指定的QoS等级和Subscriber在订阅时与Broker协商的QoS等级,这两个QoS等级中的最小那一个。"这就是所谓QoS降级。
QoS0:ender不关心Receiver是否收到消息,它"尽力"发送消息。
QoS1:Sender发送的一条消息,Receiver至少能收到一次,也就是说Sender向Receiver发送消息,如果发送失败,会继续重试,直到Receiver收到消息为止,但是因为重传的原因,Receiver有可能会收到重复的消息。
1)Sender向Receiver发送一个带有消息数据的PUBLISH包,并在本地保存这个PUBLISH包。
2)Receiver收到PUBLISH包以后,向Sender发送一个PUBACK数据包,PUBACK数据包没有消息体(Payload),在可变头中(Variableheader)中有一个包标识(PacketIdentifier),和它收到的PUBLISH包中的PacketIdentifier一致。
3)Sender收到PUBACK之后,根据PUBACK包中的PacketIdentifier找到本地保存的PUBLISH包,然后丢弃掉,一次消息的发送完成。
4)如果Sender在一段时间内没有收到PUBLISH包对应的PUBACK,它将该PUBLISH包的DUP标识设为1(代表是重新发送的PUBLISH包),然后重新发送该PUBLISH包。重复这个流程,直到收到PUBACK,然后执行第3步。
QoS2:Sender发送的一条消息,Receiver确保能收到而且只收到一次,也就是说Sender尽力向Receiver发送消息,如果发送失败,会继续重试,直到Receiver收到消息为止,同时保证Receiver不会因为消息重传而收到重复的消息。
QoS2使用2套请求/应答流程(一个4段的握手)来确保Receiver收到来自Sender的消息,且不重复:
1)Sender发送QoS为2的PUBLISH数据包,数据包PacketIdentifier为P,并在本地保存该PUBLISH包;
2)Receiver收到PUBLISH数据包以后,在本地保存PUBLISH包的PacketIdentifierP,并回复Sender一个PUBREC数据包,PUBREC数据包可变头中的PacketIdentifier为P,没有消息体(Payload);
3)当Sender收到PUBREC,它就可以安全地丢弃掉初始的PacketIdentifier为P的PUBLISH数据包,同时保存该PUBREC数据包,同时回复Receiver一个PUBREL数据包,PUBREL数据包可变头中的PacketIdentifier为P,没有消息体;如果Sender在一定时间内没有收到PUBREC,它会把PUBLISH包的DUP标识设为1,重新发送该PUBLISH数据包(Payload);
4)当Receiver收到PUBREL数据包,它可以丢弃掉保存的PUBLISH包的PacketIdentifierP,并回复Sender一个PUBCOMP数据包,PUBCOMP数据包可变头中的PacketIdentifier为P,没有消息体(Payload);
5)当Sender收到PUBCOMP包,那么它认为数据包传输已完成,它会丢弃掉对应的PUBREC包。如果Sender在一定时间内没有收到PUBCOMP包,它会重新发送PUBREL数据包。
我们可以看到在QoS2中,完成一次消息的传递,Sender和Reciever之间至少要发送四个数据包,QoS2是最安全也是最慢的一种QoS等级了。
6MQTT协议格式举例
(1)CONNECT-连接请求报文
客户端到服务端的网络连接建立后,客户端发送给服务端的第一个报文必须是CONNECT,连接服务端报文。其格式如下图:
在一个网络连接上,客户端只能发送一次CONNECT报文。服务端必须将客户端发送的第二个CONNECT报文当作协议违规处理并断开客户端的连接。CONNECT报文头第一个字节固定为0x01,代表CONNECT报文,第二个字节代表余下的数据包字节长度。可变报头按下列次序包含四个字段:
协议名(ProtocolName),值固定为字符“MQTT”的UTF-8编码的字符串,MQTT规范的后续版本不会改变这个字符串的偏移和长度。占用6个字节。
协议级别(ProtocolLevel),对MQTT3.1.1来说,值为4,占用1个字节。
对于3.1.1版协议,协议级别字段的值是4(0x04)。如果发现不支持的协议级别,服务端必须给发送一个返回码为0x01(不支持的协议级别)的CONNACK报文响应CONNECT报文,然后断开客户端的连接。
连接标志(ConnectFlags):连接标志字节包含一些用于指定MQTT连接行为的参数。它还指出有效载荷中的字段是否存在。byte8[0]必须为0,否则断开连接。
用户名标识(UserNameFlag):消息体中是1否0有用户名字段。
密码标识(PasswordFlag):消息体中是1否0有密码字段。
遗嘱消息Retain标识(WillRetain):标识遗嘱消息是1否0是Retain消息。如果遗嘱标识被设置为0,遗嘱保留(WillRetain)标志也必须设置为0。如果遗嘱标志被设置为1:
如果遗嘱保留被设置为0,服务端必须将遗嘱消息当作非保留消息发布。
如果遗嘱保留被设置为1,服务端必须将遗嘱消息当作保留消息发布。
遗嘱消息QOS标识(WillQos):标识遗嘱消息的Qos,2bit。如果遗嘱标志被设置为0,遗嘱QoS也必须设置为0(0x00)。
遗嘱标识(WillFlag):标识是1否0使用遗嘱消息。
会话清除标识(CleanSession):标识Client是0否1建立一个持久化的会话。当CleanSession的标识设为0时,代表Client希望建立一个持久会话的连接,Broker将存储该Client订阅的主题和未接受的消息,否则(设置为1)Broker不会存储这些数据,同时在建立连接时清除这个Client之前存在的持久化会话所保存的数据。持久会话只在QoS等级大于等于1的消息中有效。
连接保活(KeepAlive):设置一个单位为秒的时间间隔,指在client传输完成一个控制报文的时刻到发送下一个报文的时刻,client与broker两者之间允许空闲的最大时间间隔(单位:秒),MQTT协议中约定:在1.5*KeepAlive的时间间隔内,如果Broker没有收到来自Client的任何数据包,那么Broker认为它和Client之间的连接已经断开;同样地,如果Client没有收到来自Broker的任何数据包,那么Client认为它和Broker之间的连接已经断开。MQTT还有一对PINGREQ/PINGRESP数据包,当Broker和Client之间没有任何数据包传输的时候,可以通过PINGREQ/PINGRESP来满足KeepAlive的约定和侦测连接状态。
CONNECT的payload字段是根据可变报头的连接标志决定是否存在,如果存在,必须按照客户端标识符(clientID)、遗嘱主题(willtopic)、用户名(username)和密码(password)的顺序出现,其中客户端标识符(clientID)必须存在而且必须是CONNECT报文有效载荷的第1个字段,必须用UTF-8进行编码。
服务端使用客户端标识符(ClientId)识别客户端。连接服务端的每个客户端都有唯一的客户端标识符(ClientId)。遗嘱主题(willtopic)、用户名(username)和密码(password)这三个字段分别由一个两字节的长度和消息的有效载荷组成,表示为零字节或多个字节序列。长度给出了跟在后面的数据的字节数,不包含长度字段本身占用的两个字节。
(2)CONNACK–确认连接请求报文
服务端发送CONNACK报文响应从客户端收到的CONNECT报文。服务端发送给客户端的第一个报文必须是CONNACK。如果客户端在合理的时间内没有收到服务端的CONNACK报文,客户端应该关闭网络连接。报文格式如下图:
除了固定头,可变报文图中有两个字段:连接确认标志(ConnectAcknowledgeFlags)和连接返回码(ConnectReturncode)。
连接确认标志ConnectAcknowledgeFlags只用了第0位,第0(也叫SP)位是当前会话(SessionPresent)标志。如果服务端收到清理会话(CleanSession)标志为1的连接,除了将CONNACK报文中的返回码设置为0之外,还必须将CONNACK报文中的当前会话设置(SessionPresent)标志为0。
如果服务端收到一个CleanSession为0的连接,当前会话标志的值取决于服务端是否已经保存了ClientId对应客户端的会话状态。
如果服务端已经保存了会话状态,它必须将CONNACK报文中的SP设置为1。
如果服务端没有已保存的会话状态,它必须将CONNACK报文中的SP设置为0;还需要将CONNACK报文中的返回码设置为0。(此时代表连接成功)
SP使服务端和客户端在是否有已存储的会话状态上保持一致。
一旦完成了会话的初始化设置,已经保存会话状态的客户端将期望服务端维持它存储的会话状态。如果客户端从服务端收到的当前的值与预期的不同,客户端可以选择继续这个会话或者断开连接。客户端可以丢弃客户端和服务端之间的会话状态,方法是,断开连接,将清理会话标志设置为1,再次连接,然后再次断开连接。
如果服务端发送了一个包含非零返回码的CONNACK报文,它必须将当前会话标志设置为0。
连接返回码ConnectReturncode使用一个字节的无符号值,在下表中列出:
(3)PUBLISH–发布消息报文
PUBLISH控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息。客户端使用PUBLISH报文发送应用消息给服务端,目的是分发到其它订阅匹配的客户端。服务端使用PUBLISH报文发送应用消息给每一个订阅匹配的客户端。其格式如下图:
第一个字节为固定头,其中高4位为消息类型(0x03),低4位有3个重要标志位:
重发标志DUP:如果DUP标志被设置为0:表示这是客户端或服务端第一次请求发送这个PUBLISH报文。对于QoS0的消息,DUP标志必须设置为0。如果DUP标志被设置为1:表示这可能是一个早前报文请求的重发。客户端或服务端请求重发一个PUBLISH报文时,必须将DUP标志设置为1。
服务质量等级QoS:PUBLISH报文不能将QoS所有的位设置为1。如果服务端或客户端收到QoS所有位都为1的PUBLISH报文,它必须关闭网络连接。当QoS设置为1时,客户端或服务器发布消息时,需要得到对方的确认(PUBACK),如果一段时间后没收到PUBACK,那么会再次发送当前消息,并将DUP字段标记为1。
保留标志RETAIN:如果服务端收到一条保留(RETAIN)标志为1的QoS0消息,它必须丢弃之前为那个主题保留的任何消息。它应该将这个新的QoS0消息当作那个主题的新保留消息,但是任何时候都可以选择丢弃它—如果这种情况发生了,那个主题将没有保留消息。
服务端发送PUBLISH报文给客户端时,如果消息是作为客户端一个新订阅的结果发送,它必须将报文的保留标志设为1[MQTT-3.3.1-8]。当一个PUBLISH报文发送给客户端是因为匹配一个已建立的订阅时,服务端必须将保留标志设为0,不管它收到的这个消息中保留标志的值是多少。
保留标志为1且有效载荷为零字节的PUBLISH报文会被服务端当作正常消息处理,它会被发送给订阅主题匹配的客户端。此外,同一个主题下任何现存的保留消息必须被移除,因此这个主题之后的任何订阅者都不会收到一个保留消息。
PUBLISH的可变头
可变报头按顺序包含主题名(TopicName)和报文标识符(PacketIdentifier)。
主题名(TopicName)用于识别有效载荷数据应该被发布到哪一个信息通道。服务端发送给订阅客户端的PUBLISH报文的主题名必须匹配该订阅的主题过滤器。
报文标识符PacketIdentifier:只有当QoS等级是1或2时,报文标识符(PacketIdentifier)字段才能出现在PUBLISH报文中。QoS等于0的PUBLISH报文不能包含报文标识符。
报文标识符用来区分报文,特别是在重发的报文中用来标识是否是同一个报文,并在需要应答的场景中用于确定是对哪个发送报文的应答。可变报头的报文标识符(PacketIdentifier)字段存在于在多个类型的报文里(占用2个字节)。
PUBLISH报文的接收者必须按照根据PUBLISH报文中的QoS等级发送响应,响应报文参见QoS流程。
(4)SUBSCRIBE订阅报文
客户端向服务端发送SUBSCRIBE报文用于创建一个或多个订阅。每个订阅注册客户端关心的一个或多个主题。为了将应用消息转发给与那些订阅匹配的主题,服务端发送PUBLISH报文给客户端。SUBSCRIBE报文也(为每个订阅)指定了最大的QoS等级,服务端根据这个发送应用消息给客户端。SUBSCRIBE报文格式如下图:
SUBSCRIBE报文的有效载荷必须包含至少一对主题过滤器和QoS等级字段组合。每一个过滤器后面跟着一个字节,这个字节被叫做服务质量要求(RequestedQoS)。它给出了服务端向客户端发送应用消息所允许的最大QoS等级。请求的最大服务质量等级QoS:字段编码为一个字节,主题过滤器和QoS等级组合是连续地打包。
如果服务端收到一个SUBSCRIBE报文,报文的主题过滤器与一个现存订阅的主题过滤器相同,那么必须使用新的订阅彻底替换现存的订阅。新订阅的主题过滤器和之前订阅的相同,但是它的最大QoS值可以不同。与这个主题过滤器匹配的任何现存的保留消息必须被重发,但是发布流程不能中断。
如果主题过滤器不同于任何现存订阅的过滤器,服务端会创建一个新的订阅并发送所有匹配的保留消息。
如果服务端收到包含多个主题过滤器的SUBSCRIBE报文,它必须如同收到了一系列的多个SUBSCRIBE报文一样处理那个,除了需要将它们的响应合并到一个单独的SUBACK报文发送[MQTT-3.8.4-4]。
客户端使用带通配符的主题过滤器请求订阅时,客户端的订阅可能会重复,因此发布的消息可能会匹配多个过滤器。对于这种情况,服务端必须将消息分发给所有订阅匹配的QoS等级最高的客户端。服务端之后可以按照订阅的QoS等级,分发消息的副本给每一个匹配的订阅者。
(5)SUBACK订阅确认报文
服务端收到客户端发送的一个SUBSCRIBE报文时,必须使用SUBACK报文响应,报文格式如下图:
固定报文头为MQTT控制报文类型(0x9),剩余长度字段:等于可变报头的长度加上有效载荷的长度。可变报头包含等待确认的SUBSCRIBE报文的报文标识符,占用2个字节。SUBACK报文必须和等待确认的SUBSCRIBE报文有相同的报文标识符。SUBACK报文包含一个返回码清单,它们指定了SUBSCRIBE请求的每个订阅被授予的最大QoS等级。返回码的顺序必须和SUBSCRIBE报文中主题过滤器的顺序相同。
Part3MQTT应用开发
7MQTT应用开发详解
MQTT应用开发涉及到客户端和服务器两部分,客户端主要