1、點評
IM聊天消息的可靠投遞,是每個線上產(chǎn)品都要考慮的IM熱點技術(shù)問題。
IM聊天消息能保證可靠送達,對于用戶來說,就好比把錢存在銀行不怕被偷一樣,是信任的問題。試想,如果用戶能明顯感知到聊天消息無法保證送達,誰還愿意來用你的APP?誰也不希望自已的話就像浮云一樣隨風飄逝。
必竟用IM聊天,雖然很多時候是費話,但總有關(guān)鍵時刻存在——比如向女神表白(哪怕明知被拒),作為合格的舔狗一定不希望女神錯過這條消息。
所以,消息的可靠投遞是每款I(lǐng)M產(chǎn)品和立足之本,也是IM開發(fā)者們孜孜不倦追求的技術(shù)目標。
本文作者將以自已IM開發(fā)過程中的真實總結(jié),分享針對大量離線聊天消息,在確保用戶端體驗不降級的前提下,保證離線消息的可靠投遞。
學習交流:
- 即時通訊/推送技術(shù)開發(fā)交流5群:215477170[推薦]
- 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM》
本文已同步發(fā)布于“即時通訊技術(shù)圈”公眾號,歡迎關(guān)注:
▲ 本文在公眾號上的鏈接是:https://mp.weixin.qq.com/s/T2w9h_AN_T2UnqNdVikX0Q,原文鏈接是:http://www.52im.net/thread-3069-1-1.html
2、本文作者
fzully(柳林勇):2005年數(shù)學系畢業(yè),先后就職于福建新大陸、福建富士通、北京世紀奧通。長期從事服務端軟件開發(fā),涉及SIP服務器、內(nèi)核RTP轉(zhuǎn)送、電信級AAA認證系統(tǒng)、IM即時通訊系統(tǒng)等。在分布式高性能系統(tǒng)設(shè)計有多年經(jīng)驗積累。
本作者的另一篇:《IM群聊消息的已讀未讀功能在存儲空間方面的實現(xiàn)思路探討》也已被即時通訊網(wǎng)收錄并整理發(fā)布,有興趣可以前往閱讀。
3、相關(guān)文章
《從客戶端的角度來談談移動端IM的消息可靠性和送達機制》
《移動端IM中大規(guī)模群消息的推送如何保證效率、實時性?》
《IM消息送達保證機制實現(xiàn)(一):保證在線實時消息的可靠投遞》
《IM消息送達保證機制實現(xiàn)(二):保證離線消息的可靠投遞》(* 強烈推薦)
《如何保證IM實時消息的“時序性”與“一致性”?》
《一個低成本確保IM消息時序的方法探討》
《IM群聊消息如此復雜,如何保證不丟不重?》(* 強烈推薦)
《移動端IM登錄時拉取數(shù)據(jù)如何作到省流量?》(* 強烈推薦)
《完全自已開發(fā)的IM該如何設(shè)計“失敗重試”機制?》
《IM開發(fā)干貨分享:我是如何解決大量離線消息導致客戶端卡頓的》(* 強烈推薦)
4、正文引言
暗戀女神良久,終于鼓起勇氣決定向女神寫一封情書。但如何表達才能感動女神?自感才疏學淺,于是通讀四書五經(jīng)、熟背唐詩宋詞、遍覽四大名著,已然腹有詩書氣自華。一周末冥思苦想整日才寫就一首七言律詩,雖無驚天地泣鬼神之勢,但誠摯的愛念在字里行間里流淌,亦歌亦詩,相信會感動到女神,手機欣然發(fā)出。
發(fā)出一秒后,手心冒汗,感覺臉頰發(fā)燙,心臟像受驚嚇的野兔一樣快速跳動,就像第一次看見女神那時的感覺。閉著眼睛,想象女神看到消息時的情形,她是否也期盼我的表白?看到消息時是否心跳加速、小臉緋紅?
一分鐘后,緊盯手機屏幕,等待、期盼女神回復。
時間一分一秒地逝去,等一分鐘像等一年一樣漫長。
一小時后,仍然杳無音訊,難道她沒看到消息么?或許在忙什么而沒留意手機吧!
一天過去了,坐立不安,等待是一種痛苦的煎熬,期待和煎熬在心中交織翻滾,有幾個瞬間甚至希望女神趕快拒絕自己,好讓自己解脫!茶飯無味,失眠多天,整日魂不守舍。
一個月過去了,死心。
半年后,女神出嫁,婚禮那天前去祝福。席間亦隨眾觥籌交錯,略有醉意,向女神敬酒:祝福你,但愿以后能遇見像你這樣的女人。女神先是愣住、收起笑容,低下頭,目光無神地看著大紅地毯,長嘆一聲,言:我等你表白,等了一年!空氣凝滯了幾秒,女神強作歡顏:從今往后,各自安好吧,干杯!
我轉(zhuǎn)身踱回到座位,拿起手機,打開那個App,看著曾經(jīng)發(fā)出的情書,一切仿佛還在昨日,但故事腳本已被別人書寫,欲哭無淚,嘆老天為何如此捉弄我?為何我發(fā)的消息女神沒收到啊!
失魂落魄地回到家里,從冰箱里拿出幾瓶羅斯福10號來麻醉自己,在酒精強烈的作用下,迷迷入睡。
第二天醒來,我明白了一個道理:對IM系統(tǒng)而言,消息必達永遠擺在第一位!
以上是胡說八道,以下開始正文。。。
5、用全量離線消息實現(xiàn)消息必達
我們在重構(gòu)IM系統(tǒng)時,需解決上一代設(shè)計的痛點之一就是確保消息必達。
5.1 離線消息實現(xiàn)消息必達的流程
自然而然地會想到這么做——即由服務端為每個人保存一個“離線消息列表”。
具體的思路是這樣:
- 1)當用戶在線時,由IMS主動確保消息下發(fā)且收到客戶端的應答確認時,才認為消息送達客戶端,相應地把消息從“離線消息列表”移除;
- 2)如果客戶端沒有發(fā)回應答確認,IM服務端會再發(fā)送。
以此來確保消息一定送到客戶端,看起來是很符合邏輯。當時調(diào)查過市面上多款I(lǐng)M,行為基本如此。
5.2 海嘯般的離線消息
5.2.1)和平時期:
重構(gòu)后的IM上線,內(nèi)部測試及在公網(wǎng)運行,離線消息的工作一直很正常。
5.2.2)被簽到簽死了:
后來,為某客戶部署的私有環(huán)境,其用戶量達幾十萬,其中的一個組織接近三萬人,全員群也接近三萬人;還有,底下的部門也有相應的群組,幾百到幾千人群不等。
“報到”、“簽到”。。。大量的類似消息被發(fā)到幾千、幾萬人的群內(nèi),然后如果有人一兩天沒上線,或者被加入到多個組織內(nèi),等到其上線時,幾萬條離線消息像海嘯一般涌來,您想象一下:手機用戶剛登陸的幾分鐘內(nèi),是什么場景?
用戶真的很無辜:我不就是登陸了一下App,叮叮咚咚響了幾分鐘,還卡,還發(fā)熱。。。
客戶端承受不起大規(guī)模離線消息的轟炸,怎么辦?
5.3 臨時運用方案
- 1)對若干大組織的全員群,對非管理員禁言;
- 2)通知所有用戶不要在大群簽到。
我承認,這確實不算是個正經(jīng)方案。。。
6、遠離全量離線消息
我承認,一開始設(shè)計離線消息時,真沒想到是這樣的使用場景。對于大多數(shù)IM的開發(fā)者,或許不會碰到這種場景(但凡事住最壞的可能性想,總是沒錯的)。
6.1 放棄以離線消息的形式實現(xiàn)消息必達
我開始思考什么是消息必達,以前的想法是:把用戶該收的消息都送到其客戶端,是消息必達。
后來,給消息必達下了新的定義:
- 1)用戶有新消息時,確保讓用戶知道;
- 2)當用戶要查看這些消息時,確保其可一條不漏地看到。
打個比方:
- 1)客戶要把錢給您,不必送到您家里才算送到;
- 2)而是轉(zhuǎn)賬到您的銀行賬戶上,并告知您;
- 3)當您要用錢時,直接從銀行賬戶上消費即可。
從此,不會在用戶上線時向其發(fā)送大量離線消息(即全量推送)。
6.2 以會話列表為基礎(chǔ)來實現(xiàn)消息必達
客戶端在上線時,先從服務端更新會話列表,也就是你通常在每個IM客戶端的首頁看到的這個(如下圖所示)。
(上圖引用自《IM開發(fā)快速入門(一):什么是IM系統(tǒng)?》)
每一個會話列表項包含如下信息(此處簡化了與本文無關(guān)的成員變量):
{
// 會話對象的角色類型,比如私聊、群聊、系統(tǒng)通知、業(yè)務通知。。。
uint32 session_role;
// 會話對象的ID
uint32 session_id;
// 會話時間戳,用于消息同步;
// 指會話的最后操作時間,比如清除角標的時間,與會話最后一條的消息時間未必一致
uint64 session_timestamp;
// true表示新增或更新,false表示被刪除
boolis_add;
// 當is_add=false時,忽略以下信息
// 僅用于顯示角標的未讀數(shù)量,當用戶查看該會話后清零,且客戶端多端同步
uint32 new_msg_count;
// 會話的最后一條消息
MessageItem latest_msg;
// 跳轉(zhuǎn)消息的時間戳,即new_msg_count的最舊1條消息的時間
uint64 goto_timestamp;
}
為方便討論,假設(shè)以下前提:
- 1)周五傍晚18:00下班,我關(guān)閉App,我是9527;
- 2)有1小姐姐向我發(fā)了5條消息留言,約我周末去海邊玩,她是楊冪3306;
- 3)然后,另1小姐姐也向我發(fā)了33條消息留言,內(nèi)容我不便透露,她是景甜5672;
- 4)嚴正聲明:我跟她們很清白,其實我喜歡的是6379。
對,既然是假設(shè),假一點也無妨。
我下班回到家,看到手機有通知欄消息,打開App將會發(fā)生哪些事呢?
App和IM后端的交互:
1)登錄后,App以18:00填充參數(shù)latest_session_time,向IMS獲取會話列表(其實不是以下線時間18:00,但這樣更易理解);
2)IM后端檢查發(fā)現(xiàn)我從18:00開始,有2個會話更新了,于是向App發(fā)送應答,以增量形式攜帶2個會話項:楊冪3306,景甜5672。其中景甜5672的會話項信息如下:
{
uint32 session_role = Role_User; //表示私聊
uint32 session_id = 5672; //景甜的ID
uint64 session_timestamp = 1594464295335672; //最后一條消息的時間戳,微秒
boolis_add = true; // true表示是更新項
uint32 new_msg_count = 33; // 景甜向我發(fā)了33條消息
MessageItem latest_msg = "房號是0520"; //最后1條消息,結(jié)構(gòu)體MessageItem簡略不表
uint64 goto_timestamp = 1594463697556677; // 向我發(fā)的33條消息的最早1條的時間
}
3)App收到步驟2的應答,我在App的會話列表窗口里,能看到2項更新,景甜發(fā)來的未讀消息數(shù)33條,楊冪的是5條,如下圖所示:
4)點開景甜5672的會話,App將向IMS發(fā)起同步消息的請求,獲取最新的10條聊天消息(為了顯示一屏):
{
uint32 session_role = Role_User; //表示私聊
uint32 session_id = 5672; //景甜的ID
uint64 begin_time = 1594464295335672; //步驟2返回的session_timestamp
uint64 end_time = 1594434153444222; //景甜上午向我發(fā)的最后一條消息的時間
uint32 max_pieces = 10; //本次最多取10條,PC屏幕大則不妨取20條
}
5)IM后端收到步驟4請求,將返回33條新消息的最后10條給App,呈現(xiàn)聊天窗口內(nèi),且聊天窗口上方有一個tip:“↑ 33條新消息”,如下圖所示:
6)我可以向上翻動聊天記錄,那么App將持續(xù)向IMS獲取第2批同步消息;或者也可以點擊tip:“↑ 33條新消息”,直接跳轉(zhuǎn)到33條消息的最舊一條,這樣支持從最舊的消息向新的翻看。
相比于客戶端簡單地被動接收服務端的離線通知方式,這種設(shè)計使得客戶端的處理邏輯更復雜。
主要體現(xiàn)在:
- 1)客戶端向服務端取的同步消息是未必完整,這些存在客戶端的消息,在時間區(qū)間上可能不連續(xù)的;
- 2)客戶端需要知道不同消息之間是否有斷代,如果有則需要向服務端查詢同步消息來merge本地信息,使其連續(xù),即客戶端要實現(xiàn)消息融合。
我的建議:用C++實現(xiàn)一個統(tǒng)一的底層imsdk庫,來負責這些共通的消息處理和存儲。避免各客戶端(Windows,iOS,Android等)各自實現(xiàn)這些邏輯,減少工作量,也降低各端不一致的風險。
6.3 以會話列表為基礎(chǔ)與用全量離線消息的方案對比
6.3.1)用全量離線消息實現(xiàn)的方案優(yōu)缺點:
實現(xiàn)原理:由IM服務端確保消息送達客戶端,客戶端存儲后發(fā)回確認。
方案優(yōu)點:邏輯簡單。
在聊天消息不同數(shù)量級時的表現(xiàn):
- a. 離線消息量不多(如幾百條):沒有效率問題,且消息全部達到客戶端本地,方便進行查找等動作;
- b. 離線消息量巨大(如幾萬條):用戶登錄瞬間CS間瞬時流量大,客戶端瞬時要存儲、更新的數(shù)據(jù)量巨大,可能出現(xiàn)卡頓、假死等情況。
6.3.2)用會話列表為基礎(chǔ)的方案優(yōu)缺點:
實現(xiàn)原理:客戶端先同步會話列表,由用戶驅(qū)動不定次獲取同步消息。
方案缺點:邏輯復雜,客戶端增加不少工作。
在聊天消息不同數(shù)量級時的表現(xiàn):
- a. 離線消息量不多(如幾百條):沒優(yōu)勢;
- b. 離線消息量巨大(如幾萬條):登錄時交互數(shù)據(jù)小,對IM后端、客戶端、用戶體驗,都比較友好。
7、多終端條件下,如何得到完整消息履歷?
由于同一個用戶的每個終端,其會話最后更新時間、每個會話的最后一條時間可能都不一樣,參照上一節(jié)的實現(xiàn)思路,可以得到解決方案。
具體如下:
- 1)參照第6.2章節(jié)的“App和IM后端的交互”第1個步驟,可取到不同的增量變化的會話列表項;
- 2)參照第6.2章節(jié)的“App和IM后端的交互”第4個步驟,可取到任一區(qū)間的同步消息,得到完整消息。
8、離線消息是否就徹底廢棄了?
有若干情況,仍然需要保留離線消息,以確保消息送達。
比如以下情形:
- 1)別人向我發(fā)送離線文件:這種情況下不能依賴同步消息來獲取。因為不以離線消息通知的話,用戶在沒有拉取到對應的同步消息前,是不知道有離線文件的;
- 2)撤回消息:即使接收者不拉取同步,仍然要保證在上線后其數(shù)據(jù)在第一時間被撤回。注意:這里可能存在多端撤回問題;
- 3)用戶在線時的消息下發(fā):由于用戶在線時,IM后端向客戶端發(fā)送消息可能碰到網(wǎng)絡抖動等情況,導致消息下發(fā)失敗,這些消息先可以直接存在離線消息隊列,IM后端可在收到客戶端的心跳包時重發(fā)消息。相當于維護了一個在線消息的離線隊列。
9、本文結(jié)語
曾經(jīng)有一段真摯的愛情擺在我面前,如果時間倒流到半年前,我會選擇一個靠譜的IM來發(fā)送消息,也許故事的腳本就由自己書寫——是否要整一個時光倒流的版本,抱得美人歸的那種?
不整了不整了,我得不到女神,你們才歡喜,我太了解你們了。。。各位爺歡喜就好。
附錄:IM開發(fā)干貨系列文章
本文是系列文章中的第26篇,總目錄如下:
《IM消息送達保證機制實現(xiàn)(一):保證在線實時消息的可靠投遞》
《IM消息送達保證機制實現(xiàn)(二):保證離線消息的可靠投遞》
《如何保證IM實時消息的“時序性”與“一致性”?》
《IM單聊和群聊中的在線狀態(tài)同步應該用“推”還是“拉”?》
《IM群聊消息如此復雜,如何保證不丟不重?》
《一種Android端IM智能心跳算法的設(shè)計與實現(xiàn)探討(含樣例代碼)》
《移動端IM登錄時拉取數(shù)據(jù)如何作到省流量?》
《通俗易懂:基于集群的移動端IM接入層負載均衡方案分享》
《淺談移動端IM的多點登陸和消息漫游原理》
《IM開發(fā)基礎(chǔ)知識補課(一):正確理解前置HTTP SSO單點登陸接口的原理》
《IM開發(fā)基礎(chǔ)知識補課(二):如何設(shè)計大量圖片文件的服務端存儲架構(gòu)?》
《IM開發(fā)基礎(chǔ)知識補課(三):快速理解服務端數(shù)據(jù)庫讀寫分離原理及實踐建議》
《IM開發(fā)基礎(chǔ)知識補課(四):正確理解HTTP短連接中的Cookie、Session和Token》
《IM群聊消息的已讀回執(zhí)功能該怎么實現(xiàn)?》
《IM群聊消息究竟是存1份(即擴散讀)還是存多份(即擴散寫)?》
《IM開發(fā)基礎(chǔ)知識補課(五):通俗易懂,正確理解并用好MQ消息隊列》
《一個低成本確保IM消息時序的方法探討》
《IM開發(fā)基礎(chǔ)知識補課(六):數(shù)據(jù)庫用NoSQL還是SQL?讀這篇就夠了!》
《IM里“附近的人”功能實現(xiàn)原理是什么?如何高效率地實現(xiàn)它?》
《IM開發(fā)基礎(chǔ)知識補課(七):主流移動端賬號登錄方式的原理及設(shè)計思路》
《IM開發(fā)基礎(chǔ)知識補課(八):史上最通俗,徹底搞懂字符亂碼問題的本質(zhì)》
《IM的掃碼登功能如何實現(xiàn)?一文搞懂主流應用的掃碼登陸技術(shù)原理》
《IM要做手機掃碼登陸?先看看微信的掃碼登錄功能技術(shù)原理》
《IM開發(fā)基礎(chǔ)知識補課(九):想開發(fā)IM集群?先搞懂什么是RPC!》
《IM開發(fā)實戰(zhàn)干貨:我是如何解決大量離線聊天消息導致客戶端卡頓的》
《IM開發(fā)干貨分享:如何優(yōu)雅的實現(xiàn)大量離線消息的可靠投遞》(* 本文)
另外,如果您是IM開發(fā)初學者,強烈建議首先閱讀《新手入門一篇就夠:從零開發(fā)移動端IM》。
(本文同步發(fā)布于:http://www.52im.net/thread-3069-1-1.html)