(馬蜂窩技術原創內容,公眾號 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 移動端研發團隊。
(馬蜂窩技術原創內容)