1、什么是MQTT?
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(Publish/Subscribe)模式的轻量级通讯协议,该协议构建于TCP/IP协议上。MQTT最大的优点在于可以以极少的代码和有限的带宽,为远程设备提供实时可靠的消息服务。做为一种低开销、低带宽占用的即时通讯协议,MQTT在物联网、小型设备、移动应用等方面有广泛的应用。
2、MQTT通信机制
一张图看懂MQTT通信机制,如下图:
首先要明白发布者、订阅者和代理之间的关系,发布者和订阅者作为客户端(一个客户端可以同时成为发布者和订阅者),代理作为服务器(通常都是对接云服务,比如阿里云服务)。客户端先要通过TCP/IP协议和服务器建立连接,然后双方才可以进行MQTT通信。通信消息类型有连接与应答、发布与应答、心跳与应答、订阅和取消订阅、关闭等类型。具体的可以看第4节MQTT协议数据传输格式。
3、数据表示
由于数据网络传输用的是大端序的(big-endian,高位字节在低位字节前面),数据格式和嵌入式编程有所不同(一般都是小端序),进行封包时要有格式转换,具体请看下边。
3.1、单字节数据
字节中的位从0到7。第7位是最高有效位,第0位是最低有效位。
3.2、双字节整数
双字节整数数值是使用大端序(big-endian,高位字节在低位字节前面)的16位无符号整数。这意味着一个16位的字在网络上表示为最高有效位(MSB),后面跟着最低有效位(LSB)。
3.3、四字节整数(MQTT5.0新增)
四字节整数数据值是按大端序(big-endian,高位字节在低位字节前面)的32位无符号整数:高位字节先于连续的低位字节。 这意味着一个32位字在网络上显示为最高有效字节(MSB),然后是下一个最高有效字节(MSB),然后是下一个最高有效字节(MSB),然后是最低有效字节(LSB)。
3.4、UTF-8编码字符串
后续描述的MQTT控制报文中的文本字段编码为 UTF-8 格式的字符串。UTF-8 是Unicode字符的有效编码,优化ASCII字符的编码以支持基于文本的通信。
除非另有说明,否则所有UTF-8编码的字符串长度都可以在0到65535字节之间。
3.5、UTF-8字符串键值对(MQTT5.0新增)
UTF-8字符串对由两个UTF-8编码的字符串组成,用来表示名字-值对,第一个字符串表示名字,第二个字符串表示值。
所有的字符串必须遵循UTF-8字符串编码规范,如果接受者(客户端或者服务端)接受到一个字符串对,然而其编码并不遵循规范,则此报文为无效报文。
4、MQTT协议数据传输格式
总体结构分为3个部分:固定报头、可变报头和有效载荷。
4.1、固定报头:
最少2个字节,第1个字节高4位是报文类型,低四位从高到低分别对应为DUP(重发标志占1bit)、QOS(服务等级占2个bit)、RETAIN(保留标志占1个bit)。第2个字节开始表示的是可变报头和有效载荷(数据)的长度(所占字节长度非固定字节数,最多4个字节)。
4.1.1、第一个字节:
报文类型:bit7~bit4
标志位:bit3~bit0
4.1.1.1、报文类型:
4.1.1.2、标志位:
4.1.1.3、DUP重发标志位:
只有在QOS>1时,才有可能用到。当第一次发布消息时,置0,如果是重复发布的要置1。
4.1.1.4、QOS服务等级:
QOS = 0时,“最多一次”,尽操作环境所能提供的最大努力分发消息。消息可能会丢失。例如,这个等级可用于环境传感器数据,单次的数据丢失没关系,因为不久之后会再次发送。
QOS = 1时,“至少一次”,保证消息可以到达,但是可能会重复。
QOS = 2时,“仅一次”,保证消息只到达一次。例如,这个等级可用在一个计费系统中,这里如果消息重复或丢失会导致不正确的收费。
4.1.1.5、RETAIN保留标志位:
当置1时,服务端将保留此条应用消息,分发给未来的订阅者。如果载荷为空,相当于清空消息。
4.1.2、第二个字节:(msg_len)
可变报头长度加上有效载荷长度,编码为变长字节整数。此长度的所占字节数要根据字节的最高位来决定,如果当前字节的最高位置1,说明下一个字节还表示长度,表示长度最多占4个字节,所以长度的最后一个字节最高为必须是0,每个字节表示最大值是128(十进制),也就是说每一包数据最大长度是128*128*128*128。
4.2、可变报头报文类型:
可变报头是根据报文类型和标志位变化,所以对每个不同的报文类型做解析。报文类型分为以下6类:
连接与响应、发布与响应、订阅与取消订阅、Ping(心跳)、关闭、认证交换(5.0支持)。
4.2.1、连接与响应:
4.2.1.1、CONNECT类型(5.0):
可变报头的顺序是:协议名(Protocol Name),协议级别(Protocol Level),连接标志(Connect Flags),保持连接(Keep Alive)和属性(Properties),客户端标识符,遗嘱主题,遗嘱消息,用户名,密码。
协议名(Protocol Name):UTF-8格式字符串。
协议级别(Protocol Level):协议版本号。占1个字节。
连接标志位(Connect Flags):
用户名标志 User Name Flag:置1表明后面有用户名字段,占1个Bit。
密码标志 Password Flag:置1名后面有密码字段,占1个Bit。
遗嘱保留标志 Will Retain:在遗嘱标志为1时才有效,否则错误。置1,发送遗嘱后是否保留信息,占1个Bit。
遗嘱服务质量 Will QoS:在遗嘱标志为1时才有效,否则错误。遗嘱的服务等级,占2个Bit。
遗嘱标志 Will Flag:置1表明后面字段有遗嘱主题和遗嘱消息,其内容是在正常关闭后发布,占1个Bit。
新开始 Clean Start:置1为新连接。占1个Bit。
保留 Reserved:此位置1说明报文错误,占1个Bit。
保持连接(Keep Alive):
客户端和服务端保持连接的心跳时间,占2个字节的时间值,单位为秒。如果服务端在回复CONNACK中有配置间隔时间,客户端还需另配置此值。允许的最大值是18小时12分15秒。
属性(Properties):(5.0新增)
属性第一个字节表示可变长度,同第二个字节(msg_len)。
属性标识符数值及数据类型对照表如下:
4.2.1.1.1、可变报头示例:
4.2.1.1.2、有效载荷:(以CONNECT为例)
客户端标识符(Client Identifier):UTF-8 编码字符串。
当连接标志位遗嘱标志 Will Flag置1时有效:
遗嘱属性长度(Property Length):属性长度被编码为可变长字节整数。(5.0新增)。
遗嘱属性标识符及字段():查询属性(5.0新增)。
遗嘱主题(Will Topic):UTF-8编码的字符串。
遗嘱荷载(Will Payload):这个字段由一个两字节的长度和遗嘱消息的有效载荷组成,此字段为二进制数据。
用户名:UTF-8 编码字符串 。
密码:UTF-8 编码字符串 。
4.2.1.2、CONNACK类型:确认连接请求
连接确认标志(Connect Acknowledge Flags):
第1个字节是连接确认标志,位7-1是保留位且必须设置为0 ,第0(SP)位是会话存在标志(Session Present Flag)。
此标志位置1,且连接原因码成功(0x00),说明服务端已经保存了客户端ID。
连接原因码(Reason Code):
第2个字节是连接原因码。占用1个字节。详情看下方连接原因码 Connect Reason Code。
属性(Properties):
长度上边参考属性标识符数值及数据类型对照表。
连接原因码 Connect Reason Code:
4.2.2、发布与响应
4.2.2.1、PUBLISH类型:发布
固定报头:
解析字节长度方法同上。
可变报头:
主题名(Topic Name):UTF-8编码的字符串 。
报文标识符(Packet Identifier):消息ID。
只有当QoS等级是1或2时,报文标识符(Packet Identifier)字段才能出现在PUBLISH报文中。
属性(Properties):
长度上边参考属性标识符数值及数据类型对照表。
可变报头示例:
有效载荷:
载荷包含将被发布的应用消息。载荷的内容和格式由应用程序指定。有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度。包含零长度有效载荷的PUBLISH报文是合法的。
服务端在接收到客户端的PUBACK,PUBCOMP或包含原因码大于等于128的PUBREC报文之前,不能发送数量超过客户端的接收最大值(Receive Maximum)的QoS为1和2的PUBLISH报文。客户端在发送PUBACK或PUBCOMP响应之前,如果收到数量超过服务端的接收最大值的QoS为1和2的PUBLISH报文,客户端使用包含原因码为0x93(超出接收最大值)的DISCONNECT报文断开网络连接。
4.2.2.2、PUBACK类型、PUBREC类型、PUBREL类型、PUBCOMP类型:回应
都是收到消息回应,PUBACK是对QoS1回应,PUBREC是QoS2模式下对发布方回应(第一个回应报文),PUBREL是对QoS2模式下收到PUBREC后发布方再次发给接受方(第二个回应报文),PUBCOMP是QoS2模式下对发布方最后回应(第三个回应报文)。
固定报头:
解析字节长度方法同上。
可变报头:
所确认的PUBLISH报文标识符:报文标识符(Packet Identifier)。
PUBACK原因码:详情看上方连接原因码 Connect Reason Code。
属性(Properties):长度上边参考属性标识符数值及数据类型对照表。
属性的第1个字节是可变报头中的属性长度被编码为变长字节整数。为0x00则没有属性。
... ...
有效载荷:
没有有效载荷。
4.2.3、订阅与取消订阅
4.2.3.1、SUBSCRIBE类型:订阅请求
客户端向服务端发送SUBSCRIBE报文用于创建一个或多个订阅。每个订阅(Subscription)注册客户端所感兴趣的一个或多个主题。服务端向客户端发送PUBLISH报文以转发被发布到符合这些订阅主题的应用消息。SUBSCRIBE报文同样(为每个订阅)指定了服务端可以向其发送的应用消息最大QoS等级。
固定报头:
解析字节长度方法同上。SUBSCRIBE报文固定报头第1个字节的第3,2,1,0比特位是保留位,必须被设置为0,0,1,0。(QoS1)服务端必须将其他的任何值都当做是不合法的并关闭网络连接。
可变报头:
报文标识符(Packet Identifier):消息ID。
属性(Properties):
属性的第1个字节是可变报头中的属性长度被编码为变长字节整数。为0x00则没有属性。
... ...
有效载荷:
载荷包含一列主题过滤器,指明客户端希望订阅的主题。主题过滤器必须为UTF-8 编码的字符串 。每个主题过滤器之后跟着一个订阅选项(Subscription Options)字节。载荷必须包含至少一个主题过滤器/订阅选项对。不包含载荷的SUBSCRIBE报文将造成协议错误(Protocol Error)。
主题过滤器:
UTF-8 编码的字符串。
订阅选项:占1个字节
第0和1比特代表最大服务质量字段。此字段给出服务端可以向此客户端发送的应用消息的最大QoS等级。最大服务质量字段为3将造成协议错误(Protocol Error)。
第2比特表示非本地(No Local)选项。值为1,表示应用消息不能被转发给发布此消息的客户标识符 。共享订阅时把非本地选项设为1将造成协议错误(Protocol Error)。
第3比特表示发布保留(Retain As Published)选项。值为1,表示向此订阅转发应用消息时保持消息被发布时设置的保留(RETAIN)标志。值为0,表示向此订阅转发应用消息时把保留标志设置为0。当订阅建立之后,发送保留消息时保留标志设置为1。
第4和5比特表示保留操作(Retain Handling)选项。此选项指示当订阅建立时,是否发送保留消息。此选项不影响之后的任何保留消息的发送。如果没有匹配主题过滤器的保留消息,则此选项所有值的行为都一样。值可以设置为:0 = 订阅建立时发送保留消息1 = 订阅建立时,若该订阅当前不存在则发送保留消息2 = 订阅建立时不要发送保留消息保留操作的值设置为3将造成协议错误(Protocol Error)。
第6和7比特为将来所保留。服务端必须把此保留位非0的SUBSCRIBE报文当做无效报文。
SUBSCRIBE类型的有效载荷结构:
RAP指发布保留(Retain as Published)。NL指非本地(No Local)。
4.2.3.2、SUBACK类型:订阅确认
服务端发送SUBACK报文给客户端,用于确认它已收到并且正在处理SUBSCRIBE报文。SUBACK报文包含一个原因码列表,用于指定授予的最大QoS等级或SUBSCRIBE报文所请求的每个订阅发生的错误。
固定报头:
解析字节长度方法同上。
可变报头:
所确认的SUBSCRIBE报文标识符:消息ID。
属性(Properties):
属性的第1个字节是SUBACK可变报头中的属性长度被编码为变长字节整数。为0x00则没有属性。
... ...
有效载荷:
有效载荷包含一个原因码列表。每个原因码对应SUBSCRIBE报文中的一个被确认的主题过滤器。SUBACK报文中的原因码顺序必须与SUBSCRIBE报文中的主题过滤器顺序相匹配。
订阅原因码 Subscribe Reason Codes:
4.2.3.3、UNSUBSCRIBE类型:取消订阅请求
客户端发送UNSUBSCRIBE报文给服务端,用于取消订阅主题。
固定报头:
解析字节长度方法同上。UNSUBSCRIBE固定报头的第3,2,1,0位是保留位且必须分别设置为0,0,1,0。服务端必须认为任何其它的值都是不合法的并关闭网络连接。
可变报头:
报文标识符:消息ID。
属性(Properties):
属性的第1个字节是可变报头中的属性长度被编码为变长字节整数。为0x00则没有属性。
... ...
有效载荷:
UNSUBSCRIBE报文有效载荷包含一列客户端希望取消订阅的主题过滤器。UNSUBSCRIBE报文中的主题过滤器必须为UTF-8编码字符串 ,且连续填充。UNSUBSCRIBE报文有效载荷必须包含至少一个主题过滤器 。不包含有效载荷的UNSUBSCRIBE报文将造成协议错误(Protocol Error)。
UNSUBSCRIBE报文有效载荷示例:
4.2.3.4、UNSUBACK类型:取消订阅确认
服务端发送UNSUBACK报文给客户端用于确认收到UNSUBSCRIBE报文。
固定报头:
解析字节长度方法同上。
表示可变报头的长度,对UNSUBACK报文这个值等于2。
可变报头:
所确认的UNSUBSCRIBE报文标识符:消息ID。
属性( Properties):
属性的第1个字节是可变报头中的属性长度被编码为变长字节整数。为0x00则没有属性。
... ...
有效载荷:
有效载荷包含一个原因码列表。每个原因码对应UNSUBSCRIBE报文中的一个被确认的主题过滤器。UNSUBACK报文中的原因码顺序必须与UNSUBSCRIBE报文中的主题过滤器顺序相匹配 。单字节无符号取消订阅原因码的值如下所示。服务端发送UNSUBACK报文时对于每个收到的主题过滤器,必须使用一个取消订阅原因码。
UNSUBACK原因码:
4.2.4、Ping(心跳)
4.2.4.1、PINGREQ类型:ping请求
客户端发送PINGREQ报文给服务端,可被用于:
在没有任何其他MQTT控制报文从客户端发给服务端时,告知服务端客户端还活着。
请求服务端发送响应以确认服务端还活着。
使用网络已确认网络连接没有断开。
此报文被用在保持连接(Keep Alive)的处理中。
只有2个字节的固定报头:0xC0,0x00。没有可变报头和有效载荷。
4.2.4.2、PINGRESP类型:心跳响应
服务端发送PINGRESP报文响应客户端的PINGREQ报文。表示服务端还活着。
保持连接(Keep Alive)处理中用到这个报文。
只有2个字节的固定报头:0xD0,0x00。没有可变报头和有效载荷。
4.2.5、关闭
DISCONNECT类型:
DISCONNECT报文是客户端发给服务端的最后一个MQTT控制报文。表示客户端为什么断开网络连接的原因。客户端和服务端在关闭网络连接之前可以发送一个DISCONNECT报文。如果在客户端没有首先发送包含原因码为0x00(正常断开)DISCONNECT报文并且连接包含遗嘱消息的情况下,遗嘱消息会被发布。
固定包头:
服务端或客户端必须验证所有的保留位都被设置为0,如果他们不为0,发送包含原因码为0x81(无效报文)的DISCONNECT报文。
可变报头:
断开原因码:
占1个字节,看下面断开原因值。
属性(Properties):
属性的第1个字节是可变报头中的属性长度被编码为变长字节整数。为0x00则没有属性。
... ...
有效载荷:
DISCONNECT报文没有有效载荷。
断开原因值 Disconnect Reason Code:
4.2.6、AUTH类型:认证交换
AUTH报文固定报头第3,2,1,0位是保留位,必须全设置为0。客户端或服务端必须把其他值当做无效值并关闭网络连接 。
固定报头:
可变报头:如果原因码为0x00(成功)并且没有属性字段,则可以省略原因码和属性长度。这种情况下,AUTH固定报文剩余长度为0。
认证原因码(Authentication Reason Code):
1个字节,详情看下面认证原因码。
属性(Properties):
属性的第1个字节是可变报头中的属性长度被编码为变长字节整数。为0x00则没有属性。
... ...
有效载荷:
AUTH报文没有有效载荷。
认证原因码 Authenticate Reason Codes:
5、举例:WireShark抓MQTT包分析
5.1、Connect 包
5.2、Connect Ack 包
5.3、Publish Message 包
5.4、Publish Ack 包