MQTT控制报文
文章目录
控制报文结构
MQTT 控制报文由三部分组成,按照 下图描述的顺序:
固定报头
每个 MQTT 控制报文都包含一个固定报头。下图描述了固定报头的格式,固定报头的第一字节是固定的,用于表示控制报文类型和标志位,不再赘述请参考 MQTT协议规范
紧接的是剩余长度这里重点记录一下剩余长度的含义以及计算方式。
剩余长度
**剩余长度(Remaining Length)**表示当前报文剩余部分的字节数,包括可变报头(Variable header)和负载(Payload)的数据
- 剩余长度字段使用一个变长度编码方案,从byte2开始,最多可达4个字节,也就是说可能用byte2~byte5来编码表示剩余长度
- 对于小于128的值使用单字节编码,大于128的值使用低7位编码数据,最高位指示后续是否有更多的字节
展开来说:
byte2 ~ byte 5 | 二进制 | 解释 | 剩余长度最小值 | 剩余长度最大值 |
---|---|---|---|---|
byte2 | 0XXX XXXX | 高位为0,仅byte2用于编码剩余长度 | 0 (0x00) | 127 (0x7F) |
byte2byte3 | 1XXX XXXX 0XXX XXXX | 高位为1,byte2和byte3用于编码剩余长度 | 128 (0x80, 0x01) | 16 383 (0xFF, 0x7F) |
byte2byte3byte4 | 1XXX XXXX 1XXX XXXX 0XXX XXXX | byte2和byte3的高位为1,byte2,byte3和byte4用于编码剩余长度 | 16384 (0x80, 0x80, 0x01) | 2097151 (0xFF, 0xFF, 0x7F) |
Byte2byte3byte4byte5 | 1XXX XXXX 1XXX XXXX 1XXX XXXX 0XXX XXXX | byte2,byte3和byte4的高位为1,byte2,byte3,byte4和byte5用于编码剩余长度 | 2097152 (0x80, 0x80, 0x80, 0x01) | 268435455 (0xFF, 0xFF, 0xFF, 0x7F) |
剩余长度不包括用于编码剩余长度字段本身的字节数
剩余长度单位是字节
对于byte2~byte5,计算剩余长度数值的时候,遵循以下的算法:
1len = byte5(16进制)&~(0x80)*128^3 + byte4(16进制)&~(0x80)*128^2 + byte3(16进制)&~(0x80)*128^1 + byte2(16进制)&~(0x80)*128^0
以剩余长度最大值0xFF, 0xFF, 0xFF, 0x7F,计算过程如下:
- 0xFF - 对于byte2~byte5,最高位是标记位,不能参与计算,去掉最高位就是0X7F(0111 1111),对应十进制127
- 公式里的 bytex(16进制)&~(0x80)目的就是去掉最高位标记位
- 套用上面的公式:
127*128^3 + 127*128^2 + 127*128 + 127 = 268435455 bytes = 256MB
MQTT控制报文
可变报头位于剩余长度和Payload之间。可变不是可选(optional)的意思,可变报头的内容根据报文类型的不同而不同。具体取决于报文类型,有的类型包含可变报头,有的没有。
CONNECT - 连接服务端(Broker)
可变报头
包括:协议名长度(Protocol Name Length)、 协议名(Protocol Name)、协议级别(Protocol Level/Version),连接标志(Connection Flags)和保持连接(Keep Alive)
由于v3.1.1版本的Version对应的值是4,这也是MQTT5的由来
连接标志位(Connection Flags)各bit含义如下:
1 - Clean Session:会话清除标志位。当设置为0时,服务端必须基于当前会话(通过客户端标识符识别)的状态恢复与客户端的通信,即持久化会话。持久化会话只对QoS为1或者2 的消息有效,客户端和服务端需要将QoS1和2的消息保存为会话的一部分;当设置为1时,客户端和服务端必须丢弃之前任何会话并开始一个新的会话 (保留消息不是服务端会话状态的一部分,会话终止时不能删除保留信息)
2 - Will Flag:遗嘱标志。这个是和Will Retain和Will QoS一起使用的。当遗嘱标志位0时,这几个都要置0,表明网络断开的时候不能发送医嘱信息;当遗嘱标志位设置为1时
3/4 - Will QoS:遗嘱消息QoS,值为0,1和2
5 - Will Retain:遗嘱保留
6 - Password Flag:密码标志,标识有效载荷中是否包含用户密码
7 - User Name Flag:用户名标志,标识有效载荷中是否包含用户名
保持连接(Keep Alive):以秒为单位的时间间隔,是指在客户端传输完成一个控制报文的时刻到发送下一个报文的时刻,两者之间允许空闲的最大时间间隔。
对于Keep Alive机制:
- MQTT 协议中约定 - 在 1.5*Keep Alive 的时间间隔内,如果 Broker 没有收到来自 Client 的任何数据包,那么 Broker 认为它和 Client 之间的连接已经断开;同样地, 如果 Client 没有收到来自 Broker 的任何数据包,那么 Client 认为它和 Broker 之间的连接已经断开
- 如果在一个Keep Alive时间间隔内,Client和Broker有过数据包传输(比如PUBLISH),Client 就没有必要再使用 PINGREQ了。在网络资源比较紧张的情况下这点很重要
- Keep Alive 值是由 Client 指定的,不同的 Client 可以指定不同的值
- Keep Alive 的最大值为 18 小时 12 分 15 秒
- Keep Alive 值如果设为 0 的话,代表不使用 Keep Alive 机制
有效载荷
客户端标识符(Client ID):客户端和服务器必须使用Client ID标识两者之间的MQTT会话状态
遗嘱主题(Will Topic):可变报头中的连接标志位中设置了Will Flag时,就必须设置遗嘱主题
遗嘱消息(Will Message):发布到遗嘱主题的消息
用户名(User Name) | 密码(Password):用于连接MQTT的Broker的用户名和密码
CONNACK - 确认连接请求
服务端发送给客户端的第一个报文必须是CONNACK
可变报头
连接确认标志位(Acknowledge Flags):总共一个字节,7~1位为保留位,值都为0。第零位也叫SP位,其值和ONNECT里的Clean Session有关
- 如果Clean Session为1,那么每次连接都是新的会话,SP的值置为0
- 如果Clean Session为0,服务根据客户端标识判断是否已经有该客户端会话,那么SP的值置为1,否则为0
连接返回码(Return Code):连接返回码,用于标识连接成功与否。如果连接失败,如下表,根据该值判断连接失败的原因
值 | 返回码响应 | 描述 |
---|---|---|
0 | 0x00连接已接受 | 连接已被服务端接受 |
1 | 0x01连接已拒绝,不支持的协议版本 | 服务端不支持客户端请求的MQTT协议级别 |
2 | 0x02连接已拒绝,不合格的客户端标识符 | 客户端标识符是正确的UTF-8编码,但服务端不允许使用 |
3 | 0x03连接已拒绝,服务端不可用 | 网络连接已建立,但MQTT服务不可用 |
4 | 0x04连接已拒绝,无效的用户名或密码 | 用户名或密码的数据格式无效 |
5 | 0x05连接已拒绝,未授权 | 客户端未被授权连接到此服务器 |
6-255 | 保留 |
如果认为上表中的所有连接返回码都不太合适,那么服务端必须关闭网络连接,不需要发送 CONNACK 报文
有效载荷
无
DISCONNECT - 断开连接
客户端发送给服务端的最后一个控制报文,表示客户端正常断开连接。除了固定头以外,就不包含可变报头和有效负载
PUBLISH - 发布消息
固定报头
大多数MQTT固定报头标志位都是保留位,但是PUBLISH报文的标志位有其特殊的用途,如下图所示:
- DUP - 如果为0,说明这是第一次发送这个PUBLISH报文;如果为1,则对应的QoS为1或者2,说明这是重发的报文
- RETAIN - 保留消息标志
当客户端为新接入的客户端,且订阅了匹配保留消息的主题,服务端发送给新客户端的PUBLISH 消息的RETAIN标志位要置为1;否则,如果是一个已存在的客户端且匹配到了保留消息的主题,那么服务发送的PUBLISH消息的RETAIN标志位置0
可变报头
可变报头按顺序包含主题名(Topic Name)
和报文标识符(Packet Identifier)
主题名:服务端发送的PUBLISH报文的主题名不能包含通配符
报文标识符:当QoS为1或者2的时候,报文标识符才能出现在PUBLISH报文中
报文标识符用来区分报文,特别是在重发的报文中用来标识是否是同一个报文,并在需要应答的场景中用于确定是对哪个发送报文的应答。可变报头的报文标识符(Message Identifier)字段存在于在多个类型的报文里(占用2个字节)。这些报文是:PUBLISH(QoS > 0时)
, PUBACK
,PUBREC
,PUBREL
,PUBCOMP
,SUBSCRIBE,
SUBACK
,UNSUBSCRIBE
,UNSUBACK
。
报文标识符的一些规则:
- QoS0的PUBLISH报文不能包含报文标识符
- 报文标识符默认是从1(0x01)开始并自增,最大为255(0xff)
- 客户端/服务端每次发送一个新的PUBLISH报文时都必须分配一个当前未使用的报文标识符
- 对于重发的PUBLISH报文,报文标识符需要和之前的保持一致
- 当客户端和服务端完成相应的QoS流程后,报文标识符可以释放被再一次使用
有效载荷
提供给上层应用的具体的消息
PUBACK - 发布确认报文 (QoS 1)
PUBACK 报文比较简单,它是对QoS 1等级的PUBLISH报文的响应。PUBACK 报文的组成(没有有效载荷) = 一个固定头(0x40 0x20) + 报文标识符。
PUBREC – 发布收到报文 (QoS 2,第一步)
PUBREC报文是对QoS等级2的PUBLISH报文的响应。PUBREC 报文的 组成 (没有 有效载荷) = 一个固定头(0x50 0x20) + 报文标识符。
PUBREL – 发布释放(QoS 2,第二步)
PUBREL报文是对PUBREC报文的响应。PUBREL 报文的 组成 (没有 有效载荷) = 一个固定头(0x62 0x02) + 报文标识符。
PUBREL控制报文固定报头的第3,2,1,0位是保留位,必须被设置为0,0,1,0。
PUBCOMP – 发布完成(QoS 2,第三步)
PUBCOMP报文是对PUBREL报文的响应。PUBCOMP 报文的 组成 (没有 有效载荷) = 一个固定头(0x70 0x02) + 报文标识符。
SUBSCRIBE - 订阅主题
可变报头
SUBSCRIBE的可变头中只有报文标识符(Message Identifier)
这一个字段。
有效载荷
SUBSCRIBE 报文的有效载荷包含了一个主题过滤器列表,它们表示客户端想要订阅的主题。这个列表包含了一些列的主题过滤器和QoS的组合
SUBSCRIBE响应
服务端收到客户端发送的一个SUBSCRIBE报文时,必须使用SUBACK报文响应。
- SUBACK报文必须和等待确认的SUBSCRIBE报文有相同的报文标识符
- 如果服务端收到一个SUBSCRIBE报文,报文的主题过滤器与一个现存订阅的主题过滤器相同,那么必须使用新的订阅彻底替换现存的订阅。新订阅的主题过滤器和之前订阅的相同,但是它的最大QoS值可以不同。与这个主题过滤器匹配的任何现存的保留消息必须被重发,但是发布流程不能中断
- 如果主题过滤器不同于任何现存订阅的过滤器,服务端会创建一个新的订阅并发送所有匹配的保留消息
- 如果服务端收到包含多个主题过滤器的SUBSCRIBE报文,它必须如同收到了一系列的多个SUBSCRIBE报文一样处理那个,除了需要将它们的响应合并到一个单独的SUBACK报文发送
- 服务端发送给客户端的SUBACK报文对每一对主题过滤器 和QoS等级都必须包含一个返回码。这个返回码必须表示那个订阅被授予的最大QoS等级,或者表示这个订阅失败 。服务端可以授予比订阅者要求的低一些的QoS等级。为响应订阅而发出的消息的有效载荷的QoS必须是原始发布消息的QoS和服务端授予的QoS两者中的最小值。如果原始消息的QoS是1而被授予的最大QoS是0,允许服务端重复发送一个消息的副本给订阅者(什么情况下会重传?QoS应该降级为0,不应该重传才是??)
订阅降级的一些例子
如果正在订阅的客户端被授予的最大 QoS 等级是 0,那么原来按 QoS 等级 2 发布给客户端的应用 消息在繁忙时可能会丢失,但是服务端不应该发送重复的消息副本。发布给同一主题的 QoS 等级 1 的消息在传输给客户端时可能会丢失或重复。
对某个特定的主题过滤器,如果正在订阅的客户端被授予的最大QoS等级是1,那么匹配这个过滤器的QoS等级0的应用消息会按QoS等级0分发给这个客户端。这意味着客户端最多收到这个消息的一个副本。从另一方面说,发布给同一主题的QoS等级2的消息会被服务端降级到QoS等级1再分发给客户端,因此客户端可能会收到重复的消息副本。
使用QoS等级2订阅一个主题过滤器等于是说:我想要按照它们发布时的QoS等级接受匹配这个过滤器的消息 。这意味着,确定消息分发时可能的最大QoS等级是发布者的责任,而订阅者可以要求服务端降低QoS到更适合它的等级。
SUBACK - 订阅确认
可变报头
SUBACK 的可变头中只有报文标识符(Message Identifier)
这一个字段。
有效载荷
有效载荷包含一个返回码清单。每个返回码对应等待确认的SUBSCRIBE报文中的一个主题过滤器。下图中的SUBACK报文里的Granted QoS就是服务端授予客户端订阅的最大QoS。
每一个返回码占用1个字节,允许的返回码值:
- 0x00 - 最大QoS 0
- 0x01 - 成功 – 最大QoS 1
- 0x02 - 成功 – 最大 QoS 2
- 0x80 - Failure 失败
0x00, 0x01, 0x02, 0x80之外的SUBACK返回码是保留的,不能使用。返回码的顺序必须和SUBSCRIBE报文中主题过滤器的顺序相同。
UNSUBSCRIBE – 取消订阅报文
客户端发送UNSUBSCRIBE报文给服务端,用于取消订阅主题。在 UNSUBSCRIBE 报文中,除了有效荷载中不包含QoS等级,其他都是和 SUBSCRIBE 非常相似。
可变报头
UNSUBSCRIBE的可变头中只有报文标识符(Message Identifier)
这一个字段。
有效载荷
UNSUBSCRIBE报文的有效载荷包含客户端想要取消订阅的主题过滤器列表。UNSUBSCRIBE报文中的主题过滤器必须是连续的。SUBSCRIBE报文的有效载荷必须包含至少1个主题过滤器。
UNSUBSCRIBE响应
UNSUBSCRIBE 报文提供的主题过滤器(无论是否包含通配符)必须与服务端持有的这个客户端的当前主题过滤器集合逐个字符比较。如果有任何过滤器完全匹配,那么它(服务端)自己的订阅将被删除,否则不会有进一步的处理。
如果服务端删除了一个订阅:
- 它必须停止分发任何新消息给这个客户端
- 它必须完成分发任何已经开始往客户端发送的QoS 1和QoS 2的消息
- 它可以继续发送任何现存的准备分发给客户端的缓存消息
UNSUBACK – 取消订阅确认报文
服务端发送UNSUBACK报文给客户端用于确认收到UNSUBSCRIBE报文。UNSUBACK报文是对UNSUBSCRIBE报文的响应。
UNSUBACK 报文的组成 (没有有效载荷) = 一个固定头(0xb 0x02) + Message Identifier。
PINGREQ – 心跳请求报文
当Client在一个Keep Alive时间间隔内没有向Broker发送任何数据包时,它应该向Broker发送 PINGREQ数据包。
PINGREQ数据包没有可变头和有效载荷,那么PINGREQ 报文的全部内容(共2个字节)就是 : 0xc0 0x00
PINGRESP – 心跳响应 报文
当Broker收到来自Client的PINGREQ数据包,它应该回复Client一个PINGRESP数据包。
PINGRESP数据包没有可变头和有效载荷,那么PINGRESP报文的全部内容(共2个字节)就是 : 0xd0 0x00