目录
概述
物联网时代,每个传感器都想接入互联网进行数据交换,MQTT就非常适合这样的场合。MQTT协议(Message Queuing Telemetry Transport),即消息队列遥测传输协议。它是一种基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布。MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务。作为一种低开销、低带宽占用的即时通讯协议,使其在物联网、小型设备、移动应用等方面有较广泛的应用。
目前国内的主流IOT 服务器供应商均提供对MQTT 协议的解析比如百度云计算,阿里云计算等。MQTT 协议的实现也非常简单,对带宽的要求不高,对网络链接的可靠性要求也不高,而且协议本身制定了一定的机制来处理突发事件
1
工作原理
上图诠释了MQTT协议里的三个角色:发布者客户端(负责发送消息)、代理服务器(负责接收和分发消息)、订阅者服务端(扶着接收消息)。发布者和订阅者可以是同一台设备,也可以是不同设备,只要这台设备可以通过代理服务器验证,并且遵循MQTT协议,就可以发布或者订阅消息。
通信流程
还是以上图为例,大致通信流程如下:
(1)、 sensor采集温度信息,每间隔1 分钟就上传一次温度信息到服务器,同时他发送的主题是a ;
(2)、 服务器接收到sensor的温度信息后,会查找当前都有哪些订阅者想看主题是a 的信息;
(3)、 laptop订阅了主题是a 的内容,只要小刘发送一次信息,laptop就可以立马接收到对应的信息;
(4) 、sensor和laptop都需要事先通过账号密码的方式连接到服务器;
(5) 、如果有1000 个sensor这样的角色不停地给服务器发送温度数据。我们都知道服务器有数据保存和数据处理的能力,这时候就可以结 合机器学习的相关知识去处理和分析这些数据,从而为人类的决策提供参考。
MQTT协议一些概念
主题
可以理解为消息的类型,订阅者订阅(Subscribe)后,才会收到该主题的消息内容(payload)。
负载(Payload)
消息订阅者所具体接收的内容。
消息编码UTF-8规范
在MQTT 通信协议里,数据传送是以Bit(位)为单位的。MQTT 协议约定:数据传送时,高字节在前,同时,和网络字节传输一样,每个字节里面的最高位先传输,
UTF-8 编码规范简单点理解的话,可以看作在一个字符串的前方加了一个长度的表示。如下表所示。字符串“test”的UTF-8 写法
bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 1 | 字符串长度高8位 0x00 | |||||||
byte 2 | 字符串长度高8位 0x04 | |||||||
byte 3 | 字符串数据 “t” | |||||||
byte 4 | 字符串数据 “e” | |||||||
byte 5 | 字符串数据 “s” | |||||||
byte 6 | 字符串数据 “t” |
前两个字节表示一个16bit 的无符号型整数,表示字符串长度,同时也约束了最大长度位65536,后面跟随的是用ASCII码表示的字符。
MQTT协议数据包结构
在MQTT协议中,一个MQTT数据包由:固定头、可变头、消息体payload三部分构成。除了固定头是必须要的,其他两部分可有可无,例如心跳检测包的发送接收就只要固定头
- 固定头:存在于所有
MQTT
数据包中,表示数据包类型及数据包的分组类标识固定头 - 可变头:存在于部分
MQTT
数据包中,数据包类型决定了可变头是否存在及其具体内容 - 消息体:存在于部分
MQTT
数据包中,表示客户端收到的具体内容
MQTT固定头
固定头存在于在于所有MQTT
数据包中,其结构如下:
MQTT数据包类型
位置:byte 1, bits 7-4。
相于一个4位的无符号值,MQTT的消息类型如下:
名称 | 值 | 流方向 | 描述 |
---|---|---|---|
Reserved | 0 | 不可用 | 保留位 |
CONNECT | 1 | 客户端到服务器 | 客户端请求连接到服务器 |
CONNACK | 2 | 服务器到客户端 | 连接确认 |
PUBLISH | 3 | 双向 | 发布消息 |
PUBACK | 4 | 双向 | 发布确认 |
PUBREC | 5 | 双向 | 发布收到(保证第1部分到达) |
PUBREL | 6 | 双赂 | 发布释放(保证第2部分到达) |
PUBCOMP | 7 | 双向 | 发布完成(保证第3部分到达) |
SUBSCRIBE | 8 | 客户端到服务器 | 客户端请求订阅 |
SUBACK | 9 | 服务器到客户端 | 订阅确认 |
UNSUBSCRIBE | 10 | 客户端到服务器 | 请求取消订阅 |
UNSUBACK | 11 | 服务器到客户端 | 取消订阅确认 |
PINGREQ | 12 | 客户端到服务器 | PING请求 |
PINGRESP | 13 | 服务器到客户端 | PING应答 |
DISCONNECT | 14 | 客户端到服务器 | 中断连接 |
Reserved | 15 | 不可用 | 保留位 |
描述:
1,CONNECT – 连接服务端:客户端到服务端的网络连接建立后, 客户端发送给服务端的第一个报文必须是CONNECT报文
2,CONNACK – 确认连接请求:服务端发送CONNACK报文响应从客户端收到的CONNECT报文。 服务端发送给客户端的第一个报文必须是CONNACK。如果客户端在合理的时间内没有收到服务端的CONNACK报文, 客户端应该关闭网络连接。合理的时间取决于应用的类型和通信基础设施。
3,PUBLISH – 发布消息:PUBLISH控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息。
4,PUBACK –发布确认:PUBACK报文是对QoS 1等级的PUBLISH报文的响应。
5,PUBREC – 发布收到( QoS 2, 第一步):PUBREC报文是对QoS等级2的PUBLISH报文的响应。 它是QoS 2等级协议交换的第二个报文。
6,PUBREL – 发布释放( QoS 2, 第二步):PUBREL报文是对PUBREC报文的响应。 它是QoS 2等级协议交换的第三个报文。
7,PUBCOMP – 发布完成( QoS 2, 第三步):PUBCOMP报文是对PUBREL报文的响应。 它是QoS 2等级协议交换的第四个也是最后一个报文。
8,SUBSCRIBE - 订阅主题:客户端向服务端发送SUBSCRIBE报文用于创建一个或多个订阅。 每个订阅注册客户端关心的一个或多个主题。 为了将应用消息转发给与那些订阅匹配的主题, 服务端发送PUBLISH报文给客户端。 SUBSCRIBE报文也( 为每个订阅) 指定了最大的QoS等级, 服务端根据这个发送应用消息给客户端。
9,SUBACK – 订阅确认:服务端发送SUBACK报文给客户端, 用于确认它已收到并且正在处理SUBSCRIBE报文。
10,UNSUBSCRIBE –取消订阅:客户端发送UNSUBSCRIBE报文给服务端, 用于取消订阅主题。
11,UNSUBACK – 取消订阅确认:服务端发送UNSUBACK报文给客户端用于确认收到UNSUBSCRIBE报文。
12,PINGREQ – 心跳请求:客户端发送PINGREQ报文给服务端的。 用于:1. 在没有任何其它控制报文从客户端发给服务的时, 告知服务端客户端还活着。2. 请求服务端发送 响应确认它还活着。3. 使用网络以确认网络连接没有断开。
13,PINGRESP – 心跳响应:服务端发送PINGRESP报文响应客户端的PINGREQ报文。 表示服务端还活着。
14,DISCONNECT –断开连接:DISCONNECT报文是客户端发给服务端的最后一个控制报文。 表示客户端正常断开连接。
标识位
位置:byte 1, bits 3-0。
在不使用标识位的消息类型中,标识位被做为保留位。如果收到无效的标志时,接收端必须关闭网络连接
数据包 | 标识位 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
---|---|---|---|---|---|
CONNECT | 保留位 | 0 | 0 | 0 | 0 |
CONNACK | 保留位 | 0 | 0 | 0 | 0 |
PUBLISH | MQTT 3.1.1使用 | DUP1 | QoS2 | QoS2 | RETAIN3 |
PUBACK | 保留位 | 0 | 0 | 0 | 0 |
PUBREC | 保留位 | 0 | 0 | 0 | 0 |
PUBREL | 保留位 | 0 | 0 | 0 | 0 |
PUBCOMP | 保留位 | 0 | 0 | 0 | 0 |
SUBSCRIBE | 保留位 | 0 | 0 | 0 | 0 |
SUBACK | 保留位 | 0 | 0 | 0 | 0 |
UNSUBSCRIBE | 保留位 | 0 | 0 | 0 | 0 |
UNSUBACK | 保留位 | 0 | 0 | 0 | 0 |
PINGREQ | 保留位 | 0 | 0 | 0 | 0 |
PINGRESP | 保留位 | 0 | 0 | 0 | 0 |
DISCONNECT | 保留位 | 0 | 0 | 0 | 0 |
DUP
:发布消息的副本。用来在保证消息的可靠传输,如果设置为 1,则在下面的变长中增加MessageId,并且需要回复确认,以保证消息传输完成,但不能用于检测消息重复发送。QoS
:发布消息的服务质量,即:保证消息传递的次数00
:最多一次,即:<=101
:至少一次,即:>=110
:一次,即:=111
:预留
RETAIN
: 发布保留标识,表示服务器要保留这次推送的信息,如果有新的订阅者出现,就把这消息推送给它,如果设有那么推送至当前订阅者后释放。
根据上面的规则,我们很容易得到14种固定头的第一个字节
在最简单的应用中,可以发布一条质量等级为0,不需要确认的消息,此时设置上表中橙色部分
为0X30.
剩余长度(Remaining Length)
位置:Byte 2。
剩余长度 = 可变头(有些没有)长度 + 有效内容(有些没有)长度
固定头的第二字节用来保存变长头部和消息体的总大小的,但不是直接保存的。这一字节是可以扩展,其保存机制,前7位用于保存长度,后一部用做标识。当最后一位为 1时,表示长度不足,需要使用二个字节继续保存。即一个字节表述的数值范围是0~127,超过127需要再用一个字节表示才行。这个编码规则本身最多占用四个字节,所以最大能表示0xff 0xff 0xff 0x7f,即268 435 455
举例:
数字68:16进制为0x44,大小小于127,用一个字节表示就行。编码规范和常规一样就是0x44
数字321:16进制为0x141(0b 1 0100 0001),按照编码规范第一个字节为(0b 0000 0010)0x02,第二个字节(0b 1100 0001),即0xc1,0x02
MQTT可变头
可变头编码范围
MQTT
数据包中包含一个可变头,它驻位于固定的头和负载之间。可变头的内容因数据包类型而不同,较常的应用是做为包的标识:
不同报文出现的可变头类型如下:
可变头的内容里,有一个字节叫做“连接的标识符”,它描述了当前的这个连接的一些设置的内容。这个字节在客户端连接服务器时候用到的:
Clean Session:如果该位被设置为0,则该连接被认为是持久连接,其具体表现为:当该客户断开后,任何订阅的主题和QoS 被设置为1 或2 的信息都会保存,直到该客户端再次连接上server 端(百度云物接入服务支持将该消息保留24 小时)。若“clean session”被设置为1,当该客户断开后,所有的订阅主题都会被移除。
Will Flag:当一个客户端断开连接的时候,它希望客户端可以发送它指定的消息。该消息和普通消息的结
构相同。(通过在帧消息的有效报文中设置Will Topic 和Will Message 实现)
Will QoS:服务器在发生意外的情况下发送遗嘱消息的服务等级。如果Will Flag 是0,那么这个必须是0。
Will Retain:遗嘱保留,如果勾选遗嘱保留,遗嘱消息发布时将会保留且发送给新的订阅消息。
MQTT消息体
消息体帧内容依据传输内容的不一样,所占字节的长度也不一样。比如会包含用户名,密码的信息,不同的服务器不同的用户肯定不一样。注意:帧内容里的数据主要是字符串,需要符合UTF-8 编码规范
帧出现的场合和内容如下:
不同报文类型出现的消息如下:
Payload消息体位MQTT数据包的第三部分,包含CONNECT、SUBSCRIBE、SUBACK、UNSUBSCRIBE四种类型的消息:
(1)CONNECT,消息体内容主要是:客户端的ClientID、订阅的Topic、Message以及用户名和密码。
(2)SUBSCRIBE,消息体内容是一系列的要订阅的主题以及QoS。
(3)SUBACK,消息体内容是服务器对于SUBSCRIBE所申请的主题及QoS进行确认和回复。
(4)UNSUBSCRIBE,消息体内容是要订阅的主题。
协议示例
下面举一个例子,描述MQTT协议具体构成:
连接服务器 + 心跳包
“CONNECT”,客户端发送给服务器的连接请求
固定头:第一个字节肯定是0x10,后续的帧长度要看后面跟随多少信息,待定
可变头:协议名(UTF-8 编码)+协议版本1 字节+连接的标识符1 字节+心跳包时间2 字节 (具体可见xxxxxx)
一个完整的可变头如下:
字节 | 描述 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
byte 1 | Length MSB 0x00 | 0x00 | |||||||
byte 2 | Length LSB 0x04 | 0x04 | |||||||
byte 3 | ‘M’ | 0x4d | |||||||
byte 4 | ‘Q’ | 0x51 | |||||||
byte 5 | ‘T’ | 0x54 | |||||||
byte 6 | ‘T’ | 0x54 | |||||||
协议版本(固定值) | |||||||||
Description | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
byte 7 (0X04) | Level (4) | 0x04 | |||||||
连接的标识符(有用户名,有密码,客户端掉线后服务器清空客户端的信息) | |||||||||
byte 8 | User Name Flag (1) Password Flag (1) Will Retain (0) Will QoS (00) Will Flag (0) Clean Session (1) Reserved (0) | 0xc2 | |||||||
心跳包时间设置,单位是S (这里表示60 秒,意思是如果客户端超过60S 没有发送有效信息到服务器,那么服务器认为这个客户端已失联,会主动断开与客户端的连接) | |||||||||
byte 9 | Keep Alive MSB | 0x00 | |||||||
byte 10 | Keep Alive LSB | 0x3c |
有效内容: “用户ID” + “临终消息主题” + “临终消息” + “用户名” + “密码”
- 用户ID 必须保持唯一,在一个服务器上的所有设备,每个设备的ID 都不一样;
- 应该从服务器那里获取用户名和密码;
- ”临终消息主题”和“临终消息”如果可变头里面的连接标识符没有允许,就不要添加了。
注意:我们在测试之前需要配置好服务器,然后获取一个用户名和密码。比如下面这样的格式:
用户名:test/test 用户ID:test(用户ID)
密码:ymjohJfqMO9KFzjKhVqeR78wnRpt0U0Xxrqq5VEHdcI=
依据上述的设置,本例子在连接服务器的时候,有效内容应该如下表所示:
字节 | 描述 |
发送用户ID,UTF8 编码 | |
Byte1 0X00 | Length MSB |
Byte2 0X04 | Length LSB |
Byte3 0X74 | ‘t’ |
Byte4 0X65 | ’e‘ |
Byte5 0X73 | ’s‘ |
Byte6 0X74 | ‘t’ |
发送用户名,UTF8 编码 | |
Byte7 0X00 | Length MSB |
Byte8 0X00 | Length LSB |
Byte9 0X74 | ‘t’ |
Byte10 0X65 | ’e‘ |
Byte11 0X73 | ’s‘ |
Byte12 0X74 | ‘t’ |
发送密码,UTF8 编码 | |
Byte13 0X00 | Length MSB |
Byte14 0X2C | Length LSB |
Byte15 0X79 | y |
Byte16 0X6D | m |
Byte17 0X6A | j |
。。。略 | 。。。略 |
Byte57 0X49 | | |
Byte58 0X3D | = |
至此,连接服务器需要发送的信息都已经完成了。那么我们计算一下一共需要发送多少个字节,然后就可以确定固定头的“剩余字节数” 的参数了:可变头10 个字节 + 有效信息58个字节。一共是68 个字节
所以“剩余字节数应该是0x44,所以,发送以下信息到服务器即可:(16 进制格式)
10 44
00 04 4D 51 54 54 04 C2 00 3C
00 04 74 65 73 74
00 04 74 65 73 74
00 2C 79 6D 6A 6F 68 4A 66 71 4D 4F 39 4B 46 7A 6A 4B 68 56 71 65 52 37 38 77 6E 52 70 74 30 55 30 58 78 72 71 71 35 56 45 48 64 63 49 3D
发送成功之后,服务器会返回(16 进制格式) 20 02 00 00
如果接收到上述返回的消息,证明已经成功和服务器建立了可靠连接。分析一下来自服务器的消息含义:
第一个字节20即CONNACK,是服务器发送的数据,叫做“连接确认”;第二个字节02 表示后面还有两个数据;后面的两个数据00 00 ,表示两个有效数据。后面发送两个数据的一层意思是,验证从机连接的协议是否正确,即从机接收到02 这个数据后应该判断是否真的接收到了两个数据,如果不是,那证明通信时有问题的。
详细可以参见:MQTT协议中文版