MQTT練習
使用MQTT 協議的設備都運行在網路受限的環境下,而只依靠底層的TCP 傳輸協議,並不能完全保證訊息的可靠到達。因此,MQTT 提供了QoS 機制,其核心是設計了多種訊息互動機制來提供不同的服務質量,來滿足使用者在各種場景下對訊息可靠性的要求。
MQTT 定義了三個QoS 等級,分別為:
- QoS 0,最多交付一次。
- QoS 1,至少交貨一次。
- QoS 2,只交付一次。
其中,
使用QoS 0 可能會遺失訊息,
使用QoS 1 可以保證收到訊息,但訊息可能會重複,
使用QoS 2 可以保證訊息既不遺失也不重複。
QoS 等級從低到高,不僅意味著訊息可靠性的提升,也意味著傳輸複雜程度的提升。
QoS 0 - 最多交付一次
QoS 0 是最低的QoS 等級。QoS 0 訊息即發即棄,不需要等待確認,不需要儲存和重傳,因此對於接收者來說,永遠都不需要擔心收到重複的訊息。
為什麼QoS 0 訊息會遺失?
當我們使用QoS 0 傳遞訊息時,訊息的可靠性完全依賴底層的TCP 協定。
而TCP 只能保證在連線穩定不關閉的情況下訊息的可靠到達,一旦出現連線關閉、重置,仍有可能遺失目前處於網路連結或作業系統底層緩衝區的訊息。這也是QoS 0 訊息最主要的遺失場景。
QoS 1 - 至少交付一次
為了確保訊息到達,QoS 1 加入了應答與重傳機制,發送方只有在收到接收方的PUBACK 報文以後,才能認為訊息投遞成功,在此之前,發送方需要儲存該PUBLISH 封包以便下次重傳。
QoS 1 需要在PUBLISH 封包中設定Packet ID,而作為回應的PUBACK 封包,則會使用與PUBLISH 封包相同的Packet ID,以便在發送方收到後刪除正確的PUBLISH 封包快取。
為什麼QoS 1 訊息會重複?
對於發送方來說,沒收到PUBACK 封包分為以下兩種情況:
- PUBLISH 未到達接收方
- PUBLISH 已到達接收方,接收方的PUBACK 封包尚未到達發送方
在第一種情況下,發送方雖然重傳了PUBLISH 報文,但是對於接收方來說,實際上仍然只收到了一次訊息。
但在第二種情況下,在發送方重傳時,接收方已經收到過了這個PUBLISH 封包,這就導致接收方將收到重複的訊息。
雖然重傳時PUBLISH 封包中的DUP 標誌會被設定為1,用以表示這是一個重傳的封包。但是接收方並不能因此假定自己曾經接收過這個訊息,仍然需要將其視為一個全新的訊息。
這是因為對於接收方來說,可能存在以下兩種情況:
第一種情況,發送方因為沒有收到PUBACK 封包而重傳了PUBLISH 封包。此時,接收方收到的前後兩個PUBLISH 封包使用了相同的Packet ID,而第二個PUBLISH 封包的DUP 標誌為1,此時它確實是一個重複的訊息。
第二種情況,第一個PUBLISH 封包已經完成了投遞,1024 這個Packet ID 重新變成可用狀態。發送方使用這個Packet ID 發送了一個全新的PUBLISH 報文,但這次報文未能到達對端,所以發送方後續重傳了這個PUBLISH 報文。這使得雖然接收方收到的第二個PUBLISH 封包同樣是相同的Packet ID,且DUP 為1,但確實是一個全新的訊息。
由於我們無法區分這兩種情況,所以只能讓接收方將這些PUBLISH 封包都當作全新的訊息來處理。因此當我們使用QoS 1 時,訊息的重複在協議層面上是無法避免的。
甚至在比較極端的情況下,例如Broker 從發布方收到了重複的PUBLISH 報文,而在將這些報文轉發給訂閱方的過程中,再次發生重傳,這將導致訂閱方最終收到更多的重複訊息。
在下圖表示的例子中,雖然發布者的本意只是發布一則訊息,但對接收者來說,最終卻收到了三條相同的訊息:
以上,就是QoS 1 保證訊息到達帶來的副作用。
QoS 2 - 只交付一次
QoS 2 解決了QoS 0、1 訊息可能遺失或重複的問題,但相應地,它也帶來了最複雜的互動流程和最高的開銷。每一次的QoS 2 訊息投遞,都要求發送方與接收方進行至少兩次請求/回應流程。
- 首先,發送方儲存並傳送QoS 為2 的PUBLISH 封包以啟動一次QoS 2 訊息的傳輸,然後等待接收方回覆PUBREC 訊息。這一部分與QoS 1 基本一致,只是回應封包從PUBACK 變成了PUBREC。
- 當發送方收到PUBREC 報文,即可確認對端已經收到了PUBLISH 報文,發送方將不再需要重傳這個報文,也不能再重傳這個報文。所以此時發送方可以刪除本地儲存的PUBLISH 封包,然後發送PUBREL 封包,通知對端自己準備將本次使用的Packet ID 標記為可用了。就像PUBLISH 封包一樣,我們需要確保PUBREL 封包到達對端,所以也需要一個回應封包,而這個PUBREL 封包需要被儲存下來以便後續重傳。
- 當接收方收到PUBREL 報文,也可以確認在這次的傳輸流程中不會再有重傳的PUBLISH 報文到達,因此回覆PUBCOMP 報文表示自己也準備好將目前的Packet ID 用於新的消息了。
- 當發送方收到PUBCOMP 封包,這次的QoS 2 訊息傳輸就算正式完成了。在這之後,發送方可以再次使用目前的Packet ID 發送新的訊息,而接收方再次收到使用這個Packet ID 的PUBLISH 訊息時,也會將它視為一個全新的訊息。
為什麼QoS 2 訊息不會重複?
QoS 2 訊息保證不會遺失的邏輯與QoS 1 相同,所以這裡我們就不再重複了。
與QoS 1 相比,QoS 2 新增了PUBREL 封包和PUBCOMP 封包的流程,也正是這個新增的流程帶來了訊息不會重複的保證。
在我們更進一步之前,我們先快速回顧一下QoS 1 訊息無法避免重複的原因。
當我們使用QoS 1 訊息時,對接收者來說,回復完PUBACK 這個回應封包以後Packet ID 就重新可用了,也不管回應是否確實已經到達了發送者。所以就無法得知之後到達的,攜帶了相同Packet ID 的PUBLISH 報文,到底是發送方因為沒有收到回應而重傳的,還是發送方因為收到了回應所以重新使用了這個Packet ID 發送了一個全新的訊息。
所以,訊息去重的關鍵就在於,通訊雙方如何正確地同步釋放Packet ID,換句話說,不管發送方是重傳訊息還是發布新訊息,一定是和對端達成共識了的。
而QoS 2 中增加的PUBREL 流程,正是提供了幫助通訊雙方協商Packet ID 何時可以重複使用的能力。
QoS 2 規定,發送方只有在收到PUBREC 封包之前可以重傳PUBLISH 封包。一旦收到PUBREC 封包並發出PUBREL 封包,發送者就進入了Packet ID 釋放流程,不可以再使用目前Packet ID 重傳PUBLISH 封包。同時,在收到對端回覆的PUBCOMP 封包確認雙方都完成Packet ID 釋放之前,也不可以使用目前Packet ID 傳送新的訊息。
因此,對於接收方來說,能夠以PUBREL 報文為界限,凡是在PUBREL 報文之前到達的PUBLISH 報文,都必然是重複的消息;而凡是在PUBREL 報文之後到達的PUBLISH 報文,都必然是全新的訊息。
一旦有了這個前提,我們就能夠在協議層面完成QoS 2 訊息的去重。
不同QoS 的適用場景和注意事項
QoS 0
QoS 0 的缺點是可能會遺失訊息,訊息遺失的頻率依賴於你所處的網路環境,並且可能使你錯過斷開連線期間的訊息,不過優點是投遞的效率較高。
所以我們通常選擇使用QoS 0 傳輸一些高頻且不那麼重要的數據,例如感測器數據,週期性更新,即使遺漏幾個週期的數據也可以接受。
QoS 1
QoS 1 可以保證訊息到達,所以適合傳輸一些較為重要的數據,例如下達關鍵指令、更新重要的有即時性要求的狀態等。
但因為QoS 1 也可能會導致訊息重複,所以當我們選擇使用QoS 1 時,還需要能夠處理訊息的重複,或是能夠允許訊息的重複。
在我們決定使用QoS 1 並且不對其進行去重處理之前,我們需要先了解,允許訊息的重複,可能意味著什麼。
如果我們不對QoS 1 進行去重處理,我們可能會遭遇這種情況,發布者以1、2 的順序發布訊息,但最終訂閱方接收到的訊息順序可能是1、2、1、2。如果1 表示開燈指令,2 表示關燈指令,我想大部分使用者都不會接受自己僅僅進行了開燈然後關燈的操作,結果燈在開和關的狀態來回變化。
QoS 2
QoS 2 既可以保證訊息到達,也可以保證訊息不會重複,但傳輸成本最高。如果我們不願意自行實現去重方案,並且能夠接受QoS 2 帶來的額外開銷,那麼QoS 2 將是一個合適的選擇。通常我們會在金融、航空等行業場景下會更多地見到QoS 2 的使用。
(1) MQTT (發行Qos=0)
MQTT Box 的設定畫面
case1 : MyMQTT 尚未準備 接收先送出 (Publish)
case2 : MyMQTT 準備好接收再送出訊息 (Publish)
MyMQTT (手機App)的設定畫面
Host: boker.hivemq.com 或 broker.mqtt-dasgboard.com
Port : 1883
(2) MQTT (發行Qos=1)
等MQTTBox送出後再打開APP MyMQTT
(3) MQTT (發行Qos=2)
沒有留言:
張貼留言