1、前言
本文的上篇《IM消息送達保證機制實現(xiàn)(一):保證在線實時消息的可靠投遞》中擂橘,我們討論了在線實時消息的投遞可以通過應用層的確認盏檐、發(fā)送方的超時重傳睹簇、接收方的去重等手段來保證業(yè)務層面消息的不丟不重。
但實時在線投遞針對的是消息收發(fā)雙方都在線的情況(如當發(fā)送方用戶A發(fā)送消息給接收方用戶B時蕾盯,用戶B是在線的)幕屹,那如果消息的接收方用戶B不在線,系統(tǒng)是如何保證消息的可達性的呢级遭?這就是本文要討論的問題望拖。
2、IM開發(fā)干貨系列文章
本文是系列文章中的第2篇挫鸽,總目錄如下:
《IM消息送達保證機制實現(xiàn)(一):保證在線實時消息的可靠投遞》
《IM消息送達保證機制實現(xiàn)(二):保證離線消息的可靠投遞》(本文)
《如何保證IM實時消息的“時序性”與“一致性”说敏?》
《IM單聊和群聊中的在線狀態(tài)同步應該用“推”還是“拉”?》
《IM群聊消息如此復雜丢郊,如何保證不丟不重盔沫?》
《一種Android端IM智能心跳算法的設(shè)計與實現(xiàn)探討(含樣例代碼)》
《移動端IM登錄時拉取數(shù)據(jù)如何作到省流量?》
《通俗易懂:基于集群的移動端IM接入層負載均衡方案分享》
《淺談移動端IM的多點登陸和消息漫游原理》
3枫匾、消息接收方不在線時的典型消息發(fā)送流程
如上圖所述架诞,通常此類情況下消息的發(fā)送流程如下:
Step 1:用戶A發(fā)送一條消息給用戶B;
Step 2:服務器查看用戶B的狀態(tài)干茉,發(fā)現(xiàn)B的狀態(tài)為“offline”(即B當前不在線)谴忧;
Step 3:服務器將此條消息以離線消息的形式持久化存儲到DB中(當然,具體的持久化方案可由您IM的具體技術(shù)實現(xiàn)為準);
Step 4:服務器返回用戶A“發(fā)送成功”ACK確認包(注:對于消息發(fā)送方而言沾谓,消息一旦落地存儲至DB就認為是發(fā)送成功了)委造。
關(guān)于 “Step 4” 的補充說明:
請一定要理解“Step 4”,因為現(xiàn)在無論是傳統(tǒng)的PC端IM(類似QQ這樣的——可以在UI上看到好友的在線均驶、離線狀態(tài))還是目前主流的移動端IM(強調(diào)的是用戶全時在線——即你看不到好友到底在線還是離線昏兆,反正給你的假像就是這個好友“應該”是在線的),消息發(fā)送出去后辣恋,無論是對方實時在線收到還是對方不在線而被服務端離線存儲了亮垫,對于發(fā)送方而言只要消息沒有因為網(wǎng)絡(luò)等原因莫名消失,就應該認為是“被收到了”伟骨。
從技術(shù)的角度講饮潦,消息接收方收到的消息應答ACK包的真正發(fā)起者,實際上有兩種可能性:一種是由接收方發(fā)出携狭、而另一種是由服務端代為發(fā)送(這在MobileIMSDK開源工程里被稱作“偽應答”)继蜡。
4、典型離線消息表的設(shè)計以及拉取離線消息的過程
① 存儲離線消看書的表主要字段大致如下:
-- 消息接收者ID
receiver_uidvarchar(50),
-- 消息的唯一指紋碼(即消息ID)逛腿,用于去重等場景稀并,單機情況下此id可能是個自增值、分布式場景下可能是類似于UUID這樣的東西
msg_idvarchar(70),
-- 消息發(fā)出時的時間戳(如果是個跨國IM单默,則此時間戳可能是GMT-0標準時間)? ? ?
send_timetime,
-- 消息發(fā)送者ID
sender_uidvarchar(50),
-- 消息類型(標識此條消息是:文本碘举、圖片還是語音留言等)
msg_typeint,
-- 消息內(nèi)容(如果是圖片或語音留言等類型,由此字段存放的可能是對應文件的存儲地址或CDN的訪問URL)
msg_contentvarchar(1024),
…
② 離線消息拉取模式:
接收方B要拉取發(fā)送方A給ta發(fā)送的離線消息搁廓,只需在receiver_uid(即接收方B的用戶ID), sender_uid(即發(fā)送方A的用戶ID)上查詢引颈,然后把離線消息刪除,再把消息返回B即可境蜕。
③ 離線消息的拉取蝙场,如果用SQL語句來描述的話,它可以是:
SELECT msg_id, send_time, msg_type, msg_content
FROM offline_msgs
WHERE receiver_uid = ? and sender_uid = ?
④ 離線拉取的整體流程如下圖所示:
Stelp 1:用戶B開始拉取用戶A發(fā)送給ta的離線消息粱年;
Stelp 2:服務器從DB(或?qū)某志没萜鳎┲欣‰x線消息售滤;
Stelp 3:服務器從DB(或?qū)某志没萜鳎┲邪央x線消息刪除;
Stelp 4:服務器返回給用戶B想要的離線消息台诗。
5完箩、上述流程存在的問題以及優(yōu)化方案
如果用戶B有很多好友,登陸時客戶端需要對所有好友進行離線消息拉取拉庶,客戶端與服務器交互次數(shù)就會比較多嗜憔。
① 拉取好友離線消息的客戶端偽代碼:
// 登陸時所有好友都要拉取
for(all uid in B’s friend-list){
? ? // 與服務器交互
? ? get_offline_msg(B,uid);?
}
② 優(yōu)化方案1:
先拉取各個好友的離線消息數(shù)量,真正用戶B進去看離線消息時氏仗,才往服務器發(fā)送拉取請求(手機端為了節(jié)省流量吉捶,經(jīng)常會使用這個按需拉取的優(yōu)化)夺鲜。
③ 優(yōu)化方案2:
如下圖所示,一次性拉取所有好友發(fā)送給用戶B的離線消息呐舔,到客戶端本地再根據(jù)sender_uid進行計算币励,這樣的話,離校消息表的訪問模式就變?yōu)?>只需要按照receiver_uid來查詢了珊拼。登錄時與服務器的交互次數(shù)降低為了1次食呻。
④ 方案小結(jié):
通常情況下,主流的的移動端IM(比如微信澎现、手Q等)通常都是以“優(yōu)化方案2”為主仅胞,因為移動網(wǎng)絡(luò)的不可靠性加上電量、流量等資源的昂貴性剑辫,能盡量一次性干完的事干旧,就盡可能一次搞定,從而提供整個APP的用戶體驗(對于移動端應用而言妹蔽,省電椎眯、省流量同樣是用戶體驗的一部分)。
6胳岂、消息接收方一次拉取大量離線消息導致速度慢编整、卡頓的解決方法
用戶B一次性拉取所有好友發(fā)給ta的離線消息,消息量很大時乳丰,一個請求包很大掌测、速度慢,容易卡頓怎么辦产园?
正如上圖所示赏半,我們可以分頁拉取:根據(jù)業(yè)務需求淆两,先拉取最新(或者最舊)的一頁消息,再按需一頁頁拉取拂酣,這樣便能很好地解決用戶體驗問題秋冰。
7、優(yōu)化離線消息的拉取過程婶熬,保證離線消息不會丟失
如何保證可達性剑勾,上述步驟第三步執(zhí)行完畢之后,第四個步驟離線消息返回給客戶端過程中赵颅,服務器掛點虽另,路由器丟消息,或者客戶端crash了饺谬,那離線消息豈不是丟了么(數(shù)據(jù)庫已刪除捂刺,用戶還沒收到)?
確實,如果按照上述的1族展、2森缠、3、4步流程仪缸,的確是的贵涵,那如何保證離線消息的絕對可靠性、可達性恰画?
如同在線消息的應用層ACK機制一樣宾茂,離線消息拉時,不能夠直接刪除數(shù)據(jù)庫中的離線消息拴还,而必須等應用層的離線消息ACK(說明用戶B真的收到離線消息了)跨晴,才能刪除數(shù)據(jù)庫中的離線消息。這個應用層的ACK可以通過實時消息通道告之服務端自沧,也可以通過服務端提供的REST接口坟奥,以更通用、簡單的方式通知服務端拇厢。
8爱谁、進一步優(yōu)化,解決重復拉取離線消息的問題
如果用戶B拉取了一頁離線消息孝偎,卻在ACK之前crash了访敌,下次登錄時會拉取到重復的離線消息么?
確實衣盾,拉取了離線消息卻沒有ACK寺旺,服務器不會刪除之前的離線消息,故下次登錄時系統(tǒng)層面還會拉取到势决。但在業(yè)務層面阻塑,可以根據(jù)msg_id去重。SMC理論:系統(tǒng)層面無法做到消息不丟不重果复,業(yè)務層面可以做到陈莽,對用戶無感知。
優(yōu)化后的拉取過程虽抄,如下圖所示:
9走搁、進一步優(yōu)化,降低離線拉取ACK帶來的額外與服務器的交互次數(shù)
假設(shè)有N頁離線消息迈窟,現(xiàn)在每個離線消息需要一個ACK私植,那么豈不是客戶端與服務器的交互次數(shù)又加倍了?有沒有優(yōu)化空間车酣?
如上圖所示曲稼,不用每一頁消息都ACK索绪,在拉取第二頁消息時相當于第一頁消息的ACK,此時服務器再刪除第一頁的離線消息即可躯肌,最后一頁消息再ACK一次(實際上:最后一頁拉取的肯定是空返回者春,這樣可以極大地簡化這個分頁過程,否則客戶端得知道當前離線消息的總頁數(shù)清女,而由于消息讀取延遲的存在钱烟,這個總頁數(shù)理論上并非絕對不變,從而加大了數(shù)據(jù)讀取不一致的可能性)嫡丙。這樣的效果是拴袭,不管拉取多少頁離線消息,只會多一個ACK請求曙博,與服務器多一次交互拥刻。
10、本文小結(jié)
正如本文中所列舉的問題所描述的那樣父泳,保證“離線消息”的可達性比大家想象的要復雜一些般哼,常見優(yōu)化總結(jié)如下:
1)對于同一個用戶B,一次性拉取所有用戶發(fā)給ta的離線消息惠窄,再在客戶端本地進行發(fā)送方分析蒸眠,相比按照發(fā)送方一個個進行消息拉取,能大大減少服務器交互次數(shù)杆融;
2)分頁拉取楞卡,先拉取計數(shù)再按需拉取,是無線端的常見優(yōu)化脾歇;
3)應用層的ACK蒋腮,應用層的去重,才能保證離線消息的不丟不重藕各;
4)下一頁的拉取池摧,同時作為上一頁的ACK,能夠極大減少與服務器的交互次數(shù)激况。