文章大纲
- 引言
- 五、MQTT协议控制包结构概述
- 1、 MQTT固定包头
- 1.1、 MQTT控制包类型
- 1.2、 标识位
- 1.3、 剩余长度
- 2、 MQTT可变头
- 2.1、包唯一标识
- 3、载荷 Payload
- 4、MQTT控制包之CONNECT 包的结构
- 4.1、固定包头
- 4.2、可变包头
- 4.2.1、协议名
- 4.2.2、协议等级
- 4.2.3、连接标识
- 4.2.3.1、Clean Session
- 4.2.3.2、Will Flag
- 4.2.3.3、Will QoS
- 4.2.3.4、Will Retain
- 4.2.3.5、Password Flag
- 4.2.3.6、User Name Flag
- 4.2.4、Keep Alive
- 4.3、载荷
- 4.3.1、客户端标识
- 4.3.2、Will Topic
- 4.3.3 Will Message
- 4.3.4 User Name
- 4.3.5 Password
- 5、MQTT 控制包之CONNACK 包的结构
- 5.1、固定包头
- 5.2、可变包头
- 5.2.1、连接确认标识
- 5.2.1、Session Present
- 5.2.3、连接返回码
引言
书接上文网络编程——物联网利器MQTT通信协议详解(二) 系列文章链接:
五、MQTT协议控制包结构概述
一个字节有8个位,从0到7。位7是最高有效位,位0是最低有效位。MQTT协议中整型数据值是16位大端序列:高阶字节在低阶字节之前。即一个16位的字被放到网络上的时候,前面是最高有效位,后面是最低有效位。
在MQTT协议中,一个MQTT控制包由:**固定头(Fixed header)、可变头(Variable header)、载荷(Payload)**三部分构成。
- 固定头(Fixed header)——存在于所有MQTT控制包中,表示控制包类型及控制包的分组类标识。
- 可变头(Variable header)——存在于部分MQTT控制包中,控制包类型决定了可变头是否存在及其具体内容。
- 载荷(Payload)——存在于部分MQTT控制包中,表示客户端收到的具体内容。
1、 MQTT固定包头
每一个MQTT控制包都包含一个固定包头(2个字节),其结构如下:
|Bit |7 |6 |5 |4 |3 |2 |1 |0
|byte 1 |MQTT Control Packet type |Flags specific to each MQTT Control Packet type
|byte 2 |Remaining Length
MQTT控制包的固定头大部分都是2字节依次由包类型标识和剩余长度组成。
1.1、 MQTT控制包类型
固定头中的字节1中 [7-4]位相于一个4位的无符号值用于表示MQTT控制包类型,取值及描述如下:
Name | Value | Direction of flow | Description |
Reserved | 0 | Forbidden | Reserved |
CONNECT | 1 | Client to Server | |
CONNACK | 2 | Server to Client | Connect acknowledgment |
PUBLISH | 3 | Client to Server or Server to Client | Publish message |
PUBACK | 4 | Client to Server or Server to Client | Publish acknowledgment |
PUBREC | 5 | Client to Server or Server to Client | Publish received (assured delivery part 1) |
PUBREL | 6 | Client to Server or Server to Client | Publish release (assured delivery part 2) |
PUBCOMP | 7 | Client to Server or Server to Client | Publish complete (assured delivery part 3) |
SUBSCRIBE | 8 | Client to Server | Client subscribe request |
SUBACK | 9 | Server to Client | Subscribe acknowledgment |
UNSUBSCRIBE | 10 | Client to Server | Unsubscribe request |
UNSUBACK | 11 | Server to Client | Unsubscribe acknowledgment |
PINGREQ | 12 | Client to Server | PING request |
PINGRESP | 13 | Server to Client | PING response |
DISCONNECT | 14 | Client to Server | Client is disconnecting |
Reserved | 15 | Forbidden | ReservedControl Package |
1.2、 标识位
固定包头字节1中剩下的位[3-0]包含了每个MQTT控制包类型的特殊标识,如下表 - Flag Bits。
Control Package | Fixed header flags | bit3 | bit2 | bit1 | bit0 |
CONNECT | Reserved | 0 | 0 | 0 | 0 |
CONNACK | Reserved | 0 | 0 | 0 | 0 |
PUBLISH | Used in MQTT 3.1.1 | DUP1 | QoS2 | QoS2 | RETAIN3 |
PUBACK | Reserved | 0 | 0 | 0 | 0 |
PUBREC | Reserved | 0 | 0 | 0 | 0 |
PUBREL | Reserved | 0 | 0 | 1 | 0 |
PUBCOMP | Reserved | 0 | 0 | 0 | 0 |
SUBSCRIBE | Reserved | 0 | 0 | 1 | 0 |
SUBACK | Reserved | 0 | 0 | 0 | 0 |
UNSUBSCRIBE | Reserved | 0 | 0 | 1 | 0 |
UNSUBACK | Reserved | 0 | 0 | 0 | 0 |
PINGREQ | Reserved | 0 | 0 | 0 | 0 |
PINGRESP | Reserved | 0 | 0 | 0 | 0 |
DISCONNECT | Reserved | 0 | 0 | 0 | 0 |
表中被标识为“预留”的标识位也必须赋值,如果收到不可用的标识,接收方必须关闭网络连接。DUP2 = 重复发送PUBLISH控制包 QoS2 = PUBLISH质量服务 RETAIN3 = PUBLISH保留标识 关于PUBLISH控制包的DUP,QoS,以及RETAIN标识
- DUP——发布消息的副本。用来在保证消息的可靠传输,如果设置为1,则在下面的变长中增加MessageId,并且需要回复确认,以保证消息传输完成,但不能用于检测消息重复发送。
- QoS——发布消息的服务质量,即:保证消息传递的次数
1.3、 剩余长度
从第二个字节开始,剩余长度是指当前包中的剩余字节,包括可变包头的数据以及载荷。剩余长度不包含用来编码剩余长度的字节。剩余长度使用了一种可变长度的结构来编码,这种结构使用单一字节表示0-127的值,当大于127的值如下处理:每个字节的低7位用来编码数据,最高位用来表示是否还有后续字节。因此每个字节可以编码128个值,再加上一个标识位,剩余长度最多可以用四个字节来表示。
这将允许应用发送最多256M大小的控制包。这个数字用16进制表示为:0xFF,0xFF,0xFF,0x7F。Table 2.4 展示了随着字节数的增多,剩余长度可表示的值。
2、 MQTT可变头
某些类型的**MQTT控制包包含一个位于固定包头和载荷之间的可变包头结构,**其内容取决于包的类型。
2.1、包唯一标识
很多类型控制包(像PUBLISH (QoS > 0)、PUBACK、PUBREC、PUBREL、PUBCOMP、SUBSCRIBE、SUBACK、UNSUBSCRIBE、UNSUBACK等类型控制包)中都包括一个2字节的控制包标识字段且必须包含非零的唯一标识。
Control Packet | Packet Identifier field |
CONNECT | NO |
CONNACK | NO |
PUBLISH | YES (If QoS > 0) |
PUBACK | YES |
PUBREC | YES |
PUBREL | YES |
PUBCOMP | YES |
SUBSCRIBE | YES |
SUBACK | YES |
UNSUBSCRIBE | YES |
UNSUBACK | YES |
PINGREQ | NO |
PINGRESP | NO |
DISCONNECT | NO |
YES 代表发送该种控制包时必须包含包唯一标识。
每次客户端发送上述控制包的时候,必须分配一个未使用过的唯一标识。如果一个客户端重新发送一个特别的控制包,必须使用相同的唯一标识符,唯一标识会在客户端收到相应的确认包之后变为可用。例如PUBLIST在QoS1的时候对应PUBACK;而在QoS2时对应PUBCOMP;对于SUBSCRIBE和UNSUBSCRIBE对应SUBACK和UNSUBACK;服务端发送QoS>0的PUBLISH时,上述内容同样适用。但QoS为0的PUBLISH包不允许包含唯一标识。PUBACK,PUBREC,PUBREL包的唯一标识必须和对应的PUBLISH包相同;同样的SUBACK和UNSUBACK的唯一标识必须与对应的SUBSCRIBE和UNSUBSCRIBE包相同。由于客户端和服务端各自独立分配唯一标识。因此,**一对客户端和服务端交换数据的时候可以使用相同的唯一标识。**例如这种情况客户端发送一个PUBLISH包,唯一标识位0x1234,在收到相应的PUBACK之前,接着先收到了一个服务端发来的不同的PUBLISH包,唯一标识同样是0x1234。
Client Server
PUBLISH Packet Identifier = 0x1234 ->
<- PUBLISH Packet Identifier = 0x1234
PUBACK Packet Identifier = 0x1234 ->
<- PUBACK Packet Identifier = 0x1234
3、载荷 Payload
一些MQTT控制包的最后一部分包含载荷,PUBLISH包相当于应用消息。Table 2.6 - Control Packets that contain a Payload显示了控制包对载荷的需求。
Control Packet | Payload |
CONNECT | Required |
CONNACK | None |
PUBLISH | Optional |
PUBACK | None |
PUBREC | None |
PUBREL | None |
PUBCOMP | None |
SUBSCRIBE | Required |
SUBACK | Required |
UNSUBSCRIBE | Required |
UNSUBACK | None |
PINGREQ | None |
PINGRESP | None |
DISCONNECT | None |
Required 表示必须包含载荷部分,None 表示没有载荷。
4、MQTT控制包之CONNECT 包的结构
CONNECT——客户端请求连接服务器
客户端请求连接服务器,客户端和服务端建立网络连接后,第一个从客户端发送给服务端的包必须是CONNECT包。每个网络连接客户端只能发送一次CONNECT包,服务端必须把客户端发来的第二个CONNECT包当作违反协议处理,并断开与客户端的连接。载荷包含一个或多个编码字段,用来指定客户端的唯一标识,话题,信息,用户名和密码。除了客户端唯一标识,其他都是可选项,是否存在取决于可变包头里的标识。
4.1、固定包头
CONNECT包的固定包头结构和上面MQTT固定头所讲的结构一致。
Figure 3.1 - CONNECT Packet fixed header
|Bit |7 |6 |5 |4 |3 |2 |1 |0
|byte 1 |MQTT Control Packet type (1) |Reserved
| |0 |0 |0 |1 |0 |0 |0 |0
|byte 2… |Remaining Length
CONNECT包的剩余长度是可变包头长度(10字节)加上载荷的长度
4.2、可变包头
CONNECT包的可变包头由四个字段按照如下顺序构成:协议名字,协议等级,连接标识,保持连接
4.2.1、协议名
协议名字是一个UTF-8编码的全大写的字符串“MQTT”,这个字符串的偏移量和长度不会在未来版本的MQTT规范中有所改变。如果协议名字不正确,服务端可以选择断开连接,也可以选择基于其他规范继续保持连接。防火墙机制可以用协议名来鉴别MQTT传输
4.2.2、协议等级
用8位无符号值表示客户端的版本等级,例如3.1.1版本的协议等级是4(0x04)。如果协议等级不被服务端支持,服务端必须响应一个包含代码0x01(不接受的协议等级)CONNACK包,然后断开和客户端的连接。
4.2.3、连接标识
1字节连接标识包含了一些参数来指定MQTT的连接行为,它也能表示负载中的字段是否存在。服务端必须验证CONNECT控制包的预留字段是否为0,如果不为0断开与客户端的连接。
Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
User Name Flag | Password Flag | Will Retain | Will QoS | Will QoS | Will Flag | Clean Session | Reserved | |
byte 8 | X | X | X | X | X | X | X | 0 |
服务端必须验证CONNECT控制包的预留字段是否为0,如果不为0断开与客户端的连接。
4.2.3.1、Clean Session
连接标识字节的bit 1指明了会话状态的处理方式。客户端和服务端可以存储会话状态,以便能够在一系列的网络连接中可靠的传递消息,这个bit 1用来控制会话状态的生命周期。
- 当CleanSession被设置为0,服务器必须根据当前的会话状态恢复与客户端的通信(客户端的唯一标识作为会话的标识)。如果没有与客户端唯一标识相关的会话,服务端必须创建一个新的会话,客户端和服务端在断开连接后必须存储会话。当CleanSession为0的会话断开后,服务器还必须将所有和客户端订阅相关的QoS1和QoS2的消息作为会话状态的一部分存储起来,也可以选择把QoS0的消息也存储起来。
- 当CleanSession被设置为1,客户端和服务端必须断开之前的会话启动一个新的会话。只要网络连接存在会话就存在,一个会话的状态数据一定不能被随后的会话复用,客户端和服务端不需要自动处理状态的删除。
客户端会话状态:
- 已经发送到服务端,但没有收到确认的QoS 1和QoS 2消息
- 接收到的从服务端QoS 2消息,还没有收到确认的
服务端会话状态:
- 即使会话状态为空,会话本身也必须存在。
- 客户端的订阅。
- 发送到客户端的但没有得到确认的QoS 1和QoS 2消息。
- 等待发送到客户端的QoS 1和QoS 2消息。
- 从客户端收到的QoS 2消息,但还没有确认的。
- 可选项,等待发送到客户端的QoS 0消息。
在服务端,保留消息不属于会话状态,它们不必在会话结束的时候被删除。
4.2.3.2、Will Flag
连接标识的bit 2,如果Will Flag被设置为1,如果连接请求被接受,服务端必须存储一个Will Message并和网络连接关联起来。之后在网络连接断开的时候必须发布Will Message,除非服务端收到DISCONNECT包删掉了Will Message。另外Will Message会在某些情况下发布包括(但不限于):
- 服务端发现I/O错误或网络失败。
- 客户端在Keep Alive时间内通信失败。
- 客户端没有发送DISCONNECT包就关闭了网络连接。
- 服务端因协议错误关闭了网络连接。
如果Will Flag被设置为1,连接标识中的Will QoS和Will Retain字段将会被服务端用到而且Will Topic和Will Message字段必定会出现在载荷中。一旦被发布过或者服务端收到了客户端的DISCONNECT包,Will Message必须从服务端存储的会话状态中移除;如果Will Flag被设置为0,连接标识中的Will QoS和Will Retain字段必须设置为零且Will Topic和Will Message字段不能够出现在载荷中,Will Message将不会再网络连接结束的时候发布。例如服务端关闭或故障,只能推迟Will Message的发布,直到服务端重启。如果这种情况发生,就会出现从服务器故障到Will Message被发布之间的延迟。
4.2.3.3、Will QoS
连接标识的bit 4和3,这两个bit表示发布Will Message时使用QoS的等级。如果Will Flag设置为0,那么Will QoS也必须设置为0;如果Will Flag设置为1,那么Will QoS的值可是是0(0x00),1(0x01),2(0x02)。一定不会是3(0x03)。
4.2.3.4、Will Retain
连接标识的bit 5,表示Will Message在发布之后是否需要保留。如果Will Flag设置为0,那么Will Retain必须是0;如果Will Flag设置为1:
- 如果Will Retain设置为0,那么服务端必须发布Will Message,不必保存[MQTT-3.1.2-16]。
- 如果Will Retain设置为1,那么服务端必须发布Will Message,并保存[MQTT-3.1.2-17]。
4.2.3.5、Password Flag
连接标识的bit 6。
- 如果Password Flag设置为0,那么密码不必出现在载荷中
- 如果Password Flag设置为1,那么密码必须出现在载荷中
- 如果User Name Flag设置为0,那么Password Flag必须设置为0
4.2.3.6、User Name Flag
连接标识的bit 7。
- 如果User Name Flag设置为0,那么用户名不必出现在载荷中
- 如果User Name Flag设置为1,那么用户名必须出现在载荷中
4.2.4、Keep Alive
Figure 3.5 Keep Alive bytes
|Bit |7 |6 |5 |4 |3 |2 |1 |0
|byte 9 |Keep Alive MSB
|byte 10 |Keep Alive LSB
紧跟连接标识后用16-bit字表示的Keep Alive是以秒为单位的时间间隔指的是客户端从发送完成一个控制包到开始发送下一个的最大时间间隔。客户端有责任确保两个控制包发送的间隔不能超过Keep Alive的值。如果没有其他控制包可发,客户端必须发送PINGREQ包。客户端可以在任何时间发送PINGREQ包,不用关心Keep Alive的值,用PINGRESP来判断与服务端的网络连接是否正常。如果Keep Alive的值非0,而且服务端在一个半Keep Alive的周期内没有收到客户端的控制包,服务端必须作为网络故障断开网络连接。而如果客户端在发送了PINGREQ后,在一个合理的时间都没有收到PINGRESP包,客户端应该关闭和服务端的网络连接。Keep Alive的值为0,就关闭了维持的机制。在这种情况下,服务端不会断开静默的客户端。
服务端在任何时候都可以断开它认为不活跃或没有响应的客户端,而不需要遵照客户端提供的Keep Alive。
4.3、载荷
CONNECT包的载荷可以包含一个或多个带有长度前缀的字段,这些字段(客户端的ClientID、订阅的Topic、Message以及用户名和密码等)是否存在取决于可变包头的标识。如果存在,必须按照这样的顺序:客户端唯一标识,Will Topic,Will Message,User Name,Password。
4.3.1、客户端标识
客户端标识(ClientId)必须存在且是以UTF-8编码的字符串存储于CONNECT包载荷的第一个字段,服务端必须能够接纳的ClientId是长度为1到23的UTF-8编码的字节,而且只包含字符“0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”当然服务端也可以接纳大于23个字节编码的ClientId,服务端也可以允许ClientId包含上述字符之外的字符。服务端允许客户端提供0字节长度的ClientId时,服务端必须把这也当作一个特例,作为客户端的唯一ClientId正常处理。如果客户端提供了一个0字节的ClientId,客户端必须设置CleanSession为1,如果客户端提供了一个0字节的ClientId且CleanSession设置为0,那么服务端必须响应一个CONNACK包,带有代码0x02(标识被拒绝),然后关闭网络连接,如果服务端拒绝了ClientId,必须返回一个CONNACK包,带有代码0x02(标识被拒绝),然后关闭网络连接。
4.3.2、Will Topic
如果Will Flag设置为1,Will Topic是载荷的下一个字段且必须是UTF-8编码的字符串。
4.3.3 Will Message
如果Will Flag设置为1,Will Message是载荷的下一个字段,Will Message定义了将要发布给Will Topic的应用消息。这个字段的前两个字节表示长度,后面是一段字节序列。长度代表字节序列的字节数,不包括长度本身的两个字节。
当Will Message被发布给Will Topic的时候,只是包括字节序列那部分数据,不包含代表长度的头两个字节。
4.3.4 User Name
如果User Name Flag设置为1,这将是载荷的下一个字段。User Name必须是1.5.3节定义的UTF-8编码的字符串[MQTT-3.1.3-11]。服务端根据它来认证和授权。
4.3.5 Password
如果Password Flag设置为1,这将是载荷的下一个字段。Password字段包含0-65535字节的二进制数据,数据前面有两个字节来定义二进制数据占用的字节数(不包括这两个字节本身)。
5、MQTT 控制包之CONNACK 包的结构
服务端可能在同一个TCP端口或其他网络终端会支持多种协议(包括本协议的早期版本)。如果服务端确认协议是MQTT 3.1.1,那么它必须确认下列连接请求:
- 如果服务端没有收到CONNECT包,在网络连接建立后的一个合理的时间内,服务端应该关闭网络连接。
- 服务端必须验证CONNECT包是否符合规范,如果不符合,关闭网络连接且不需要发送CONNACK
- 服务端可以对CONNECT包的内容进行进一步的限制验证,可以检查认证和授权。如果任何检查失败,应该发送适合的CONNACK包,然后关闭网络连接。
如果验证成功,服务端执行下列步骤。
- 如果ClientId标识的客户端已经连接了服务端,服务端必须断开已经存在的客户端
- 服务端必须处理相应的CleanSession
- 服务端必须用一个包含返回码0的CONNACK包作为CONNECT包的确认。
- 启动消息传递和维持监测,客户端可以在发送CONNECT包之后立马发送其他控制包,客户端不需要等待CONNACK包。如果服务端拒绝了CONNECT,就不要处理客服端在CONNECT包之后发送的任何数据
CONNACK包是服务端发送的用来相应客户端CONNECT包的一种数据包且是从服务端发往客户端的第一个包。如果客户端在一个合理的时间内没有收到服务端的CONNACK包,客户端应该关闭网络连接,“合理”的时间取决于应用的类型和沟通。
5.1、固定包头
除了**剩余长度是可变包头的长度(即2个字节)**外,其固定包头整体结构与其他包一致。
5.2、可变包头
Figure 3.9 - CONNACK Packet variable header
| |Description |7 |6 |5 |4 |3 |2 |1 |0
|Connect Acknowledge Flags |Reserved |SP1
|byte 1 | |0 |0 |0 |0 |0 |0 |0 |X
|Connect Return code
|byte 2 | |X |X |X |X |X |X |X |X
可变包头 依次由连接确认标识(中存在Session Present)、连接返回码组成,无载荷部分。
5.2.1、连接确认标识
可变包头的字节1是“连接确认标识”,其中位7-1是保留位必须设置为0
5.2.1、Session Present
Session Present标识使得客户端能够建立连接,不论客户端和服务端在是否已经存储了会话状态上达成共识,连接确认标识”的位0(SP1)是Session Present标识:
- 如果服务端接受了一个CleanSession设置为1的连接,服务端必须将CONNACK包中的Session Present设置为0,并且CONNACK包的返回码也设置为0。
- 如果服务端接受了一个CleanSession设置为0的连接,Session Present的值取决于服务端是否已经存储了客户端Id对应的绘画状态。如果服务端已经存储了会话状态,CONNACK包中的Session Present必须设置为1[MQTT-3.2.2-2]。如果服务端没有存储会话状态,CONNACK包的Session Present必须设置为0。另外CONNACK包中的返回码必须设为0。
一旦会话的初始设置完成,存储会话状态的客户端会期望服务端也存储了会话状态。万一Session Present的值不符合预期,客户端可以选择是继续处理这个会话还是断开连接。客户端可以通过断开连接,把CleanSession设置为1重新连接,然后再断开连接,来决定客户端和服务端的会话状态。
5.2.3、连接返回码
可变包头的第二个字节是连接返回码,无符号单字节连接返回码字段的值如下表所列
Value | Return Code Response | Description |
0 | 0x00 Connection Accepted | Connection accepted |
1 | 0x01 Connection Refused, unacceptable protocol version | The Server does not support the level of the MQTT protocol requested by the Client |
2 | 0x02 Connection Refused, identifier rejected | The Client identifier is correct UTF-8 but not allowed by the Server |
3 | 0x03 Connection Refused, Server unavailable | The Network Connection has been made but the MQTT service is unavailable |
4 | 0x04 Connection Refused, bad user name or password | The data in the user name or password is malformed |
5 | 0x05 Connection Refused, not authorized | The Client is not authorized to connect |
6-255 | Reserved for future use |
如果服务端收到了一个格式良好的CONNECT包,但是服务端由于某种原因不能处理,那么服务端应该尝试发送一个带有非零返回码的CONNACK包。如果服务端发送了一个包含非零返回码的CONNACK包,必须关闭网络连接。
如果上表中的返回码都不适用,那么服务端必须直接关闭网络连接,不发送CONNACK包
未完待续…