大部分消息進行服務端存儲,是為了便於查看歷史消息或者用於暫存離線消息。
一個支持用戶點對點聊天的消息收發架構主要包括三部分:消息存儲、消息未讀和消息收發通道。
一、消息存儲
假設收發雙方的歷史消息都是相互獨立的,即一方發送消息后刪除了消息,另一方仍可獲取到這條消息,則消息的存儲需要用到兩張表:消息內容表(圖中的 user_message 表)和消息索引表(圖中的 user_message_history 表)。前者主要存儲消息ID、消息內容、消息類型、消息創建時間等,后者可以理解為歷史聊天記錄,記錄了收發雙方的用戶ID,通過消息ID和前者關聯。
一般 IM 系統還需要一個最近聯系人列表(圖中的 user_message_contacter 表),使互動雙方能快速查找需要聊天的對象,聯系人列表還會攜帶兩人最近一條聊天消息用於展示,該表與消息索引表的區別在於:消息索引表存儲收發雙方的歷史消息記錄,聯系人表主要用於查詢某個用戶最近的所有聯系人。
假設張三給李四發送了一條消息,會先向消息內容表插入一條數據(假設是圖中 user_message 表 ID 為 1001 的記錄),然后向消息索引表插入兩條數據,一條是用戶ID為張三的記錄(假設是圖中 user_message_history 表 ID 為 30923 的記錄),一條是用戶ID為李四的記錄(假設是圖中 user_message_history 表 ID 為 30922 的記錄),兩條記錄的消息ID都是 1001。
同時會分別更新張三的最近聯系人和李四的最近聯系人,前者是查找聯系人表中是否有用戶ID為張三USERID且互動人ID為李四USERID的記錄,如果沒有,則插入一條新的聯系人記錄,最新消息ID就是 1001;反之,如果張三和李四之前已經有過聊天記錄,就更新最新消息ID即可。同樣的辦法更新李四的最近聯系人。
如果想列出張三與李四的對話消息記錄,可以使用如下 SQL 語句查詢:
select * from user_message_history where user_id = 張三USERID or contacter_id = 張三USERID;
二、消息未讀
如果一方發送消息,而接收方不在線或限制通知欄提醒權限,則需要有未讀提醒來作為補救措施。具體實現是設置一個未讀消息總數和針對某個接收方會話的消息未讀數。
當張三給李四發送消息,IM 服務端接收到消息后,給李四的總未讀數加 1,給李四和張三的會話未讀數加 1;
李四查看這條消息后會執行未讀數變更,將李四的總未讀數減 1,將李四和張三的會話未讀數減 1。
一般,需要支持“消息多終端漫游”的功能,未讀數存儲在 IM 服務端,反之選擇本地存儲即可。
三、消息收發通道
- 發送通道
客戶端 和 IM 服務端之間維持一個 TCP 長連接,IM 服務端提供發送消息的 API;
當客戶端有消息發送時,會以私有協議封裝這條消息,然后調用 API 把消息發給 IM 服務端。
- 接收通道
IM 服務端的網關服務和接收消息的客戶端之間維持一個長連接(TCP長連接 或 Websocket長連接),借助 TCP 能同時接收與發送數據的能力,把消息從 IM 服務端推送給接收方。若接收方不在線(無網絡或未打開APP),可借助第三方操作系統系別的輔助通道、各種設備的廠商通道,將消息通過通知欄的方式推送給接收方。
四、思考題
1、消息存儲中,內容表和索引表如果需要分庫處理,應該按什么字段來哈希?內容表和索引表可以合並成一個表嗎?
答:如果對內容表進行分庫分表處理,應該按消息ID(主鍵ID)來哈希,有利於定位某一條具體的消息;如果對索引表進行分庫分表處理,應該按用戶ID來哈希,這樣可以使與該用戶互動的所有聯系人都落在一張表上。
索引表與內容表可以合成一張表,優點是能減少拉取歷史消息時的數據庫IO,缺點是消息內容冗余存儲,浪費了空間。
2、能從索引表里獲取到最近聯系人所需要的信息,為什么還要單獨設置聯系人表呢?
如果從索引表中獲取一個用戶的所有聯系人的最后一條消息記錄(消息內容和時間),SQL語句中會有分組后取top 1的操作,性能不理想;另外當前用戶與單個聯系人之間的未讀數需要維護,用聯系人表的一個字段來存儲,比用索引表方便許多。