MQTT-保留訊息和遺囑訊息

木子七發表於2023-05-09

遺囑訊息

為什麼需要遺囑訊息

       MQTT的訂閱釋出機制,解耦了訊息的傳送方和接收方,這使我們沒有辦法獲取對端的狀態,為了解決該問題,MQTT提供了遺囑訊息,為意外斷線的客戶端提供了對外發出通知的能力

如何使用遺囑訊息

       使用遺囑訊息,客戶端需要在連線時,也就是connect報文中指定遺囑訊息,除了正常CONNECT報文欄位,需要為遺囑訊息提供以下欄位

Will Topic #遺囑訊息主題

Will QoS  # 遺囑訊息級別

Will Retain  # 將遺囑訊息設定為保留訊息
'''
遺囑訊息一旦釋出,就會在服務端的會話狀態中刪除,不能多次消費,我們不能保證遺囑訊息發出的時候訂閱端是否線上,為了避免錯過遺囑訊息,可以使用Will Retain = True 欄位將遺囑訊息設定為保留訊息,這樣訂閱了該主題的客戶端不管什麼時候上線,都可以收到另外一方的離線通知
'''

Will Properties # 遺囑訊息屬性  ↓

Will Delay Interval #遺囑訊息的屬性↑, 設定遺囑訊息的延遲傳送時間
'''
在MQTT 5.0中,可以使用 Will Delay Interval 設定延遲傳送遺囑訊息,單位是S,如果客戶端及時恢復,那麼遺囑訊息的發生倒數計時就會終止,可以避免客戶端在短暫離線後恢復,可以繼續服務時但是遺囑訊息已經發出的情況,和保留訊息不同的是,遺囑訊息是會話狀態的一部分,沒有辦法存在比會話更長的時間,如果遺囑延遲時間大於會話過期時間,會話結束的時候遺囑會立即釋出
'''

Will Payload  # 遺囑訊息內容

​ 在客戶端連線成功後,遺囑訊息就會儲存在服務端中,一旦客戶端連線異常斷開,服務端就會把遺囑訊息傳送給對應的訂閱者,如果客戶端是正常斷開,遺囑訊息則不會發布,在MQTT中,客戶端的意外斷開可以分為以下幾種情況

  • 服務端檢測到了一個I/O故障或者網路故障
  • 客戶端在心跳的時間內未能通訊
  • 客戶端在沒有發生Reason Code 為0的DISCONNECT報文的情況下關閉了網路連線
  • 服務端在沒有收到DISCONNECT報文的情況下主動關閉了網路連線
  • 在MQTT 3.1.1中,如果滿足任意一個情況,服務端會在連線斷開後立刻釋出遺囑訊息,5.0中可以透過設定屬性延遲釋出

保留訊息

為什麼需要保留訊息

       如果不考慮持久會話的因素,那麼MQTT訂閱只有在客戶端連線之後才能建立,所以服務端不能提前預知某個主題會被哪些服務端訂閱或者某個客戶端會訂閱哪些主題,所以當訊息到達服務端之後,服務端只會把訊息分發給當前已經存在的訂閱者,分發完成訊息就會從服務端中刪除,如果當前沒有任何訂閱者,訊息就會立即丟棄,如果訂閱端在在這之後上線的,就會錯過這個訊息,為了解決該場景的問題,MQTT提供了保留訊息


如何使用保留訊息

       我們可以在傳送PUBLISH的時候把Retain設定為1或者True,表示當前訊息是保留訊息,保留訊息進入服務端,會像普通訊息一樣轉發給當前的訂閱者,還會被保留在MQTT的服務端中,每當有新的訂閱建立,MQTT服務端都會檢索是否存在與這個訂閱匹配的保留訊息,然後把匹配的保留訊息下發給訂閱者,由於訂閱的時候可以使用主題萬用字元,所以可能匹配到多個保留訊息,這些訊息將依次下發給訂閱者

保留訊息的更新

       透過保留訊息,我們可以使訂閱者上線後立即獲得資料更新,不必等待新一次的訊息,每個主題最多可以儲存一條保留訊息,如果主題下的保留訊息已經存在,那麼新到達的保留訊息就會替換原來的保留訊息,保留訊息會一直儲存在服務端中,由於他不屬於會話狀態的一部分,所以即便釋出端會話過期,也不會影響保留訊息儲存,如果想要清空這個主題的保留訊息,可以透過傳送一個payload為空的保留訊息來實現,這個為空的保留訊息會合其他保留訊息一樣轉發給訂閱者,區別是在於不會被服務端儲存,使用這種方式的話,我們需要確保訂閱端不會把為空的訊息視為一個錯誤


       需要注意的是,QoS 0 可能丟訊息的特性,可能會導致保留訊息刪除不成功,QoS 1 可能重複到達的特性,保留訊息又可能多次刪除,如果不希望出現刪除失敗或者多次刪除的情況,可以使用QoS 2 來發布 payload為空的保留訊息

保留訊息的過期時間

       如果擔心一條保留訊息失去了時效性,還長時間保留在服務端中導致被後來的訂閱者消費的話,MQTT 5.0版本可以為保留訊息設定過期時間,PUBLISH的 Message Expiry Interval 屬性,單位是秒

保留訊息的傳送機制

       預設情況下,當保留訊息當成普通訊息向訂閱者轉發的時候,保留訊息中的retain標識會被清除也就是設定為0,只有當新的訂閱建立的時候,傳送保留訊息的retain會設定為1,表示這是一個保留訊息


       針對上述情況多個服務端橋接的時候,會衍生一個問題,比如服務端A向服務端B訂閱了主題,當服務端B收到一個保留訊息向服務端A轉發的時候清除retain標識,導致服務端A收到該保留訊息後只會轉發給訂閱者,不會儲存保留訊息,在MQTT 5.0中,提供了 Retain As Published 的訂閱選項,如果該選項設定為0,就是預設的邏輯,如果設定為1,那麼保留訊息當做普通訊息轉發的時候,Retain標識不會被清除,依然是1


       還有一個選項會影響保留訊息的行為,在某些場景下,雖然客戶端複用了上一次的會話,但是無法確定上一次會話中是否成功訂閱了某個主題,所以只能再次訂閱,如果訂閱已經存在,其實服務端已經給客戶端快取了離線期間的訊息,這種情況下,客戶端在重連後,其實並不需要獲取保留訊息,但是現在只要有訂閱建立,訂閱匹配就會下發保留訊息,為了解決這個問題,在MQTT 5.0中,提供了 Retain Handling的訂閱選項

  • Retain Handling = 0,訂閱建立的時候傳送保留訊息
  • Retain Handling = 1,訂閱建立時若該訂閱當前不存在則傳送保留訊息
  • Retain Handling = 2,訂閱建立時不傳送保留訊息

保留訊息的注意事項
  • 在MQTT中,同一條普通訊息只能被同一個客戶端消費一次,保留訊息可能會被重複消費,客戶端進行訂閱,服務端下發匹配的保留訊息,即時這個訊息之前已經下發過了,只要保留訊息在客戶端的兩次訂閱期間沒有更新,客戶端就會重複消費到同一條訊息,如果客戶端的訂閱是在保留訊息到達服務端之前建立的,訊息轉發後 客戶端重新連線,沒有更新過保留訊息,就是重複收到了兩條同樣的訊息
  • 不能透過主動刪除已經消費過的保留訊息來避免重複,因為可能其他人也使用該主題下的保留訊息,我們就不能去刪除他,其次也沒有辦法正確判斷當前伺服器中的保留訊息有沒有被自己消費過,我們可以參考QoS 1 去重的做法,在保留訊息的payload中增加一個時間戳,訂閱者記錄最後消費的訊息的時間戳,和新到達的保留訊息的時間戳進行比較,如果後者的時間戳更新,就是一個新的訊息,反之就是一個重複的訊息
  • 我們可以透過保留訊息減少訊息的釋出頻次,對於一些固定週期、狀態等以確保新上線的客戶端儘快取得資料,有了保留訊息,我們可以只在狀態發生變更時進行釋出

相關文章