IM開發干貨分享:如何優雅的實現大量離線消息的可靠投遞


1、點評

IM聊天消息的可靠投遞,是每個線上產品都要考慮的IM熱點技術問題。

IM聊天消息能保證可靠送達,對於用戶來說,就好比把錢存在銀行不怕被偷一樣,是信任的問題。試想,如果用戶能明顯感知到聊天消息無法保證送達,誰還願意來用你的APP?誰也不希望自已的話就像浮雲一樣隨風飄逝。

必竟用IM聊天,雖然很多時候是費話,但總有關鍵時刻存在——比如向女神表白(哪怕明知被拒),作為合格的舔狗一定不希望女神錯過這條消息。

所以,消息的可靠投遞是每款IM產品和立足之本,也是IM開發者們孜孜不倦追求的技術目標。

本文作者將以自已IM開發過程中的真實總結,分享針對大量離線聊天消息,在確保用戶端體驗不降級的前提下,保證離線消息的可靠投遞。

學習交流:

- 即時通訊/推送技術開發交流5群:215477170[推薦]

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM

本文已同步發布於“即時通訊技術圈”公眾號,歡迎關注:

▲ 本文在公眾號上的鏈接是:https://mp.weixin.qq.com/s/T2w9h_AN_T2UnqNdVikX0Q,原文鏈接是:http://www.52im.net/thread-3069-1-1.html

 

2、本文作者 

fzully(柳林勇):2005年數學系畢業,先后就職於福建新大陸、福建富士通、北京世紀奧通。長期從事服務端軟件開發,涉及SIP服務器、內核RTP轉送、電信級AAA認證系統、IM即時通訊系統等。在分布式高性能系統設計有多年經驗積累。

本作者的另一篇:《IM群聊消息的已讀未讀功能在存儲空間方面的實現思路探討》也已被即時通訊網收錄並整理發布,有興趣可以前往閱讀。

3、相關文章

從客戶端的角度來談談移動端IM的消息可靠性和送達機制

移動端IM中大規模群消息的推送如何保證效率、實時性?

IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞

IM消息送達保證機制實現(二):保證離線消息的可靠投遞》(* 強烈推薦

如何保證IM實時消息的“時序性”與“一致性”?

一個低成本確保IM消息時序的方法探討

IM群聊消息如此復雜,如何保證不丟不重?》(* 強烈推薦

移動端IM登錄時拉取數據如何作到省流量?》(* 強烈推薦

完全自已開發的IM該如何設計“失敗重試”機制?

IM開發干貨分享:我是如何解決大量離線消息導致客戶端卡頓的》(* 強烈推薦

4、正文引言

暗戀女神良久,終於鼓起勇氣決定向女神寫一封情書。但如何表達才能感動女神?自感才疏學淺,於是通讀四書五經、熟背唐詩宋詞、遍覽四大名著,已然腹有詩書氣自華。一周末冥思苦想整日才寫就一首七言律詩,雖無驚天地泣鬼神之勢,但誠摯的愛念在字里行間里流淌,亦歌亦詩,相信會感動到女神,手機欣然發出。

發出一秒后,手心冒汗,感覺臉頰發燙,心臟像受驚嚇的野兔一樣快速跳動,就像第一次看見女神那時的感覺。閉着眼睛,想象女神看到消息時的情形,她是否也期盼我的表白?看到消息時是否心跳加速、小臉緋紅?

一分鍾后,緊盯手機屏幕,等待、期盼女神回復。

時間一分一秒地逝去,等一分鍾像等一年一樣漫長。

一小時后,仍然杳無音訊,難道她沒看到消息么?或許在忙什么而沒留意手機吧!

一天過去了,坐立不安,等待是一種痛苦的煎熬,期待和煎熬在心中交織翻滾,有幾個瞬間甚至希望女神趕快拒絕自己,好讓自己解脫!茶飯無味,失眠多天,整日魂不守舍。

一個月過去了,死心。

半年后,女神出嫁,婚禮那天前去祝福。席間亦隨眾觥籌交錯,略有醉意,向女神敬酒:祝福你,但願以后能遇見像你這樣的女人。女神先是愣住、收起笑容,低下頭,目光無神地看着大紅地毯,長嘆一聲,言:我等你表白,等了一年!空氣凝滯了幾秒,女神強作歡顏:從今往后,各自安好吧,干杯!

我轉身踱回到座位,拿起手機,打開那個App,看着曾經發出的情書,一切仿佛還在昨日,但故事腳本已被別人書寫,欲哭無淚,嘆老天為何如此捉弄我?為何我發的消息女神沒收到啊!

失魂落魄地回到家里,從冰箱里拿出幾瓶羅斯福10號來麻醉自己,在酒精強烈的作用下,迷迷入睡。

第二天醒來,我明白了一個道理:對IM系統而言,消息必達永遠擺在第一位!

以上是胡說八道,以下開始正文。。。

5、用全量離線消息實現消息必達

我們在重構IM系統時,需解決上一代設計的痛點之一就是確保消息必達。

5.1 離線消息實現消息必達的流程

自然而然地會想到這么做——即由服務端為每個人保存一個“離線消息列表”。

具體的思路是這樣:

  • 1)當用戶在線時,由IMS主動確保消息下發且收到客戶端的應答確認時,才認為消息送達客戶端,相應地把消息從“離線消息列表”移除;
  • 2)如果客戶端沒有發回應答確認,IM服務端會再發送。

以此來確保消息一定送到客戶端,看起來是很符合邏輯。當時調查過市面上多款IM,行為基本如此。

5.2 海嘯般的離線消息

5.2.1)和平時期:

重構后的IM上線,內部測試及在公網運行,離線消息的工作一直很正常。

5.2.2)被簽到簽死了:

后來,為某客戶部署的私有環境,其用戶量達幾十萬,其中的一個組織接近三萬人,全員群也接近三萬人;還有,底下的部門也有相應的群組,幾百到幾千人群不等。

“報到”、“簽到”。。。大量的類似消息被發到幾千、幾萬人的群內,然后如果有人一兩天沒上線,或者被加入到多個組織內,等到其上線時,幾萬條離線消息像海嘯一般涌來,您想象一下:手機用戶剛登陸的幾分鍾內,是什么場景?

用戶真的很無辜:我不就是登陸了一下App,叮叮咚咚響了幾分鍾,還卡,還發熱。。。

客戶端承受不起大規模離線消息的轟炸,怎么辦?

5.3 臨時運用方案

  • 1)對若干大組織的全員群,對非管理員禁言;
  • 2)通知所有用戶不要在大群簽到。

我承認,這確實不算是個正經方案。。。

6、遠離全量離線消息

我承認,一開始設計離線消息時,真沒想到是這樣的使用場景。對於大多數IM的開發者,或許不會碰到這種場景(但凡事住最壞的可能性想,總是沒錯的)。

6.1 放棄以離線消息的形式實現消息必達

我開始思考什么是消息必達,以前的想法是:把用戶該收的消息都送到其客戶端,是消息必達。

后來,給消息必達下了新的定義:

  • 1)用戶有新消息時,確保讓用戶知道;
  • 2)當用戶要查看這些消息時,確保其可一條不漏地看到。

打個比方:

  • 1)客戶要把錢給您,不必送到您家里才算送到;
  • 2)而是轉賬到您的銀行賬戶上,並告知您;
  • 3)當您要用錢時,直接從銀行賬戶上消費即可。

從此,不會在用戶上線時向其發送大量離線消息(即全量推送)。

6.2 以會話列表為基礎來實現消息必達

客戶端在上線時,先從服務端更新會話列表,也就是你通常在每個IM客戶端的首頁看到的這個(如下圖所示)。

上圖引用自《IM開發快速入門(一):什么是IM系統?

每一個會話列表項包含如下信息(此處簡化了與本文無關的成員變量):

{

        // 會話對象的角色類型,比如私聊、群聊、系統通知、業務通知。。。

        uint32  session_role;

        // 會話對象的ID

        uint32        session_id;

        // 會話時間戳,用於消息同步;

        // 指會話的最后操作時間,比如清除角標的時間,與會話最后一條的消息時間未必一致

        uint64 session_timestamp;

        // true表示新增或更新,false表示被刪除

        boolis_add;

 

        // 當is_add=false時,忽略以下信息

 

        // 僅用於顯示角標的未讀數量,當用戶查看該會話后清零,且客戶端多端同步

        uint32 new_msg_count;

        // 會話的最后一條消息

        MessageItem         latest_msg;

        // 跳轉消息的時間戳,即new_msg_count的最舊1條消息的時間

        uint64 goto_timestamp;

}

為方便討論,假設以下前提:

  • 1)周五傍晚18:00下班,我關閉App,我是9527;
  • 2)有1小姐姐向我發了5條消息留言,約我周末去海邊玩,她是楊冪3306;
  • 3)然后,另1小姐姐也向我發了33條消息留言,內容我不便透露,她是景甜5672;
  • 4)嚴正聲明:我跟她們很清白,其實我喜歡的是6379。

對,既然是假設,假一點也無妨。

我下班回到家,看到手機有通知欄消息,打開App將會發生哪些事呢?

App和IM后端的交互:

1)登錄后,App以18:00填充參數latest_session_time,向IMS獲取會話列表(其實不是以下線時間18:00,但這樣更易理解);

2)IM后端檢查發現我從18:00開始,有2個會話更新了,於是向App發送應答,以增量形式攜帶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; // 景甜向我發了33條消息

        MessageItem         latest_msg = "房號是0520"; //最后1條消息,結構體MessageItem簡略不表

        uint64  goto_timestamp = 1594463697556677; // 向我發的33條消息的最早1條的時間

}

3)App收到步驟2的應答,我在App的會話列表窗口里,能看到2項更新,景甜發來的未讀消息數33條,楊冪的是5條,如下圖所示:

 

4)點開景甜5672的會話,App將向IMS發起同步消息的請求,獲取最新的10條聊天消息(為了顯示一屏):

{

        uint32  session_role = Role_User; //表示私聊

        uint32        session_id = 5672; //景甜的ID

        uint64        begin_time  = 1594464295335672; //步驟2返回的session_timestamp

        uint64        end_time  = 1594434153444222; //景甜上午向我發的最后一條消息的時間

        uint32        max_pieces = 10; //本次最多取10條,PC屏幕大則不妨取20條

}

5)IM后端收到步驟4請求,將返回33條新消息的最后10條給App,呈現聊天窗口內,且聊天窗口上方有一個tip:“↑ 33條新消息”,如下圖所示:

 
 

6)我可以向上翻動聊天記錄,那么App將持續向IMS獲取第2批同步消息;或者也可以點擊tip:“↑ 33條新消息”,直接跳轉到33條消息的最舊一條,這樣支持從最舊的消息向新的翻看。

相比於客戶端簡單地被動接收服務端的離線通知方式,這種設計使得客戶端的處理邏輯更復雜。

主要體現在:

  • 1)客戶端向服務端取的同步消息是未必完整,這些存在客戶端的消息,在時間區間上可能不連續的;
  • 2)客戶端需要知道不同消息之間是否有斷代,如果有則需要向服務端查詢同步消息來merge本地信息,使其連續,即客戶端要實現消息融合。

我的建議:用C++實現一個統一的底層imsdk庫,來負責這些共通的消息處理和存儲。避免各客戶端(Windows,iOS,Android等)各自實現這些邏輯,減少工作量,也降低各端不一致的風險。

6.3 以會話列表為基礎與用全量離線消息的方案對比

6.3.1)用全量離線消息實現的方案優缺點:

實現原理:由IM服務端確保消息送達客戶端,客戶端存儲后發回確認。

方案優點:邏輯簡單。

在聊天消息不同數量級時的表現:

  • a. 離線消息量不多(如幾百條):沒有效率問題,且消息全部達到客戶端本地,方便進行查找等動作;
  • b. 離線消息量巨大(如幾萬條):用戶登錄瞬間CS間瞬時流量大,客戶端瞬時要存儲、更新的數據量巨大,可能出現卡頓、假死等情況。

6.3.2)用會話列表為基礎的方案優缺點:

實現原理:客戶端先同步會話列表,由用戶驅動不定次獲取同步消息。

方案缺點:邏輯復雜,客戶端增加不少工作。

在聊天消息不同數量級時的表現:

  • a. 離線消息量不多(如幾百條):沒優勢;
  • b. 離線消息量巨大(如幾萬條):登錄時交互數據小,對IM后端、客戶端、用戶體驗,都比較友好。

7、多終端條件下,如何得到完整消息履歷?

由於同一個用戶的每個終端,其會話最后更新時間、每個會話的最后一條時間可能都不一樣,參照上一節的實現思路,可以得到解決方案。

具體如下:

  • 1)參照第6.2章節的“App和IM后端的交互”第1個步驟,可取到不同的增量變化的會話列表項;
  • 2)參照第6.2章節的“App和IM后端的交互”第4個步驟,可取到任一區間的同步消息,得到完整消息。

8、離線消息是否就徹底廢棄了?

有若干情況,仍然需要保留離線消息,以確保消息送達。

比如以下情形:

  • 1)別人向我發送離線文件:這種情況下不能依賴同步消息來獲取。因為不以離線消息通知的話,用戶在沒有拉取到對應的同步消息前,是不知道有離線文件的;
  • 2)撤回消息:即使接收者不拉取同步,仍然要保證在上線后其數據在第一時間被撤回。注意:這里可能存在多端撤回問題;
  • 3)用戶在線時的消息下發:由於用戶在線時,IM后端向客戶端發送消息可能碰到網絡抖動等情況,導致消息下發失敗,這些消息先可以直接存在離線消息隊列,IM后端可在收到客戶端的心跳包時重發消息。相當於維護了一個在線消息的離線隊列。

9、本文結語

曾經有一段真摯的愛情擺在我面前,如果時間倒流到半年前,我會選擇一個靠譜的IM來發送消息,也許故事的腳本就由自己書寫——是否要整一個時光倒流的版本,抱得美人歸的那種?

不整了不整了,我得不到女神,你們才歡喜,我太了解你們了。。。各位爺歡喜就好。

附錄:IM開發干貨系列文章

本文是系列文章中的第26篇,總目錄如下:

IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞

IM消息送達保證機制實現(二):保證離線消息的可靠投遞

如何保證IM實時消息的“時序性”與“一致性”?

IM單聊和群聊中的在線狀態同步應該用“推”還是“拉”?

IM群聊消息如此復雜,如何保證不丟不重?

一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)

移動端IM登錄時拉取數據如何作到省流量?

通俗易懂:基於集群的移動端IM接入層負載均衡方案分享

淺談移動端IM的多點登陸和消息漫游原理

IM開發基礎知識補課(一):正確理解前置HTTP SSO單點登陸接口的原理

IM開發基礎知識補課(二):如何設計大量圖片文件的服務端存儲架構?

IM開發基礎知識補課(三):快速理解服務端數據庫讀寫分離原理及實踐建議

IM開發基礎知識補課(四):正確理解HTTP短連接中的Cookie、Session和Token

IM群聊消息的已讀回執功能該怎么實現?

IM群聊消息究竟是存1份(即擴散讀)還是存多份(即擴散寫)?

IM開發基礎知識補課(五):通俗易懂,正確理解並用好MQ消息隊列

一個低成本確保IM消息時序的方法探討

IM開發基礎知識補課(六):數據庫用NoSQL還是SQL?讀這篇就夠了!

IM里“附近的人”功能實現原理是什么?如何高效率地實現它?

IM開發基礎知識補課(七):主流移動端賬號登錄方式的原理及設計思路

IM開發基礎知識補課(八):史上最通俗,徹底搞懂字符亂碼問題的本質

IM的掃碼登功能如何實現?一文搞懂主流應用的掃碼登陸技術原理

IM要做手機掃碼登陸?先看看微信的掃碼登錄功能技術原理

IM開發基礎知識補課(九):想開發IM集群?先搞懂什么是RPC!

IM開發實戰干貨:我是如何解決大量離線聊天消息導致客戶端卡頓的

IM開發干貨分享:如何優雅的實現大量離線消息的可靠投遞》(* 本文)

另外,如果您是IM開發初學者,強烈建議首先閱讀《新手入門一篇就夠:從零開發移動端IM》。

(本文同步發布於:http://www.52im.net/thread-3069-1-1.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM