1. 程式人生 > >IM訊息送達保證機制實現(二):保證離線訊息的可靠投遞

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

1、前言

本文的上篇《IM訊息送達保證機制實現(一):保證線上實時訊息的可靠投遞》中,我們討論了線上實時訊息的投遞可以通過應用層的確認、傳送方的超時重傳、接收方的去重等手段來保證業務層面訊息的不丟不重。 但實時線上投遞針對的是訊息收發雙方都線上的情況(如當傳送方使用者A傳送訊息給接收方使用者B時,使用者B是線上的),那如果訊息的接收方使用者B不線上,系統是如何保證訊息的可達性的呢?這就是本文要討論的問題。

2、IM開發乾貨系列文章

3、訊息接收方不線上時的典型訊息傳送流程

215119i6746o666z54hlhe.png (368Ã187)

如上圖所述,通常此類情況下訊息的傳送流程如下:  

  • 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想要的離線訊息。

215431o65s63cjohza616l.png (399Ã189)

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次。

215940bhkdklcbldeud5a5.png (455Ã212)

④ 方案小結:通常情況下,主流的的移動端IM(比如微信、手Q等)通常都是以“優化方案2”為主,因為行動網路的不可靠性加上電量、流量等資源的昂貴性,能儘量一次性幹完的事,就儘可能一次搞定,從而提供整個APP的使用者體驗(對於移動端應用而言,省電、省流量同樣是使用者體驗的一部分)。這方面的文章,可以進一步參閱《談談移動端 IM 開發中登入請求的優化》、《移動端IM實踐:iOS版微信介面卡頓監測方案》、《移動端IM實踐:Android版微信如何大幅提升互動效能(二)》

6、訊息接收方一次拉取大量離線訊息導致速度慢、卡頓的解決方法

使用者B一次性拉取所有好友發給ta的離線訊息,訊息量很大時,一個請求包很大、速度慢,容易卡頓怎麼辦?

220127bu48j3unjoy62z6b.png (462Ã219)

正如上圖所示,我們可以分頁拉取:根據業務需求,先拉取最新(或者最舊)的一頁訊息,再按需一頁頁拉取,這樣便能很好地解決使用者體驗問題。

7、優化離線訊息的拉取過程,保證離線訊息不會丟失

如何保證可達性,上述步驟第三步執行完畢之後,第四個步驟離線訊息返回給客戶端過程中,伺服器掛點,路由器丟訊息,或者客戶端crash了,那離線訊息豈不是丟了麼(資料庫已刪除,使用者還沒收到)? 確實,如果按照上述的1、2、3、4步流程,的確是的,那如何保證離線訊息的絕對可靠性、可達性?

220500izi6zogh3g32y2hn.png (255Ã216)

如同線上訊息的應用層ACK機制一樣,離線訊息拉時,不能夠直接刪除資料庫中的離線訊息,而必須等應用層的離線訊息ACK(說明使用者B真的收到離線訊息了),才能刪除資料庫中的離線訊息。這個應用層的ACK可以通過實時訊息通道告之服務端,也可以通過服務端提供的REST介面,以更通用、簡單的方式通知服務端。

8、進一步優化,解決重複拉取離線訊息的問題

如果使用者B拉取了一頁離線訊息,卻在ACK之前crash了,下次登入時會拉取到重複的離線訊息麼?確實,拉取了離線訊息卻沒有ACK,伺服器不會刪除之前的離線訊息,故下次登入時系統層面還會拉取到。但在業務層面,可以根據msg_id去重。SMC理論:系統層面無法做到訊息不丟不重,業務層面可以做到,對使用者無感知。優化後的拉取過程,如下圖所示:

220827kmg5ugtmgmxsrwsg.png (300Ã242)

9、進一步優化,降低離線拉取ACK帶來的額外與伺服器的互動次數

假設有N頁離線訊息,現在每個離線訊息需要一個ACK,那麼豈不是客戶端與伺服器的互動次數又加倍了?有沒有優化空間?

220909nweeezwyeewiqavi.png (291Ã219)

如上圖所示,不用每一頁訊息都ACK,在拉取第二頁訊息時相當於第一頁訊息的ACK,此時伺服器再刪除第一頁的離線訊息即可,最後一頁訊息再ACK一次(實際上:最後一頁拉取的肯定是空返回,這樣可以極大地簡化這個分頁過程,否則客戶端得知道當前離線訊息的總頁數,而由於訊息讀取延遲的存在,這個總頁數理論上並非絕對不變,從而加大了資料讀取不一致的可能性)。這樣的效果是,不管拉取多少頁離線訊息,只會多一個ACK請求,與伺服器多一次互動。

10、本文小結

正如本文中所列舉的問題所描述的那樣,保證“離線訊息”的可達性比大家想象的要複雜一些,常見優化總結如下:  

  • 1)對於同一個使用者B,一次性拉取所有使用者發給ta的離線訊息,再在客戶端本地進行傳送方分析,相比按照發送方一個個進行訊息拉取,能大大減少伺服器互動次數;
  • 2)分頁拉取,先拉取計數再按需拉取,是無線端的常見優化;
  • 3)應用層的ACK,應用層的去重,才能保證離線訊息的不丟不重;
  • 4)下一頁的拉取,同時作為上一頁的ACK,能夠極大減少與伺服器的互動次數。

網易雲信,你身邊的即時通訊和音視訊技術專家,瞭解我們,請戳網易雲信官網

想要閱讀更多行業洞察和技術乾貨,請關注網易雲信部落格

本文轉載自52im,作者:JackJiang