MQTT协议通俗讲解
參考 Reference
v3.1.1
- 英文原版
- 中文翻譯版
其他資源
網站
- MQTT官方主頁
- Eclipse Paho 項目主頁
測試工具
- MQTT Spy(基于JDK)
- Chrome插件 MQTTLens(需FQ)
基本概念 Basic Conception
Session 會話
定義
- 定義:某個客戶端(由ClientID作為標識)和某個服務器之間的邏輯層面的通信
- 生命周期(存在時間):會話 >= 網絡連接
ClientID
- 客戶端唯一標識,服務端用于關聯一個Session
- 只能包含這些?大寫字母,小寫字母 和 數字(0-9a-zA-Z),23個字符以內
- 如果 ClientID 在多次 TCP連接中保持一致,客戶端和服務器端會保留會話信息(Session)
- 同一時間內 Server 和同一個 ClientID 只能保持一個 TCP 連接,再次連接會踢掉前一個
CleanSession 標記
- 在Connect時,由客戶端設置?
- 0 —— 開啟會話重用機制。網絡斷開重連后,恢復之前的Session信息。需要客戶端和服務器有相關Session持久化機制。
- 1 —— 關閉會話重用機制。每次Connect都是一個新Session,會話僅持續和網絡連接同樣長的時間。
客戶端 Session
- 已經發送給服務端,但是還沒有完成確認的 QoS 1 和 QoS 2 級別的消息?
- 已從服務端接收,但是還沒有完成確認的 QoS 2 級別的消息
服務器端 Session
- 會話是否存在,即使會話狀態的其它部分都是空? (SessionFlag)
- 客戶端的訂閱信息? (ClientSubcription)
- 已經發送給客戶端,但是還沒有完成確認的 QoS 1 和 QoS 2 級別的消息
- 即將傳輸給客戶端的 QoS 1 和 QoS 2 級別的消息
- 已從客戶端接收,但是還沒有完成確認的 QoS 2 級別的消息
- (可選)準備發送給客戶端的 QoS 0 級別的消息
長連接維護與管理
Keep Alive 心跳
- 目的是保持長連接的可靠性,以及雙方對彼此是否在線的確認。
- 客戶端在Connect的時候設置 Keep Alive 時長。如果服務端在 1.5 * KeepAlive 時間內沒有收到客戶端的報文,它必須斷開客戶端的網絡連接
- Keep Alive 的值由具體應用指定,一般是幾分鐘。允許的最大值是 18 小時 12 分 15 秒
Will 遺囑
- 遺囑消息(Will Message)存儲在服務端,當網絡連接關閉時,服務端必須發布這個遺囑消息,所以被形象地稱之為遺囑,可用于通知異常斷線。
- 客戶端發送 DISCONNECT 關閉鏈接,遺囑失效并刪除
- 遺囑消息發布的條件,包括:?
- 服務端檢測到了一個 I/O 錯誤或者網絡故障
- 客戶端在保持連接(Keep Alive)的時間內未能通訊
- 客戶端沒有先發送 DISCONNECT 報文直接關閉了網絡連接
- 由于協議錯誤服務端關閉了網絡連接
- 相關設置項,需要在Connect時,由客戶端指定
- Will Flag —— 遺囑的總開關
- 0 -- 關閉遺囑功能,Will QoS 和 Will Retain 必須為 0
- 1 --? 開啟遺囑功能,需要設置?Will Retain?和?Will QoS
- Will QoS —— 遺囑消息 QoS
- 可取值 0、1、2,含義與消息QoS相同
- Will Retain —— 遺囑是否保留
- 0 -- 遺囑消息不保留,后面再訂閱不會收到消息
- 1 -- 遺囑消息保留,持久存儲
- Will Topic —— 遺囑話題
- Will Payload —— 遺囑消息內容
消息基本概念
報文標識 Packet?Identifier?????????????????????????????????????????????????????
- 存在報文的可變報頭部分,非零兩個字節整數 (0-65535]
- 一個流程中重復:這些報文包含 PacketID,而且在一次通信流程內保持一致:
- PUBLISH(QoS>0?時),PUBACK,PUBREC,PUBREL,PUBCOMP
- SUBSCRIBE,??SUBACK
- UNSUBSCIBE,UNSUBACK????????????????????????????????????????
- 新的不重復:客戶端每次發送一個新的這些類型的報文時都必須分配一個當前 未使用的PacketID
- 當客戶端處理完這個報文對應的確認后,這個報文標識符就釋放可重用。
- 獨立維護:客戶端和服務端彼此獨立地分配報文標識符。因此,客戶端服務端組合使用相同的報文標識符可以實現?并發?的消息交換。可能出現一下情況,并不算異常:
Payload 有效載荷,消息體
- 最大允許 256MB
- Publish 的 Payload 允許為空。在很多場合下,代表將持久消息(或者遺囑消息)清空。
- UTF-8編碼
Retain 持久消息(粘性消息)
- RETAIN 標記:每個Publish消息都需要指定的標記
- 0 ——?服務端不能存儲這個消息,也不能移除或替換任何 現存的保留消息???????????????
- 1 ——?服務端必須存儲這個應用消息和它的QoS等級,以便它可以被分發給未來的訂閱者?
- 每個Topic只會保留最多一個 Retain 持久消息
- 客戶端訂閱帶有持久消息的Topic,會立即受到這條消息
- 服務器可以選擇丟棄持久消息,比如內存或者存儲吃緊的時候
- 如果客戶端想要刪除某個Topic 上面的持久消息,可以向這個Topic發送一個Payload為空的持久消息
- 遺囑消息(Will)的Retain持久機制同理
QoS 服務等級(消息可靠性)
最多一次 At most Once(QoS == 0)
- 沒有回復,不需要存儲。有可能丟失(網絡異常斷開,業務層繁忙或者錯誤)
至少一次 At least Once(QoS == 1?)
- 發送者S 發送前需要做持久化存儲,接受者R 不需要持久化存儲
- 如果 發送者S 沒有收到 接收者R 的回復 PUBACK,過一段時間 發送者S 會重新發送,DUP標記為1(在同一Session內)。
- 接受者R 發送 PUBACK 后,不需要知道對方是否收到,馬上把消息交給上層業務。如果此時網絡異常,會導致發送者重發。這樣接受者收到多個消息(所以叫至少一次)。
有且僅有一次 Exactly Once(QoS == 2 )
- 發送者S 發送 PUBLISH 前,需要做持久化存儲。接受者R 回復PUBREC 后,也需要做持久化存儲
- 如果 發送者S 沒有收到 接收者R 的回復 PUBREC,過一段時間 發送者S 會重新發送,DUP標記為1(在同一Session內)。
- 如果 接受者R 沒有收到 發送者S 的回復 PUBREL,過一段時間 接受者R 會重新發送PUBREC。
- 發送者S 收到 PUBREC后,刪除持久化消息,但是要保存 PacketID
- 接收者R 受到 PUBREL后,刪除持久化PUBREC。然后將消息發給上層,同時回復 PUBCOMP。
- 發送者S 收到 PUBCOMP 后,刪除 PacketID,通信完美結束。
- 這套流程可以?嚴格保證 一個包不管在什么情況下 接收者R 只收到一次 。
重傳標記 DUP 與重傳機制 (QoS > 0)
- 如果客戶端或者服務器發送了一個 Publish 消息,一段時間內沒收到 PublishAck 回復,則認為消息丟失,進行重傳。
- 在一個Session內,進行重傳的時候,頭部的?DUP 重傳標志 設置為1。
- 客戶端有可能收到?DUP == 0 的重傳包(Payload相同,PacketID不同)。因為可能因為網絡問題,下次重傳時間較久,Session已經釋放,PacketID 已經變更。
- 客戶端發給服務器的和服務器轉發給別的客戶端的 Publish 消息,DUP 重傳標志不會傳遞
- 接收者收到一個?DUP 標志為 1 的控制報文時,并不能保證之前收到過相同的報文
消息重傳順序????????????????????????????????????
- 重發任何之前的?PUBLISH?報文時,必須按原始?PUBLISH?報文的發送順序重發?(適用于QoS 1?和?QoS 2?消息)
- 必須按照對應的?PUBLISH?報文的順序發送?PUBACK?報文?(QoS 1?消息)
- 必須按照對應的?PUBLISH?報文的順序發送?PUBREC?報文?(QoS 2?消息)
- 必須按照對應的?PUBREC?報文的順序發送?PUBREL?報文?(QoS 2?消息)
- QoS == 1 時,雖然是PUBLISH有序的,但是可能會重復。例如,發布者按順序?1,2,3,4?發送消息,訂閱者收到的順序可能是?1,2,3,2,3,4。?
- QoS == 1 時,如果限制?傳輸窗口 (in-flight window)?==1,即同一時刻只有一個包在傳輸,就可以保證亂序。例如,訂閱者收到的順序可能是?1,2,3,3,4,而不是?1,2,3,2,3,4?
- QoS == 2 時,肯定不會存在亂序的問題。
?
話題 與訂閱機制??Topic & Subcribe
Topic 話題 和?TopicFilter 話題過濾器
- Pub-Sub消息模型的核心機制
- UTF-8?編碼字符串,不能超過?65535?字節。層級數量沒有限制
- 不能包含任何的下文中提到的特殊符號(/、+、#),必須至少包含一個字符??
- 區分大小寫,可以包含空格,不能包含空字符?(Unicode U+0000)??
- 在收部或尾部增加 斜杠?“/”,會產生不同的Topic和TopicFilter。舉例:
- “/A”?和?“A”?是不同的
- “A” 和 “A/” 是不同的
- 只包含斜杠?“/”?的?Topic?或?TopicFilter?是合法的???????????????????????????????????????????????????????????
TopicFilter中的特殊符號
- 層級分隔符?/
- 用于分割主題的每個層級,為主題名提供一個分層結構???????
- 主題層級分隔符可以出現在 Topic 或 TopicFilter 的任何位置???????????????????????????
- 特例:相鄰的主題層次分隔符表示一個零長度的主題層級?????????
- 單層通配符 +
- 只能用于單個主題層級匹配的通配符。例如,“a/b/+” 匹配 “a/b/c1” 和 “a/b/c2” ,但是不匹配 “a/b/c/d”??????
- 可以匹配?任意層級,包括第一個和最后一個層級。例如,“+”?是有效的,“sport/+/player1”?也是有效的。
- 可以在多個層級中使用它,也可以和多層通配符一起使用。?例如,“+/tennis/#”?是有效的。????????????????
- 只能匹配本級不能匹配上級。例如,“sport/+”?不匹配?“sport”?但是卻匹配“sport/”,“/finance”?匹配?“+/+”?和?“/+”?,但是不匹配?“+”。?
- 多層通配符 #
- 用于匹配主題中任意層級的通配符
- 匹配包含本身的層級和子層級。例如?“a/b/c/#" 可以匹配 “a/b/c”、“a/b/c/d”?和??“a/b/c/d/e”
- 必須是最后的結尾。例如“sport/tennis/#/ranking”是無效的??????
- “#”是有效的,會收到所有的應用消息。?(服務器端應將此類 TopicFilter禁掉 )
以$開頭的,服務器保留
- 服務端不能將 $ 字符開頭的 Topic 匹配通配符 (#或+) 開頭的 TopicFilter
- 服務端應該阻止客戶端使用這種 Topic 與其它客戶端交換消息。服務端實現可以將 $ 開頭的主題名用作其他目的。
- $SYS/?被廣泛用作包含服務器特定信息或控制接口的主題的前綴
- 客戶端不特意訂閱 $開頭的 Topic,就不會收到對應的消息
- 訂閱 “#” 的客戶端不會收到任何發布到以 “$” 開頭主題的消息
- 訂閱 “+/A/B” 的客戶端不會收到任何發布到 “$SYS/A/B” 的消息
- 訂閱 “$SYS/#” 的客戶端會收到發布到以 “$SYS/” 開頭主題的消息
- 訂閱 “$SYS/A/+” 的客戶端會收到發布到 “$SYS/A/B” 主題的消息
- 如果客戶端想同時接受以 “$SYS/” 開頭主題的消息和不以 $ 開頭主題的消息,它需要同時 訂閱 “#” 和 “$SYS/#”
訂閱?Subscribe??與 QoS降級
- 訂閱機制基于TopicFilter匹配
- 一個Subsribe請求?可訂閱多個?Topic(節省帶寬,多訂閱盡量用一次請求)。取消訂閱也同理
- 每一個訂閱需要指定一個QoS,指定了客戶端接收消息所允許的最大QoS級別。但是服務器端最終授權返回的QoS可能會小于等于客戶端請求的QoS
- 對于高于QoS的消息(比如說訂閱的QoS限制到1,消息的QoS指定到2),那么客戶端會收到一個QoS降低為指定的?限制QoS 的消息(消息的QoS降為1,不保證只收到一次)
- 訂閱關系可以被覆蓋,以TopicFilter為標識。如果后面訂閱一個相同的TopicFilter,但是指定的QoS不同,則以后面的為準,QoS升高后,重發相應等級的 Retain 消息
安全傳輸與鑒權認證? Security & Certification
傳輸層
- 可以采用 TCP、SSL/TLS?[RFC5246]?、WebSocket?作為傳輸層。UDP不可以,因為不保證可靠傳輸與有序傳輸。
- 服務器端返回的數據極有可能出現?粘包 的情況。客戶端經常會在連接建立之后,連續調用多個訂閱,這樣服務器端就會回復多個訂閱ACK包,同時還有各個Topic上的持久消息,一般粘成一個TCP包返回過來
- 端口(IANA分發)
- 1883:over TCP,無加密
- 8883:over SSL/TLS,單向認證(強烈建議)
- 8884:over SSL/TLS,雙向認證
- 8080:over WebSockets,未加密
- 8081:over WebSockets,加密??????????????????????????????????????????????????
- 可使用SOCKS代理,可利用安全隧道(如SSH)
潛在的風險與應對機制
- 潛在風險
- 設備可能會被盜用
- 客戶端和服務端的靜態數據可以被訪問(比如客戶端Root導致數據泄露、服務器被拖庫)
- 協議規定的行為可能有副作用 (如計時器攻擊? “timing attacks”)
- 拒絕服務攻擊(DoS)
- 通信可能會被攔截、修改、重定向或者泄露(抓包、中間人)
- 虛假控制報文注入
- 應對的機制
- 用戶和設備身份認證
- 服務端資源訪問授權
- 控制報文和 Payload 的完整性校驗
- 控制報文和 Payload 的隱私控制
客戶端身份驗證與授權? (Authentication & Authorization of Client)
- 用戶名+密碼驗證:Connect 登錄的時候,傳入 UserName 和 Password
- 相關標記位:在Connect時,由客戶端設置
- 用戶名(UserName Flag)標記設置為1,才可以穿入
- 密碼(Password Flag)標記設置為1
- 外部驗證:LDAP、OAuth 或者 操作系統的認證機制
- 用戶名密碼加密:防止中間人攻擊和重放攻擊
- 應用層:客戶端通過應用消息給服務端發送憑證用于身份驗證。
- 授權:基于客戶端提供的信息如用戶名、客戶端標識符(ClientId)、客戶端的主機名或 IP 地址,或者身份認證的結果,服務端可以限制對某些服務端資源的訪問
服務端身份驗證 (Authentication of?Server?by Client)
- MQTT 協議不是雙向信任的,它沒有提供客戶端驗證服務端身份的機制
- TLS:客戶端可以使用服務端發送的SSL證書驗證服務端的身份
- 應用層:可以通過服務端給客戶端發送憑證用于身份驗證的應用層消息
- VPN:在客戶端和服務端之間使用虛擬專用網(VPN)可以確保客戶端連接的是預期的服務器。
控制報文和 Payload 的完整性(Integrity)
- TLS:提供了對網絡傳輸的數據做完整性校驗的哈希算法
- 應用層:可以在應用消息中單獨包含哈希值。這樣做可以為 PUBLISH 控制報文的網絡傳輸和靜態數據提供內容的完整性檢查
- VPN:在客戶端和服務端之間使用虛擬專用網(VPN)連接可以在 VPN 覆蓋的網絡段提供數據完整性檢查
控制報文和?Payload?的保密性(Privacy)
- 輕量級加密:AES or DES,可適用于低端設備
- TLS:可以對網絡傳輸的數據加密
- 應用層:可以單獨加密 Payload 內容。這可以提供 Payload 傳輸途中和靜態數據的私密性。但不能給應用消息的其它屬性如 Topic 加密
- 靜態數據加密:客戶端和服務端實現可以加密存儲靜態數據,例如可以將應用消息作為會話的一部分存儲
- VPN:在客戶端和服務端之間使用虛擬專用網(VPN)連接可以在 VPN 覆蓋的網絡段保證數據的私密性
異常行為的檢測
- 服務端實現可以監視客戶端的行為,檢測潛在的安全風險。例如:
- 重復的連接請求
- 重復的身份驗證請求
- 連接的異常終止
- 主題掃描 (請求發送或訂閱大量主題)
- 發送無法送達的消息 (沒有訂閱者的主題)???
- 客戶端連接但是不發送數據
- 應對策略
- 發現違反安全規則的行為,服務端實現可以斷開客戶端連接
- 可以基于 IP地址 或 ClientID 實現一個?動態黑名單列表
- 可以使用網絡層面的控制,實現基于 IP 地址或其它信息的?速率限制?或黑名單
- 連接拒絕與錯誤碼
?
?
最佳實踐? Best Practice
客戶端 Client
選型?
- Android
- iOS
- Javascript
- PHP
- Java
服務器端 Server
Broker選型
- Mosquitto
- ActiveMQ
- RabbitMQ
- HiveMQ
- VerneMQ
- EMQ
- Apollo (from ActiveMQ)
- RocketMQ
- 商用版?
- Mosca
Benchmark
- Mosquitto + Apollo + Rabbit MQ + Mosca + VerneMQ?測評
- RabbitMQ vs ActiveMQ?
- RocketMQ vs Kafka(18項差異)
- RocketMQ vs Kafka vs RabbitMQ 消息中間件的對比 —— 消息發送性能
- 業界主流MQ對比
總結
以上是生活随笔為你收集整理的MQTT协议通俗讲解的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: __stdcall 和 __cdecl
- 下一篇: getaddrinfo函数
