使用場景
在實(shí)現(xiàn)業(yè)務(wù)的時(shí)候,我們常常有些需求需要系統(tǒng)主動發(fā)送消息給客戶端彰檬,方案有輪詢和長連接,但輪詢需要不斷的創(chuàng)建銷毀http連接独榴,對客戶端僧叉、對服務(wù)器來說都挺消耗資源的,消息推送也不夠?qū)崟r(shí)棺榔。這里我們選擇了WebSocket長連接的方案瓶堕。
有大量的項(xiàng)目需要服務(wù)端主動向客戶端推送消息,為了減少重復(fù)開發(fā)症歇,我們做成了微服務(wù)郎笆。
使用于服務(wù)器需要主動向客戶端推送消息谭梗、客戶端需要實(shí)時(shí)獲取消息的請求。例如聊天宛蚓、廣播消息激捏、多人游戲消息推送、任務(wù)執(zhí)行結(jié)果推送等方面凄吏。
使用流程
用Websocket客戶端連接本服務(wù)远舅,服務(wù)端會返回客戶端一個(gè)唯一的client id铃辖,通過這個(gè)client id可以知道是哪個(gè)連接柏副,客戶端拿到這個(gè)id之后上報(bào)到服務(wù)端,服務(wù)端根據(jù)業(yè)務(wù)需求可以給這個(gè)長連接發(fā)送指定信息报破,或者綁定到分組任连。
分布式方案
維持大量的長連接對單臺服務(wù)器的壓力也挺大的蚤吹,這里也就要求該服務(wù)需要可以擴(kuò)容,也就是分布式地?cái)U(kuò)展随抠。分布式對于可存儲的公共資源有一套完整的解決方案裁着,但對于WebSocket來說,操作對象就是每一個(gè)連接拱她,它是維持在每一個(gè)程序中的二驰。每一個(gè)連接不能存儲起來共享、不能在不同的程序之間共享秉沼。所以我能想到的方案是不同程序之間進(jìn)行通訊诸蚕。
那么,怎樣知道某個(gè)連接在哪個(gè)應(yīng)用呢氧猬?答案是通過client id去判斷。那么通過client id又是如何知道的呢坏瘩?有以下幾種方案:
-
一致性hash算法
一致性hash算法是將整個(gè)哈希值空間組織成一個(gè)虛擬的圓環(huán)盅抚,在redis集群中哈希函數(shù)的值空間為0-2^32-1(32位無符號整型)。把服務(wù)器的IP或主機(jī)名作為關(guān)鍵字倔矾,通過哈希函數(shù)計(jì)算出相應(yīng)的值妄均,對應(yīng)到這個(gè)虛擬的圓環(huán)空間。我們再通過哈希函數(shù)計(jì)算key的值哪自,得到一個(gè)在圓環(huán)空間的位置丰包,按順時(shí)針方向找到的第一個(gè)節(jié)點(diǎn)就是存放該key數(shù)據(jù)的服務(wù)器節(jié)點(diǎn)。
在沒有節(jié)點(diǎn)的增減的時(shí)候壤巷,可以滿足我們的需求邑彪,但如果此時(shí)一個(gè)節(jié)點(diǎn)掛掉了或者新增一個(gè)機(jī)器怎么辦?節(jié)點(diǎn)掛點(diǎn)之后胧华,會在圓環(huán)上刪除節(jié)點(diǎn)寄症,增加節(jié)點(diǎn)則反之宙彪。這時(shí)候按順時(shí)針方向找的數(shù)據(jù)就不準(zhǔn)確,在某些業(yè)務(wù)上來說可以接受有巧,但在WebSocket微服務(wù)上來說释漆,影響范圍內(nèi)的連接會斷掉,如果要求沒那么高篮迎,客戶端再進(jìn)行重連也可以男图。
-
hash slot(哈希槽)
服務(wù)器的IP或者主機(jī)名作為key,對每個(gè)key進(jìn)行計(jì)算CRC16值甜橱,然后對16384進(jìn)行取模逊笆,得出一個(gè)對應(yīng)key的hash slot。
HASH_SLOT = CRC16(key) mod 16384
我們根據(jù)節(jié)點(diǎn)的數(shù)量渗鬼,給每個(gè)節(jié)點(diǎn)劃分范圍览露,這個(gè)范圍是0-16384。hash slot的重點(diǎn)就在這個(gè)虛擬表譬胎,key對應(yīng)的hash slot是永不變的差牛,增減節(jié)點(diǎn)就是維護(hù)這張?zhí)摂M表。
以上兩種方案都可以實(shí)現(xiàn)需求堰乔,但一致性hash算法的方案會使部分key找到的節(jié)點(diǎn)不準(zhǔn)確偏化;hash slot的方案需要維護(hù)一張?zhí)摂M表,在實(shí)現(xiàn)起來需要有一個(gè)功能去判斷服務(wù)器是否掛了镐侯。修改這張?zhí)摂M表侦讨,新增節(jié)點(diǎn)也一樣,在實(shí)現(xiàn)起來會遇到很多問題苟翻。
然后我采取的方案是韵卤,每個(gè)連接都保存在本應(yīng)用,然后用對稱加密加密服務(wù)器IP和端口崇猫,得到的值作為client id沈条。對指定client id進(jìn)行操作時(shí),只需要解密這個(gè)key诅炉,就能得到相應(yīng)的IP和端口蜡歹。判斷是否為本機(jī),不是本機(jī)的話進(jìn)行RPC通訊告訴相應(yīng)的程序涕烧。長連接的連接數(shù)據(jù)不可遷移月而,程序掛掉了相應(yīng)的連接也就掛了,在該程序上的連接也就斷開了议纯,這時(shí)重連的話會找到另一個(gè)可用的程序父款。
Golang實(shí)現(xiàn)的分布式WebSocket微服務(wù)
簡介
本系統(tǒng)基于Golang、Redis、RPC實(shí)現(xiàn)分布式WebSocket微服務(wù)铛漓,也可以單機(jī)部署溯香,單機(jī)部署不需要Redis、RPC浓恶。分布式部署可以支持nginx負(fù)責(zé)均衡玫坛、水平擴(kuò)容部署,程序之間使用RPC通信包晰。
目前實(shí)現(xiàn)的功能有湿镀,給指定客戶端發(fā)送消息、綁定客戶端到分組伐憾、給分組里的客戶端批量發(fā)送消息勉痴、獲取在線的客戶端、上下線自動通知树肃。適用于長連接的大部分場景蒸矛,分組可以理解為聊天室,綁定客戶端到分組相當(dāng)于把客戶端添加到聊天室胸嘴,給分組發(fā)送信息相當(dāng)于給聊天室的每個(gè)人發(fā)送消息雏掠。
架構(gòu)圖
單機(jī)服務(wù)
<center>單機(jī)服務(wù)</certer>
分布式
<center>分布式</certer>
時(shí)序圖
單發(fā)消息
- 客戶端發(fā)送連接請求,連接請求通過nginx負(fù)載均衡找到一臺ws服務(wù)器劣像;
- ws服務(wù)器響應(yīng)連接請求乡话,通過對稱加密服務(wù)器IP和端口號,得到的值作為client id耳奕,并返回绑青。
- 客戶端拿到client id之后,交給業(yè)務(wù)系統(tǒng)屋群;
- 業(yè)務(wù)系統(tǒng)拿到client id之后闸婴,通過http發(fā)送相關(guān)消息,經(jīng)過nginx負(fù)載分配到一臺ws服務(wù)器芍躏;
- 這臺ws服務(wù)器拿到clinet id和消息掠拳,解密出對應(yīng)的服務(wù)器IP和端口;
- 拿到IP地址和端口纸肉,通過PRC協(xié)議給指定ws程序發(fā)送信息;
- 該ws程序接收到client id和信息喊熟,給指定的連接發(fā)送信息柏肪;
- 客戶端收到信息。
<center>WebSocket微服務(wù)單發(fā)時(shí)序圖</certer>
群發(fā)消息
- 前3個(gè)步驟跟單發(fā)的一樣芥牌;
- 業(yè)務(wù)系統(tǒng)拿到client id之后烦味,通過http給指定分組發(fā)送消息,經(jīng)過nginx負(fù)載分配到一臺ws服務(wù)器;
- 這臺ws服務(wù)器拿到分組ID和消息谬俄,去Redis查詢服務(wù)器列表柏靶,然后發(fā)送RPC廣播;
- 所有收到廣播的服務(wù)溃论,找到本機(jī)所有該分組的連接屎蜓;
- 給所有這些連接發(fā)送消息;
- 客戶端收到信息钥勋。
<center>WebSocket微服務(wù)群發(fā)消息時(shí)序圖</certer>
使用
下載本項(xiàng)目:
這里已經(jīng)打包好了炬转,下載相應(yīng)的環(huán)境,支持Linux算灸、Windows扼劈、MacOS環(huán)境。
https://github.com/woodylan/go-websocket/releases
你也可以選擇自己編譯:
git clone https://github.com/woodylan/go-websocket.git
編譯:
// 編譯適用于本機(jī)的版本
go build
// 編譯Linux版本
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
// 編譯Windows 64位版本
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
// 編譯MacOS版本
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
執(zhí)行:
編譯成功之后會得到一個(gè)二進(jìn)制文件go-websocket
菲驴,執(zhí)行該二進(jìn)制文件荐吵,文件名后面跟著的是端口號,下面的命令666
則表示端口號赊瞬,你可以可以改成其他的先煎。
./go-websocket 666
連接測試:
打開支持Websocket的客戶端,輸入 ws://127.0.0.1:666/ws
進(jìn)行連接森逮,連接成功會返回clientId
榨婆。
單機(jī)部署
單機(jī)部署很簡單,不需要配置Redis褒侧、RabbitMQ良风,只需要編譯然后運(yùn)行該二進(jìn)制文件就可以了,步驟如上闷供。
分布式部署
安裝Redis: 參考網(wǎng)上教程
配置文件:
配置文件位于項(xiàng)目根目錄的configs/config.ini
烟央,cluster
為true表示分布式部署。
[common]
# 是否分布式部署
cluster = true
# 對稱加密key 16位
crypto_key = xxxxxxxxxxxxxxxx
[redis]
host = 127.0.0.1
port = 6379
password =
運(yùn)行項(xiàng)目:
在不同的機(jī)器運(yùn)行本項(xiàng)目歪脏,注意配置號端口號疑俭,項(xiàng)目如果在同一機(jī)器,則必須用不同的端口婿失。你可以用supervisor
做進(jìn)程管理钞艇。
配置Nginx負(fù)載均衡:
upstream ws_cluster {
server 127.0.0.1:666;
server 127.0.0.1:667;
}
server {
listen 660;
server_name ws.example.com;
access_log /logs/access.log;
error_log /logs/error.log;
location /ws {
proxy_pass http://ws_cluster; # 代理轉(zhuǎn)發(fā)地址
proxy_http_version 1.1;
proxy_read_timeout 60s; # 超時(shí)設(shè)置
# 啟用支持websocket連接
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /api {
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://ws_cluster; # 代理轉(zhuǎn)發(fā)地址
}
}
至此,項(xiàng)目部署完成豪硅。
源碼
github:https://github.com/woodylan/go-websocket
交流
QQ群:1028314856