老讀者應(yīng)該還記得我在去年國慶節(jié)前分享過一篇《技術(shù)干貨:從零開始,教你設(shè)計(jì)一個(gè)百萬級(jí)的消息推送系統(tǒng)》,雖然我在文中有貼一些偽代碼,依然有些朋友希望能直接分享一些可以運(yùn)行的源碼。好吧,質(zhì)疑我窮我無話可說(因?yàn)槭钦娓F。。),懷疑我擼碼的能力那是絕對(duì)不行,所以這次準(zhǔn)備拉起鍵盤大干一場——徒手?jǐn)]套分布式IM出來!^_^!
創(chuàng)新互聯(lián)成立與2013年,先為尼瑪?shù)确?wù)建站,尼瑪?shù)鹊仄髽I(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為尼瑪企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。
本文記錄了我開發(fā)的一款面向IM學(xué)習(xí)者的 IM系統(tǒng)——CIM(全稱:CROSS-IM),同時(shí)提供了一些組件幫助開發(fā)者構(gòu)建一款屬于自己可水平擴(kuò)展的 IM。
通過學(xué)習(xí)本文和CIM代碼,你可以獲得以下知識(shí):
1)如何從頭開發(fā)一套IM(CIM的客戶有點(diǎn)弱,見諒見諒);
2)如何設(shè)計(jì)分布式的IM架構(gòu);
3)如何將你的分布式IM架構(gòu)用代碼和相關(guān)技術(shù)實(shí)現(xiàn)出來。
本文配套的CIM源碼地址:
主要鏡像:https://github.com/crossoverJie/cim
備用鏡像:https://github.com/52im/cim
* 友情提示:閱讀本文和CIM源碼,需要您具備一定的網(wǎng)絡(luò)編程、IM理論等知識(shí)等,如果您還不具備這些,請先閱讀《新手入門一篇就夠:從零開發(fā)移動(dòng)端IM》,完全來的及!
本文同步發(fā)布于:http://www.52im.net/thread-2775-1-1.html
crossoverJie(陳杰):?90后,畢業(yè)于重慶信息工程學(xué)院,現(xiàn)供職于重慶豬八戒網(wǎng)絡(luò)有限公司。
本次特地錄了兩段視頻演示(群聊、私聊),點(diǎn)擊下方鏈接可以查看視頻版 Demo。
CIM 私聊視頻演示:https://www.bilibili.com/video/av39405821
CIM 群聊視頻演示:https://www.bilibili.com/video/av39405501
下面來看看具體的架構(gòu)設(shè)計(jì):
?
架構(gòu)說明:
1)CIM 中的各個(gè)組件均采用 SpringBoot 構(gòu)建;
2)采用 Netty + Protocol Buffer 構(gòu)建底層通信;
3)redis 存放各個(gè)客戶端的路由信息、賬號(hào)信息、在線狀態(tài)等;
4)Zookeeper 用于 IM-server 服務(wù)的注冊與發(fā)現(xiàn)。
整體主要由以下模塊組成:
1)cim-server——IM 服務(wù)端:用于接收 client 連接、消息透傳、消息推送等功能。支持集群部署;
2)cim-forward-route——消息路由服務(wù)器:用于處理消息路由、消息轉(zhuǎn)發(fā)、用戶登錄、用戶下線以及一些運(yùn)營工具(獲取在線用戶數(shù)等);
3)cim-client——IM 客戶端:給用戶使用的消息終端,一個(gè)命令即可啟動(dòng)并向其他人發(fā)起通訊(群聊、私聊);同時(shí)內(nèi)置了一些常用命令方便使用。
整體的流程也比較簡單,流程圖如下:
?
流程解釋如下:
1)客戶端向 route 發(fā)起登錄;
2)登錄成功從 Zookeeper 中選擇可用 IM-server 返回給客戶端,并保存登錄、路由信息到 Redis;
3)客戶端向 IM-server 發(fā)起長連接,成功后保持心跳;
4)客戶端下線時(shí)通過 route 清除狀態(tài)信息。
所以當(dāng)我們自己部署時(shí)需要以下步驟:
1)搭建基礎(chǔ)中間件?Redis、Zookeeper;
2)部署 cim-server,這是真正的 IM 服務(wù)器,為了滿足性能需求所以支持水平擴(kuò)展,只需要注冊到同一個(gè) Zookeeper 即可;
3)部署 cim-forward-route,這是路由服務(wù)器,所有的消息都需要經(jīng)過它。由于它是無狀態(tài)的,所以也可以利用 Nginx 代理提高可用性;
4)cim-client 真正面向用戶的客戶端;啟動(dòng)之后會(huì)自動(dòng)連接 IM 服務(wù)器便可以在控制臺(tái)收發(fā)消息了。
更多使用介紹可以參考快速啟動(dòng)。
接下來各章將重點(diǎn)看看具體的詳細(xì)設(shè)計(jì)實(shí)現(xiàn),比如群聊、私聊消息如何流轉(zhuǎn);IM 服務(wù)端負(fù)載均衡;服務(wù)如何注冊發(fā)現(xiàn)等等。
先來看看服務(wù)端:主要是實(shí)現(xiàn)客戶端上下線、消息下發(fā)等功能。
首先是服務(wù)啟動(dòng):
?
?
由于是在 SpringBoot 中搭建的,所以在應(yīng)用啟動(dòng)時(shí)需要啟動(dòng) Netty 服務(wù)。
從 pipline 中可以看出使用了?Protobuf?的編解碼(具體報(bào)文在客戶端中分析,相關(guān)知識(shí)請見:《Protobuf通信協(xié)議詳解:代碼演示、詳細(xì)原理介紹等》)。
需要滿足 IM 服務(wù)端的水平擴(kuò)展需求,所以 cim-server 是需要將自身數(shù)據(jù)發(fā)布到注冊中心的。這里參考之前分享的《搞定服務(wù)注冊與發(fā)現(xiàn)》有具體介紹。
所以在應(yīng)用啟動(dòng)成功后需要將自身數(shù)據(jù)注冊到 Zookeeper 中:
?
?
最主要的目的就是將當(dāng)前應(yīng)用的 ip + cim-server-port+ http-port 注冊上去:
?
上圖是我在演示環(huán)境中注冊的兩個(gè) cim-server 實(shí)例(由于在一臺(tái)服務(wù)器,所以只是端口不同)。這樣在客戶端(監(jiān)聽這個(gè) Zookeeper 節(jié)點(diǎn))就能實(shí)時(shí)的知道目前可用的服務(wù)信息。
當(dāng)客戶端請求 cim-forward-route 中的登錄接口(詳見下文)做完業(yè)務(wù)驗(yàn)證(就相當(dāng)于日常登錄其他網(wǎng)站一樣)之后,客戶端會(huì)向服務(wù)端發(fā)起一個(gè)長連接。
如之前的流程所示:
?
這時(shí)客戶端會(huì)發(fā)送一個(gè)特殊報(bào)文,表明當(dāng)前是登錄信息。服務(wù)端收到后就需要將該客戶端的 userID 和當(dāng)前 Channel 通道關(guān)系保存起來。
?
?
同時(shí)也緩存了用戶的信息,也就是 userID 和 用戶名。
當(dāng)客戶端斷線后也需要將剛才緩存的信息清除掉。
?
同時(shí)也需要調(diào)用 route 接口清除相關(guān)信息(具體接口看下文)。
?
從架構(gòu)圖中可以看出,路由層是非常重要的一環(huán);它提供了一系列的 HTTP 服務(wù)承接了客戶端和服務(wù)端。
目前主要是以下幾個(gè)接口。
?
?
由于每一個(gè)客戶端都是需要登錄才能使用的,所以第一步自然是注冊。
這里就設(shè)計(jì)的比較簡單,直接利用 Redis 來存儲(chǔ)用戶信息;用戶信息也只有 ID 和 userName 而已。只是為了方便查詢在 Redis 中的 KV 又反過來存儲(chǔ)了一份 VK,這樣 ID 和 userName 都必須唯一。
這里的登錄和 cim-server 中的登錄不一樣,具有業(yè)務(wù)性質(zhì):
?
具體的流程:
1)登錄成功之后需要判斷是否是重復(fù)登錄(一個(gè)用戶只能運(yùn)行一個(gè)客戶端);
2)登錄成功后需要從 Zookeeper 中獲取服務(wù)列表(cim-server)并根據(jù)某種算法選擇一臺(tái)服務(wù)返回給客戶端;
3)登錄成功之后還需要保存路由信息,也就是當(dāng)前用戶分配的服務(wù)實(shí)例保存到 Redis 中。
為了實(shí)現(xiàn)只能一個(gè)用戶登錄,使用了 Redis 中的 set 來保存登錄信息;利用 userID 作為 key ,重復(fù)的登錄就會(huì)寫入失敗。
?
?
?
類似于 Java 中的 HashSet,只能去重保存。
獲取一臺(tái)可用的路由實(shí)例也比較簡單:
?
1)先從 Zookeeper 獲取所有的服務(wù)實(shí)例做一個(gè)內(nèi)部緩存;
2)輪詢選擇一臺(tái)服務(wù)器(目前只有這一種算法,后續(xù)會(huì)新增)。
當(dāng)然要獲取 Zookeeper 中的服務(wù)實(shí)例前自然是需要監(jiān)聽 cim-server 之前注冊上去的那個(gè)節(jié)點(diǎn)。
具體代碼如下:
?
?
?
也是在應(yīng)用啟動(dòng)之后監(jiān)聽 Zookeeper 中的路由節(jié)點(diǎn),一旦發(fā)生變化就會(huì)更新內(nèi)部緩存。這里使用的是?Guava?的 cache,它基于 ConcurrentHashMap,所以可以保證清除、新增緩存的原子性。
這是一個(gè)真正發(fā)消息的接口,實(shí)現(xiàn)的效果就是其中一個(gè)客戶端發(fā)消息,其余所有客戶端都能收到!流程肯定是客戶端發(fā)送一條消息到服務(wù)端,服務(wù)端收到后在上文介紹的 SessionSocketHolder 中遍歷所有 Channel(通道)然后下發(fā)消息即可。服務(wù)端是單機(jī)倒也可以,但現(xiàn)在是集群設(shè)計(jì)。所以所有的客戶端會(huì)根據(jù)之前的輪詢算法分配到不同的 cim-server 實(shí)例中。
因此就需要路由層來發(fā)揮作用了。
?
?
路由接口收到消息后首先遍歷出所有的客戶端和服務(wù)實(shí)例的關(guān)系。
路由關(guān)系在 Redis 中的存放如下:
?
由于 Redis 單線程的特質(zhì),當(dāng)數(shù)據(jù)量大時(shí);一旦使用 keys 匹配所有 cim-route:* 數(shù)據(jù),會(huì)導(dǎo)致 Redis 不能處理其他請求。所以這里改為使用 scan 命令來遍歷所有的 cim-route:*。
接著會(huì)挨個(gè)調(diào)用每個(gè)客戶端所在的服務(wù)端的 HTTP 接口用于推送消息。
在 cim-server 中的實(shí)現(xiàn)如下:
?
?
cim-server 收到消息后會(huì)在內(nèi)部緩存中查詢該 userID 的通道,接著只需要發(fā)消息即可。
這是一個(gè)輔助接口,可以查詢出當(dāng)前在線用戶信息。
?
?
實(shí)現(xiàn)也很簡單,也就是查詢之前保存 ”用戶登錄狀態(tài)的那個(gè)去重 set “即可。
之所以說獲取在線用戶是一個(gè)輔助接口,其實(shí)就是用于輔助私聊使用的。一般我們使用私聊的前提肯定得知道當(dāng)前哪些用戶在線,接著你才會(huì)知道你要和誰進(jìn)行私聊。
類似于這樣:
?
在我們這個(gè)場景中,私聊的前提就是需要獲得在線用戶的 userID:
?
所以私聊接口在收到消息后需要查詢到接收者所在的 cim-server 實(shí)例信息,后續(xù)的步驟就和群聊一致了。調(diào)用接收者所在實(shí)例的 HTTP 接口下發(fā)信息。只是群聊是遍歷所有的在線用戶,私聊只發(fā)送一個(gè)的區(qū)別。
一旦客戶端下線,我們就需要將之前存放在 Redis 中的一些信息刪除掉(路由信息、登錄狀態(tài))。
?
?
客戶端中的一些邏輯其實(shí)在上文已經(jīng)談到一些了。
第一步也就是登錄,需要在啟動(dòng)時(shí)調(diào)用 route 的登錄接口,獲得 cim-server 信息再創(chuàng)建連接。
?
?
?
登錄過程中 route 接口會(huì)判斷是否為重復(fù)登錄,重復(fù)登錄則會(huì)直接退出程序。
?
接下來是利用 route 接口返回的 cim-server 實(shí)例信息(ip+port)創(chuàng)建連接。最后一步就是發(fā)送一個(gè)登錄標(biāo)志的信息到服務(wù)端,讓它保持客戶端和 Channel 的關(guān)系。
?
上文提到的一些登錄報(bào)文、真正的消息報(bào)文這些其實(shí)都是在我們自定義協(xié)議中可以區(qū)別出來的。由于是使用?Protocol Buffer?編解碼,所以先看看原始格式。
?
其實(shí)這個(gè)協(xié)議中目前一共就三個(gè)字段:
1)requestId 可以理解為 userId;
2)reqMsg 就是真正的消息;
3)type 也就是上文提到的消息類別。
目前主要是三種類型,分別對(duì)應(yīng)不同的業(yè)務(wù):
?
為了保持客戶端和服務(wù)端的連接,每隔一段時(shí)間沒有發(fā)送消息都需要自動(dòng)的發(fā)送心跳。
目前的策略是每隔一分鐘就是發(fā)送一個(gè)心跳包到服務(wù)端:
?
?
這樣服務(wù)端每隔一分鐘沒有收到業(yè)務(wù)消息時(shí)就會(huì)收到 ping 的心跳包:
?
客戶端也內(nèi)置了一些基本命令來方便使用。
?
?
比如輸入 :q 就會(huì)退出客戶端,同時(shí)會(huì)關(guān)閉一些系統(tǒng)資源。
?
?
當(dāng)輸入 :olu(onlineUser 的簡寫)就會(huì)去調(diào)用 route 的獲取所有在線用戶接口。
?
?
群聊的使用非常簡單,只需要在控制臺(tái)輸入消息回車即可。這時(shí)會(huì)去調(diào)用 route 的群聊接口。
?
私聊也是同理,但前提是需要觸發(fā)關(guān)鍵字;使用 userId;;消息內(nèi)容 這樣的格式才會(huì)給某個(gè)用戶發(fā)送消息,所以一般都需要先使用?
lu 命令獲取所以在線用戶才方便使用。
?
為了滿足一些定制需求,比如消息需要保存之類的。所以在客戶端收到消息之后會(huì)回調(diào)一個(gè)接口,在這個(gè)接口中可以自定義實(shí)現(xiàn)。
?
?
因此先創(chuàng)建了一個(gè) caller 的 bean,這個(gè) bean 中包含了一個(gè) CustomMsgHandleListener 接口,需要自行處理只需要實(shí)現(xiàn)此接口即可。
由于我自己不怎么會(huì)寫界面,但保不準(zhǔn)有其他大牛會(huì)寫。所以客戶端中的群聊、私聊、獲取在線用戶、消息回調(diào)等業(yè)務(wù)(以及之后的業(yè)務(wù))都是以接口形式提供。
也方便后面做頁面集成,只需要調(diào)這些接口就行了;具體實(shí)現(xiàn)不用怎么關(guān)心。
cim 目前只是第一版,BUG 多,功能少(只拉了幾個(gè)群友做了測試);不過后續(xù)還會(huì)接著完善,至少這一版會(huì)給那些沒有相關(guān)經(jīng)驗(yàn)的朋友帶來一些思路。
后續(xù)計(jì)劃:
?
本文同步發(fā)布于:http://www.52im.net/thread-2775-1-1.html