從零開始開發IM(即時通訊)服務端
好訊息:IM1.0.0版本已經上線啦,支援特性:
- 私聊傳送文字/檔案
- 已傳送/已送達/已讀回執
- 支援使用ldap登入
- 支援接入外部的登入認證系統
- 提供客戶端jar包,方便客戶端開發
github連結: https://github.com/yuanrw/IM
前言
首先講講IM(即時通訊)技術可以用來做什麼:
聊天:qq、微信
直播:鬥魚直播、抖音
實時位置共享、遊戲多人互動等等
可以說幾乎所有高實時性的應用場景都需要用到IM技術。
本篇將帶大家從零開始搭建一個輕量級的IM服務端,麻雀雖小,五臟俱全,我們搭建的IM服務端實現以下功能:
- 一對一的文字訊息、檔案訊息通訊
- 每個訊息有“已傳送”/“已送達”/“已讀”回執
- 儲存離線訊息
- 支援使用者登入,好友關係等基本功能。
- 能夠方便地水平擴充套件
通過這個專案能學到什麼?
這個專案涵蓋了很多後端必備知識:
- rpc通訊
- 資料庫
- 快取
- 訊息佇列
- 分散式、高併發的架構設計
- docker部署
訊息通訊
文字訊息
我們先從最簡單的特性開始實現:一個普通訊息的傳送
訊息格式如下:
message ChatMsg{
id = 1;
//訊息id
fromId = Alice
//傳送者userId
destId = Bob
//接收者userId
msgBody = hello
//訊息體
}
如上圖,我們現在有兩個使用者:Alice和Bob連線到了伺服器,當Alice傳送訊息message(hello)
傳送回執
那我們要怎麼來實現回執的傳送呢?
我們定義一種回執資料格式ACK,MsgType有三種,分別是sent
(已傳送),delivered
(已送達), read
(已讀):
message AckMsg {
id;
//訊息id
fromId;
//傳送者id
destId;
//接收者id
msgType;
//訊息型別
ackMsgId;
//確認的訊息id
}
enum MsgType {
DELIVERED;
READ;
}
當服務端接受到Alice發來的訊息時:
- 向Alice傳送一個
sent(hello)
表示訊息已經被髮送到伺服器。
message AckMsg {
id = 2;
fromId = Alice;
destId = Bob;
msgType = SENT;
ackMsgId = 1;
}
- 伺服器把
hello
轉發給Bob後,立刻向Alice傳送delivered(hello)
表示訊息已經發送給Bob。
message AckMsg {
id = 3;
fromId = Bob;
destId = Alice;
msgType = DELIVERED;
ackMsgId = 1;
}
- Bob閱讀訊息後,客戶端向伺服器傳送
read(hello)
表示訊息已讀
message AckMsg {
id = 4;
fromId = Bob;
destId = Alice;
msgType = READ;
ackMsgId = 1;
}
這個訊息會像一個普通聊天訊息一樣被伺服器處理,最終傳送給Alice。
在伺服器這裡不區分ChatMsg
和AckMsg
,處理過程都是一樣的:解析訊息的destId
並進行轉發。
水平擴充套件
當用戶量越來越大,必然需要增加伺服器的數量,使用者的連線被分散在不同的機器上。此時,就需要儲存使用者連線在哪臺機器上。
我們引入一個新的模組來管理使用者的連線資訊。
管理使用者狀態
模組叫做user status
,共有三個介面:
public interface UserStatusService {
/**
* 使用者上線,儲存userId與機器id的關係
*
* @param userId
* @param connectorId
* @return 如果當前使用者線上,則返回他連線的機器id,否則返回null
*/
String online(String userId, String connectorId);
/**
* 使用者下線
*
* @param userId
*/
void offline(String userId);
/**
* 通過使用者id查詢他當前連線的機器id
*
* @param userId
* @return
*/
String getConnectorId(String userId);
}
這樣我們就能夠對使用者連線狀態進行管理了,具體的實現應考慮服務的使用者量、期望效能等進行實現。
此處我們使用redis來實現,將userId和connectorId的關係以key-value的形式儲存。
訊息轉發
除此之外,還需要一個模組在不同的機器上轉發訊息,如下結構:
此時我們的服務被拆分成了connector
和transfer
兩個模組,connector
模組用於維持使用者的長連結,而transfer
的作用是將訊息在多個connector
之間轉發。
現在Alice和Bob連線到了兩臺connector上,那麼訊息要如何傳遞呢?
- Alice上線,連線到
機器[1]
上時- 將Alice和它的連線存入記憶體中。
- 呼叫
user status
的online
方法記錄Alice上線。
- Alice傳送了一條訊息給Bob
機器[1]
收到訊息後,解析destId,在記憶體中查詢是否有Bob。- 如果沒有,代表Bob未連線到這臺機器,則轉發給
transfer
。
transfer
呼叫user status
的getConnectorId(Bob)
方法找到Bob所連線的connector,返回機器[2]
,則轉發給機器[2]
。
流程圖:
總結:
- 引入
user status
模組管理使用者連線,transfer
模組在不同的機器之間轉發,使服務可以水平擴充套件。 - 為了滿足實時轉發,
transfer
需要和每臺connector
機器都保持長連結。
離線訊息
如果使用者當前不線上,就必須把訊息持久化下來,等待使用者下次上線再推送,這裡使用mysql儲存離線訊息。
為了方便地水平擴充套件,我們使用訊息佇列進行解耦。
transfer
接收到訊息後如果發現使用者不線上,就傳送給訊息佇列入庫。- 使用者登入時,伺服器從庫里拉取離線訊息進行推送。
使用者登入、好友關係
使用者的註冊登入、賬戶管理、好友關係鏈等功能更適合使用http協議,因此我們將這個模組做成一個restful服務,對外暴露http介面供客戶端呼叫。
至此服務端的基本架構就完成了:
總結
以上就是這篇部落格的所有內容,本篇幫大家構建了IM服務端的架構,但還有很多細節需要我們去思考,例如:
- 如何保證訊息的順序和唯一
- 多個裝置線上如何保證訊息一致性
- 如何處理訊息傳送失敗
- 訊息的安全性
- 如果要儲存聊天記錄要怎麼做
- 資料庫分表分庫
- 服務高可用
……
更多細節實現就留到下一篇啦~
IM1.0.0版本已上線,github連結:
https://github.com/yuanrw/IM
覺得對你有幫助請點個star吧~!