使用場景
在實現業務的時候,我們常常有些需求需要系統主動傳送訊息給客戶端,方案有輪詢和長連線,但輪詢需要不斷的建立銷燬http連線,對客戶端、對伺服器來說都挺消耗資源的,訊息推送也不夠實時。這裡我們選擇了WebSocket長連線的方案。
有大量的專案需要服務端主動向客戶端推送訊息,為了減少重複開發,我們做成了微服務。
使用於伺服器需要主動向客戶端推送訊息、客戶端需要實時獲取訊息的請求。例如聊天、廣播訊息、多人遊戲訊息推送、任務執行結果推送等方面。
使用流程
用Websocket客戶端連線本服務,服務端會返回客戶端一個唯一的client id,透過這個client id可以知道是哪個連線,客戶端拿到這個id之後上報到服務端,服務端根據業務需求可以給這個長連線傳送指定資訊,或者繫結到分組。
分散式方案
維持大量的長連線對單臺伺服器的壓力也挺大的,這裡也就要求該服務需要可以擴容,也就是分散式地擴充套件。分散式對於可儲存的公共資源有一套完整的解決方案,但對於WebSocket來說,操作物件就是每一個連線,它是維持在每一個程式中的。每一個連線不能儲存起來共享、不能在不同的程式之間共享。所以我能想到的方案是不同程式之間進行通訊。
那麼,怎樣知道某個連線在哪個應用呢?答案是透過client id去判斷。那麼透過client id又是如何知道的呢?有以下幾種方案:
一致性hash演算法
一致性hash演算法是將整個雜湊值空間組織成一個虛擬的圓環,在redis叢集中雜湊函式的值空間為0-2^32-1(32位無符號整型)。把伺服器的IP或主機名作為關鍵字,透過雜湊函式計算出相應的值,對應到這個虛擬的圓環空間。我們再透過雜湊函式計算key的值,得到一個在圓環空間的位置,按順時針方向找到的第一個節點就是存放該key資料的伺服器節點。
在沒有節點的增減的時候,可以滿足我們的需求,但如果此時一個節點掛掉了或者新增一個機器怎麼辦?節點掛點之後,會在圓環上刪除節點,增加節點則反之。這時候按順時針方向找的資料就不準確,在某些業務上來說可以接受,但在WebSocket微服務上來說,影響範圍內的連線會斷掉,如果要求沒那麼高,客戶端再進行重連也可以。
hash slot(雜湊槽)
伺服器的IP或者主機名作為key,對每個key進行計算CRC16值,然後對16384進行取模,得出一個對應key的hash slot。
HASH_SLOT = CRC16(key) mod 16384
我們根據節點的數量,給每個節點劃分範圍,這個範圍是0-16384。hash slot的重點就在這個虛擬表,key對應的hash slot是永不變的,增減節點就是維護這張虛擬表。
以上兩種方案都可以實現需求,但一致性hash演算法的方案會使部分key找到的節點不準確;hash slot的方案需要維護一張虛擬表,在實現起來需要有一個功能去判斷伺服器是否掛了。修改這張虛擬表,新增節點也一樣,在實現起來會遇到很多問題。
然後我採取的方案是,每個連線都儲存在本應用,然後用對稱加密加密伺服器IP和埠,得到的值作為client id。對指定client id進行操作時,只需要解密這個key,就能得到相應的IP和埠。判斷是否為本機,不是本機的話進行RPC通訊告訴相應的程式。長連線的連線資料不可遷移,程式掛掉了相應的連線也就掛了,在該程式上的連線也就斷開了,這時重連的話會找到另一個可用的程式。
Golang實現的分散式WebSocket微服務
簡介
本系統基於Golang、Redis、RPC實現分散式WebSocket微服務,也可以單機部署,單機部署不需要Redis、RPC。分散式部署可以支援nginx負責均衡、水平擴容部署,程式之間使用RPC通訊。
目前實現的功能有,給指定客戶端傳送訊息、繫結客戶端到分組、給分組裡的客戶端批次傳送訊息、獲取線上的客戶端、上下線自動通知。適用於長連線的大部分場景,分組可以理解為聊天室,繫結客戶端到分組相當於把客戶端新增到聊天室,給分組傳送資訊相當於給聊天室的每個人傳送訊息。
架構圖
單機服務
單機服務
分散式
分散式
時序圖
單發訊息
- 客戶端傳送連線請求,連線請求透過nginx負載均衡找到一臺ws伺服器;
- ws伺服器響應連線請求,透過對稱加密伺服器IP和埠號,得到的值作為client id,並返回。
- 客戶端拿到client id之後,交給業務系統;
- 業務系統拿到client id之後,透過http傳送相關訊息,經過nginx負載分配到一臺ws伺服器;
- 這臺ws伺服器拿到clinet id和訊息,解密出對應的伺服器IP和埠;
- 拿到IP地址和埠,透過PRC協議給指定ws程式傳送資訊;
- 該ws程式接收到client id和資訊,給指定的連線傳送資訊;
- 客戶端收到資訊。
WebSocket微服務單發時序圖
群發訊息
- 前3個步驟跟單發的一樣;
- 業務系統拿到client id之後,透過http給指定分組傳送訊息,經過nginx負載分配到一臺ws伺服器;
- 這臺ws伺服器拿到分組ID和訊息,去Redis查詢伺服器列表,然後傳送RPC廣播;
- 所有收到廣播的服務,找到本機所有該分組的連線;
- 給所有這些連線傳送訊息;
- 客戶端收到資訊。
WebSocket微服務群發訊息時序圖
使用
下載本專案:
這裡已經打包好了,下載相應的環境,支援Linux、Windows、MacOS環境。
https://github.com/woodylan/go-websocket/r...
你也可以選擇自己編譯:
git clone https://github.com/woodylan/go-websocket.git
編譯:
// 編譯適用於本機的版本
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
執行:
編譯成功之後會得到一個二進位制檔案go-websocket
,執行該二進位制檔案,檔名後面跟著的是埠號,下面的命令666
則表示埠號,你可以可以改成其他的。
./go-websocket 666
連線測試:
開啟支援Websocket的客戶端,輸入 ws://127.0.0.1:666/ws
進行連線,連線成功會返回clientId
。
單機部署
單機部署很簡單,不需要配置Redis、RabbitMQ,只需要編譯然後執行該二進位制檔案就可以了,步驟如上。
分散式部署
安裝Redis: 參考網上教程
配置檔案:
配置檔案位於專案根目錄的configs/config.ini
,cluster
為true表示分散式部署。
[common]
# 是否分散式部署
cluster = true
# 對稱加密key 16位
crypto_key = xxxxxxxxxxxxxxxx
[redis]
host = 127.0.0.1
port = 6379
password =
執行專案:
在不同的機器執行本專案,注意配置號埠號,專案如果在同一機器,則必須用不同的埠。你可以用supervisor
做程式管理。
配置Nginx負載均衡:
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; # 代理轉發地址
proxy_http_version 1.1;
proxy_read_timeout 60s; # 超時設定
# 啟用支援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; # 代理轉發地址
}
}
至此,專案部署完成。
原始碼
github:https://github.com/woodylan/go-websocket
交流
QQ群:1028314856
本作品採用《CC 協議》,轉載必須註明作者和本文連結