前言
大家新年快樂!
新的一年第一篇技術文章希望開個好頭,所以元旦三天我也沒怎麼閒著,希望給大家帶來一篇比較感興趣的乾貨內容。
老讀者應該還記得我在去年國慶節前分享過一篇《設計一個百萬級的訊息推送系統》;雖然我在文中有貼一些虛擬碼,依然有些朋友希望能直接分享一些可以執行的原始碼;這麼久了是時候把坑填上了。
目錄結構:


本文較長,高能預警;帶好瓜子板凳。



於是在之前的基礎上我完善了一些內容,先來看看這個專案的介紹吧:
CIM(CROSS-IM)
一款面向開發者的 IM(即時通訊)
系統;同時提供了一些元件幫助開發者構建一款屬於自己可水平擴充套件的 IM
。
藉助 CIM
你可以實現以下需求:
IM
即時通訊系統。- 適用於
APP
的訊息推送中介軟體。 IOT
海量連線場景中的訊息透傳中介軟體。
完整原始碼託管在 GitHub : github.com/crossoverJi…
演示
本次主要涉及到 IM 即時通訊,所以特地錄了兩段視訊演示(群聊、私聊)。
點選下方連結可以檢視視訊版 Demo。
YouTube | Bilibili |
---|---|
群聊 私聊 | 群聊 私聊 |
![]() |
![]() |
也在公網部署了一套演示環境,想要試一試的可以聯絡我加入內測群獲取賬號一起尬聊?。
架構設計
下面來看看具體的架構設計。

CIM
中的各個元件均採用SpringBoot
構建。- 採用
Netty + Google Protocol Buffer
構建底層通訊。 Redis
存放各個客戶端的路由資訊、賬號資訊、線上狀態等。Zookeeper
用於IM-server
服務的註冊與發現。
整體主要由以下模組組成:
cim-server
IM
服務端;用於接收 client
連線、訊息透傳、訊息推送等功能。
支援叢集部署。
cim-forward-route
訊息路由伺服器;用於處理訊息路由、訊息轉發、使用者登入、使用者下線以及一些運營工具(獲取線上使用者數等)。
cim-client
IM
客戶端;給使用者使用的訊息終端,一個命令即可啟動並向其他人發起通訊(群聊、私聊);同時內建了一些常用命令方便使用。
流程圖
整體的流程也比較簡單,流程圖如下:

- 客戶端向
route
發起登入。 - 登入成功從
Zookeeper
中選擇可用IM-server
返回給客戶端,並儲存登入、路由資訊到Redis
。 - 客戶端向
IM-server
發起長連線,成功後保持心跳。 - 客戶端下線時通過
route
清除狀態資訊。
所以當我們自己部署時需要以下步驟:
- 搭建基礎中介軟體
Redis、Zookeeper
。 - 部署
cim-server
,這是真正的 IM 伺服器,為了滿足效能需求所以支援水平擴充套件,只需要註冊到同一個Zookeeper
即可。 - 部署
cim-forward-route
,這是路由伺服器,所有的訊息都需要經過它。由於它是無狀態的,所以也可以利用Nginx
代理提高可用性。 cim-client
真正面向使用者的客戶端;啟動之後會自動連線 IM 伺服器便可以在控制檯收發訊息了。
更多使用介紹可以參考快速啟動。
詳細設計
接下來重點看看具體的實現,比如群聊、私聊訊息如何流轉;IM 服務端負載均衡;服務如何註冊發現等等。
IM 服務端
先來看看服務端;主要是實現客戶端上下線、訊息下發等功能。
首先是服務啟動:


由於是在 SpringBoot
中搭建的,所以在應用啟動時需要啟動 Netty
服務。
從 pipline
中可以看出使用了 Protobuf
的編解碼(具體報文在客戶端中分析)。
註冊發現
需要滿足 IM
服務端的水平擴充套件需求,所以 cim-server
是需要將自身資料釋出到註冊中心的。
這裡參考之前分享的《搞定服務註冊與發現》有具體介紹。
所以在應用啟動成功後需要將自身資料註冊到 Zookeeper
中。


最主要的目的就是將當前應用的 ip + cim-server-port+ http-port
註冊上去。

上圖是我在演示環境中註冊的兩個 cim-server
例項(由於在一臺伺服器,所以只是埠不同)。
這樣在客戶端(監聽這個 Zookeeper
節點)就能實時的知道目前可用的服務資訊。
登入
當客戶端請求 cim-forward-route
中的登入介面(詳見下文)做完業務驗證(就相當於日常登入其他網站一樣)之後,客戶端會向服務端發起一個長連線,如之前的流程所示:

這時客戶端會傳送一個特殊報文,表明當前是登入資訊。
服務端收到後就需要將該客戶端的 userID
和當前 Channel
通道關係儲存起來。


同時也快取了使用者的資訊,也就是 userID
和 使用者名稱。
離線
當客戶端斷線後也需要將剛才快取的資訊清除掉。

同時也需要呼叫 route
介面清除相關資訊(具體介面看下文)。
IM 路由

從架構圖中可以看出,路由層是非常重要的一環;它提供了一系列的 HTTP
服務承接了客戶端和服務端。
目前主要是以下幾個介面。
註冊介面


由於每一個客戶端都是需要登入才能使用的,所以第一步自然是註冊。
這裡就設計的比較簡單,直接利用 Redis
來儲存使用者資訊;使用者資訊也只有 ID
和 userName
而已。
只是為了方便查詢在 Redis
中的 KV
又反過來儲存了一份 VK
,這樣 ID
和 userName
都必須唯一。
登入介面
這裡的登入和 cim-server
中的登入不一樣,具有業務性質,

- 登入成功之後需要判斷是否是重複登入(一個使用者只能執行一個客戶端)。
- 登入成功後需要從
Zookeeper
中獲取服務列表(cim-server
)並根據某種演算法選擇一臺服務返回給客戶端。 - 登入成功之後還需要儲存路由資訊,也就是當前使用者分配的服務例項儲存到
Redis
中。
為了實現只能一個使用者登入,使用了 Redis
中的 set
來儲存登入資訊;利用 userID
作為 key
,重複的登入就會寫入失敗。


類似於 Java 中的 HashSet,只能去重儲存。
獲取一臺可用的路由例項也比較簡單:

- 先從
Zookeeper
獲取所有的服務例項做一個內部快取。 - 輪詢選擇一臺伺服器(目前只有這一種演算法,後續會新增)。
當然要獲取 Zookeeper
中的服務例項前自然是需要監聽 cim-server
之前註冊上去的那個節點。
具體程式碼如下:



也是在應用啟動之後監聽 Zookeeper
中的路由節點,一旦發生變化就會更新內部快取。
這裡使用的是 Guava 的 cache,它基於
ConcurrentHashMap
,所以可以保證清除、新增快取
的原子性。
群聊介面
這是一個真正發訊息的介面,實現的效果就是其中一個客戶端發訊息,其餘所有客戶端都能收到!
流程肯定是客戶端傳送一條訊息到服務端,服務端收到後在上文介紹的 SessionSocketHolder
中遍歷所有 Channel
(通道)然後下發訊息即可。
服務端是單機倒也可以,但現在是叢集設計。所以所有的客戶端會根據之前的輪詢演算法分配到不同的 cim-server
例項中。
因此就需要路由層來發揮作用了。


路由介面收到訊息後首先遍歷出所有的客戶端和服務例項的關係。
路由關係在 Redis
中的存放如下:

由於 Redis
單執行緒的特質,當資料量大時;一旦使用 keys 匹配所有 cim-route:*
資料,會導致 Redis 不能處理其他請求。
所以這裡改為使用 scan 命令來遍歷所有的 cim-route:*
。
接著會挨個呼叫每個客戶端所在的服務端的 HTTP
介面用於推送訊息。
在 cim-server
中的實現如下:


cim-server
收到訊息後會在內部快取中查詢該 userID 的通道,接著只需要發訊息即可。
線上使用者介面
這是一個輔助介面,可以查詢出當前線上使用者資訊。


實現也很簡單,也就是查詢之前儲存 ”使用者登入狀態的那個去重 set
“即可。
私聊介面
之所以說獲取線上使用者是一個輔助介面,其實就是用於輔助私聊使用的。
一般我們使用私聊的前提肯定得知道當前哪些使用者線上,接著你才會知道你要和誰進行私聊。
類似於這樣:

在我們這個場景中,私聊的前提就是需要獲得線上使用者的 userID
。

所以私聊介面在收到訊息後需要查詢到接收者所在的 cim-server
例項資訊,後續的步驟就和群聊一致了。呼叫接收者所在例項的 HTTP
介面下發資訊。
只是群聊是遍歷所有的線上使用者,私聊只傳送一個的區別。
下線介面
一旦客戶端下線,我們就需要將之前存放在 Redis
中的一些資訊刪除掉(路由資訊、登入狀態)。


IM 客戶端
客戶端中的一些邏輯其實在上文已經談到一些了。
登入
第一步也就是登入,需要在啟動時呼叫 route
的登入介面,獲得 cim-server
資訊再建立連線。



登入過程中 route
介面會判斷是否為重複登入,重複登入則會直接退出程式。

接下來是利用 route
介面返回的 cim-server
例項資訊(ip+port
)建立連線。
最後一步就是傳送一個登入標誌的資訊到服務端,讓它保持客戶端和 Channel
的關係。

自定義協議
上文提到的一些登入報文、真正的訊息報文
這些其實都是在我們自定義協議中可以區別出來的。
由於是使用 Google Protocol Buffer
編解碼,所以先看看原始格式。

其實這個協議中目前一共就三個欄位:
requestId
可以理解為userId
。reqMsg
就是真正的訊息。type
也就是上文提到的訊息類別。
目前主要是三種型別,分別對應不同的業務:

心跳
為了保持客戶端和服務端的連線,每隔一段時間沒有傳送訊息都需要自動的傳送心跳。
目前的策略是每隔一分鐘就是傳送一個心跳包到服務端:


這樣服務端每隔一分鐘沒有收到業務訊息時就會收到 ping
的心跳包:

內建命令
客戶端也內建了一些基本命令來方便使用。
命令 | 描述 |
---|---|
:q |
退出客戶端 |
:olu |
獲取所有線上使用者資訊 |
:all |
獲取所有命令 |
: |
更多命令正在開發中。。 |

比如輸入 :q
就會退出客戶端,同時會關閉一些系統資源。


當輸入 :olu
(onlineUser
的簡寫)就會去呼叫 route
的獲取所有線上使用者介面。


群聊
群聊的使用非常簡單,只需要在控制檯輸入訊息回車即可。
這時會去呼叫 route
的群聊介面。

私聊
私聊也是同理,但前提是需要觸發關鍵字;使用 userId;;訊息內容
這樣的格式才會給某個使用者傳送訊息,所以一般都需要先使用 :olu
命令獲取所以線上使用者才方便使用。

訊息回撥
為了滿足一些定製需求,比如訊息需要儲存之類的。
所以在客戶端收到訊息之後會回撥一個介面,在這個介面中可以自定義實現。


因此先建立了一個 caller
的 bean
,這個 bean
中包含了一個 CustomMsgHandleListener
介面,需要自行處理只需要實現此介面即可。
自定義介面
由於我自己不怎麼會寫介面,但保不準有其他大牛會寫。所以客戶端中的群聊、私聊、獲取線上使用者、訊息回撥等業務(以及之後的業務)都是以介面形式提供。
也方便後面做頁面整合,只需要調這些介面就行了;具體實現不用怎麼關心。
總結
cim
目前只是第一版,BUG 多,功能少(只拉了幾個群友做了測試);不過後續還會接著完善,至少這一版會給那些沒有相關經驗的朋友帶來一些思路。
後續計劃:

完整原始碼:
如果這篇對你有所幫助還請不吝轉發。
