基于websocket单台机器支持百万连接分布式聊天(IM)系统
本文將介紹如何實現一個基于websocket分布式聊天(IM)系統。
使用golang實現websocket通訊,單機可以支持百萬連接,使用gin框架、nginx負載、可以水平部署、程序內部相互通訊、使用grpc通訊協議。
本文內容比較長,如果直接想clone項目體驗直接進入項目體驗 goWebSocket項目下載 ,文本從介紹webSocket是什么開始,然后開始介紹這個項目,以及在Nginx中配置域名做webSocket的轉發,然后介紹如何搭建一個分布式系統。
目錄
- 1、項目說明
- 1.1 goWebSocket
- 1.2 項目體驗
- 2、介紹webSocket
- 2.1 webSocket 是什么
- 2.2 webSocket的兼容性
- 2.3 為什么要用webSocket
- 2.4 webSocket建立過程
- 3、如何實現基于webSocket的長連接系統
- 3.1 使用go實現webSocket服務端
- 3.1.1 啟動端口監聽
- 3.1.2 升級協議
- 3.1.3 客戶端連接的管理
- 3.1.4 注冊客戶端的socket的寫的異步處理程序
- 3.1.5 注冊客戶端的socket的讀的異步處理程序
- 3.1.6 接收客戶端數據并處理
- 3.1.7 使用路由的方式處理客戶端的請求數據
- 3.1.8 防止內存溢出和Goroutine不回收
- 3.2 使用javaScript實現webSocket客戶端
- 3.2.1 啟動并注冊監聽程序
- 3.2.2 發送數據
- 3.1 使用go實現webSocket服務端
- 4、goWebSocket 項目
- 4.1 項目說明
- 4.2 項目依賴
- 4.3 項目啟動
- 5、webSocket項目Nginx配置
- 5.1 為什么要配置Nginx
- 5.2 nginx配置
- 5.3 問題處理
- 6、壓測
- 6.1 Linux內核優化
- 6.2 壓測準備
- 6.3 壓測數據
- 7、如何基于webSocket實現一個分布式Im
- 7.1 說明
- 7.2 架構
- 7.3 分布式系統部署
- 8、回顧和反思
- 8.1 在其它系統應用
- 8.2 需要完善、優化
- 8.3 總結
- 9、參考文獻
1、項目說明
1.1 goWebSocket
本文將介紹如何實現一個基于websocket聊天(IM)分布式系統。
使用golang實現websocket通訊,單機支持百萬連接,使用gin框架、nginx負載、可以水平部署、程序內部相互通訊、使用grpc通訊協議。
- 一般項目中webSocket使用的架構圖
1.2 項目體驗
- 項目地址 gowebsocket
- IM-聊天首頁 或者在新的窗口打開 http://im.91vh.com/home/index
- 打開連接以后進入聊天界面
- 多人群聊可以同時打開兩個窗口
2、介紹webSocket
2.1 webSocket 是什么
WebSocket 協議在2008年誕生,2011年成為國際標準。所有瀏覽器都已經支持了。
它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話,屬于服務器推送技術的一種。
-
HTTP和WebSocket在通訊過程的比較
-
HTTP和webSocket都支持配置證書,ws:// 無證書 wss:// 配置證書的協議標識
2.2 webSocket的兼容性
- 瀏覽器的兼容性,開始支持webSocket的版本
- 服務端的支持
golang、java、php、node.js、python、nginx 都有不錯的支持
- Android和IOS的支持
Android可以使用java-webSocket對webSocket支持
iOS 4.2及更高版本具有WebSockets支持
2.3 為什么要用webSocket
- 從業務上出發,需要一個主動通達客戶端的能力
目前大多數的請求都是使用HTTP,都是由客戶端發起一個請求,有服務端處理,然后返回結果,不可以服務端主動向某一個客戶端主動發送數據
- 大多數場景我們需要主動通知用戶,如:聊天系統、用戶完成任務主動告訴用戶、一些運營活動需要通知到在線的用戶
- 可以獲取用戶在線狀態
- 在沒有長連接的時候通過客戶端主動輪詢獲取數據
- 可以通過一種方式實現,多種不同平臺(H5/Android/IOS)去使用
2.4 webSocket建立過程
- 客戶端先發起升級協議的請求
客戶端發起升級協議的請求,采用標準的HTTP報文格式,在報文中添加頭部信息
Connection: Upgrade表明連接需要升級
Upgrade: websocket需要升級到 websocket協議
Sec-WebSocket-Version: 13 協議的版本為13
Sec-WebSocket-Key: I6qjdEaqYljv3+9x+GrhqA== 這個是base64 encode 的值,是瀏覽器隨機生成的,與服務器響應的 Sec-WebSocket-Accept對應
# 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- 服務器響應升級協議
服務端接收到升級協議的請求,如果服務端支持升級協議會做如下響應
返回:
Status Code: 101 Switching Protocols 表示支持切換協議
# Response Headers Connection: upgrade Date: Fri, 09 Aug 2019 07:36:59 GMT Sec-WebSocket-Accept: mB5emvxi2jwTUhDdlRtADuBax9E= Server: nginx/1.12.1 Upgrade: websocket- 升級協議完成以后,客戶端和服務器就可以相互發送數據
3、如何實現基于webSocket的長連接系統
3.1 使用go實現webSocket服務端
3.1.1 啟動端口監聽
- websocket需要監聽端口,所以需要在golang 成功的 main 函數中用協程的方式去啟動程序
- main.go 實現啟動
- init_acc.go 啟動程序
3.1.2 升級協議
- 客戶端是通過http請求發送到服務端,我們需要對http協議進行升級為websocket協議
- 對http請求協議進行升級 golang 庫gorilla/websocket 已經做得很好了,我們直接使用就可以了
- 在實際使用的時候,建議每個連接使用兩個協程處理客戶端請求數據和向客戶端發送數據,雖然開啟協程會占用一些內存,但是讀取分離,減少收發數據堵塞的可能
- init_acc.go
3.1.3 客戶端連接的管理
- 當前程序有多少用戶連接,還需要對用戶廣播的需要,這里我們就需要一個管理者(clientManager),處理這些事件:
- 記錄全部的連接、登錄用戶的可以通過 appId+uuid 查到用戶連接
- 使用map存儲,就涉及到多協程并發讀寫的問題,所以需要加讀寫鎖
- 定義四個channel ,分別處理客戶端建立連接、用戶登錄、斷開連接、全員廣播事件
3.1.4 注冊客戶端的socket的寫的異步處理程序
- 防止發生程序崩潰,所以需要捕獲異常
- 為了顯示異常崩潰位置這里使用string(debug.Stack())打印調用堆棧信息
- 如果寫入數據失敗了,可能連接有問題,就關閉連接
- client.go
3.1.5 注冊客戶端的socket的讀的異步處理程序
- 循環讀取客戶端發送的數據并處理
- 如果讀取數據失敗了,關閉channel
- client.go
3.1.6 接收客戶端數據并處理
-
約定發送和接收請求數據格式,為了js處理方便,采用了json的數據格式發送和接收數據(人類可以閱讀的格式在工作開發中使用是比較方便的)
-
登錄發送數據示例:
- 登錄響應數據示例:
-
websocket是雙向的數據通訊,可以連續發送,如果發送的數據需要服務端回復,就需要一個seq來確定服務端的響應是回復哪一次的請求數據
-
cmd 是用來確定動作,websocket沒有類似于http的url,所以規定 cmd 是什么動作
-
目前的動作有:login/heartbeat 用來發送登錄請求和連接保活(長時間沒有數據發送的長連接容易被瀏覽器、移動中間商、nginx、服務端程序斷開)
-
為什么需要AppId,UserId是表示用戶的唯一字段,設計的時候為了做成通用性,設計AppId用來表示用戶在哪個平臺登錄的(web、app、ios等),方便后續擴展
-
request_model.go 約定的請求數據格式
- response_model.go
3.1.7 使用路由的方式處理客戶端的請求數據
- 使用路由的方式處理由客戶端發送過來的請求數據
- 以后添加請求類型以后就可以用類是用http相類似的方式(router-controller)去處理
- acc_routers.go
3.1.8 防止內存溢出和Goroutine不回收
- 定時任務清除超時連接
沒有登錄的連接和登錄的連接6分鐘沒有心跳則斷開連接
client_manager.go
// 定時清理超時連接 func ClearTimeoutConnections() {currentTime := uint64(time.Now().Unix())for client := range clientManager.Clients {if client.IsHeartbeatTimeout(currentTime) {fmt.Println("心跳時間超時 關閉連接", client.Addr, client.UserId, client.LoginTime, client.HeartbeatTime)client.Socket.Close()}} }- 讀寫的Goroutine有一個失敗,則相互關閉
write()Goroutine寫入數據失敗,關閉c.Socket.Close()連接,會關閉read()Goroutine
read()Goroutine讀取數據失敗,關閉close(c.Send)連接,會關閉write()Goroutine - 客戶端主動關閉
關閉讀寫的Goroutine
從ClientManager刪除連接 - 監控用戶連接、Goroutine數
十個內存溢出有九個和Goroutine有關
添加一個http的接口,可以查看系統的狀態,防止Goroutine不回收
查看系統狀態 - Nginx 配置不活躍的連接釋放時間,防止忘記關閉的連接
- 使用 pprof 分析性能、耗時
3.2 使用javaScript實現webSocket客戶端
3.2.1 啟動并注冊監聽程序
- js 建立連接,并處理連接成功、收到數據、斷開連接的事件處理
3.2.2 發送數據
- 需要注意:連接建立成功以后才可以發送數據
- 建立連接以后由客戶端向服務器發送數據示例
4、goWebSocket 項目
4.1 項目說明
-
本項目是基于webSocket實現的分布式IM系統
-
客戶端隨機分配用戶名,所有人進入一個聊天室,實現群聊的功能
-
單臺機器(24核128G內存)支持百萬客戶端連接
-
支持水平部署,部署的機器之間可以相互通訊
-
項目架構圖
4.2 項目依賴
- 本項目只需要使用 redis 和 golang
- 本項目使用govendor管理依賴,克隆本項目就可以直接使用
4.3 項目啟動
- 克隆項目
- 修改項目配置
- 配置文件說明
- 啟動項目
- 進入IM聊天地址
http://127.0.0.1:8080/home/index - 到這里,就可以體驗到基于webSocket的IM系統
5、webSocket項目Nginx配置
5.1 為什么要配置Nginx
- 使用nginx實現內外網分離,對外只暴露Nginx的Ip(一般的互聯網企業會在nginx之前加一層LVS做負載均衡),減少入侵的可能
- 使用Nginx可以利用Nginx的負載功能,前端再使用的時候只需要連接固定的域名,通過Nginx將流量分發了到不同的機器
- 同時我們也可以使用Nginx的不同的負載策略(輪詢、weight、ip_hash)
5.2 nginx配置
- 使用域名 im.91vh.com 為示例,參考配置
- 一級目錄im.91vh.com/acc 是給webSocket使用,是用nginx stream轉發功能(nginx 1.3.31 開始支持,使用Tengine配置也是相同的),轉發到golang 8089 端口處理
- 其它目錄是給HTTP使用,轉發到golang 8080 端口處理
5.3 問題處理
- 運行nginx測試命令,查看配置文件是否正確
- 如果出現錯誤
- 處理方法
- 在nginx.com添加
- 原因:Nginx代理webSocket的時候就會遇到Nginx的設計問題 End-to-end and Hop-by-hop Headers
6、壓測
6.1 Linux內核優化
- 設置文件打開句柄數
- 設置sockets連接參數
6.2 壓測準備
-
待壓測,如果大家有壓測的結果歡迎補充
-
后續會出專門的教程,從申請機器、寫壓測用例、內核優化、得出壓測數據
-
關于壓測請移步 go-stress-testing,從申請機器開始,優化內核,部署項目壓測,解釋壓測的原理
6.3 壓測數據
- 項目在實際使用的時候,每個連接約占 24Kb內存,一個Goroutine 約占11kb
- 支持百萬連接需要22G內存
| 1W | ||||
| 10W | ||||
| 100W |
7、如何基于webSocket實現一個分布式Im
7.1 說明
-
參考本項目源碼
-
gowebsocket v1.0.0 單機版Im系統
-
gowebsocket v2.0.0 分布式Im系統
-
為了方便演示,IM系統和webSocket(acc)系統合并在一個系統中
-
IM系統接口:
獲取全部在線的用戶,查詢單前服務的全部用戶+集群中服務的全部用戶
發送消息,這里采用的是http接口發送(微信網頁版發送消息也是http接口),這里考慮主要是兩點:
1.服務分離,讓acc系統盡量的簡單一點,不摻雜其它業務邏輯
2.發送消息是走http接口,不使用webSocket連接,才用收和發送數據分離的方式,可以加快收發數據的效率
7.2 架構
- 項目啟動注冊和用戶連接時序圖
- 其它系統(IM、任務)向webSocket(acc)系統連接的用戶發送消息時序圖
7.3 分布式系統部署
- 用水平部署兩個項目(gowebsocket和gowebsocket1)演示分部署
- 項目之間如何相互通訊:項目啟動以后將項目Ip、rpcPort注冊到redis中,讓其它項目可以發現,需要通訊的時候使用gRpc進行通訊
- gowebsocket
- gowebsocket1
- Nginx配置
在之前Nginx配置項中添加第二臺機器的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
- 重啟以后請求,驗證是否符合預期:
查看請求是否落在兩個項目上
實驗兩個用戶分別連接不同的項目(gowebsocket和gowebsocket1)是否也可以相互發送消息
- 關于分布式部署
本項目只是演示了這個項目如何分布式部署,以及分布式部署以后模塊如何進行相互通訊
完全解決系統沒有單點的故障,還需 Nginx集群、redis cluster等
8、回顧和反思
8.1 在其它系統應用
- 本系統設計的初衷就是:和客戶端保持一個長連接、對外部系統兩個接口(查詢用戶是否在線、給在線的用戶推送消息),實現業務的分離
- 只有和業務分離可,才可以供多個業務使用,而不是每個業務都建立一個長連接
8.2 已經實現的功能
- gin log日志(請求日志+debug日志)
- 讀取配置文件 完成
- 定時腳本,清理過期未心跳連接 完成
- http接口,獲取登錄、連接數量 完成
- http接口,發送push、查詢有多少人在線 完成
- grpc 程序內部通訊,發送消息 完成
- appIds 一個用戶在多個平臺登錄
- 界面,把所有在線的人拉倒一個群里面,發送消息 完成
- 單聊、群聊 完成
- 實現分布式,水平擴張 完成
- 壓測腳本
- 文檔整理
- 文檔目錄、百萬長連接的實現、為什么要實現一個IM、怎么實現一個Im
- 架構圖以及擴展
IM實現細節:
- 定義文本消息結構 完成
- html發送文本消息 完成
- 接口接收文本消息并發送給全體 完成
- html接收到消息 顯示到界面 完成
- 界面優化 需要持續優化
- 有人加入以后廣播全體 完成
- 定義加入聊天室的消息結構 完成
- 引入機器人 待定
8.2 需要完善、優化
- 登錄,使用微信登錄 獲取昵稱、頭像等
- 有賬號系統、資料系統
- 界面優化、適配手機端
- 消息 文本消息(支持表情)、圖片、語音、視頻消息
- 微服務注冊、發現、熔斷等
- 添加配置項,單臺機器最大連接數量
8.3 總結
- 雖然實現了一個分布式在聊天的IM,但是有很多細節沒有處理(登錄沒有鑒權、界面還待優化等),但是可以通過這個示例可以了解到:通過WebSocket解決很多業務上需求
- 本文雖然號稱單臺機器能有百萬長連接(內存上能滿足),但是實際在場景遠比這個復雜(cpu有些壓力),當然了如果你有這么大的業務量可以購買更多的機器更好的去支撐你的業務,本程序只是演示如何在實際工作用使用webSocket.
- 參考本文,你可以實現出來符合你需要的程序
9、參考文獻
維基百科 WebSocket
阮一峰 WebSocket教程
WebSocket協議:5分鐘從入門到精通
go-stress-testing 單臺機器100w連接壓測實戰
github 搜:link1st 查看項目 gowebsocket
https://github.com/link1st/gowebsocket
總結
以上是生活随笔為你收集整理的基于websocket单台机器支持百万连接分布式聊天(IM)系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 二进制的学习总结
- 下一篇: Spring AOP / AspectJ