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,因為'#'號需要跟在'/'后面。