9.18. MQTT,逐位元組解析¶
到了這個階段,相機已經具備在開放網際網路上與真實服務通訊所需的每一個元件:TCP socket、用來包裹它的 TLS、用來為對端命名的 DNS,以及讓同一份指令碼能在連線開啟時同時處理其他工作的 asyncio。MQTT 是第一個把這些全部整合在一起、形成已部署產品實際會使用之物的線上通訊協定。
本頁面說明通訊協定本身——線上格式、各參與者所扮演的角色,以及其設計上的取捨——說明得夠誠實,好讓隨附的 mqtt 用戶端看起來像是對已知事物的顯而易見之封裝,而不是一次需要信念的跳躍。
9.18.1. 發布/訂閱 vs 請求/回應¶
HTTP——大多數相機專案最先採用的通訊協定——屬於 請求/回應 模式。用戶端向特定的伺服器索取特定的資源;伺服器作答。每一次交換都是一對一的,而且兩端事先就知道彼此的位址。
MQTT 屬於 發布/訂閱 模式。用戶端連線到一個位於中間的第三方,稱為 broker(代理伺服器)。發布者 將訊息送往一個具名的 topic(主題),而無須知道或在意是誰在接收。訂閱者 告訴 broker 它想要哪些主題,之後便會收到每一則發布到那些主題的訊息。broker 負責扇出:在 yard-cam/motion 上的一次發布,會送達每一個訂閱了 yard-cam/motion 的裝置,無論它們有零個、一個或五十個。
這個模型上的改變帶來三項結果:
解耦。 發布者不必知道訂閱者是否存在。訂閱者可以來去自如,而發布者毫無察覺。新增第二個儀表板只需在新儀表板上加一行程式碼;相機本身完全不變。
扇出。 broker 處理每一份副本,因此無論有多少裝置在讀取,相機都只送出一個封包。這正是 MQTT 設計時所針對的使用情境。
非對稱性。 現在 broker 成了一個必需的基礎設施——沒有它,通訊協定就無法運作。對於家用專案而言,這通常是一個免費的公開 broker(
test.mosquitto.org、broker.hivemq.com),或是你自行架設的一個小型 broker。
9.18.2. 主題¶
主題是以斜線分隔的字串。慣例是最一般的放在左邊,最具體的放在右邊:
yard-cam/motion
yard-cam/temperature
workshop-cam/motion
workshop-cam/temperature/sensor-3
有兩個萬用字元可用於 訂閱(不可用於發布):
+比對單一層級。+/motion訂閱每一台相機的 motion 主題;yard-cam/+訂閱每一個 yard-cam 子主題。#比對一個或多個結尾層級。yard-cam/#訂閱yard-cam/motion、yard-cam/temperature、yard-cam/temperature/sensor-3,以及yard-cam/底下的任何其他內容。它必須出現在訂閱字串的結尾。
主題字串區分大小寫。依規格,開頭的 $ 標記著 broker 內部的主題($SYS/...),發布者不應寫入這些主題。
9.18.3. 封包格式¶
MQTT 在 TCP 之上運作。每一個控制封包都以一個位元組的 固定標頭 開頭,後接一個可變長度的 Remaining Length 欄位,然後是依封包類型而定的 可變標頭,最後是 payload。同一套外層格式涵蓋每一個命令——CONNECT、PUBLISH、SUBSCRIBE、PUBACK、DISCONNECT 以及其餘的——這正是為什麼 MQTT 用戶端可以用區區數百行寫成的原因。
固定標頭為一個位元組:
第 7..4 位元是 控制封包類型。
0x3是 PUBLISH(因此第一個位元組通常以0x3?開頭)。0x1是 CONNECT、0x2是 CONNACK、0x8是 SUBSCRIBE、0xC是 PINGREQ、0xE是 DISCONNECT 等等。第 3..0 位元是依封包類型而定的 旗標。對 PUBLISH 而言,這些旗標編碼了 DUP 重傳旗標、QoS 等級(2 位元),以及 RETAIN 旗標。
Remaining Length 是一個 1 到 4 位元組的可變長度整數,計算其自身之後的每一個位元組。每個位元組的最高位元是延續標記——1 表示「後面還有一個長度位元組」,0 表示「這是最後一個」。長度小於 128 可塞進一個位元組;較大的 payload 則使用更多位元組。可編碼的最大長度為 256 MiB。
對 PUBLISH 而言,可變標頭是主題名稱——一個 2 位元組的長度,然後是 UTF-8 位元組——後接一個 2 位元組的 封包識別碼,此識別碼僅在 QoS 為 1 或 2 時才存在。其餘的位元組是 payload,被通訊協定視為不透明的位元組。
一個將 ok 以 QoS-0 發布到 a/b 的最小封包為:
30 07 00 03 'a' '/' 'b' 'o' 'k'
30—— PUBLISH,所有旗標皆為零。07—— 後面跟著 7 個位元組。00 03—— 主題長度 3。'a' '/' 'b'—— 主題。'o' 'k'—— payload。
線上九個位元組,訊息便會送達 broker 上每一個訂閱了 a/b 的訂閱者。
9.18.4. QoS 等級¶
服務品質(Quality-of-Service)控制 broker(與用戶端)為了確保送達要費多大的力氣。三個等級:
QoS 0 —— 至多一次。 發送後不管。PUBLISH 封包送出後從不確認。若 TCP 成功送達,broker 便轉送。若連線在傳送途中中斷,訊息就遺失了。大多數感測器遙測在 QoS 0 下都沒問題——在一個每 30 秒發出一次的串流中,漏掉單一筆溫度讀數並無關緊要。
QoS 1 —— 至少一次。 發布者納入一個封包識別碼並等待 PUBACK。若在逾時前沒有 PUBACK 到達,發布者便設定 DUP 旗標重傳。broker 最終可能會在同一等級上將同一則訊息送給訂閱者兩次;訂閱者必須願意處理重複的訊息。
QoS 2 —— 恰好一次。 一個四步驟的交握(PUBREC / PUBREL / PUBCOMP)確保訊息恰好送達一次,即使跨越重新連線亦然。在往返次數與 broker 狀態上代價高昂。少有相機應用程式需要它。
隨附的 mqtt 用戶端實作了 QoS 0 與 QoS 1;若你要求 QoS 2 則會引發例外。對於一台回報感測器讀數的相機而言,QoS 0 幾乎總是正確的答案。
9.18.5. 保留訊息與遺言¶
有兩項功能值得了解,因為它們改變了 broker 對你的主題所記得的內容。
RETAIN。 若一個 PUBLISH 設定了 RETAIN 旗標,broker 會儲存該訊息,並在每一個 未來 的訂閱者一訂閱的當下就把訊息轉送給它。這就是 MQTT 處理「目前的值是什麼?」的方式——感測器將其最新讀數以保留方式發布,而一個十分鐘後才訂閱的儀表板仍能收到最近一次的值,而不必等待下一次發布。以相同主題重新發布會覆寫保留值;發布一個空的 payload 則會清除它。
遺言(Last will)。 用戶端在連線時可以給 broker 一份「遺囑」:一個主題、一個 payload、一個 QoS 與一個保留旗標。若該用戶端 不乾淨地 中斷連線——TCP RESET、斷電、網路掉線而沒有 DISCONNECT 封包——broker 便會代替該用戶端發布這份遺言。訂閱者會看到它,作為相機已離線的通知。相機本身從不送出遺言;是由 broker 送出,因為到那時相機已經不在了。
9.18.6. 保持連線(Keepalive)與重新連線¶
CONNECT 攜帶一個以秒為單位的 keepalive 間隔。若用戶端沉默了那麼久,broker 便認定它已死亡。為了避免這種情況,用戶端會週期性地送出一個 PINGREQ(一個位元組:0xC0)並收到一個 PINGRESP(0xD0)回應——這是通訊協定所能攜帶的最小、最廉價的心跳。大多數相機應用程式會把 keepalive 設為 30 或 60 秒。
若 TCP 連線中斷,兩端都會察覺並從頭重新連線。中斷前所做的訂閱會遺失,除非用戶端在連線時使用了 持久工作階段;對於簡單的相機應用程式而言,重新連線時重新訂閱的模式較為簡短,而且一樣好用。
這已足夠用來閱讀 MQTT 規格,或在 socket.socket 之上手刻一個用戶端。mqtt 中隨附的用戶端正是這麼做的,再加上一套適合應用程式碼使用的合理 API。