基于websocket單臺機(jī)器支持百萬連接分布式聊天(IM)系統(tǒng)
本文將介紹如何實現(xiàn)一個基于websocket分布式聊天(IM)系統(tǒng)。
使用golang實現(xiàn)websocket通訊,單機(jī)可以支持百萬連接,使用gin框架、nginx負(fù)載、可以水平部署、程序內(nèi)部相互通訊、使用grpc通訊協(xié)議。
本文內(nèi)容比較長,如果直接想clone項目體驗直接進(jìn)入項目體驗 goWebSocket項目下載 ,文本從介紹webSocket是什么開始,然后開始介紹這個項目,以及在Nginx中配置域名做webSocket的轉(zhuǎn)發(fā),然后介紹如何搭建一個分布式系統(tǒng)。
目錄
- 1、項目說明
- 1.1 goWebSocket
- 1.2 項目體驗
- 2、介紹webSocket
- 2.1 webSocket 是什么
- 2.2 webSocket的兼容性
- 2.3 為什么要用webSocket
- 2.4 webSocket建立過程
- 3、如何實現(xiàn)基于webSocket的長連接系統(tǒng)
- 3.1 使用go實現(xiàn)webSocket服務(wù)端
- 3.1.1 啟動端口監(jiān)聽
- 3.1.2 升級協(xié)議
- 3.1.3 客戶端連接的管理
- 3.1.4 注冊客戶端的socket的寫的異步處理程序
- 3.1.5 注冊客戶端的socket的讀的異步處理程序
- 3.1.6 接收客戶端數(shù)據(jù)并處理
- 3.1.7 使用路由的方式處理客戶端的請求數(shù)據(jù)
- 3.1.8 防止內(nèi)存溢出和Goroutine不回收
- 3.2 使用javaScript實現(xiàn)webSocket客戶端
- 3.2.1 啟動并注冊監(jiān)聽程序
- 3.2.2 發(fā)送數(shù)據(jù)
- 3.1 使用go實現(xiàn)webSocket服務(wù)端
- 4、goWebSocket 項目
- 4.1 項目說明
- 4.2 項目依賴
- 4.3 項目啟動
- 5、webSocket項目Nginx配置
- 5.1 為什么要配置Nginx
- 5.2 nginx配置
- 5.3 問題處理
- 6、壓測
- 6.1 Linux內(nèi)核優(yōu)化
- 6.2 壓測準(zhǔn)備
- 6.3 壓測數(shù)據(jù)
- 7、如何基于webSocket實現(xiàn)一個分布式Im
- 7.1 說明
- 7.2 架構(gòu)
- 7.3 分布式系統(tǒng)部署
- 8、回顧和反思
- 8.1 在其它系統(tǒng)應(yīng)用
- 8.2 需要完善、優(yōu)化
- 8.3 總結(jié)
- 9、參考文獻(xiàn)
1、項目說明
1.1 goWebSocket
本文將介紹如何實現(xiàn)一個基于websocket聊天(IM)分布式系統(tǒng)。
使用golang實現(xiàn)websocket通訊,單機(jī)支持百萬連接,使用gin框架、nginx負(fù)載、可以水平部署、程序內(nèi)部相互通訊、使用grpc通訊協(xié)議。
-
一般項目中webSocket使用的架構(gòu)圖
網(wǎng)站架構(gòu)圖
1.2 項目體驗
- 項目地址 gowebsocket
- IM-聊天首頁 或者在新的窗口打開 http://im.91vh.com/home/index
- 打開連接以后進(jìn)入聊天界面
- 多人群聊可以同時打開兩個窗口
2、介紹webSocket
2.1 webSocket 是什么
WebSocket 協(xié)議在2008年誕生,2011年成為國際標(biāo)準(zhǔn)。所有瀏覽器都已經(jīng)支持了。
它的最大特點就是,服務(wù)器可以主動向客戶端推送信息,客戶端也可以主動向服務(wù)器發(fā)送信息,是真正的雙向平等對話,屬于服務(wù)器推送技術(shù)的一種。
-
HTTP和WebSocket在通訊過程的比較
HTTP協(xié)議和WebSocket比較 -
HTTP和webSocket都支持配置證書,
ws://無證書wss://配置證書的協(xié)議標(biāo)識
HTTP協(xié)議和WebSocket比較
2.2 webSocket的兼容性
- 瀏覽器的兼容性,開始支持webSocket的版本

- 服務(wù)端的支持
golang、java、php、node.js、python、nginx 都有不錯的支持
- Android和IOS的支持
Android可以使用java-webSocket對webSocket支持
iOS 4.2及更高版本具有WebSockets支持
2.3 為什么要用webSocket
- 從業(yè)務(wù)上出發(fā),需要一個主動通達(dá)客戶端的能力
目前大多數(shù)的請求都是使用HTTP,都是由客戶端發(fā)起一個請求,有服務(wù)端處理,然后返回結(jié)果,不可以服務(wù)端主動向某一個客戶端主動發(fā)送數(shù)據(jù)

- 大多數(shù)場景我們需要主動通知用戶,如:聊天系統(tǒng)、用戶完成任務(wù)主動告訴用戶、一些運(yùn)營活動需要通知到在線的用戶
- 可以獲取用戶在線狀態(tài)
- 在沒有長連接的時候通過客戶端主動輪詢獲取數(shù)據(jù)
- 可以通過一種方式實現(xiàn),多種不同平臺(H5/Android/IOS)去使用
2.4 webSocket建立過程
- 客戶端先發(fā)起升級協(xié)議的請求
客戶端發(fā)起升級協(xié)議的請求,采用標(biāo)準(zhǔn)的HTTP報文格式,在報文中添加頭部信息
Connection: Upgrade表明連接需要升級
Upgrade: websocket需要升級到 websocket協(xié)議
Sec-WebSocket-Version: 13 協(xié)議的版本為13
Sec-WebSocket-Key: I6qjdEaqYljv3+9x+GrhqA== 這個是base64 encode 的值,是瀏覽器隨機(jī)生成的,與服務(wù)器響應(yīng)的 Sec-WebSocket-Accept對應(yīng)
# Request Headers
Connection: Upgrade
Host: im.91vh.com
Origin: http://im.91vh.com
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: I6qjdEaqYljv3+9x+GrhqA==
Sec-WebSocket-Version: 13
Upgrade: websocket

- 服務(wù)器響應(yīng)升級協(xié)議
服務(wù)端接收到升級協(xié)議的請求,如果服務(wù)端支持升級協(xié)議會做如下響應(yīng)
返回:
Status Code: 101 Switching Protocols 表示支持切換協(xié)議
# Response Headers
Connection: upgrade
Date: Fri, 09 Aug 2019 07:36:59 GMT
Sec-WebSocket-Accept: mB5emvxi2jwTUhDdlRtADuBax9E=
Server: nginx/1.12.1
Upgrade: websocket
- 升級協(xié)議完成以后,客戶端和服務(wù)器就可以相互發(fā)送數(shù)據(jù)

3、如何實現(xiàn)基于webSocket的長連接系統(tǒng)
3.1 使用go實現(xiàn)webSocket服務(wù)端
3.1.1 啟動端口監(jiān)聽
- websocket需要監(jiān)聽端口,所以需要在
golang成功的main函數(shù)中用協(xié)程的方式去啟動程序 - main.go 實現(xiàn)啟動
go websocket.StartWebSocket()
- init_acc.go 啟動程序
// 啟動程序
func StartWebSocket() {
http.HandleFunc("/acc", wsPage)
http.ListenAndServe(":8089", nil)
}
3.1.2 升級協(xié)議
- 客戶端是通過http請求發(fā)送到服務(wù)端,我們需要對http協(xié)議進(jìn)行升級為websocket協(xié)議
- 對http請求協(xié)議進(jìn)行升級 golang 庫gorilla/websocket 已經(jīng)做得很好了,我們直接使用就可以了
- 在實際使用的時候,建議每個連接使用兩個協(xié)程處理客戶端請求數(shù)據(jù)和向客戶端發(fā)送數(shù)據(jù),雖然開啟協(xié)程會占用一些內(nèi)存,但是讀取分離,減少收發(fā)數(shù)據(jù)堵塞的可能
- init_acc.go
func wsPage(w http.ResponseWriter, req *http.Request) {
// 升級協(xié)議
conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
fmt.Println("升級協(xié)議", "ua:", r.Header["User-Agent"], "referer:", r.Header["Referer"])
return true
}}).Upgrade(w, req, nil)
if err != nil {
http.NotFound(w, req)
return
}
fmt.Println("webSocket 建立連接:", conn.RemoteAddr().String())
currentTime := uint64(time.Now().Unix())
client := NewClient(conn.RemoteAddr().String(), conn, currentTime)
go client.read()
go client.write()
// 用戶連接事件
clientManager.Register <- client
}
3.1.3 客戶端連接的管理
- 當(dāng)前程序有多少用戶連接,還需要對用戶廣播的需要,這里我們就需要一個管理者(clientManager),處理這些事件:
- 記錄全部的連接、登錄用戶的可以通過 appId+uuid 查到用戶連接
- 使用map存儲,就涉及到多協(xié)程并發(fā)讀寫的問題,所以需要加讀寫鎖
- 定義四個channel ,分別處理客戶端建立連接、用戶登錄、斷開連接、全員廣播事件
// 連接管理
type ClientManager struct {
Clients map[*Client]bool // 全部的連接
ClientsLock sync.RWMutex // 讀寫鎖
Users map[string]*Client // 登錄的用戶 // appId+uuid
UserLock sync.RWMutex // 讀寫鎖
Register chan *Client // 連接連接處理
Login chan *login // 用戶登錄處理
Unregister chan *Client // 斷開連接處理程序
Broadcast chan []byte // 廣播 向全部成員發(fā)送數(shù)據(jù)
}
// 初始化
func NewClientManager() (clientManager *ClientManager) {
clientManager = &ClientManager{
Clients: make(map[*Client]bool),
Users: make(map[string]*Client),
Register: make(chan *Client, 1000),
Login: make(chan *login, 1000),
Unregister: make(chan *Client, 1000),
Broadcast: make(chan []byte, 1000),
}
return
}
3.1.4 注冊客戶端的socket的寫的異步處理程序
- 防止發(fā)生程序崩潰,所以需要捕獲異常
- 為了顯示異常崩潰位置這里使用
string(debug.Stack())打印調(diào)用堆棧信息 - 如果寫入數(shù)據(jù)失敗了,可能連接有問題,就關(guān)閉連接
- client.go
// 向客戶端寫數(shù)據(jù)
func (c *Client) write() {
defer func() {
if r := recover(); r != nil {
fmt.Println("write stop", string(debug.Stack()), r)
}
}()
defer func() {
clientManager.Unregister <- c
c.Socket.Close()
fmt.Println("Client發(fā)送數(shù)據(jù) defer", c)
}()
for {
select {
case message, ok := <-c.Send:
if !ok {
// 發(fā)送數(shù)據(jù)錯誤 關(guān)閉連接
fmt.Println("Client發(fā)送數(shù)據(jù) 關(guān)閉連接", c.Addr, "ok", ok)
return
}
c.Socket.WriteMessage(websocket.TextMessage, message)
}
}
}
3.1.5 注冊客戶端的socket的讀的異步處理程序
- 循環(huán)讀取客戶端發(fā)送的數(shù)據(jù)并處理
- 如果讀取數(shù)據(jù)失敗了,關(guān)閉channel
- client.go
// 讀取客戶端數(shù)據(jù)
func (c *Client) read() {
defer func() {
if r := recover(); r != nil {
fmt.Println("write stop", string(debug.Stack()), r)
}
}()
defer func() {
fmt.Println("讀取客戶端數(shù)據(jù) 關(guān)閉send", c)
close(c.Send)
}()
for {
_, message, err := c.Socket.ReadMessage()
if err != nil {
fmt.Println("讀取客戶端數(shù)據(jù) 錯誤", c.Addr, err)
return
}
// 處理程序
fmt.Println("讀取客戶端數(shù)據(jù) 處理:", string(message))
ProcessData(c, message)
}
}
3.1.6 接收客戶端數(shù)據(jù)并處理
約定發(fā)送和接收請求數(shù)據(jù)格式,為了js處理方便,采用了
json的數(shù)據(jù)格式發(fā)送和接收數(shù)據(jù)(人類可以閱讀的格式在工作開發(fā)中使用是比較方便的)登錄發(fā)送數(shù)據(jù)示例:
{"seq":"1565336219141-266129","cmd":"login","data":{"userId":"馬遠(yuǎn)","appId":101}}
- 登錄響應(yīng)數(shù)據(jù)示例:
{"seq":"1565336219141-266129","cmd":"login","response":{"code":200,"codeMsg":"Success","data":null}}
websocket是雙向的數(shù)據(jù)通訊,可以連續(xù)發(fā)送,如果發(fā)送的數(shù)據(jù)需要服務(wù)端回復(fù),就需要一個seq來確定服務(wù)端的響應(yīng)是回復(fù)哪一次的請求數(shù)據(jù)
cmd 是用來確定動作,websocket沒有類似于http的url,所以規(guī)定 cmd 是什么動作
目前的動作有:login/heartbeat 用來發(fā)送登錄請求和連接保活(長時間沒有數(shù)據(jù)發(fā)送的長連接容易被瀏覽器、移動中間商、nginx、服務(wù)端程序斷開)
為什么需要AppId,UserId是表示用戶的唯一字段,設(shè)計的時候為了做成通用性,設(shè)計AppId用來表示用戶在哪個平臺登錄的(web、app、ios等),方便后續(xù)擴(kuò)展
request_model.go 約定的請求數(shù)據(jù)格式
/************************ 請求數(shù)據(jù) **************************/
// 通用請求數(shù)據(jù)格式
type Request struct {
Seq string `json:"seq"` // 消息的唯一Id
Cmd string `json:"cmd"` // 請求命令字
Data interface{} `json:"data,omitempty"` // 數(shù)據(jù) json
}
// 登錄請求數(shù)據(jù)
type Login struct {
ServiceToken string `json:"serviceToken"` // 驗證用戶是否登錄
AppId uint32 `json:"appId,omitempty"`
UserId string `json:"userId,omitempty"`
}
// 心跳請求數(shù)據(jù)
type HeartBeat struct {
UserId string `json:"userId,omitempty"`
}
- response_model.go
/************************ 響應(yīng)數(shù)據(jù) **************************/
type Head struct {
Seq string `json:"seq"` // 消息的Id
Cmd string `json:"cmd"` // 消息的cmd 動作
Response *Response `json:"response"` // 消息體
}
type Response struct {
Code uint32 `json:"code"`
CodeMsg string `json:"codeMsg"`
Data interface{} `json:"data"` // 數(shù)據(jù) json
}
3.1.7 使用路由的方式處理客戶端的請求數(shù)據(jù)
- 使用路由的方式處理由客戶端發(fā)送過來的請求數(shù)據(jù)
- 以后添加請求類型以后就可以用類是用http相類似的方式(router-controller)去處理
- acc_routers.go
// Websocket 路由
func WebsocketInit() {
websocket.Register("login", websocket.LoginController)
websocket.Register("heartbeat", websocket.HeartbeatController)
}
3.1.8 防止內(nèi)存溢出和Goroutine不回收
- 定時任務(wù)清除超時連接
沒有登錄的連接和登錄的連接6分鐘沒有心跳則斷開連接
- 定時任務(wù)清除超時連接
client_manager.go
// 定時清理超時連接
func ClearTimeoutConnections() {
currentTime := uint64(time.Now().Unix())
for client := range clientManager.Clients {
if client.IsHeartbeatTimeout(currentTime) {
fmt.Println("心跳時間超時 關(guān)閉連接", client.Addr, client.UserId, client.LoginTime, client.HeartbeatTime)
client.Socket.Close()
}
}
}
- 讀寫的Goroutine有一個失敗,則相互關(guān)閉
write()Goroutine寫入數(shù)據(jù)失敗,關(guān)閉c.Socket.Close()連接,會關(guān)閉read()Goroutine
read()Goroutine讀取數(shù)據(jù)失敗,關(guān)閉close(c.Send)連接,會關(guān)閉write()Goroutine
- 讀寫的Goroutine有一個失敗,則相互關(guān)閉
- 客戶端主動關(guān)閉
關(guān)閉讀寫的Goroutine
從ClientManager刪除連接
- 客戶端主動關(guān)閉
- 監(jiān)控用戶連接、Goroutine數(shù)
十個內(nèi)存溢出有九個和Goroutine有關(guān)
添加一個http的接口,可以查看系統(tǒng)的狀態(tài),防止Goroutine不回收
查看系統(tǒng)狀態(tài)
- 監(jiān)控用戶連接、Goroutine數(shù)
- Nginx 配置不活躍的連接釋放時間,防止忘記關(guān)閉的連接
- 使用 pprof 分析性能、耗時
3.2 使用javaScript實現(xiàn)webSocket客戶端
3.2.1 啟動并注冊監(jiān)聽程序
- js 建立連接,并處理連接成功、收到數(shù)據(jù)、斷開連接的事件處理
ws = new WebSocket("ws://127.0.0.1:8089/acc");
ws.onopen = function(evt) {
console.log("Connection open ...");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
data_array = JSON.parse(evt.data);
console.log( data_array);
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
3.2.2 發(fā)送數(shù)據(jù)
- 需要注意:連接建立成功以后才可以發(fā)送數(shù)據(jù)
- 建立連接以后由客戶端向服務(wù)器發(fā)送數(shù)據(jù)示例
登錄:
ws.send('{"seq":"2323","cmd":"login","data":{"userId":"11","appId":101}}');
心跳:
ws.send('{"seq":"2324","cmd":"heartbeat","data":{}}');
ping 查看服務(wù)是否正常:
ws.send('{"seq":"2325","cmd":"ping","data":{}}');
關(guān)閉連接:
ws.close();
4、goWebSocket 項目
4.1 項目說明
本項目是基于webSocket實現(xiàn)的分布式IM系統(tǒng)
客戶端隨機(jī)分配用戶名,所有人進(jìn)入一個聊天室,實現(xiàn)群聊的功能
單臺機(jī)器(24核128G內(nèi)存)支持百萬客戶端連接
支持水平部署,部署的機(jī)器之間可以相互通訊
-
項目架構(gòu)圖
網(wǎng)站架構(gòu)圖
4.2 項目依賴
- 本項目只需要使用 redis 和 golang
- 本項目使用govendor管理依賴,克隆本項目就可以直接使用
# 主要使用到的包
github.com/gin-gonic/gin@v1.4.0
github.com/go-redis/redis
github.com/gorilla/websocket
github.com/spf13/viper
google.golang.org/grpc
github.com/golang/protobuf
4.3 項目啟動
- 克隆項目
git clone git@github.com:link1st/gowebsocket.git
# 或
git clone https://github.com/link1st/gowebsocket.git
- 修改項目配置
cd gowebsocket
cd config
mv app.yaml.example app.yaml
# 修改項目監(jiān)聽端口,redis連接等(默認(rèn)127.0.0.1:3306)
vim app.yaml
# 返回項目目錄,為以后啟動做準(zhǔn)備
cd ..
- 配置文件說明
app:
logFile: log/gin.log # 日志文件位置
httpPort: 8080 # http端口
webSocketPort: 8089 # webSocket端口
rpcPort: 9001 # 分布式部署程序內(nèi)部通訊端口
httpUrl: 127.0.0.1:8080
webSocketUrl: 127.0.0.1:8089
redis:
addr: "localhost:6379"
password: ""
DB: 0
poolSize: 30
minIdleConns: 30
- 啟動項目
go run main.go
- 進(jìn)入IM聊天地址
http://127.0.0.1:8080/home/index - 到這里,就可以體驗到基于webSocket的IM系統(tǒng)
5、webSocket項目Nginx配置
5.1 為什么要配置Nginx
- 使用nginx實現(xiàn)內(nèi)外網(wǎng)分離,對外只暴露Nginx的Ip(一般的互聯(lián)網(wǎng)企業(yè)會在nginx之前加一層LVS做負(fù)載均衡),減少入侵的可能
- 使用Nginx可以利用Nginx的負(fù)載功能,前端再使用的時候只需要連接固定的域名,通過Nginx將流量分發(fā)了到不同的機(jī)器
- 同時我們也可以使用Nginx的不同的負(fù)載策略(輪詢、weight、ip_hash)
5.2 nginx配置
- 使用域名 im.91vh.com 為示例,參考配置
- 一級目錄im.91vh.com/acc 是給webSocket使用,是用nginx stream轉(zhuǎn)發(fā)功能(nginx 1.3.31 開始支持,使用Tengine配置也是相同的),轉(zhuǎn)發(fā)到golang 8089 端口處理
- 其它目錄是給HTTP使用,轉(zhuǎn)發(fā)到golang 8080 端口處理
upstream go-im
{
server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=10s;
keepalive 16;
}
upstream go-acc
{
server 127.0.0.1:8089 weight=1 max_fails=2 fail_timeout=10s;
keepalive 16;
}
server {
listen 80 ;
server_name im.91vh.com;
index index.html index.htm ;
location /acc {
proxy_set_header Host $host;
proxy_pass http://go-acc;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Connection "";
proxy_redirect off;
proxy_intercept_errors on;
client_max_body_size 10m;
}
location /
{
proxy_set_header Host $host;
proxy_pass http://go-im;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_redirect off;
proxy_intercept_errors on;
client_max_body_size 30m;
}
access_log /link/log/nginx/access/im.log;
error_log /link/log/nginx/access/im.error.log;
}
5.3 問題處理
- 運(yùn)行nginx測試命令,查看配置文件是否正確
/link/server/tengine/sbin/nginx -t
- 如果出現(xiàn)錯誤
nginx: [emerg] unknown "connection_upgrade" variable
configuration file /link/server/tengine/conf/nginx.conf test failed
- 處理方法
- 在nginx.com添加
http{
fastcgi_temp_file_write_size 128k;
..... # 需要添加的內(nèi)容
#support websocket
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
.....
gzip on;
}
- 原因:Nginx代理webSocket的時候就會遇到Nginx的設(shè)計問題 End-to-end and Hop-by-hop Headers
6、壓測
6.1 Linux內(nèi)核優(yōu)化
- 設(shè)置文件打開句柄數(shù)
ulimit -n 1000000
- 設(shè)置sockets連接參數(shù)
vim /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0
6.2 壓測準(zhǔn)備
待壓測,如果大家有壓測的結(jié)果歡迎補(bǔ)充
后續(xù)會出專門的教程,從申請機(jī)器、寫壓測用例、內(nèi)核優(yōu)化、得出壓測數(shù)據(jù)
關(guān)于壓測請移步 go-stress-testing,從申請機(jī)器開始,優(yōu)化內(nèi)核,部署項目壓測,解釋壓測的原理
6.3 壓測數(shù)據(jù)
- 項目在實際使用的時候,每個連接約占 24Kb內(nèi)存,一個Goroutine 約占11kb
- 支持百萬連接需要22G內(nèi)存
| 在線用戶數(shù) | cup | 內(nèi)存 | I/O | net.out |
|---|---|---|---|---|
| 1W | ||||
| 10W | ||||
| 100W |
7、如何基于webSocket實現(xiàn)一個分布式Im
7.1 說明
參考本項目源碼
為了方便演示,IM系統(tǒng)和webSocket(acc)系統(tǒng)合并在一個系統(tǒng)中
IM系統(tǒng)接口:
獲取全部在線的用戶,查詢單前服務(wù)的全部用戶+集群中服務(wù)的全部用戶
發(fā)送消息,這里采用的是http接口發(fā)送(微信網(wǎng)頁版發(fā)送消息也是http接口),這里考慮主要是兩點:
1.服務(wù)分離,讓acc系統(tǒng)盡量的簡單一點,不摻雜其它業(yè)務(wù)邏輯
2.發(fā)送消息是走h(yuǎn)ttp接口,不使用webSocket連接,才用收和發(fā)送數(shù)據(jù)分離的方式,可以加快收發(fā)數(shù)據(jù)的效率
7.2 架構(gòu)
- 項目啟動注冊和用戶連接時序圖

- 其它系統(tǒng)(IM、任務(wù))向webSocket(acc)系統(tǒng)連接的用戶發(fā)送消息時序圖

7.3 分布式系統(tǒng)部署
- 用水平部署兩個項目(gowebsocket和gowebsocket1)演示分部署
- 項目之間如何相互通訊:項目啟動以后將項目Ip、rpcPort注冊到redis中,讓其它項目可以發(fā)現(xiàn),需要通訊的時候使用gRpc進(jìn)行通訊
- gowebsocket
# app.yaml 配置文件信息
app:
logFile: log/gin.log
httpPort: 8080
webSocketPort: 8089
rpcPort: 9001
httpUrl: im.91vh.com
webSocketUrl: im.91vh.com
# 在啟動項目
go run main.go
- gowebsocket1
# 將第一個項目拷貝一份
cp -rf gowebsocket gowebsocket1
# app.yaml 修改配置文件
app:
logFile: log/gin.log
httpPort: 8081
webSocketPort: 8090
rpcPort: 9002
httpUrl: im.91vh.com
webSocketUrl: im.91vh.com
# 在啟動第二個項目
go run main.go
- Nginx配置
在之前Nginx配置項中添加第二臺機(jī)器的Ip和端口
upstream go-im
{
server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=10s;
server 127.0.0.1:8081 weight=1 max_fails=2 fail_timeout=10s;
keepalive 16;
}
upstream go-acc
{
server 127.0.0.1:8089 weight=1 max_fails=2 fail_timeout=10s;
server 127.0.0.1:8090 weight=1 max_fails=2 fail_timeout=10s;
keepalive 16;
}
- 配置完成以后重啟Nginx
- 重啟以后請求,驗證是否符合預(yù)期:
查看請求是否落在兩個項目上
實驗兩個用戶分別連接不同的項目(gowebsocket和gowebsocket1)是否也可以相互發(fā)送消息
- 關(guān)于分布式部署
本項目只是演示了這個項目如何分布式部署,以及分布式部署以后模塊如何進(jìn)行相互通訊
完全解決系統(tǒng)沒有單點的故障,還需 Nginx集群、redis cluster等
8、回顧和反思
8.1 在其它系統(tǒng)應(yīng)用
- 本系統(tǒng)設(shè)計的初衷就是:和客戶端保持一個長連接、對外部系統(tǒng)兩個接口(查詢用戶是否在線、給在線的用戶推送消息),實現(xiàn)業(yè)務(wù)的分離
- 只有和業(yè)務(wù)分離可,才可以供多個業(yè)務(wù)使用,而不是每個業(yè)務(wù)都建立一個長連接
8.2 已經(jīng)實現(xiàn)的功能
- gin log日志(請求日志+debug日志)
- 讀取配置文件 完成
- 定時腳本,清理過期未心跳連接 完成
- http接口,獲取登錄、連接數(shù)量 完成
- http接口,發(fā)送push、查詢有多少人在線 完成
- grpc 程序內(nèi)部通訊,發(fā)送消息 完成
- appIds 一個用戶在多個平臺登錄
- 界面,把所有在線的人拉倒一個群里面,發(fā)送消息 完成
-
單聊、群聊 完成 - 實現(xiàn)分布式,水平擴(kuò)張 完成
- 壓測腳本
- 文檔整理
- 文檔目錄、百萬長連接的實現(xiàn)、為什么要實現(xiàn)一個IM、怎么實現(xiàn)一個Im
- 架構(gòu)圖以及擴(kuò)展
IM實現(xiàn)細(xì)節(jié):
- 定義文本消息結(jié)構(gòu) 完成
- html發(fā)送文本消息 完成
- 接口接收文本消息并發(fā)送給全體 完成
- html接收到消息 顯示到界面 完成
- 界面優(yōu)化 需要持續(xù)優(yōu)化
- 有人加入以后廣播全體 完成
- 定義加入聊天室的消息結(jié)構(gòu) 完成
- 引入機(jī)器人 待定
8.2 需要完善、優(yōu)化
- 登錄,使用微信登錄 獲取昵稱、頭像等
- 有賬號系統(tǒng)、資料系統(tǒng)
- 界面優(yōu)化、適配手機(jī)端
- 消息 文本消息(支持表情)、圖片、語音、視頻消息
- 微服務(wù)注冊、發(fā)現(xiàn)、熔斷等
- 添加配置項,單臺機(jī)器最大連接數(shù)量
8.3 總結(jié)
- 雖然實現(xiàn)了一個分布式在聊天的IM,但是有很多細(xì)節(jié)沒有處理(登錄沒有鑒權(quán)、界面還待優(yōu)化等),但是可以通過這個示例可以了解到:通過WebSocket解決很多業(yè)務(wù)上需求
- 本文雖然號稱單臺機(jī)器能有百萬長連接(內(nèi)存上能滿足),但是實際在場景遠(yuǎn)比這個復(fù)雜(cpu有些壓力),當(dāng)然了如果你有這么大的業(yè)務(wù)量可以購買更多的機(jī)器更好的去支撐你的業(yè)務(wù),本程序只是演示如何在實際工作用使用webSocket.
- 參考本文,你可以實現(xiàn)出來符合你需要的程序
9、參考文獻(xiàn)
go-stress-testing 單臺機(jī)器100w連接壓測實戰(zhàn)
github 搜:link1st 查看項目 gowebsocket



