--- title: protocol-app-mqtt-6-subscribe date: 2020-02-07 11:26:51 categories: tags: - mqtt - protocol ---

背景

之前我们提到了怎么发布消息对应的报文;现在我们来看,订阅一个主题的报文是怎么样的。

SUBSCRIBE - 订阅主题

客户端向服务端发送SUBSCRIBE报文用于创建一个或多个订阅。每个订阅注册客户端关心的一个或多个主题。为了将应用消息转发给与那些订阅匹配的主题,服务端发送PUBLISH报文给客户端。SUBSCRIBE报文也(为每个订阅)指定了最大的QoS等级,服务端根据这个发送应用消息给客户端。

SUBSCRIBE 的 固定报头

Bit

7

6

5

4

3

2

1

0

byte 1

MQTT控制报文类型 (0x8)

保留位(0x2)

1

0

0

0

0

0

1

0

byte 2

剩余长度

SUBSCRIBE控制报固定报头的第3,2,1,0位是保留位,必须分别设置为0,0,1,0。
剩余长度字段 等于可变报头的长度(2字节)加上有效载荷的长度。

SUBSCRIBE 的 可变头

SUBSCRIBE 的 可变头 中只有 报文标识符(Packet Identifier) 这一个字段。

报文标识符(Packet Identifier) 占用2个字节。没什么新的知识点,这里不再介绍。

SUBSCRIBE 的 有效荷载

SUBSCRIBE报文的有效载荷必须包含至少一对主题过滤器QoS等级字段组合

描述

7

6

5

4

3

2

1

0

主题过滤器

byte 1

长度 MSB

byte 2

长度 LSB

byte 3..N

主题过滤器(Topic Filter)

服务质量要求(Requested QoS)

保留位

服务质量等级

byte N+1

0

0

0

0

0

0

X

X

当前版本的协议没有用到服务质量要求(Requested QoS)字节的高六位。如果其中的任何位是非零值,或者QoS不等于0,1或2,服务端必须认为SUBSCRIBE报文是不合法的并关闭网络连接。

每一个过滤器后面跟着一个字节,这个字节被叫做 服务质量要求(Requested QoS)。它给出了服务端向客户端发送应用消息所允许的最大QoS等级。

主题过滤器列表:表示客户端想要订阅的主题,必须是UTF-8字符串。服务端应该支持包含通配符的主题过滤器。如果服务端选择不支持包含通配符的主题过滤器,必须拒绝任何包含通配符过滤器的订阅请求。

SUBSCRIBE 的消息体中包含 Client 想要订阅的主题列表,列表中的每一项由订阅主题名和对应的 QoS 组成。主题名中可以包含通配符,单层通配符'+'和多层通配符'#'。使用包含通配符的主题名可以订阅满足匹配条件的所有主题。(下同)为了和 PUBLISH 中的主题区分,我们叫 SUBSCRIBE 中的主题名为主题过滤器(Topic Filter)。关于主题过滤器,在文章末尾有介绍。

请求的最大服务质量等级QoS:字段编码为一个字节,

主题过滤器 和 QoS等级组合是连续地打包。

SUBSCRIBE 报文内容示例

# 使用的命令: mosquitto_sub -v  -u admin -P root -t 'topic' -q 2  -t 'a\b'

MQ Telemetry Transport Protocol, Subscribe Request
    Header Flags: 0x82, Message Type: Subscribe Request
        1000 .... = Message Type: Subscribe Request (8)
        .... 0010 = Reserved: 2
    Msg Len: 20
    Message Identifier: 1
    Topic Length: 7
    Topic: 'topic'
    Requested QoS: Exactly once delivery (Assured Delivery) (2)
    Topic Length: 5
    Topic: 'a\b'
    Requested QoS: Exactly once delivery (Assured Delivery) (2)


0040   82 14 00 01 00 07 27 74 6f 70 69 63 27 02 00 05   ......'topic'...
0050   27 61 5c 62 27 02                                 'a\b'.

SUBSCRIBE 响应

服务端收到客户端发送的一个SUBSCRIBE报文时,必须使用SUBACK报文响应。SUBACK报文必须和等待确认的SUBSCRIBE报文有相同的报文标识符。

允许服务端在发送SUBACK报文之前就开始发送与订阅匹配的PUBLISH报文。

关于 QoS 等级 与 流程可以参考 :《Qos等级 与 会话》 PUBLISH报文 中的 Packet Identifier 是什么,下面 的 Packet Identifier便是什么。

如果服务端收到一个SUBSCRIBE报文,报文的主题过滤器与一个现存订阅的主题过滤器相同,那么必须使用新的订阅彻底替换现存的订阅。新订阅的主题过滤器和之前订阅的相同,但是它的最大QoS值可以不同。与这个主题过滤器匹配的任何现存的保留消息必须被重发,但是发布流程不能中断。

如果主题过滤器不同于任何现存订阅的过滤器,服务端会创建一个新的订阅并发送所有匹配的保留消息。

如果服务端收到包含多个主题过滤器的SUBSCRIBE报文,它必须如同收到了一系列的多个SUBSCRIBE报文一样处理那个,除了需要将它们的响应合并到一个单独的SUBACK报文发送 [MQTT-3.8.4-4]。

服务端发送给客户端的SUBACK报文对每一对主题过滤器 和QoS等级都必须包含一个返回码。这个返回码必须表示那个订阅被授予的最大QoS等级,或者表示这个订阅失败 [MQTT-3.8.4-5]。服务端可以授予比订阅者要求的低一些的QoS等级。为响应订阅而发出的消息的有效载荷的QoS必须是原始发布消息的QoS和服务端授予的QoS两者中的最小值。如果原始消息的QoS是1而被授予的最大QoS是0,允许服务端重复发送一个消息的副本给订阅者 [MQTT-3.8.4-6]。

非规范示例
对某个特定的主题过滤器,如果正在订阅的客户端被授予的最大QoS等级是1,那么匹配这个过滤器的QoS等级0的应用消息会按QoS等级0分发给这个客户端。这意味着客户端最多收到这个消息的一个副本。从另一方面说,发布给同一主题的QoS等级2的消息会被服务端降级到QoS等级1再分发给客户端,因此客户端可能会收到重复的消息副本。

如果正在订阅的客户端被授予的最大QoS等级是0,那么原来按QoS等级2发布给客户端的应用消息在繁忙时可能会丢失,但是服务端不应该发送重复的消息副本。发布给同一主题的 QoS等级1的消息在传输给客户端时可能会丢失或重复。

使用QoS等级2订阅一个主题过滤器等于是说:我想要按照它们发布时的QoS等级接受匹配这个过滤器的消息 。这意味着,确定消息分发时可能的最大QoS等级是发布者的责任,而订阅者可以要求服务端降低QoS到更适合它的等级。

SUBACK – 订阅确认 报文

服务端发送SUBACK报文给客户端,用于确认它已收到并且正在处理SUBSCRIBE报文。

SUBACK报文包含一个返回码清单,它们指定了SUBSCRIBE请求的每个订阅被授予的最大QoS等级。

SUBACK 的 固定报头

Bit

7

6

5

4

3

2

1

0

byte 1

MQTT控制报文类型 (0x9)

保留位(0x0)

1

0

0

1

0

0

0

0

byte 2

剩余长度

剩余长度字段:等于可变报头的长度加上有效载荷的长度。

SUBACK 的 可变报头

可变报头包含等待确认的SUBSCRIBE报文的报文标识符,占用2个字节。

SUBACK 的 有效载荷

有效载荷包含一个返回码清单(n个返回码)。每个返回码对应等待确认的SUBSCRIBE报文中的一个主题过滤器.

每一个返回码占用1个字节,允许的返回码值:

  • 0x00 - 最大QoS 0
  • 0x01 - 成功 – 最大QoS 1
  • 0x02 - 成功 – 最大 QoS 2
  • 0x80 - Failure 失败

0x00, 0x01, 0x02, 0x80之外的SUBACK返回码是保留的,不能使用

返回码的顺序必须和SUBSCRIBE报文中主题过滤器的顺序相同。

SUBACK 的报文示例

MQ Telemetry Transport Protocol, Subscribe Ack
    Header Flags: 0x90, Message Type: Subscribe Ack
        1001 .... = Message Type: Subscribe Ack (9)
        .... 0000 = Reserved: 0
    Msg Len: 4
    Message Identifier: 1
    Granted QoS: Exactly once delivery (Assured Delivery) (2)
    Granted QoS: Exactly once delivery (Assured Delivery) (2)


0040   90 04 00 01 02 02                                 ......

UNSUBSCRIBE – 取消订阅 报文

客户端发送UNSUBSCRIBE报文给服务端,用于取消订阅主题。

在没有介绍 UNSUBSCRIBE 报文的格式,各位读者能否猜测 UNSUBSCRIBE 报文的格式是怎么样的呢?

如果清楚 SUBSCRIBE 报文 那么聪明的读者可能一下子就知道了。
其实,在 UNSUBSCRIBE 报文中,除了 有效荷载中不包含Qos等级,其他都是和 UNSUBSCRIBE 非常相似。
那么 UNSUBACK 的报文格式呢?

UNSUBSCRIBE 的 固定报头

Bit

7

6

5

4

3

2

1

0

byte 1

MQTT控制报文类型 (0xa)

保留位(**0x2**)

1

0

0

0

0

0

1

0

byte 2

剩余长度

UNSUBSCRIBE控制报固定报头的第3,2,1,0位是保留位,必须分别设置为0,0,1,0。
剩余长度字段 等于可变报头的长度(2字节)加上有效载荷的长度。

UNSUBSCRIBE 的 可变头

UNSUBSCRIBE 的 可变头 中只有 报文标识符(Packet Identifier) 这一个字段。

报文标识符(Packet Identifier) 占用2个字节。没什么新的知识点,这里不再介绍。

UNSUBSCRIBE 的 有效荷载

UNSUBSCRIBE报文的有效载荷包含客户端想要取消订阅的主题过滤器列表。UNSUBSCRIBE报文中的主题过滤器必须是连续打包的。
SUBSCRIBE报文的有效载荷必须包含至少1个主题过滤器

主题过滤器列表:表示客户端想要订阅的主题,必须是UTF-8字符串。服务端应该支持包含通配符的主题过滤器。如果服务端选择不支持包含通配符的主题过滤器,必须拒绝任何包含通配符过滤器的订阅请求。

UNSUBSCRIBE 的 响应

UNSUBSCRIBE报文提供的主题过滤器(无论是否包含通配符)必须与服务端持有的这个客户端的当前主题过滤器集合逐个字符比较。如果有任何过滤器完全匹配,那么它(服务端)自己的订阅将被删除,否则不会有进一步的处理。

如果服务端删除了一个订阅:

  • 必须停止分发任何新消息给这个客户端。
  • 必须完成分发任何已经开始往客户端发送的QoS 1和QoS 2的消息。
  • 可以继续发送任何现存的准备分发给客户端的缓存消息。

服务端必须发送UNSUBACK报文响应客户端的UNSUBSCRIBE请求。UNSUBACK报文必须包含和UNSUBSCRIBE报文相同的报文标识符。

即使没有删除任何主题订阅,服务端也必须发送一个UNSUBACK响应。

如果服务端收到包含多个主题过滤器的UNSUBSCRIBE报文,它必须如同收到了一系列的多个UNSUBSCRIBE报文一样处理那个报文,除了将它们的响应合并到一个单独的UNSUBACK报文外。

也就是说,它会发很多个 UNSUBACK 报文回来。

UNSUBSCRIBE 的报文示例

# mosquitto_sub -v  -u admin -P root -t 'topic' -q 2  -t 'a\b'  -U 'topic'
## -U 代表 取消订阅某个主题。

MQ Telemetry Transport Protocol, Unsubscribe Request
    Header Flags: 0xa2, Message Type: Unsubscribe Request
        1010 .... = Message Type: Unsubscribe Request (10)
        .... 0010 = Reserved: 2
    Msg Len: 11
    Message Identifier: 2
    Topic Length: 7
    Topic: 'topic'

0040   a2 0b 00 02 00 07 27 74 6f 70 69 63 27            ......'topic'

UNSUBACK – 取消订阅确认 报文

服务端发送UNSUBACK报文给客户端用于确认收到UNSUBSCRIBE报文。

UNSUBACK 报文是对UNSUBSCRIBE报文的响应。

UNSUBACK 报文的 组成 (没有 有效载荷) = 一个固定头(0xb 0x02) + Packet Identifier (from UNSUBSCRIBE's Packet Identifier)。

UNSUBACK 的抓包示例

#mosquitto_sub -v  -u admin -P root -t 'topic' -q 2  -t 'a\b'  -U 'topic' -U 'a\b'
## 我不知道 mosquitto_sub 内部的处理 机制是不是 发了 一个个 Unsubscribe 报文,但从结果来看是这样的。
687	17.900459	::1	::1	MQTT	168	Subscribe Request (id=1) ['topic'] ['a\b']
689	17.900477	::1	::1	MQTT	150	Unsubscribe Request (id=2)
691	17.900492	::1	::1	MQTT	146	Unsubscribe Request (id=3)
693	17.900538	::1	::1	MQTT	136	Subscribe Ack (id=1)
695	17.900576	::1	::1	MQTT	132	Unsubscribe Ack (id=2)
697	17.900605	::1	::1	MQTT	132	Unsubscribe Ack (id=3)

附录:主题通配符 与 主题过滤器

当我们订阅主题的时候,可以使用通配符来匹配订阅的多个主题。
MQTT 的主题是具有层级概念的,不同的层级之间用'/'分割。

单层通配符'+':用来指代任意一个层级。

例如'home/2ndfloor/+/temperature',可匹配:

  • home/2ndfloor/201/temperature
  • home/2ndfloor/202/temperature

不可匹配:

  • home/2ndfloor/201/livingroom/temperature
  • home/3ndfloor/301/temperature

多层通配符'#':可以用来指定任意多个层级

'#'和'+'的区别在于:
1)'+'用来指代任意一个层级;而'#':可以用来指定任意多个层级
2)但是'#'必须是 Topic Filter 的最后一个字符,同时它必须跟在'/'后面,除非 Topic Filter 只包含'#'这一个字符。

例如'home/2ndfloor/#',可匹配:

  • home/2ndfloor
  • home/2ndfloor/201
  • home/2ndfloor/201/temperature
  • home/2ndfloor/202/temperature
  • home/2ndfloor/201/livingroom/temperature

不可匹配:

  • home/3ndfloor/301/temperature

注意:'#'是一个合法的 Topic Filter,代表所有的主题;而'home#'不是一个合法的 Topic Filter,因为'#'号需要跟在'/'后面。

如果说我的文章对你有用,只不过是我站在巨人的肩膀上再继续努力罢了。