一、引言
上一篇《零基礎IM開發入門(二):什麼是IM系統的實時性?》講到了IM系統的「立足」之本——「實時性」這個技術特徵,本篇主要講解IM系統中的「可靠性」這個話題,內容儘可能作到只講原理不深刻展開,避開深層次的技術性探討,確保通俗易懂。html
二、系列文章
《零基礎IM開發入門(三):什麼是IM系統的可靠性?》(* 本文)性能優化
《零基礎IM開發入門(四):什麼是IM系統的消息時序一致性? (稍後發佈)》服務器
《零基礎IM開發入門(五):什麼是IM系統的安全性? (稍後發佈)》網絡
《零基礎IM開發入門(六):什麼是IM系統的的心跳機制? (稍後發佈)》架構
《零基礎IM開發入門(七):如何理解並實現IM系統消息未讀數? (稍後發佈)》app
《零基礎IM開發入門(八):如何理解並實現IM系統的多端消息漫遊? (稍後發佈)》性能
三、正文概述
通常來講,IM系統的消息「可靠性」,一般就是指聊天消息投遞的可靠性(準確的說,這個「消息」是廣義的,由於還存用戶看不見的各類指令,爲了通俗,統稱「消息」)。學習
從用戶行爲來說,消息「可靠性」應該分爲兩種類型:
- 1)在線消息的可靠性:即發送消息時,接收方當前處於「在線」狀態;
- 2)離線消息的可靠性:即發送消息時,接收方當前處於「離線」狀態。
從具體的技術表現來說,消息「可靠性」包含兩層含義:
- 1)消息不丟:這很直白,發出去的消息不能像進了黑洞同樣,一臉懵逼可不行;
- 2)消息不重:這是丟消息的反面,消息重複了也不能容忍。
對於「消息不丟」這個特徵來講,細化下來,它又包含兩重含義:
- 1)已明確被對方收到;
- 2)已明確未被對方收到。
是的,對於第1)重含義好理解,第2)重含義的意思是:當對方沒有成功收到時,你的im系統也必需要感知到,不然,它一樣屬於被「丟」範疇。
總之,一個成型的im系統,必須包含這兩種消息「可靠性」邏輯,才能堪用,缺一不可。
消息的可靠性(不丟失、不重複)無疑是IM系統的重要指標,也是IM系統實現中的難點之一。本文如下文字,將從在線消息的可靠性和離線消息的可靠性進行討論。
四、典型的在線消息收發流程
先看下面這張典型的im消息收發流程:
是的,這是一個典型的服務端中轉型IM架構。
所謂「服務端中轉型IM架構」是指:一條消息從客戶端A發出後,須要先通過 IM 服務器來進行中轉,而後再由 IM 服務器推送給客戶端B,這種模式也是目前最多見的 IM 系統的消息分發架構。
你可能會說,IM不能夠是P2P模式的嗎?是的,目前來講主流IM基本都是服務器中轉這種方式,P2P模式在IM系統中用的不多。
緣由是如下兩個很明顯的弊端:
- 1)P2P模式下,IM運營者很容易被用戶架空(沒法監管到用戶行爲,用戶涉黃了怕不怕?);
- 2)P2P模式下,羣聊這種業務形態,很難實現(我要在千人羣中發消息給,不可能我自已來分發1000次吧)。
話題有點跑偏,咱們回到正題:在上面這張圖裏,客戶A發送消息到服務端、服務端中轉消息給客戶B,假設這兩條數據連接中使用的通訊協議是TCP,你認爲在TCP所謂可靠傳輸協議加持下,真的能保證IM聊天消息的可靠性嗎?
答案是否認的。咱們繼續看下節。
五、TCP並不能保證在線消息的「可靠性」
接上節,在一個典型的服務端中轉型IM架構中,即便使用「可靠的傳輸協議」TCP,也不能保證聊天消息的可靠性。爲何這麼說?
要回答這個問題,網上的不少文章,都會從服務端的角度舉例:好比消息發送時操做系統崩潰、網絡閃斷、存儲故障等等,總之很抽象,不太容易理解。
此次咱們從客戶端角度來理解,爲何使用了可靠傳輸協議TCP的狀況下IM聊天消息仍然不可靠的問題。
具體來講:如何確保 IM 消息的可靠性是個相對複雜的話題,從客戶端發送數據到服務器,再從服務器送達目標客戶端,最終在 UI 成功展現,其間涉及的環節不少,這裏只取其中一環「接收端如何確保消息不丟失」來探討,粗略聊下我接觸過的兩種設計思路。
說到可靠送達:第一反應會聯想到 TCP 的可靠性。數據的可靠送達是個通用性的問題,不管是網絡二進制流數據,仍是上層的業務數據,都有可靠性保障問題,TCP 做爲網絡基礎設施協議,其可靠性設計的可靠性是毋庸置疑的,咱們就從 TCP 的可靠性提及。
在 TCP 這一層:全部 Sender 發送的數據,每個 byte 都有標號(Sequence Number),每一個 byte 在抵達接收端以後都會被接收端返回一個確認信息(Ack Number), 兩者關係爲 Ack = Seq + 1。簡單來講,若是 Sender 發送一個 Seq = 1,長度爲 100 bytes 的包,那麼 receiver 會返回一個 Ack = 101 的包,若是 Sender 收到了這個Ack 包,說明數據確實被 Receiver 收到了,不然 Sender 會採起某種策略重發上面的包。
第一個問題是:既然 TCP 自己是具有可靠性的,爲何還會出現消息接收端(Receiver)丟失消息的狀況?
看下圖一目瞭然:
(▲ 上圖引用自《從客戶端的角度來談談移動端IM的消息可靠性和送達機制》)
一句話總結上圖的含義:網絡層的可靠性不等同於業務層的可靠性。
數據可靠抵達網絡層以後,還須要一層層往上移交處理,可能的處理有:
- 1)安全性校驗;
- 2)binary 解析;
- 3)model 建立;
- 4)寫 db;
- 5)存入 cache;
- 6)UI 展現;
- 7)以及一些邊界問題:好比斷網、用戶忽然退出登錄、磁盤已滿、內存溢出、app奔潰、忽然關機等等。
項目的功能特性越多,網絡層往上的處理出錯的可能性就越大。
舉個最簡單的場景爲例子:消息可靠抵達網絡層以後,寫 db 以前 IM APP 崩潰(不稀奇,是 App 都有崩潰的可能),雖然數據在網絡層可靠抵達了,但沒存進 db,下次用戶打開 App 消息天然就丟失了,若是不在業務層再增長可靠性保障(好比:後面要提到的網絡層面的消息重發保障),那麼意味着這條消息對於接收端來講就永遠丟失了,也就天然不存在「可靠性」了。
六、爲在線消息增長「可靠性」保障
那麼怎樣在應用層增長可靠性保障呢?
有一個現成的機制可供咱們借鑑:TCP協議的超時、重傳、確認機制。
具體來講就是:
- 1)在應用層構造一種ACK消息,當接收方正確處理完消息後,向發送方發送ACK;
- 2)假如發送方在超時時間內沒有收到ACK,則認爲消息發送失敗,須要進行重傳或其餘處理。
增長了確認機制的消息收發過程以下:
咱們能夠把整個過程分爲兩個階段。
階段1:clientA -> server
- 1-1:clientA向server發送消息(msg-Req);
- 1-2:server收取消息,回覆ACK(msg-Ack)給clientA;
- 1-3:一旦clientA收到ACK便可認爲消息已成功投遞,第一階段結束。
不管msg-A或ack-A丟失,clientA均沒法在超時時間內收到ACK,此時能夠提示用戶發送失敗,手動進行重發。
階段2:server -> clientB
- 2-1:server向clientB發送消息(Notify-Req);
- 2-2:clientB收取消息,回覆ACK(Notify-ACk)給server;
- 2-3:server收到ACK以後將該消息標記爲已發送,第二階段結束。
不管msg-B或ack-B丟失,server均沒法在超時時間內收到ACK,此時須要重發msg-B,直到clientB返回ACK爲止。
七、典型的離線消息收發流程
說完在線消息的「可靠性」問題,咱們該瞭解一下離線消息了。
7.1 離線消息的收發也存在「不可靠性」
下圖是一張典型的IM離線消息流程圖:
如上圖所示,和在線消息收發流程相似。
離線消息收發流程也可劃分爲兩個階段:
階段1:clientA -> server
- 1-1:clientA向server發送消息(msg-Req) ;
- 1-2:server發現clientB離線,將消息存入offline-DB。
階段2:server -> clientB
- 2-1:clientB上線後向server拉取離線消息(pull-Req) ;
- 2-2:server從offline-DB檢索相應的離線消息推送給clientB(pull-res),並從offline-DB中刪除。
顯然:離線消息收發過程一樣存在消息丟失的可能性。
舉例來講:假設pull-res沒有成功送達clientB,而offline-DB中已刪除,這部分離線消息就完全丟失了。
7.2 離線消息的「可靠性」保障
與在線消息收發流程相似,咱們一樣須要在應用層增長可靠性保障機制。
下圖是增長了可靠性保障後的離線消息收發流程:
與初始的離線消息收發流程相比,上圖增長了1-三、2-四、2-5步驟:
- 1-3:server將消息存入offline-DB後,回覆ACK(msg-Ack)給clientA,clientA收到ACK便可認爲消息投遞成功;
- 2-4:clientB收到推送的離線消息,回覆ACK(res-Ack)給server;
- 2-5:server收到res-ACk後肯定離線消息已被clientB成功收取,此時才能從offline-DB中刪除。
固然,上述的保障機制,還存在性能優化空間。
當離線消息的量較大時:若是對每條消息都回復ACK,無疑會大大增長客戶端與服務器的通訊次數。這種狀況咱們一般使用批量ACK的方式,對多條消息僅回覆一個ACK。在某此後IM的實現中是將全部的離線消息按會話進行分組,每組回覆一個ACK,假如某個ACK丟失,則只須要重傳該會話的全部離線消息。
八、聊天消息重複的問題
上面章節中,經過在應用層加入重傳、確認機制後,咱們確實是杜絕了消息丟失的可能性。
但因爲重試機制的存在,咱們會遇到一個新的問題:那就是同一條消息可能被重複發送。
舉一個最簡單的例子:假設client成功收到了server推送的消息,但其後續發送的ACK丟失了,那麼server將會在超時後再次推送該消息,若是業務層不對重複消息進行處理,那麼用戶就會看到兩條徹底同樣的消息。
消息去重的方式其實很是簡單,通常是根據消息的惟一標誌(id)進行過濾。
具體過程在服務端和客戶端可能有所不一樣:
- 1)客戶端 :咱們能夠經過構造一個map來維護已接收消息的id,當收到id重複的消息時直接丟棄;
- 2)服務端 :收到消息時根據id去數據庫查詢,若庫中已存在則不進行處理,但仍然須要向客戶端回覆Ack(由於這條消息極可能來自用戶的手動重發)。
關於消息的去重問題,在一對一聊天的狀況下,邏輯並不複雜,但在羣聊模式下,會將問題複雜化,有關羣聊消息不丟和去重的詳細討論,能夠深刻閱讀:《IM羣聊消息如此複雜,如何保證不丟不重?》。
九、本文小結
保證消息的可靠性是IM系統設計中很重要的一環,能不能作到「消息不丟」、「消息不重」,對用戶的體驗影響極大。
所謂「可靠的傳輸協議」TCP也並不能保障消息在應用層的可靠性。
咱們通常經過在應用層的ACK應答和重傳機制,來實現IM消息的可靠性保障。但由此帶來的消息重複問題,須要咱們額外進行處理,最簡單的方法就是經過消息ID進行冪等去重。
關於IM系統消息可靠性的理論基礎,咱們就探討到這裏,有疑問的讀者,能夠在本文末尾留意,歡迎積極討論。
十、參考資料
[1] IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞
[2] IM消息送達保證機制實現(二):保證離線消息的可靠投遞
[3] IM開發乾貨分享:如何優雅的實現大量離線消息的可靠投遞
[4] 從客戶端的角度來談談移動端IM的消息可靠性和送達機制
[5] 聊聊IM系統的即時性和可靠性
本文已同步發佈於「即時通信技術圈」公衆號: