CIM(CROSS-IM)?一款面向開發(fā)者的?IM(即時通訊)系統(tǒng);同時提供了一些組件幫助開發(fā)者構建一款屬于自己可水平擴展的?IM?搔啊。
借助?CIM?你可以實現(xiàn)以下需求:
IM?即時通訊系統(tǒng)督笆。
適用于?APP?的消息推送中間件。
IOT?海量連接場景中的消息透傳中間件。
架構設計
下面來看看具體的架構設計破托。
CIM?中的各個組件均采用?SpringBoot?構建。
采用?Netty + Google Protocol Buffer?構建底層通信歧蒋。
Redis?存放各個客戶端的路由信息土砂、賬號信息、在線狀態(tài)等谜洽。
Zookeeper?用于?IM-server?服務的注冊與發(fā)現(xiàn)萝映。
整體主要由以下模塊組成:
cim-server
IM?服務端;用于接收?client?連接阐虚、消息透傳序臂、消息推送等功能。
支持集群部署实束。
cim-forward-route
消息路由服務器奥秆;用于處理消息路由、消息轉(zhuǎn)發(fā)咸灿、用戶登錄构订、用戶下線以及一些運營工具(獲取在線用戶數(shù)等)。
cim-client
IM?客戶端避矢;給用戶使用的消息終端悼瘾,一個命令即可啟動并向其他人發(fā)起通訊(群聊囊榜、私聊);同時內(nèi)置了一些常用命令方便使用亥宿。
流程圖
整體的流程也比較簡單卸勺,流程圖如下:
客戶端向?route?發(fā)起登錄。
登錄成功從?Zookeeper?中選擇可用?IM-server?返回給客戶端烫扼,并保存登錄曙求、路由信息到?Redis。
客戶端向?IM-server?發(fā)起長連接映企,成功后保持心跳圆到。
客戶端下線時通過?route?清除狀態(tài)信息。
所以當我們自己部署時需要以下步驟:
搭建基礎中間件?Redis卑吭、Zookeeper芽淡。
部署?cim-server,這是真正的 IM 服務器豆赏,為了滿足性能需求所以支持水平擴展挣菲,只需要注冊到同一個?Zookeeper?即可。
部署?cim-forward-route掷邦,這是路由服務器白胀,所有的消息都需要經(jīng)過它。由于它是無狀態(tài)的抚岗,所以也可以利用?Nginx?代理提高可用性或杠。
cim-client?真正面向用戶的客戶端;啟動之后會自動連接 IM 服務器便可以在控制臺收發(fā)消息了宣蔚。
詳細設計
接下來重點看看具體的實現(xiàn)向抢,比如群聊、私聊消息如何流轉(zhuǎn)胚委;IM 服務端負載均衡挟鸠;服務如何注冊發(fā)現(xiàn)等等。
IM 服務端
先來看看服務端亩冬;主要是實現(xiàn)客戶端上下線艘希、消息下發(fā)等功能。
首先是服務啟動:
由于是在?SpringBoot?中搭建的硅急,所以在應用啟動時需要啟動?Netty?服務覆享。
從?pipline?中可以看出使用了?Protobuf?的編解碼(具體報文在客戶端中分析)。
注冊發(fā)現(xiàn)
需要滿足?IM?服務端的水平擴展需求营袜,所以?cim-server?是需要將自身數(shù)據(jù)發(fā)布到注冊中心的撒顿。
所以在應用啟動成功后需要將自身數(shù)據(jù)注冊到?Zookeeper?中。
最主要的目的就是將當前應用的?ip + cim-server-port+ http-port?注冊上去连茧。
上圖是我在演示環(huán)境中注冊的兩個?cim-server?實例(由于在一臺服務器核蘸,所以只是端口不同)。
這樣在客戶端(監(jiān)聽這個?Zookeeper?節(jié)點)就能實時的知道目前可用的服務信息啸驯。
登錄
當客戶端請求?cim-forward-route?中的登錄接口(詳見下文)做完業(yè)務驗證(就相當于日常登錄其他網(wǎng)站一樣)之后客扎,客戶端會向服務端發(fā)起一個長連接,如之前的流程所示:
這時客戶端會發(fā)送一個特殊報文罚斗,表明當前是登錄信息徙鱼。
服務端收到后就需要將該客戶端的?userID?和當前?Channel?通道關系保存起來。
同時也緩存了用戶的信息针姿,也就是?userID?和 用戶名袱吆。
離線
當客戶端斷線后也需要將剛才緩存的信息清除掉。
同時也需要調(diào)用?route?接口清除相關信息(具體接口看下文)距淫。
IM 路由
從架構圖中可以看出绞绒,路由層是非常重要的一環(huán);它提供了一系列的?HTTP?服務承接了客戶端和服務端榕暇。
目前主要是以下幾個接口蓬衡。
注冊接口
由于每一個客戶端都是需要登錄才能使用的,所以第一步自然是注冊彤枢。
這里就設計的比較簡單狰晚,直接利用?Redis?來存儲用戶信息;用戶信息也只有?ID?和?userName?而已缴啡。
只是為了方便查詢在?Redis?中的?KV?又反過來存儲了一份?VK壁晒,這樣?ID?和?userName?都必須唯一。
登錄接口
這里的登錄和?cim-server?中的登錄不一樣业栅,具有業(yè)務性質(zhì)秒咐,
登錄成功之后需要判斷是否是重復登錄(一個用戶只能運行一個客戶端)。
登錄成功后需要從?Zookeeper?中獲取服務列表(cim-server)并根據(jù)某種算法選擇一臺服務返回給客戶端碘裕。
登錄成功之后還需要保存路由信息反镇,也就是當前用戶分配的服務實例保存到?Redis?中。
為了實現(xiàn)只能一個用戶登錄娘汞,使用了?Redis?中的?set?來保存登錄信息歹茶;利用?userID?作為?key?,重復的登錄就會寫入失敗你弦。
類似于 Java 中的 HashSet惊豺,只能去重保存。
獲取一臺可用的路由實例也比較簡單:
先從?Zookeeper?獲取所有的服務實例做一個內(nèi)部緩存禽作。
輪詢選擇一臺服務器(目前只有這一種算法尸昧,后續(xù)會新增)。
當然要獲取?Zookeeper?中的服務實例前自然是需要監(jiān)聽?cim-server?之前注冊上去的那個節(jié)點旷偿。
具體代碼如下:
也是在應用啟動之后監(jiān)聽?Zookeeper?中的路由節(jié)點烹俗,一旦發(fā)生變化就會更新內(nèi)部緩存爆侣。
這里使用的是 Guava 的 cache,它基于?ConcurrentHashMap幢妄,所以可以保證清除兔仰、新增緩存的原子性。
群聊接口
這是一個真正發(fā)消息的接口蕉鸳,實現(xiàn)的效果就是其中一個客戶端發(fā)消息乎赴,其余所有客戶端都能收到!
流程肯定是客戶端發(fā)送一條消息到服務端潮尝,服務端收到后在上文介紹的?SessionSocketHolder?中遍歷所有?Channel(通道)然后下發(fā)消息即可榕吼。
服務端是單機倒也可以,但現(xiàn)在是集群設計勉失。所以所有的客戶端會根據(jù)之前的輪詢算法分配到不同的?cim-server?實例中羹蚣。
因此就需要路由層來發(fā)揮作用了。
路由接口收到消息后首先遍歷出所有的客戶端和服務實例的關系乱凿。
路由關系在?Redis?中的存放如下:
由于?Redis?單線程的特質(zhì)度宦,當數(shù)據(jù)量大時;一旦使用 keys 匹配所有?cim-route:*?數(shù)據(jù)告匠,會導致 Redis 不能處理其他請求戈抄。
所以這里改為使用 scan 命令來遍歷所有的?cim-route:*。
接著會挨個調(diào)用每個客戶端所在的服務端的?HTTP?接口用于推送消息后专。
在?cim-server?中的實現(xiàn)如下:
cim-server?收到消息后會在內(nèi)部緩存中查詢該 userID 的通道划鸽,接著只需要發(fā)消息即可。
在線用戶接口
這是一個輔助接口戚哎,可以查詢出當前在線用戶信息裸诽。
實現(xiàn)也很簡單,也就是查詢之前保存 ”用戶登錄狀態(tài)的那個去重?set?“即可型凳。
私聊接口
之所以說獲取在線用戶是一個輔助接口丈冬,其實就是用于輔助私聊使用的。
一般我們使用私聊的前提肯定得知道當前哪些用戶在線甘畅,接著你才會知道你要和誰進行私聊埂蕊。
類似于這樣:
在我們這個場景中,私聊的前提就是需要獲得在線用戶的?userID疏唾。
所以私聊接口在收到消息后需要查詢到接收者所在的?cim-server?實例信息蓄氧,后續(xù)的步驟就和群聊一致了。調(diào)用接收者所在實例的?HTTP?接口下發(fā)信息槐脏。
只是群聊是遍歷所有的在線用戶喉童,私聊只發(fā)送一個的區(qū)別。
下線接口
一旦客戶端下線顿天,我們就需要將之前存放在?Redis?中的一些信息刪除掉(路由信息堂氯、登錄狀態(tài))蔑担。
IM 客戶端
客戶端中的一些邏輯其實在上文已經(jīng)談到一些了。
登錄
第一步也就是登錄咽白,需要在啟動時調(diào)用?route?的登錄接口啤握,獲得?cim-server?信息再創(chuàng)建連接。
登錄過程中?route?接口會判斷是否為重復登錄局扶,重復登錄則會直接退出程序恨统。
接下來是利用?route?接口返回的?cim-server?實例信息(ip+port)創(chuàng)建連接叁扫。
最后一步就是發(fā)送一個登錄標志的信息到服務端三妈,讓它保持客戶端和?Channel?的關系。
自定義協(xié)議
上文提到的一些登錄報文莫绣、真正的消息報文這些其實都是在我們自定義協(xié)議中可以區(qū)別出來的畴蒲。
由于是使用?Google Protocol Buffer?編解碼,所以先看看原始格式对室。
其實這個協(xié)議中目前一共就三個字段:
requestId?可以理解為?userId模燥。
reqMsg?就是真正的消息。
type?也就是上文提到的消息類別掩宜。
目前主要是三種類型蔫骂,分別對應不同的業(yè)務:
心跳
為了保持客戶端和服務端的連接,每隔一段時間沒有發(fā)送消息都需要自動的發(fā)送心跳牺汤。
目前的策略是每隔一分鐘就是發(fā)送一個心跳包到服務端:
這樣服務端每隔一分鐘沒有收到業(yè)務消息時就會收到?ping?的心跳包:
內(nèi)置命令
客戶端也內(nèi)置了一些基本命令來方便使用辽旋。
比如輸入?:q?就會退出客戶端,同時會關閉一些系統(tǒng)資源檐迟。
當輸入?:olu(onlineUser?的簡寫)就會去調(diào)用?route?的獲取所有在線用戶接口补胚。
群聊
群聊的使用非常簡單,只需要在控制臺輸入消息回車即可追迟。
這時會去調(diào)用?route?的群聊接口溶其。
私聊
私聊也是同理,但前提是需要觸發(fā)關鍵字敦间;使用?userId;;消息內(nèi)容?這樣的格式才會給某個用戶發(fā)送消息瓶逃,所以一般都需要先使用?:olu?命令獲取所以在線用戶才方便使用。
消息回調(diào)
為了滿足一些定制需求廓块,比如消息需要保存之類的金闽。
所以在客戶端收到消息之后會回調(diào)一個接口,在這個接口中可以自定義實現(xiàn)剿骨。
因此先創(chuàng)建了一個?caller?的?bean代芜,這個?bean?中包含了一個?CustomMsgHandleListener?接口,需要自行處理只需要實現(xiàn)此接口即可浓利。
自定義界面
由于我自己不怎么會寫界面挤庇,但保不準有其他大牛會寫钞速。所以客戶端中的群聊、私聊嫡秕、獲取在線用戶渴语、消息回調(diào)等業(yè)務(以及之后的業(yè)務)都是以接口形式提供。
也方便后面做頁面集成昆咽,只需要調(diào)這些接口就行了驾凶;具體實現(xiàn)不用怎么關心。
總結
cim?目前只是第一版掷酗,BUG 多调违,功能少(只拉了幾個群友做了測試);不過后續(xù)還會接著完善泻轰,至少這一版會給那些沒有相關經(jīng)驗的朋友帶來一些思路技肩。
歡迎工作一到五年的Java工程師朋友們加入Java程序員開發(fā): 721575865
群內(nèi)提供免費的Java架構學習資料(里面有高可用、高并發(fā)浮声、高性能及分布式虚婿、Jvm性能調(diào)優(yōu)、Spring源碼泳挥,MyBatis然痊,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰屉符!趁年輕剧浸,使勁拼,給未來的自己一個交代筑煮!