IM消息送达保证机制实现(二):保证离线消息的可靠投递
1、前言
本文的上篇《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》中,我們討論了在線實時消息的投遞可以通過應用層的確認、發送方的超時重傳、接收方的去重等手段來保證業務層面消息的不丟不重。
但實時在線投遞針對的是消息收發雙方都在線的情況(如當發送方用戶A發送消息給接收方用戶B時,用戶B是在線的),那如果消息的接收方用戶B不在線,系統是如何保證消息的可達性的呢?這就是本文要討論的問題。
2、IM開發干貨系列文章
- 《如何保證IM實時消息的“時序性”與“一致性”?》
- 《IM單聊和群聊中的在線狀態同步應該用“推”還是“拉”?》
3、消息接收方不在線時的典型消息發送流程
?
如上圖所述,通常此類情況下消息的發送流程如下:
?
- Step 1:用戶A發送一條消息給用戶B;
- Step 2:服務器查看用戶B的狀態,發現B的狀態為“offline”(即B當前不在線);
- Step 3:服務器將此條消息以離線消息的形式持久化存儲到DB中(當然,具體的持久化方案可由您IM的具體技術實現為準);
- Step 4:服務器返回用戶A“發送成功”ACK確認包(注:對于消息發送方而言,消息一旦落地存儲至DB就認為是發送成功了)。
關于 “Step 4” 的補充說明:
請一定要理解“Step 4”,因為現在無論是傳統的PC端IM(類似QQ這樣的——可以在UI上看到好友的在線、離線狀態)還是目前主流的移動端IM(強調的是用戶全時在線——即你看不到好友到底在線還是離線,反正給你的假像就是這個好友“應該”是在線的),消息發送出去后,無論是對方實時在線收到還是對方不在線而被服務端離線存儲了,對于發送方而言只要消息沒有因為網絡等原因莫名消失,就應該認為是“被收到了”。
從技術的角度講,消息接收方收到的消息應答ACK包的真正發起者,實際上有兩種可能性:一種是由接收方發出、而另一種是由服務端代為發送(這在MobileIMSDK開源工程里被稱作“偽應答”)。
4、典型離線消息表的設計以及拉取離線消息的過程
① 存儲離線消看書的表主要字段大致如下:
| 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | -- 消息接收者ID receiver_uid varchar(50), ? -- 消息的唯一指紋碼(即消息ID),用于去重等場景,單機情況下此id可能是個自增值、分布式場景下可能是類似于UUID這樣的東西 msg_id varchar(70), ? -- 消息發出時的時間戳(如果是個跨國IM,則此時間戳可能是GMT-0標準時間)?????? send_time time, ? -- 消息發送者ID sender_uid varchar(50), ? -- 消息類型(標識此條消息是:文本、圖片還是語音留言等) msg_type int, ? -- 消息內容(如果是圖片或語音留言等類型,由此字段存放的可能是對應文件的存儲地址或CDN的訪問URL) msg_content varchar(1024), … |
② 離線消息拉取模式:
接收方B要拉取發送方A給ta發送的離線消息,只需在receiver_uid(即接收方B的用戶ID), sender_uid(即發送方A的用戶ID)上查詢,然后把離線消息刪除,再把消息返回B即可。
③ 離線消息的拉取,如果用SQL語句來描述的話,它可以是:
| 1 2 3 | SELECT msg_id, send_time, msg_type, msg_content FROM offline_msgs WHERE receiver_uid = ? and sender_uid = ? |
④ 離線拉取的整體流程如下圖所示:
?
- Stelp 1:用戶B開始拉取用戶A發送給ta的離線消息;
- Stelp 2:服務器從DB(或對應的持久化容器)中拉取離線消息;
- Stelp 3:服務器從DB(或對應的持久化容器)中把離線消息刪除;
- Stelp 4:服務器返回給用戶B想要的離線消息。
5、上述流程存在的問題以及優化方案
如果用戶B有很多好友,登陸時客戶端需要對所有好友進行離線消息拉取,客戶端與服務器交互次數就會比較多。
① 拉取好友離線消息的客戶端偽代碼:
| 1 2 3 4 5 | // 登陸時所有好友都要拉取 for(all uid in B’s friend-list){ ?????// 與服務器交互 ?????get_offline_msg(B,uid);?? } |
② 優化方案1:
先拉取各個好友的離線消息數量,真正用戶B進去看離線消息時,才往服務器發送拉取請求(手機端為了節省流量,經常會使用這個按需拉取的優化)。
③ 優化方案2:
如下圖所示,一次性拉取所有好友發送給用戶B的離線消息,到客戶端本地再根據sender_uid進行計算,這樣的話,離校消息表的訪問模式就變為->只需要按照receiver_uid來查詢了。登錄時與服務器的交互次數降低為了1次。
④ 方案小結:
通常情況下,主流的的移動端IM(比如微信、手Q等)通常都是以“優化方案2”為主,因為移動網絡的不可靠性加上電量、流量等資源的昂貴性,能盡量一次性干完的事,就盡可能一次搞定,從而提供整個APP的用戶體驗(對于移動端應用而言,省電、省流量同樣是用戶體驗的一部分)。這方面的文章,可以進一步參閱《談談移動端 IM 開發中登錄請求的優化》、《移動端IM實踐:iOS版微信界面卡頓監測方案》、《移動端IM實踐:Android版微信如何大幅提升交互性能(二)》。
6、消息接收方一次拉取大量離線消息導致速度慢、卡頓的解決方法
用戶B一次性拉取所有好友發給ta的離線消息,消息量很大時,一個請求包很大、速度慢,容易卡頓怎么辦?
正如上圖所示,我們可以分頁拉取:根據業務需求,先拉取最新(或者最舊)的一頁消息,再按需一頁頁拉取,這樣便能很好地解決用戶體驗問題。
7、優化離線消息的拉取過程,保證離線消息不會丟失
如何保證可達性,上述步驟第三步執行完畢之后,第四個步驟離線消息返回給客戶端過程中,服務器掛點,路由器丟消息,或者客戶端crash了,那離線消息豈不是丟了么(數據庫已刪除,用戶還沒收到)?
確實,如果按照上述的1、2、3、4步流程,的確是的,那如何保證離線消息的絕對可靠性、可達性?
如同在線消息的應用層ACK機制一樣,離線消息拉時,不能夠直接刪除數據庫中的離線消息,而必須等應用層的離線消息ACK(說明用戶B真的收到離線消息了),才能刪除數據庫中的離線消息。這個應用層的ACK可以通過實時消息通道告之服務端,也可以通過服務端提供的REST接口,以更通用、簡單的方式通知服務端。
8、進一步優化,解決重復拉取離線消息的問題
如果用戶B拉取了一頁離線消息,卻在ACK之前crash了,下次登錄時會拉取到重復的離線消息么?
確實,拉取了離線消息卻沒有ACK,服務器不會刪除之前的離線消息,故下次登錄時系統層面還會拉取到。但在業務層面,可以根據msg_id去重。SMC理論:系統層面無法做到消息不丟不重,業務層面可以做到,對用戶無感知。
優化后的拉取過程,如下圖所示:
?
9、進一步優化,降低離線拉取ACK帶來的額外與服務器的交互次數
假設有N頁離線消息,現在每個離線消息需要一個ACK,那么豈不是客戶端與服務器的交互次數又加倍了?有沒有優化空間?
如上圖所示,不用每一頁消息都ACK,在拉取第二頁消息時相當于第一頁消息的ACK,此時服務器再刪除第一頁的離線消息即可,最后一頁消息再ACK一次(實際上:最后一頁拉取的肯定是空返回,這樣可以極大地簡化這個分頁過程,否則客戶端得知道當前離線消息的總頁數,而由于消息讀取延遲的存在,這個總頁數理論上并非絕對不變,從而加大了數據讀取不一致的可能性)。這樣的效果是,不管拉取多少頁離線消息,只會多一個ACK請求,與服務器多一次交互。
10、本文小結
正如本文中所列舉的問題所描述的那樣,保證“離線消息”的可達性比大家想象的要復雜一些,常見優化總結如下:
?
- 1)對于同一個用戶B,一次性拉取所有用戶發給ta的離線消息,再在客戶端本地進行發送方分析,相比按照發送方一個個進行消息拉取,能大大減少服務器交互次數;
- 2)分頁拉取,先拉取計數再按需拉取,是無線端的常見優化;
- 3)應用層的ACK,應用層的去重,才能保證離線消息的不丟不重;
- 4)下一頁的拉取,同時作為上一頁的ACK,能夠極大減少與服務器的交互次數。
網易云信,你身邊的即時通訊和音視頻技術專家,了解我們,請戳網易云信官網
想要行業洞察和技術干貨,請關注網易云信博客
本文轉載自52im,作者:JackJiang
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的IM消息送达保证机制实现(二):保证离线消息的可靠投递的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 互联网1分钟 |1107
- 下一篇: 如何保证IM实时消息的“时序性”与“一致