一文讀懂物聯網 MQTT 協議之基礎特性篇
一、前言
上個月有個讀者問我物聯網 MQTT 協議實戰相關的問題,我說後面會搞,沒想到不知不覺一個月了,太忙了,再怎麼忙答應的事情還是要給讀者一個交代,所以就有了此文。
二、MQTT 協議概要
2.1 什麼是 MQTT 協議
MQTT(Message Queuing Telemetry Transport,消息隊列遙測傳輸協議),是一種基於發佈 / 訂閱(publish/subscribe)模式的 “輕量級” 通訊協議,該協議構建於 TCP/IP 協議上,由 IBM 於 1999 年發明。MQTT 協議的主要特徵是開放、簡單、輕量級和易於實現,這些特徵使得它適用於受約束的應用環境,如:
-
網絡受限
:網絡帶寬較低且傳輸不可靠 -
終端受限
:協議運行在嵌入式設備上,嵌入式終端的處理器、內存等是受限的
通過 MQTT 協議,目前已經擴展出了數十種 MQTT 服務器端程序,可以通過 PHP、Java、Python、C、C# 等語言向 MQTT 發送消息。由於開放源代碼、耗電量小等特點,MQTT 非常適用於物聯網領域,如傳感器與服務器的通信、傳感器信息採集等。
2.2 發佈 / 訂閱模式
發佈 / 訂閱模式並不是 MQTT 協議特有的模式,像我們很多消息中間件都有使用發佈 / 訂閱模式,這裏你是不是想說,這不就是我們所說的觀察者模式嘛,還真不是,這兩個模式很容易混淆。觀察者模式只有 觀察者 + 被觀察者兩個角色,而發佈 / 訂閱模式還有一個經紀人 Broker;往更深層次的講觀察者和被觀察者,是松耦合的關係,而發佈者和訂閱者,則完全不存在耦合。
在客戶端 / 服務器模型中,客戶端直接與服務器端點通信。而發佈 / 訂閱模式 pub/sub 就不一樣了,發佈 / 訂閱模式會將發送消息的發佈者 publisher 與接收消息的訂閱者 subscribers 進行分離,publisher 與 subscribers 並不會直接通信,他們甚至都不清楚對方是否存在,他們之間的交流由第三方組件 broker 代理。
pub/sub 最重要的方面是消息的發佈者與接收者(訂閱者)的解耦。這種解耦有幾個維度:
-
空間解耦:發佈者和訂閱者不需要相互瞭解(例如,不交換 IP 地址和端口)。
-
時間解耦:發佈者和訂閱者不需要同時運行。
-
同步解耦:兩個組件的操作在發佈或接收時不需要中斷。
總之,發佈 / 訂閱模式消除了傳統客戶端 / 服務器之間的直接通信,把通信這個操作交給了 broker 進行代理,並在空間、時間、同步三個維度上進行了解耦。
2.3 可擴展性
pub/sub 比傳統的客戶端 / 服務器模式有了更好的拓展,這是由於 broker 的高度並行化,並且是基於事件驅動的模式。可擴展性還體現在消息的緩存和消息的智能路由,還可以通過集羣代理來實現數百萬的連接,使用負載均衡器將負載分配到更多的單個服務器上,這就是 MQTT 的深度應用了。
2.4 消息過濾
很明顯,broker 在 pub/sub 過程中起着舉足輕重的作用。但是代理如何過濾所有消息,以便每個訂閱者只接收感興趣的消息?broker 有幾個可以過濾的選項:
-
基於主題的過濾
此過濾基於屬於每條消息的主題。接收客戶端向代理訂閱感興趣的主題,訂閱後,broker 就會確保客戶端收到發佈到 topic 中的消息。 -
基於內容的過濾
在基於內容的過濾中,broker 會根據特定的內容過濾消息,接受客戶端會經過過濾他們感興趣的內容。這種方法的一個顯著的缺點就是必須事先知道消息的內容,不能加密或者輕易修改。 -
基於類型的過濾
當使用面向對象的語言時,基於消息(事件)的類型 / 類進行過濾是一種常見做法。例如,訂閱者可以收聽所有類型爲 Exception 或任何子類型的消息。
2.5 MQTT 與消息隊列的區別
這裏你又會說了,既然 MQTT 與主流的消息的隊列都採用發佈 / 訂閱模式,那他們就是一樣的。這裏老周得再提一嘴,確實和消息隊列很多相似的地方,但還有有些差異的,下面就來說道說道:
-
消息隊列存儲消息直到消息被消費
使用消息隊列時,每條傳入消息都存儲在隊列中,直到被客戶端(通常稱爲消費者)接收。如果沒有客戶端接收到消息,消息將保持在隊列中並等待被消費。在消息隊列中,不會存在消息沒有客戶端消費的情況,但是在 MQTT 中,卻存在 topic 無 subscriber 訂閱的情況。 -
一條消息只被一個客戶端消費
另一個很大的區別是,在傳統的消息隊列中,一條消息只能被一個消費者處理。負載分佈在隊列的所有消費者之間。在 MQTT 中,行爲完全相反:訂閱主題的每個訂閱者都會收到消息,每個訂閱者有相同的負載。 -
隊列是命名的,必須顯式創建
隊列比主題嚴格得多。在使用隊列之前,必須使用單獨的命令顯式創建隊列。只有在隊列命名和創建之後,纔可以發佈或消費消息。相比之下,MQTT 主題非常靈活,可以即時創建。
三、MQTT 重要概念
3.1 MQTT Client
publisher 和 subscriber 都屬於 MQTT Client,之所以有發佈者和訂閱者這個概念,其實是一種相對的概念,就是指當前客戶端是在發佈消息還是在接收消息,發佈和訂閱的功能也可以由同一個 MQTT Client 實現。
MQTT 客戶端是運行 MQTT 庫並通過網絡連接到 MQTT 代理的任何設備(從微控制器到成熟的服務器)。例如,MQTT 客戶端可以是一個非常小的、資源受限的設備,它通過無線網絡進行連接並具有一個最低限度的庫。基本上,任何使用 TCP/IP 協議使用 MQTT 設備的都可以稱之爲 MQTT Client。MQTT 協議的客戶端實現非常簡單直接,易於實施是 MQTT 非常適合小型設備的原因之一。MQTT 客戶端庫可用於多種編程語言。例如,Android、Arduino、C、C++、C#、Go、iOS、Java、JavaScript 和 .NET。
3.2 MQTT Broker
與 MQTT Client 對應的就是 MQTT Broker,Broker 是任何發佈 / 訂閱協議的核心,根據實現的不同,代理可以處理多達數百萬連接的 MQTT Client。
Broker 負責接收所有消息,過濾消息,確定是哪個 Client 訂閱了每條消息,並將消息發送給對應的 Client,Broker 還負責保存會話數據,這些數據包括訂閱的和錯過的消息。Broker 還負責客戶端的身份驗證和授權。
3.3 MQTT Connection
MQTT 協議基於 TCP/IP。客戶端和代理都需要有一個 TCP/IP 協議支持。
MQTT 連接始終位於一個客戶端和代理之間。客戶端從不直接相互連接。要發起連接,客戶端向代理發送 CONNECT 消息。代理使用 CONNACK 消息和狀態代碼進行響應。建立連接後,代理將保持打開狀態,直到客戶端發送斷開連接命令或連接中斷。
四、消息列表
4.1 CONNECT
爲了創建連接,客戶端向代理發送命令消息。如果此 CONNECT 消息格式錯誤(根據 MQTT 規範)或打開網絡套接字和發送連接消息之間的時間過長,代理將關閉連接。
一個 MQTT 客戶端發送一條 CONNECT 連接,這條 CONNECT 連接可能會包含下面這些信息:
我們將重點關注以下選項:
-
ClientId:ClientId
的長度可以是 1-23 個字符,在一個服務器上 ClientId 不能重複。如果超過 23 個字符,則服務器返回 CONNACK 消息中的返回碼爲 Identifier Rejected。在 MQTT 3.1.1 中,如果您不需要代理持有狀態,您可以發送一個空的 ClientId。空的 ClientId 導致連接沒有任何狀態。在這種情況下,clean session 標誌必須設置爲 true,否則代理將拒絕連接。 -
Clean Session
:Clean Session 標誌告訴代理客戶端是否要建立持久會話。在持久會話 (CleanSession = false) 中,代理存儲客戶端的所有訂閱以及以服務質量(QoS)級別 1 或 2 訂閱的客戶端的所有丟失消息。如果會話不是持久的 (CleanSession = true ),代理不爲客戶端存儲任何內容,並清除任何先前持久會話中的所有信息。 -
Username/Password
:MQTT 可以發送用戶名和密碼進行客戶端認證和授權。但是,如果此信息未加密或散列,則密碼將以純文本形式發送。我們強烈建議將用戶名和密碼與安全傳輸一起使用。像 HiveMQ 這樣的代理可以使用 SSL 證書對客戶端進行身份驗證,因此不需要用戶名和密碼。 -
Will Message
:LastWillxxx 表示的是遺願,client 在連接 broker 的時候將會設立一個遺願,這個遺願會保存在 broker 中,當 client 因爲非正常原因斷開與 broker 的連接時,broker 會將遺願發送給訂閱了這個 topic(訂閱遺願的 topic)的 client。 -
KeepAlive
:keepAlive 是 client 在連接建立時與 broker 通信的時間間隔,通常以秒爲單位。這個時間指的是 client 與 broker 在不發送消息下所能承受的最大時長。
4.2 CONNACK
當 broker 收到 CONNECT 消息時,它有義務回覆 CONNACK 消息進行響應。CONNACK 消息包括兩部分內容:
-
The session present flag
:會話當前標誌 -
A connect return code
:連接返回碼
-
Session Present flag
會話當前標誌,這個標誌會告訴 client 當前 broker 是否有一個持久性會話與 client 進行交互。SessionPresent 標誌和 CleanSession 標誌有關,當 client 在 CleanSession 設置爲 true 的情況下連接時,SessionPresent 始終爲 false,因爲沒有持久性會話可以使用。如果 CleanSession 設置爲 false,則有兩種可能性,如果 ClientId 的會話信息可用,並且 broker 已經存儲了會話信息,那麼 SessionPresent 爲 true,否則如果沒有 ClientId 的任何會話信息,那麼 SessionPresent 爲 false。
-
Connect return code
CONNACK 消息中的第二個標誌是連接確認標誌。這個標誌包含一個返回碼,告訴客戶端連接嘗試是否成功。連接確認標誌有下面這些選項:
4.3 PUBLISH
MQTT 客戶端可以在連接到 broker 後立即發佈消息,MQTT 使用的是基於 topic 主題的過濾。每條消息都必須包含一個主題,broker 可以使用該主題將消息轉發給感興趣的客戶端。通常,每條消息都有一個負載(Payload),其中包含要以字節格式傳輸的數據。MQTT 是數據無關性的,也就是說數據是由發佈者 - publisher 決定要發送的是 XML 、JSON 還是二進制數據、文本數據。
MQTT 中的 PUBLISH 消息有幾個我們想要詳細討論的屬性:
-
Topic Name
:主題名稱是一個簡單的字符串,它以正斜槓作爲分隔符進行分層結構。例如,“我的家 / 客廳 / 溫度” 或 “德國 / 慕尼黑 / 十月節 / 人”。 -
QoS
:此數字表示消息的服務質量 (QoS)。有三個級別:0、1 和 2。服務級別決定了消息到達預期接收者(客戶端或代理)的保證類型。 -
Retain Flag
:此標誌表示 broker 將最近收到的一條 RETAIN 標誌位爲 true 的消息保存在服務器端(內存或者文件)。 -
Payload
:這個是每條消息的實際內容。MQTT 是數據無關性的。可以發送任何文本、圖像、加密數據以及二進制數據。 -
Packet Identifier
:這個 packetId 標識在 client 和 broker 之間唯一的消息標識。packetId 僅與大於零的 QoS 級別相關。 -
DUP flag
:該標誌表明該消息是重複的並且由於預期的接收者(客戶端或代理)沒有確認原始消息而被重新發送。這僅與 QoS 大於 0 相關。
當客戶端向 MQTT broker 發送消息進行發佈時,broker 讀取消息、確認消息(根據 QoS 級別)並處理消息。broker 的處理包括確定哪些客戶端訂閱了主題並將消息發送給他們。
最初發布消息的客戶端只關心將 PUBLISH 消息傳遞給 broker。一旦 broker 收到 PUBLISH 消息,broker 就有責任將消息傳遞給所有訂閱者。發佈客戶端不會得到關於是否有人對發佈的消息感興趣或有多少客戶端從 broker 收到消息的任何反饋。
4.4 Subscribe
client 會向 broker 發送 SUBSCRIBE 消息來接收有關感興趣的 topic,這個 SUBSCRIBE 消息非常簡單,它包含了一個唯一的數據包標識和一個訂閱列表。
-
Packet Identifier:這個 PacketId 和上面的 PacketId 一樣,都表示消息的唯一標識符。
-
List of Subscriptions:一個 SUBSCRIBE 消息可以包含一個客戶端的多個訂閱。每個訂閱由一個主題和一個 QoS 級別組成。訂閱消息中的主題可以包含通配符,使訂閱主題模式而不是特定主題成爲可能。如果一個客戶端存在重疊訂閱,則代理會傳送該主題具有最高 QoS 級別的消息。
4.5 Suback
爲了確認每個訂閱,broker 向客戶端發送一個 SUBACK 確認消息。該消息包含原始 Subscribe 消息的數據包標識符(以明確標識該消息)和返回碼列表。
-
Packet Identifier
:包標識符是用於標識消息的唯一標識符。它與 SUBSCRIBE 消息中的相同。 -
Return Code
:broker 爲它在 SUBSCRIBE 消息中收到的每個主題 / QoS 對發送一個返回代碼。例如,如果 SUBSCRIBE 消息有五個訂閱,則 SUBACK 消息包含五個返回碼。返回碼確認每個主題並顯示 broker 授予的 QoS 級別。如果 broker 拒絕訂閱,則 SUBACK 消息包含該特定主題的失敗返回代碼。例如,如果客戶端沒有足夠的權限訂閱主題或主題格式錯誤。 -
客戶端成功發送 SUBSCRIBE 消息並收到 SUBACK 消息後,它會獲取與 SUBSCRIBE 消息包含的訂閱中的主題匹配的每條已發佈消息。
4.6 Unsubscribe
SUBSCRIBE 消息的對應是 UNSUBSCRIBE 消息。此消息刪除 broker 上客戶端的現有訂閱。UNSUBSCRIBE 消息與 SUBSCRIBE 消息類似,具有數據包標識符和主題列表。
4.7 Unsuback
爲了確認取消訂閱,broker 向客戶端發送一個 UNSUBACK 確認消息。此消息僅包含原始 UNSUBSCRIBE 消息的數據包標識符(以明確標識該消息)。
客戶端收到來自 broker 的 UNSUBACK 後,可以認爲 UNSUBSCRIBE 消息中的訂閱被刪除了。
五、Topics
前面我們說了很多 MQTT 協議的格式以及消息列表,這一節我們來說下 Topics 主題。主題在 MQTT 中很重要,因爲我們寫代碼的時候往往都是需要先確認好 MQTT 的 Topics。
在 MQTT 中,主題一詞是指 broker 用於爲每個連接的客戶端過濾消息的 UTF-8 字符串。主題由一個或多個主題級別組成。每個主題級別由正斜槓(主題級別分隔符)分隔。
與消息隊列相比,MQTT 主題非常輕量級。客戶端在發佈或訂閱它之前不需要創建所需的主題。broker 接受每個有效主題而無需任何事先初始化。
5.1 通配符
當客戶端訂閱主題時,它可以訂閱已發佈消息的確切主題,也可以使用通配符同時訂閱多個主題。通配符只能用於訂閱主題,不能用於發佈消息。有兩種不同類型的通配符:單級和多級。
單級:+
顧名思義,單級通配符替換一個主題級別。加號代表主題中的單級通配符。
如果主題包含任意字符串而不是通配符,則任何主題都與具有單級通配符的主 題匹配。例如,訂閱 myhome/groundfloor/+/temperature 可以產生以下結果:
-
多級:#
多級通配符涵蓋多個主題級別。哈希符號代表主題中的多級通配符。爲了讓代理確定哪些主題匹配,多級通配符必須作爲主題中的最後一個字符放置,並以正斜槓開頭。
當客戶端訂閱帶有多級通配符的主題時,無論主題多長或多深,它都會收到以通配符之前的模式開頭的主題的所有消息。如果您僅將多級通配符指定爲主題 (#),您將收到發送到 MQTT 代理的所有消息。如果您期望高吞吐量,單獨使用多級通配符訂閱是一種反模式(請參閱下面的最佳實踐)。
5.2 以 $ 開頭的主題
通常,您可以根據需要命名 MQTT 主題。但是,有一個例外:以 符號開頭的主題具有不同的目的。當您將多級通配符作爲主題 (#) 訂閱時,這些主題不是訂閱的一部分。$-symbol
主題保留用於 MQTT 代理的內部統計信息。客戶端無法向這些主題發佈消息。目前,此類主題尚無官方標準化。通常,$SYS/
用於所有以下信息,但代理實現各不相同。MQTT GitHub wiki 中提供了對 $SYS-topics
的一項建議 。這裏有些例子:
$SYS/broker/clients/connected
$SYS/broker/clients/disconnected
$SYS/broker/clients/total
$SYS/broker/messages/sent
$SYS/broker/uptime
可以呀,看到了最後面。授人以魚不如授人以漁,下面是一個關於 MQTT Version 3.1.1 的介紹,有些協議格式詳細的可以前往查看。
https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718035
歡迎大家關注我的公衆號【老周聊架構】,Java 後端主流技術棧的原理、源碼分析、架構以及各種互聯網高併發、高性能、高可用的解決方案。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Uago8zL2DXthPYpe4ZjXww