文章首發於我的公眾號「程式設計師cxuan」,歡迎大家關注呀~
之前有位讀者給我留言說想要了解一下什麼是 MQTT 協議,順便還把我誇了一把,有點不好意思啦。
那麼讀者的要求必須要滿足啊,所以現在 @一下這位小姐姐,來聽課啦!
什麼是 MQTT 協議
MQTT 協議的全稱是 Message Queuing Telemetry Transport,翻譯為訊息佇列傳輸探測,它是 ISO 標準下的一種基於釋出 - 訂閱模式的訊息協議,它是基於 TCP/IP 協議簇的,它是為了改善網路裝置硬體的效能和網路的效能來設計的。MQTT 一般多用於 IoT 即物聯網上,廣泛應用於工業級別的應用場景,比如汽車、製造、石油、天然氣等。
在瞭解了 MQTT 的概念和應用場景後,我們下來就來走進 MQTT 的學習中了,先來看一下 MQTT 有哪些概念。
MQTT 基礎
上面我們解釋了 MQTT 協議的基本概念,MQTT 協議總結一點就是一種輕量級的二進位制協議,MQTT 協議與 HTTP 相比具有一個明顯的優勢:資料包開銷較小,資料包開銷小就意味著更容易進行網路傳輸。還有一個優勢就是 MQTT 在客戶端容易實現,而且具有易用性,非常適合當今資源有限的裝置。
你可能對這些概念有些諱莫如深,為什麼具有 xxx 這種特性呢?這就需要從 MQTT 的設計說起了。
MQTT 協議由 Andy Stanford-Clark (IBM) 和 Arlen Nipper(Arcom,現為 Cirrus Link)於 1999 年發明。 他們需要一種通過衛星連線石油管道的協議,以最大限度地減少電池損耗和頻寬。所以他們為這個協議規定了幾種要求:
- 這個協議必須易於實現;
- 這個協議中的資料必須易於傳輸,消耗成本小;
- 這個協議必須提供服務質量管理;
- 這個協議必須支援連續的會話控制
- 假設資料不可知,不強求傳輸資料的型別與格式,保持靈活性。
這些設計也是 MQTT 的精髓所在,MQTT 經過不斷的發展,已經成為了物聯網 IoT 所必備的一種訊息探測協議,官方強烈推薦使用的版本是 MQTT 5。
釋出 - 訂閱模式
釋出 - 訂閱模式我相信接觸訊息中介軟體架構的同學都聽過,這是一種傳統的客戶端 - 伺服器架構的替代方案,因為一般傳統的客戶端-伺服器是客戶端能夠直接和伺服器進行通訊。
但是釋出 - 訂閱模式 pub/sub
就不一樣了,釋出訂閱模式會將傳送訊息的釋出者 publisher
與接收訊息的訂閱者 subscribers
進行分離,publisher 與 subscribers 並不會直接通訊,他們甚至都不清楚對方是否存在,他們之間的交流由第三方元件 broker
代理。
pub/sub 最重要的方面是 publisher 與 subscriber 的解藕,這種耦合度有下面三個維度:
- 空間解耦:publisher 與 subscriber 並不知道對方的存在,例如不會有 IP 地址和埠的互動,也更不會有訊息的互動。
- 時間解藕:publisher 與 subscriber 並不一定需要同時執行。
- 同步
Synchronization
解藕:兩個元件的操作比如 publish 和 subscribe 都不會在釋出或者接收過程中產生中斷。
總之,釋出/訂閱模式消除了傳統客戶-伺服器之間的直接通訊,把通訊這個操作交給了 broker 進行代理,並在空間、時間、同步三個維度上進行了解藕。
可擴充性
pub/sub 比傳統的客戶端-伺服器模式有了更好的擴充,這是由於 broker 的高度並行化
,並且是基於事件驅動
的模式。可擴充性還體現在訊息的快取和訊息的智慧路由,還可以通過叢集代理來實現數百萬的連線,使用負載均衡器將負載分配到更多的單個伺服器上,這就是 MQTT 的深度應用了。
你可能不明白什麼是事件驅動,我在這裡解釋下事件驅動的概念。
事件驅動是一種程式設計正規化
,程式設計正規化是軟體工程中的概念,它指的是一種程式設計方法或者說程式設計方式,比如說物件導向程式設計和麵向過程程式設計就是一種程式設計正規化,事件驅動中的程式流程會由諸如使用者操作(點選滑鼠、鍵盤)、感測器輸出或者從其他程式或傳遞的訊息事件決定。事件驅動程式設計是圖形使用者介面和其他應用程式比如 Web 中使用的主要正規化,這些應用程式能夠響應使用者輸入執行某些操作為中心,這同時也適用於驅動程式的程式設計。
訊息過濾
在 pub/sub 的架構模式中,broker 扮演著至關重要的作用,其中非常重要的一點就是 broker 能夠對訊息進行過濾,使每個訂閱者只接收自己感興趣的訊息。
broker 有幾個可以過濾的選項
- 基於主題的過濾
MQTT 是基於 subject 的訊息過濾的,每條訊息都會有一個 topic ,接收客戶端會向 borker 訂閱感興趣的 topic,訂閱後,broker 就會確保客戶端收到釋出到 topic 中的訊息。
- 基於內容的過濾
在基於內容的過濾中,broker 會根據特定的內容過濾訊息,接受客戶端會經過過濾他們感興趣的內容。這種方法的一個顯著的缺點就是必須事先知道訊息的內容,不能加密或者輕易修改。
- 基於型別的過濾
在使用物件導向的語言時,基於訊息(事件)的型別過濾是一種比較常見的過濾方式。
為了釋出/訂閱系統的挑戰,MQTT 具有三個服務質量級別,你可以指定訊息從客戶端傳到 broker 或者從 broker 傳到客戶端,在 topic 的訂閱中,會存在 topic 沒有 subscriber 訂閱的情況,作為 broker 必須知道如何處理這種情況。
MQTT 與訊息佇列的區別
我們現在知道,MQTT 是一種訊息佇列傳輸探測協議,這種協議是看似是以訊息佇列為基礎,但卻與訊息佇列有所差別。
在傳統的訊息佇列模式中,一條訊息會儲存在訊息佇列中等待被消費,每個傳入的訊息都儲存在訊息佇列中,直到它被客戶端(通常稱之為消費者)所接收,如果沒有客戶端消費訊息的話,這條訊息就會存在訊息佇列中等待被消費。但是在訊息佇列中,不會存在訊息沒有客戶端消費的情況,但是在 MQTT 中,確存在 topic 無 subscriber 訂閱的情況。
在傳統的訊息佇列模式中,一條訊息只能被一個客戶端所消費,負載
會分佈在佇列的每個消費者之間;而在 MQTT 中,每個訂閱者都會受到訊息,每個訂閱者有相同的負載。
在傳統的訊息佇列模式中,必須使用單獨的命令來顯式建立佇列,只有佇列建立後,才可以生產或者消費訊息;而在 MQTT 中,topic 比較靈活,可以即時建立。
HiveMQ 現在是開源的,HiveMQ 社群版實現了 MQTT broker 規範,併相容了 MQTT 3.1、3.1.1 和 MQTT 5。HiveMQ MQTT Client 是一個基於 Java 的 MQTT 客戶端實現,相容 MQTT 3.1.1 和 MQTT 5。這兩個專案都可以在 HiveMQ 的 github https://github.com/hivemq 上找到。
我們知道,broker 將 publisher 和 subscriber 進行分離,因此客戶端的連線由 broker 代理,所以在我們深入理解 MQTT 之前,我們需要先知道客戶端和代理的含義。
MQTT 重要概念
MQTT client
當我們討論關於客戶端的概念時,一般指的就是 MQTT Client,publisher 和 subscriber 都屬於 MQTT Client。之所以有釋出者和訂閱者這個概念,其實是一種相對的概念,就是指當前客戶端是在釋出訊息還是在接收訊息,釋出和訂閱的功能也可以由同一個 MQTT Client 實現。
MQTT 客戶端是指執行 MQTT 庫並通過網路連線到 MQTT broker 的任何裝置,這些裝置可以從微控制器到成熟的伺服器。基本上,任何使用 TCP/IP 協議使用 MQTT 裝置的都可以稱之為 MQTT Client。MQTT 協議的客戶端實現非常簡單直接。易於實施是 MQTT 非常適合小型裝置的原因之一。 MQTT 客戶端庫可用於多種程式語言。 例如,Android、Arduino、C、C++、C#、Go、iOS、Java、JavaScript 和 .NET。
MQTT broker
與 MQTT client 對應的就是 MQTT broker,broker 是任何釋出/訂閱機構的核心,根據實現的不同,代理可以處理多達數百萬連線的 MQTT client。
broker 負責接收所有訊息,過濾訊息,確定是哪個 client 訂閱了每條訊息,並將訊息傳送給對應的 client,broker 還負責儲存會話資料,這些資料包括訂閱的和錯過的訊息。broker 還負責客戶端的身份驗證和授權。
MQTT Connection
MQTT 是基於 TCP/IP 協議基礎之上的,所以 MQTT 的 client 和 broker 都需要 TCP/IP 協議的支援。
MQTT 的連線總是在 client 和 broker 之間進行,client 和 client 之間並不會相互連線。如果要發起連線的話,那麼 client 就會向 broker 發起 CONNECT
訊息,代理會使用 CONNACK
訊息和狀態碼進行響應。一旦 client 和 broker 的連線建立後,broker 就會使客戶端的連線一直處於開啟狀態,直到 client 發出斷開命令或者連線中斷。
訊息報文
MQTT 的訊息報文主要分為 CONNECT 和 CONNACK 訊息。
CONNECT
我們上面提到了為了初始化連線,需要 client 向 broker 傳送 CONNECT 訊息,如果這個 CONNECT 訊息格式錯誤或者開啟套接字(因為基於 TCP/IP 協議棧需要初始化 Socket 連線)時間過長,亦或是傳送連線訊息時間過長的話,broker 就會關閉這條連線。
一個 MQTT 客戶端傳送一條 CONNECT 連線,這條 CONNECT 連線可能會包含下面這些資訊:
我這裡解釋一下這些資訊都是什麼概念
ClientId
:顯而易見,這個就是每個客戶端的 ID 標識,也就是連線到 MQTT broker 的每個 client。這個 ID 應該是每個 client 和 broker 唯一的,如果你不需要 broker 持有狀態的話,你可以傳送一個空的 ClientId,空的 ClientId 會沒有任何狀態。在這種情況下,ClientSession 需要設定為 true,否則將會拒絕連線。
clientSession 是什麼我們下面會說。
CleanSession
:CleanSession 會話標誌會告訴 broker client 是否需要建立持久會話。在持久會話 (CleanSession = false)中,broker 儲存 client 的所有訂閱以及服務質量(Qos) 是 1 或 2 訂閱的 client 的所有丟失的訊息。如果會話不是持久的(CleanSession = true),那麼 broker 則不會為 client 儲存任何內容並且會清除先前持久會話中的所有資訊。Username/Password
:MQTT 會傳送 username 和 password 進行 client 認證和授權。如果此資訊沒有經過加密或者 hash ,那麼密碼將會以純文字的形式傳送。所以,一般強烈建議 username 和 password 要經過加密安全傳輸。像 HiveMQ 這樣的 broker 可以與 SSL 證照進行身份驗證,因此不需要使用者名稱和密碼。LastWillxxx
:LastWillxxx 表示的是遺願,client 在連線 broker 的時候將會設立一個遺願,這個遺願會儲存在 broker 中,當 client 因為非正常原因斷開與 broker 的連線時,broker 會將遺願傳送給訂閱了這個 topic(訂閱遺願的 topic)的 client。keepAlive
:keepAlive 是 client 在連線建立時與 broker 通訊的時間間隔,通常以秒為單位。這個時間指的是 client 與 broker 在不傳送訊息下所能承受的最大時長。
在聊完 client 與 broker 之間傳送建立連線的 CONNECT 訊息後,我們再來聊一下 broker 需要對 CONNECT 進行確認的 CONNACK 訊息。
CONNACK
當 broker 收到 CONNECT 訊息時,它有義務回覆 CONNACK 訊息進行響應。CONNACK 訊息包括兩部分內容
-
SessionPresent
:會話當前標識,這個標誌會告訴 client 當前 broker 是否有一個永續性會話與 client 進行互動。SessionPresent 標誌和 CleanSession 標誌有關,當 client 在 CleanSession 設定為 true 的情況下連線時,SessionPresent 始終為 false,因為沒有永續性會話可以使用。如果 CleanSession 設定為 false,則有兩種可能性,如果 ClientId 的會話資訊可用,並且 broker 已經儲存了會話資訊,那麼 SessionPresent 為 true,否則如果沒有 ClientId 的任何會話資訊,那麼 SessionPresent 為 false。 -
ReturnCode
:CONNACK 訊息中的第二個標誌是連線確認標誌。這個標誌包含一個返回碼,告訴客戶端連線嘗試是否成功。連線確認標誌有下面這些選項。
關於每個連線的詳細說明,可以參考 https://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718035
訊息型別
釋出
當 MQTT client 在連線到 broker 之後就可以傳送訊息了,MQTT 使用的是基於 topic 主題的過濾。每條訊息都應該包含一個 topic ,broker 可以使用 topic 將訊息傳送給感興趣的 client。除此之外,每條訊息還會包含一個負載(Payload)
,Payload 中包含要以位元組形式傳送的資料。
MQTT 是資料無關性的,也就是說資料是由釋出者 - publisher 決定要傳送的是 XML 、JSON 還是二進位制資料、文字資料。
MQTT 中的 PUBLISH 訊息結構如下。
Packet Identifier
:這個 PacketId 標識在 client 和 broker 之間唯一的訊息標識。packetId 僅與大於零的 Qos 級別相關。TopicName
:主題名稱是一個簡單的字串,/
代表著分層結構。Qos
:這個數字表示的是服務質量水平,服務質量水平有三個等級:0、1 和 2,服務級別決定了訊息到達 client 或者 broker 的保證型別,來決定訊息是否丟失。RetainFlag
:這個標誌表示 broker 將最近收到的一條 RETAIN 標誌位為true
的訊息儲存在伺服器端(記憶體或者檔案)。
MQTT 伺服器只會為每一個 Topic 儲存最近收到的一條 RETAIN 標誌位為
true
的訊息。也就是說,如果MQTT 伺服器上已經為某個 Topic 儲存了一條 Retained 訊息,當客戶端再次釋出一條新的 Retained 訊息時,那麼伺服器上原來的那條訊息會被覆蓋。
Payload
:這個是每條訊息的實際內容。MQTT 是資料無關性的。可以傳送任何文字、影像、加密資料以及二進位制資料。Dupflag
:這個標誌表示該訊息是重複的並且由於預期的 client 或者 broker 沒有確認所以重新傳送了一次。這個標誌僅僅與 Qos 大於 0 相關。
當 client 向 broker 傳送訊息時,broker 會讀取訊息,根據 Qos 的級別進行訊息確認,然後處理訊息。處理訊息其實就是確定哪些 subscriber 訂閱了 topic 並將訊息傳送給他們。
最初發布訊息的 client 只關心將 PUBLISH 訊息傳送給 broker,一旦 broker 收到 PUBLISH 訊息,broker 就有責任將其傳遞給所有 subscriber。釋出訊息的 client 不會知道是否有人對釋出的訊息感興趣,同時也不知道多少 client 從 broker 收到了訊息。
訂閱
client 會向 broker 傳送 SUBSCRIBE 訊息來接收有關感興趣的 topic,這個 SUBSCRIBE 訊息非常簡單,它包含了一個唯一的資料包標識和一個訂閱列表。
Packet Identifier
:這個 PacketId 和上面的 PacketId 一樣,都表示訊息的唯一識別符號。ListOfSubscriptions
:SUBSCRIBE 訊息可以包含一個 client 的多個訂閱,每個訂閱都會由一個 topic 和一個 Qos 構成。訂閱訊息中的 topic 可以包含萬用字元。
確認訊息
client 在向 broker 傳送 SUBSCRIBE 訊息後,為了確認每個訂閱,broker 會向 client 傳送 SUBACK 確認訊息。這個 SUBACK 包含原始 SUBSCRIBE 訊息的 packetId 和返回碼列表。
其中
Packet Identifier
:這個資料包識別符號和 SUBSCRIBE 中的相同。ReturnCode
:broker 為每個接收到的 SUBSCRIBE 訊息的 topic/Qos 對傳送一個返回碼。例如,如果 SUBSCRIBE 訊息有五個訂閱訊息,則 SUBACK 訊息包含五個返回碼作為響應。
到現在我們已經探討過了三種訊息型別,釋出 - 訂閱 - 確認訊息,這三種訊息的示意圖如下。
退訂
SUBSCRIBE 訊息對應的是 UNSUBSCRIBE
訊息,這條訊息傳送後,broker 會刪除關於 client 的訂閱。所以,UNSUBSCRIBE 訊息與 SUBSCRIBE 訊息類似,都具有 packetId 和 topic 列表。
確認退訂
取消訂閱也需要 broker 的確認,此時 broker 會向 client 傳送一個 UNSUBACK
訊息,這個 UNSUBACK 訊息非常簡單,只有一個 packetId 資料識別符號。
退訂和確認退訂的流程如下。
當 client 收到來自 broker 的 UNSUBACK 訊息後,就可以認為 UNSUBSCRIBE 訊息中的訂閱被刪除了。
聊聊 Topic
聊了這麼多關於 MQTT 的內容,但是我們還沒有好好聊過 Topic。在 MQTT 中,Topic 是指 broker 為每個連線的 client 過濾訊息的 UTF-8
字串。Topic 是一種分層的結構,可以由一個或者多個 Topic 組成。每個 Topic 由 /
進行分割。
與傳統的訊息佇列相比,MQTT Topic 非常輕量級,client 在釋出或訂閱之前不需要先建立所需要的 Topic,broker 在接收每個 Topic 前不用進行初始化操作。
萬用字元
當客戶端訂閱 Topic 時,它可以訂閱已釋出訊息的確切 Topic,也可以使用萬用字元來同時訂閱多個 Topic。萬用字元有兩種:單級和多級。
單級萬用字元
單級萬用字元可以替換 Topic 的一個級別,+
號代表 Topic 中的單級萬用字元。
如果 Topic 包含任意字串而不是萬用字元,則任何 Topic 都能夠和單級萬用字元匹配。例如
myhome/groundfloor/+/temperature 就有下面這幾種匹配方式。
多級萬用字元
多級萬用字元涵蓋多個 Topic,#
代表 Topic 中的多級萬用字元。為了讓 broker 能夠確定和哪些 Topic 匹配,多級萬用字元必須作為 Topic 中的最後一個字元放置,並以 /
開頭。
下面是 myhome/groundfloor/# 的幾個例子
當 client 訂閱帶有多級萬用字元的 Topic 時,不論 Topic 有多長多深,它都會收到萬用字元之前 Topic 的所有訊息。如果你只將 Topic 定義為 # 的話,那麼你將會收到所有的訊息。
我自己肝了六本 PDF,全網傳播超過10w+ ,微信搜尋「程式設計師cxuan」關注公眾號後,在後臺回覆 cxuan ,領取全部 PDF,這些 PDF 如下