(馬蜂窩技術原創內容,公眾號 ID:mfwtech)
移動互聯網技術改變了旅游的世界,這個領域過去沉重的資訊分銷成本被大大降低,用戶與服務供應商之間、用戶與用戶之間的溝通路徑逐漸打通,溝通的場景也在不斷擴展,這促使所有的移動應用開發者都要從用戶視角出發,更好地滿足用戶需求,
論壇時代的馬蜂窩,用戶之間的溝通形式比較單一,主要為單純的回帖回復等,為了以較小的成本快速滿足用戶需求,當時采用的是非實時性訊息的方案來實作用戶之間的訊息傳遞,
隨著行業和公司的發展,馬蜂窩確立了「內容+交易」的獨特商業模式,在用戶規模不斷增長及業務形態發生變化的背景下,為用戶和商家提供穩定可靠的售前和售后技術支持,成為電商移動業務線的當務之急,
一、設計思路與整體架構
我們結合 B2C,C2B,C2C 不同的業務場景設計實作了馬蜂窩旅游移動端中的私信、用戶咨詢、用戶反饋等即時通訊業務;同時為了更好地為合作商家賦能,在馬蜂窩商家移動端中加入與會話相關的咨詢用戶管理、客服管理、運營資源統計等功能,
目前 IM 涉及到的業務如下:

為了實作馬蜂窩旅游 App 及商家 IM 業務邏輯、公共資源的整合復用及 UI 個性化定制,將問題拆解為以下部分來解決:
IM 資料通道與例外重連機制,解決不同業務實時訊息下發以及穩定性保障;
IM 實時訊息訂閱分發機制,解決訊息定向發送、業務訂閱消費,避免不必要的請求資源浪費;
IM 會話串列 UI 繪制通用解決方案,解決不同訊息型別的快速迭代開發和管理復雜問題;
整體實作結構分為 4 個部分進行封裝,分別為下圖中的資料管理、訊息注冊分發管理、通用 UI 封裝及業務管理,

二、技術原理和實作程序
2.1 通用資料通道
對于常規業務展示資料的獲取,客戶端需要主動發起請求,請求和回應的程序是單向的,且對實時性要求不高,但對于 IM 訊息來說,需要同時支持接收和發送操作,且對實時性要求高,為支撐這種要求,客戶端和服務器之間需要創建一條穩定連接的資料通道,提供客戶端和服務端之間的雙向資料通信,
2.1.1 資料通道基礎互動原理
為了更好地提高資料通道對業務支撐的擴展性,我們將所有通信資料封裝為外層結構相同的資料包,使多業務型別資料使用共同的資料通道下發通信,統一分發處理,從而減少通道的創建數量,降低資料通道的維護成本,

常見的客戶端與服務端資料互動依賴于 HTTP 請求回應程序,只有客戶端主動發起請求才可以得到回應結果,結合馬蜂窩的具體業務場景,我們希望建立一種可靠的訊息通道來保障服務端主動通知客戶端,實作業務資料的傳遞,目前采用的是 HTTP 長鏈接輪詢的形式實作,各業務資料訊息型別只需遵循約定的通用資料結構,即可實作通過資料通道下發給客戶端,資料通道不必關心資料的具體內容,只需要關注接收與發送,
2.1.2 客戶端資料通道實作原理
客戶端資料通道管理的核心是維護一個業務場景請求堆疊,在不同業務場景切換程序中入堆疊不同的業務場景引數資料,每次 HTTP 長鏈接請求使用堆疊頂請求資料,可以模擬在特定業務場景 (如與不同的用戶私信) 的不同處理,資料相關處理都集中封裝在資料通道管理中,業務層只需在資料通道管理中注冊對應的接收處理即可得到需要的業務訊息資料,

2.2 訊息訂閱與分發
在軟體系統中,訂閱分發本質上是一種訊息模式,非直接傳遞訊息的一方被稱為「發布者」,接受訊息處理稱為「訂閱者」,發布者將不同的訊息進行分類后分發給對應型別的訂閱者,完成訊息的傳遞,應用訂閱分發機制的優勢為便于統一管理,可以添加不同的攔截器來處理訊息決議、訊息過濾、例外處理機制及資料采集作業,
2.2.1 訊息訂閱
業務層只專注于訊息處理,并不關心訊息接收分發的程序,訂閱的意義在于更好地將業務處理和資料通道處理解耦,業務層只需要訂閱關注的訊息型別,被動等待接收訊息即可,

業務層訂閱需要處理的業務訊息型別,在注冊后會自動監控當前頁面的生命周期,并在頁面銷毀后洗掉對應的訊息訂閱,從而避免手動撰寫成對的訂閱和取消訂閱,降低業務層的耦合,簡化呼叫邏輯,訂閱分發管理會根據各業務型別維護訂閱者佇列用于訊息接收的分發操作,
2.2.2 訊息分發
資料通道的核心在于維護多訊息型別各自對應的訂閱者集合,并將決議的訊息分發到業務層,

資料通道由多業務訊息共用,在每次請求收到新訊息串列后,根據各自業務型別重新拆分成多個訊息串列,分發給各業務型別對應的訂閱處理器,最終傳遞至業務層交予對應頁面處理展示,
2.3 會話訊息串列繪制
基于不同的場景,如社交為主的私信、用戶服務為主的咨詢反饋等,都需要會話串列的展示形式;但各場景又不完全相同,需要分析當前會話串列的共通性及可封裝復用的部分,以更好地支撐后續業務的擴展,
2.3.1 訊息在串列展示的組成結構
IM 訊息串列的特點在于訊息型別多、UI 展示多樣化,因此需要建立各型別訊息和布局的對應關系,在收到訊息后根據訊息型別匹配到對應的布局添加至對應訊息串列,

2.3.2 訊息型別與展示布局管理原理
對于不同訊息型別及展示,問題的核心在于建立訊息型別、訊息資料結構、訊息展示布局管理的映射關系,以上三者在實作程序中通過建立映射管理表來維護,各自建立串列存盤訊息型別/訊息體封裝結構/訊息展示布局管理,設定對應關系關聯 3 個串列來完成查找,

2.3.3 一次收發訊息 UI 繪制程序
各型別訊息在內容展示上各有不同,但整體會話訊息展示樣式可以分為 3 種,分別是接收訊息、發送訊息和處于頁面中間的訊息樣式,區別只在于內部的訊息樣式,所以訊息 UI 的繪制可以拆分成 2 個步驟,首先是創建通用的展示容器,然后再填充各訊息具體的展示樣式,

拆分的目的在于使各型別訊息 UI 處理只需要關注特有資料,而如通用訊息如頭像、名稱、訊息時間、是否可舉報、已讀未讀狀態、發送失敗/重試狀態等都可以統一處理,降低修改維護的成本,同時使各訊息 UI 處理邏輯更少、更清晰,更利于新型別的擴展管理,
收發到訊息后,根據訊息型別判斷是「發送接收型別」還是「居中展示型別」,找到外層的布局樣式,再根據具體訊息型別找到特有的 UI 樣式,拼接在外層布局中,得到完整的訊息卡片,然后設定對應的資料渲染到串列中,完成整個訊息的繪制,
三、細節優化 & 踩坑經驗
在實作上述 IM 系統的程序中,我們遇到了很多問題,也做了很多細節優化,在這里總結實作時需要考慮的幾點,以供大家借鑒,
3.1 訊息去重
在前面的架構中,我們使用 msg_id 來標記訊息串列中的每一條訊息,msg_id 是根據客戶端上傳的資料,進行存盤后生成的,


客戶端 A 請求 IM 服務器之后生成 msg_id,再通過請求回傳和 Polling 分發到客戶端 A 和客戶端 B,當流程成立的時候,客戶端 A 和客戶端 B 通過服務端分發的 msg_id 來進行本地去重,但這種方案存在以下問題:

當客戶端 A 因為網路出現問題,無法接受對應發送訊息的請求回傳的時候,會觸發重發機制,此時雖然 IM 服務器已經接受過一次客戶端 A 的訊息發送請求,但是因為無法確定兩個請求是否來自同一條原始訊息,只能再次接受,這就導致了重復訊息的產生,解決的方法是引入客戶端訊息標識 id,因為我們已經依附舊有的 msg_id 做了很多作業,不打算讓客戶端的訊息 id 代替 msg_id 的職能,因此重新定義一個 random_id,

random_id = random + time_stamp,random_id 標識了唯一的訊息體,由一個亂數和生成訊息體的時間戳生成,當觸發重試的時候,兩次請求的 random_id 會是相同的,服務端可以根據該欄位進行訊息去重,
3.2 本地化 Push
當我們在會話頁或串列頁的環境下,可以通過界面的變化很直觀地觀察到收取了新訊息并更新未讀數,但從會話頁或者串列頁退出之后,就無法單純地從界面上獲取這些資訊,這時需要有其他的機制,讓用戶獲知當前訊息的狀態,
系統推送與第三方推送是一個可行的選擇,但本質上推送也是基于長鏈接提供的服務,為彌補推送不穩定性與風險,我們采用資料通道+本地通知的形式來完善訊息通知機制,通過資料通道下發的訊息如需達到推送的提示效果,則攜帶對應的 Push 展示資料,同時會對當前所處的頁面進行判斷,避免對當前頁面的訊息內容進行重復提醒,

通過這種資料通道+本地通知展示的機制,可以在應用處于運行狀態的時間內提高訊息抵達率,減少對于遠程推送的依賴,降低推送系統的壓力,并提升用戶體驗,
3.3 資料通道例外重連機制
當前資料通道通過 HTTP 長鏈接輪詢 (Polling) 實作,不同業務場景下對 Polling 的影響如下圖所示:

由于用戶手機所處網路請求狀態不一,有時候會遇到網路中斷或者服務端例外的情況,從而終止 Polling 的請求,為能夠讓用戶在網路恢復后繼續會話業務,需要引入重連機制,
在重試機制 1.0 版本中,對于可能出現較多重試請求的情況,采取的是添加 60s 內連續 5 次報錯延遲重試的限制,具體流程如下:

在實踐中發現以下問題:
當服務端突然例外并持續超過 1 分鐘后,客戶端啟動執行重試機制,并每隔 1 分鐘重發一次重連請求,這對服務器而言就相當于遭受一次短暫集中的「攻擊」,甚至有可能拖垮服務器,
當客戶端斷網后立刻進行重試也并不合理,因為用戶恢復網路也需要一定時間,這期間的重連請求是無意義的,
基于以上問題分析改進,我們設計了第二版重試機制,此次將 5 次以下請求錯誤的延遲時間修改為 5 - 20 秒隨機重試,將客戶端重試請求分散在多個時間點避免同時請求形成對服務器對瞬時壓力,同時在客戶端斷網情況下也進行延遲重試,

Polling 機制修改后請求量劃分,相對之前請求分布比較均勻,不再出現集中請求的問題,

3.4 唯一會話標識
3.4.1 為何引入訊息線 ID
訊息線就是用來表示會話的聊天關系,不同訊息線代表不同物件的會話,從 DB 層面來看需要一個張表來存盤這種關系 uid + object_id + busi_type = 訊息線 ID,

在 IM 初期實作中,我們使用會話配置引數(包含業務來源和會話引數)來標識會話 id,有三個作用:
查找商家 id,獲取咨詢來源,進行管家分配
查找已存在的訊息線
判斷客戶端頁面狀態,決定要不要下發推送,進行訊息提醒
這種方式存在兩個問題:
通過業務來源和會話引數來決議對應的商家 id,兩個引數缺失一個都會導致商家 id 決議錯誤,還要各種查詢資料庫才能得到商家 id,影響效率;
通過會話型別切換介面標識當前會話型別,切換頁面會頻繁觸發網路請求;如果請求介面發生意外容易引發訊息內容錯誤問題,嚴重依賴客戶端的健壯性
用業務來源和會話引數幫助我們進行管家分配是不可避免的,但我們可以通過引入訊息線 ID 來系結消息線的方式,替代業務來源和會話引數查找訊息線的作用,另外針對下發推送的問題已通過上方講述的本地推送通知機制解決,
3.4.2 何時創建訊息線
當進入會話頁發訊息時,檢查 DB 中是否存在對應訊息線,不存在則將這條訊息 id 當作訊息線 id 使用,存在即復用,
當進入會話時,根據用戶 id 、業務型別 id 等檢查在 DB 中是否已存在對應訊息線,不存在則創建訊息線,存在即復用,
3.4.3 引入訊息線目的
減少服務端查詢訊息線的成本,
移除舊版狀態改變相關的介面請求,間接提高了推送觸達率,
降低移動端對于用戶訊息匹配的復雜度,
四、展望及近期優化
4.1 資料通道實作方式升級為 Websocket
WebSocket 是一種在單個 TCP 連接上進行全雙工通信的協議,WebSocket 使得客戶端和服務器之間的資料交換變得更加簡單,允許服務端主動向客戶端推送資料,在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就直接可以創建持久性的連接,并進行雙向資料傳輸,
與目前的 HTTP 輪詢實作機制相比, Websocket 有以下優點:
較少的控制開銷,在連接創建后,服務器和客戶端之間交換資料時,用于協議控制的資料包頭部相對較小,在不包含擴展的情況下,對于服務器到客戶端的內容,此頭部大小只有 2 至 10 位元組(和資料包長度有關);對于客戶端到服務器的內容,此頭部還需要加上額外的 4 位元組的掩碼,相對于 HTTP 請求每次都要攜帶完整的頭部,開銷顯著減少,
更強的實時性,由于協議是全雙工的,服務器可以隨時主動給客戶端下發資料,相對于 HTTP 需要等待客戶端發起請求服務端才能回應,延遲明顯更少;即使是和 Comet 等類似的長輪詢比較,其也能在短時間內更多次地傳遞資料,
保持連接狀態,與 HTTP 不同的是,Websocket 需要先創建連接,這就使其成為一種有狀態的協議,在之后通信時可以省略部分狀態資訊,而 HTTP 請求可能需要在每個請求都攜帶狀態資訊(如身份認證等),
更好的二進制支持,Websocket 定義了二進制幀,相對 HTTP,可以更輕松地處理二進制內容,
支持擴展,Websocket 定義了擴展,用戶可以擴展協議、實作部分自定義的子協議,如部分瀏覽器支持壓縮等,
更好的壓縮效果,相對于 HTTP 壓縮,Websocket 在適當的擴展支持下,可以沿用之前內容的背景關系,在傳遞類似的資料時,可以顯著地提高壓縮率,
為了進一步優化我們的資料通道設計,我們探索驗證了 Websocket 的可行性,并進行了調研和設計:

近期將對 HTTP 輪詢實作方案進行替換,進一步優化資料通道的效率,
4.2 業務功能的擴展
計劃將 IM 移動端功能模塊打造成通用的即時通訊組件,能夠更容易地賦予各業務 IM 能力,使各業務快速在自有產品線上添加聊天功能,降低研發 IM 的成本和難度,目前的 IM 功能實作主要有兩個組成,分別是公用的資料通道與 UI 組件,
隨著馬蜂窩業務發展,在現有 IM 系統上還有很多可以建設和升級的方向,比如訊息型別的支撐上,擴展對短視頻、語音訊息、快捷訊息回復等支撐,提高社交的便捷性和趣味性;對于多人場景希望增加群組,興趣頻道,多人音視頻通信等場景的支撐等,
相信未來通過對更多業務功能的擴展及應用場景的探索,馬蜂窩移動端 IM 將更好地提升用戶體驗,并持續為商家賦能,
本文作者:馬蜂窩電商業務 IM 移動端研發團隊,
(馬蜂窩技術原創內容)

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/44311.html
標籤:架構設計
下一篇:資料結構導論(第一章概論)
