基于websocket單臺(tái)機(jī)器支持百萬(wàn)連接分布式聊天(IM)系統(tǒng)

基于websocket單臺(tái)機(jī)器支持百萬(wàn)連接分布式聊天(IM)系統(tǒng)

本文將介紹如何實(shí)現(xiàn)一個(gè)基于websocket分布式聊天(IM)系統(tǒng)秒梳。

使用golang實(shí)現(xiàn)websocket通訊太援,單機(jī)可以支持百萬(wàn)連接艳馒,使用gin框架舌剂、nginx負(fù)載、可以水平部署舅逸、程序內(nèi)部相互通訊蹬碧、使用grpc通訊協(xié)議。

本文內(nèi)容比較長(zhǎng)避凝,如果直接想clone項(xiàng)目體驗(yàn)直接進(jìn)入項(xiàng)目體驗(yàn) goWebSocket項(xiàng)目下載 ,文本從介紹webSocket是什么開(kāi)始舞萄,然后開(kāi)始介紹這個(gè)項(xiàng)目,以及在Nginx中配置域名做webSocket的轉(zhuǎn)發(fā)管削,然后介紹如何搭建一個(gè)分布式系統(tǒng)倒脓。

目錄

  • 1、項(xiàng)目說(shuō)明
    • 1.1 goWebSocket
    • 1.2 項(xiàng)目體驗(yàn)
  • 2佩谣、介紹webSocket
    • 2.1 webSocket 是什么
    • 2.2 webSocket的兼容性
    • 2.3 為什么要用webSocket
    • 2.4 webSocket建立過(guò)程
  • 3把还、如何實(shí)現(xiàn)基于webSocket的長(zhǎng)連接系統(tǒng)
    • 3.1 使用go實(shí)現(xiàn)webSocket服務(wù)端
      • 3.1.1 啟動(dòng)端口監(jiān)聽(tīng)
      • 3.1.2 升級(jí)協(xié)議
      • 3.1.3 客戶(hù)端連接的管理
      • 3.1.4 注冊(cè)客戶(hù)端的socket的寫(xiě)的異步處理程序
      • 3.1.5 注冊(cè)客戶(hù)端的socket的讀的異步處理程序
      • 3.1.6 接收客戶(hù)端數(shù)據(jù)并處理
      • 3.1.7 使用路由的方式處理客戶(hù)端的請(qǐng)求數(shù)據(jù)
      • 3.1.8 防止內(nèi)存溢出和Goroutine不回收
    • 3.2 使用javaScript實(shí)現(xiàn)webSocket客戶(hù)端
      • 3.2.1 啟動(dòng)并注冊(cè)監(jiān)聽(tīng)程序
      • 3.2.2 發(fā)送數(shù)據(jù)
  • 4实蓬、goWebSocket 項(xiàng)目
    • 4.1 項(xiàng)目說(shuō)明
    • 4.2 項(xiàng)目依賴(lài)
    • 4.3 項(xiàng)目啟動(dòng)
  • 5茸俭、webSocket項(xiàng)目Nginx配置
    • 5.1 為什么要配置Nginx
    • 5.2 nginx配置
    • 5.3 問(wèn)題處理
  • 6、壓測(cè)
    • 6.1 Linux內(nèi)核優(yōu)化
    • 6.2 壓測(cè)準(zhǔn)備
    • 6.3 壓測(cè)數(shù)據(jù)
  • 7安皱、如何基于webSocket實(shí)現(xiàn)一個(gè)分布式Im
    • 7.1 說(shuō)明
    • 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腾窝、項(xiàng)目說(shuō)明

1.1 goWebSocket

本文將介紹如何實(shí)現(xiàn)一個(gè)基于websocket聊天(IM)分布式系統(tǒng)。

使用golang實(shí)現(xiàn)websocket通訊居砖,單機(jī)支持百萬(wàn)連接虹脯,使用gin框架、nginx負(fù)載奏候、可以水平部署循集、程序內(nèi)部相互通訊、使用grpc通訊協(xié)議蔗草。

  • 一般項(xiàng)目中webSocket使用的架構(gòu)圖


    網(wǎng)站架構(gòu)圖

1.2 項(xiàng)目體驗(yàn)

2咒彤、介紹webSocket

2.1 webSocket 是什么

WebSocket 協(xié)議在2008年誕生疆柔,2011年成為國(guó)際標(biāo)準(zhǔn)。所有瀏覽器都已經(jīng)支持了镶柱。

它的最大特點(diǎn)就是旷档,服務(wù)器可以主動(dòng)向客戶(hù)端推送信息,客戶(hù)端也可以主動(dòng)向服務(wù)器發(fā)送信息歇拆,是真正的雙向平等對(duì)話(huà)鞋屈,屬于服務(wù)器推送技術(shù)的一種。

  • HTTP和WebSocket在通訊過(guò)程的比較


    HTTP協(xié)議和WebSocket比較
  • HTTP和webSocket都支持配置證書(shū)故觅,ws:// 無(wú)證書(shū) wss:// 配置證書(shū)的協(xié)議標(biāo)識(shí)

    HTTP協(xié)議和WebSocket比較

2.2 webSocket的兼容性

  • 瀏覽器的兼容性谐区,開(kāi)始支持webSocket的版本
瀏覽器開(kāi)始支持webSocket的版本
  • 服務(wù)端的支持

golang、java逻卖、php宋列、node.js、python评也、nginx 都有不錯(cuò)的支持

  • Android和IOS的支持

Android可以使用java-webSocket對(duì)webSocket支持

iOS 4.2及更高版本具有WebSockets支持

2.3 為什么要用webSocket

    1. 從業(yè)務(wù)上出發(fā)炼杖,需要一個(gè)主動(dòng)通達(dá)客戶(hù)端的能力

目前大多數(shù)的請(qǐng)求都是使用HTTP,都是由客戶(hù)端發(fā)起一個(gè)請(qǐng)求盗迟,有服務(wù)端處理坤邪,然后返回結(jié)果,不可以服務(wù)端主動(dòng)向某一個(gè)客戶(hù)端主動(dòng)發(fā)送數(shù)據(jù)

服務(wù)端處理一個(gè)請(qǐng)求
    1. 大多數(shù)場(chǎng)景我們需要主動(dòng)通知用戶(hù)罚缕,如:聊天系統(tǒng)艇纺、用戶(hù)完成任務(wù)主動(dòng)告訴用戶(hù)、一些運(yùn)營(yíng)活動(dòng)需要通知到在線(xiàn)的用戶(hù)
    1. 可以獲取用戶(hù)在線(xiàn)狀態(tài)
    1. 在沒(méi)有長(zhǎng)連接的時(shí)候通過(guò)客戶(hù)端主動(dòng)輪詢(xún)獲取數(shù)據(jù)
    1. 可以通過(guò)一種方式實(shí)現(xiàn)邮弹,多種不同平臺(tái)(H5/Android/IOS)去使用

2.4 webSocket建立過(guò)程

    1. 客戶(hù)端先發(fā)起升級(jí)協(xié)議的請(qǐng)求

客戶(hù)端發(fā)起升級(jí)協(xié)議的請(qǐng)求黔衡,采用標(biāo)準(zhǔn)的HTTP報(bào)文格式,在報(bào)文中添加頭部信息

Connection: Upgrade表明連接需要升級(jí)

Upgrade: websocket需要升級(jí)到 websocket協(xié)議

Sec-WebSocket-Version: 13 協(xié)議的版本為13

Sec-WebSocket-Key: I6qjdEaqYljv3+9x+GrhqA== 這個(gè)是base64 encode 的值腌乡,是瀏覽器隨機(jī)生成的盟劫,與服務(wù)器響應(yīng)的 Sec-WebSocket-Accept對(duì)應(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
瀏覽器 Network
    1. 服務(wù)器響應(yīng)升級(jí)協(xié)議

服務(wù)端接收到升級(jí)協(xié)議的請(qǐng)求,如果服務(wù)端支持升級(jí)協(xié)議會(huì)做如下響應(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
    1. 升級(jí)協(xié)議完成以后与纽,客戶(hù)端和服務(wù)器就可以相互發(fā)送數(shù)據(jù)
websocket接收和發(fā)送數(shù)據(jù)

3侣签、如何實(shí)現(xiàn)基于webSocket的長(zhǎng)連接系統(tǒng)

3.1 使用go實(shí)現(xiàn)webSocket服務(wù)端

3.1.1 啟動(dòng)端口監(jiān)聽(tīng)

  • websocket需要監(jiān)聽(tīng)端口,所以需要在golang 成功的 main 函數(shù)中用協(xié)程的方式去啟動(dòng)程序
  • main.go 實(shí)現(xiàn)啟動(dòng)
go websocket.StartWebSocket()
  • init_acc.go 啟動(dòng)程序
// 啟動(dòng)程序
func StartWebSocket() {
    http.HandleFunc("/acc", wsPage)
    http.ListenAndServe(":8089", nil)
}

3.1.2 升級(jí)協(xié)議

  • 客戶(hù)端是通過(guò)http請(qǐng)求發(fā)送到服務(wù)端急迂,我們需要對(duì)http協(xié)議進(jìn)行升級(jí)為websocket協(xié)議
  • 對(duì)http請(qǐng)求協(xié)議進(jìn)行升級(jí) golang 庫(kù)gorilla/websocket 已經(jīng)做得很好了影所,我們直接使用就可以了
  • 在實(shí)際使用的時(shí)候,建議每個(gè)連接使用兩個(gè)協(xié)程處理客戶(hù)端請(qǐng)求數(shù)據(jù)和向客戶(hù)端發(fā)送數(shù)據(jù)僚碎,雖然開(kāi)啟協(xié)程會(huì)占用一些內(nèi)存猴娩,但是讀取分離,減少收發(fā)數(shù)據(jù)堵塞的可能
  • init_acc.go
func wsPage(w http.ResponseWriter, req *http.Request) {

    // 升級(jí)協(xié)議
    conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
        fmt.Println("升級(jí)協(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()

    // 用戶(hù)連接事件
    clientManager.Register <- client
}

3.1.3 客戶(hù)端連接的管理

  • 當(dāng)前程序有多少用戶(hù)連接,還需要對(duì)用戶(hù)廣播的需要胀溺,這里我們就需要一個(gè)管理者(clientManager)裂七,處理這些事件:
  • 記錄全部的連接、登錄用戶(hù)的可以通過(guò) appId+uuid 查到用戶(hù)連接
  • 使用map存儲(chǔ)仓坞,就涉及到多協(xié)程并發(fā)讀寫(xiě)的問(wèn)題背零,所以需要加讀寫(xiě)鎖
  • 定義四個(gè)channel ,分別處理客戶(hù)端建立連接无埃、用戶(hù)登錄徙瓶、斷開(kāi)連接、全員廣播事件
// 連接管理
type ClientManager struct {
    Clients     map[*Client]bool   // 全部的連接
    ClientsLock sync.RWMutex       // 讀寫(xiě)鎖
    Users       map[string]*Client // 登錄的用戶(hù) // appId+uuid
    UserLock    sync.RWMutex       // 讀寫(xiě)鎖
    Register    chan *Client       // 連接連接處理
    Login       chan *login        // 用戶(hù)登錄處理
    Unregister  chan *Client       // 斷開(kāi)連接處理程序
    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 注冊(cè)客戶(hù)端的socket的寫(xiě)的異步處理程序

  • 防止發(fā)生程序崩潰嫉称,所以需要捕獲異常
  • 為了顯示異常崩潰位置這里使用string(debug.Stack())打印調(diào)用堆棧信息
  • 如果寫(xiě)入數(shù)據(jù)失敗了侦镇,可能連接有問(wèn)題,就關(guān)閉連接
  • client.go
// 向客戶(hù)端寫(xiě)數(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ù)錯(cuò)誤 關(guān)閉連接
                fmt.Println("Client發(fā)送數(shù)據(jù) 關(guān)閉連接", c.Addr, "ok", ok)

                return
            }

            c.Socket.WriteMessage(websocket.TextMessage, message)
        }
    }
}

3.1.5 注冊(cè)客戶(hù)端的socket的讀的異步處理程序

  • 循環(huán)讀取客戶(hù)端發(fā)送的數(shù)據(jù)并處理
  • 如果讀取數(shù)據(jù)失敗了织阅,關(guān)閉channel
  • client.go
// 讀取客戶(hù)端數(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("讀取客戶(hù)端數(shù)據(jù) 關(guān)閉send", c)
        close(c.Send)
    }()

    for {
        _, message, err := c.Socket.ReadMessage()
        if err != nil {
            fmt.Println("讀取客戶(hù)端數(shù)據(jù) 錯(cuò)誤", c.Addr, err)

            return
        }

        // 處理程序
        fmt.Println("讀取客戶(hù)端數(shù)據(jù) 處理:", string(message))
        ProcessData(c, message)
    }
}

3.1.6 接收客戶(hù)端數(shù)據(jù)并處理

  • 約定發(fā)送和接收請(qǐng)求數(shù)據(jù)格式壳繁,為了js處理方便,采用了json的數(shù)據(jù)格式發(fā)送和接收數(shù)據(jù)(人類(lèi)可以閱讀的格式在工作開(kāi)發(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ù),就需要一個(gè)seq來(lái)確定服務(wù)端的響應(yīng)是回復(fù)哪一次的請(qǐng)求數(shù)據(jù)

  • cmd 是用來(lái)確定動(dòng)作润樱,websocket沒(méi)有類(lèi)似于http的url,所以規(guī)定 cmd 是什么動(dòng)作

  • 目前的動(dòng)作有:login/heartbeat 用來(lái)發(fā)送登錄請(qǐng)求和連接痹ィ活(長(zhǎng)時(shí)間沒(méi)有數(shù)據(jù)發(fā)送的長(zhǎng)連接容易被瀏覽器、移動(dòng)中間商壹若、nginx嗅钻、服務(wù)端程序斷開(kāi))

  • 為什么需要AppId,UserId是表示用戶(hù)的唯一字段,設(shè)計(jì)的時(shí)候?yàn)榱俗龀赏ㄓ眯缘暾梗O(shè)計(jì)AppId用來(lái)表示用戶(hù)在哪個(gè)平臺(tái)登錄的(web养篓、app、ios等)壁查,方便后續(xù)擴(kuò)展

  • request_model.go 約定的請(qǐng)求數(shù)據(jù)格式

/************************  請(qǐng)求數(shù)據(jù)  **************************/
// 通用請(qǐng)求數(shù)據(jù)格式
type Request struct {
    Seq  string      `json:"seq"`            // 消息的唯一Id
    Cmd  string      `json:"cmd"`            // 請(qǐng)求命令字
    Data interface{} `json:"data,omitempty"` // 數(shù)據(jù) json
}

// 登錄請(qǐng)求數(shù)據(jù)
type Login struct {
    ServiceToken string `json:"serviceToken"` // 驗(yàn)證用戶(hù)是否登錄
    AppId        uint32 `json:"appId,omitempty"`
    UserId       string `json:"userId,omitempty"`
}

// 心跳請(qǐng)求數(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 動(dòng)作
    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 使用路由的方式處理客戶(hù)端的請(qǐng)求數(shù)據(jù)

  • 使用路由的方式處理由客戶(hù)端發(fā)送過(guò)來(lái)的請(qǐng)求數(shù)據(jù)
  • 以后添加請(qǐng)求類(lèi)型以后就可以用類(lèi)是用http相類(lèi)似的方式(router-controller)去處理
  • acc_routers.go
// Websocket 路由
func WebsocketInit() {
    websocket.Register("login", websocket.LoginController)
    websocket.Register("heartbeat", websocket.HeartbeatController)
}

3.1.8 防止內(nèi)存溢出和Goroutine不回收

    1. 定時(shí)任務(wù)清除超時(shí)連接
      沒(méi)有登錄的連接和登錄的連接6分鐘沒(méi)有心跳則斷開(kāi)連接

client_manager.go

// 定時(shí)清理超時(shí)連接
func ClearTimeoutConnections() {
    currentTime := uint64(time.Now().Unix())

    for client := range clientManager.Clients {
        if client.IsHeartbeatTimeout(currentTime) {
            fmt.Println("心跳時(shí)間超時(shí) 關(guān)閉連接", client.Addr, client.UserId, client.LoginTime, client.HeartbeatTime)

            client.Socket.Close()
        }
    }
}
    1. 讀寫(xiě)的Goroutine有一個(gè)失敗觉至,則相互關(guān)閉
      write()Goroutine寫(xiě)入數(shù)據(jù)失敗,關(guān)閉c.Socket.Close()連接睡腿,會(huì)關(guān)閉read()Goroutine
      read()Goroutine讀取數(shù)據(jù)失敗,關(guān)閉close(c.Send)連接峻贮,會(huì)關(guān)閉write()Goroutine
    1. 客戶(hù)端主動(dòng)關(guān)閉
      關(guān)閉讀寫(xiě)的Goroutine
      ClientManager刪除連接
    1. 監(jiān)控用戶(hù)連接席怪、Goroutine數(shù)
      十個(gè)內(nèi)存溢出有九個(gè)和Goroutine有關(guān)
      添加一個(gè)http的接口,可以查看系統(tǒng)的狀態(tài)纤控,防止Goroutine不回收
      查看系統(tǒng)狀態(tài)
    1. Nginx 配置不活躍的連接釋放時(shí)間挂捻,防止忘記關(guān)閉的連接
    1. 使用 pprof 分析性能莫湘、耗時(shí)

3.2 使用javaScript實(shí)現(xiàn)webSocket客戶(hù)端

3.2.1 啟動(dòng)并注冊(cè)監(jiān)聽(tīng)程序

  • js 建立連接揍诽,并處理連接成功滨溉、收到數(shù)據(jù)重贺、斷開(kāi)連接的事件處理
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ù)
  • 建立連接以后由客戶(hù)端向服務(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 項(xiàng)目

4.1 項(xiàng)目說(shuō)明

  • 本項(xiàng)目是基于webSocket實(shí)現(xiàn)的分布式IM系統(tǒng)

  • 客戶(hù)端隨機(jī)分配用戶(hù)名声怔,所有人進(jìn)入一個(gè)聊天室态贤,實(shí)現(xiàn)群聊的功能

  • 單臺(tái)機(jī)器(24核128G內(nèi)存)支持百萬(wàn)客戶(hù)端連接

  • 支持水平部署,部署的機(jī)器之間可以相互通訊

  • 項(xiàng)目架構(gòu)圖


    網(wǎng)站架構(gòu)圖

4.2 項(xiàng)目依賴(lài)

  • 本項(xiàng)目只需要使用 redis 和 golang
  • 本項(xiàng)目使用govendor管理依賴(lài)醋火,克隆本項(xiàng)目就可以直接使用
# 主要使用到的包
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 項(xiàng)目啟動(dòng)

  • 克隆項(xiàng)目
git clone git@github.com:link1st/gowebsocket.git
# 或
git clone https://github.com/link1st/gowebsocket.git
  • 修改項(xiàng)目配置
cd gowebsocket
cd config
mv app.yaml.example app.yaml
# 修改項(xiàng)目監(jiān)聽(tīng)端口悠汽,redis連接等(默認(rèn)127.0.0.1:3306)
vim app.yaml
# 返回項(xiàng)目目錄,為以后啟動(dòng)做準(zhǔn)備
cd ..
  • 配置文件說(shuō)明
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
  • 啟動(dòng)項(xiàng)目
go run main.go

5柿冲、webSocket項(xiàng)目Nginx配置

5.1 為什么要配置Nginx

  • 使用nginx實(shí)現(xiàn)內(nèi)外網(wǎng)分離,對(duì)外只暴露Nginx的Ip(一般的互聯(lián)網(wǎng)企業(yè)會(huì)在nginx之前加一層LVS做負(fù)載均衡)兆旬,減少入侵的可能
  • 使用Nginx可以利用Nginx的負(fù)載功能假抄,前端再使用的時(shí)候只需要連接固定的域名,通過(guò)Nginx將流量分發(fā)了到不同的機(jī)器
  • 同時(shí)我們也可以使用Nginx的不同的負(fù)載策略(輪詢(xún)丽猬、weight慨亲、ip_hash)

5.2 nginx配置

  • 使用域名 im.91vh.com 為示例,參考配置
  • 一級(jí)目錄im.91vh.com/acc 是給webSocket使用宝鼓,是用nginx stream轉(zhuǎn)發(fā)功能(nginx 1.3.31 開(kāi)始支持刑棵,使用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 問(wèn)題處理

  • 運(yùn)行nginx測(cè)試命令蛉签,查看配置文件是否正確
/link/server/tengine/sbin/nginx -t

  • 如果出現(xiàn)錯(cuò)誤
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的時(shí)候就會(huì)遇到Nginx的設(shè)計(jì)問(wèn)題 End-to-end and Hop-by-hop Headers

6、壓測(cè)

6.1 Linux內(nèi)核優(yōu)化

  • 設(shè)置文件打開(kāi)句柄數(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 壓測(cè)準(zhǔn)備

  • 待壓測(cè)沥寥,如果大家有壓測(cè)的結(jié)果歡迎補(bǔ)充

  • 后續(xù)會(huì)出專(zhuān)門(mén)的教程,從申請(qǐng)機(jī)器碍舍、寫(xiě)壓測(cè)用例、內(nèi)核優(yōu)化邑雅、得出壓測(cè)數(shù)據(jù)

  • 關(guān)于壓測(cè)請(qǐng)移步 go-stress-testing片橡,從申請(qǐng)機(jī)器開(kāi)始,優(yōu)化內(nèi)核淮野,部署項(xiàng)目壓測(cè)捧书,解釋壓測(cè)的原理

6.3 壓測(cè)數(shù)據(jù)

  • 項(xiàng)目在實(shí)際使用的時(shí)候,每個(gè)連接約占 24Kb內(nèi)存骤星,一個(gè)Goroutine 約占11kb
  • 支持百萬(wàn)連接需要22G內(nèi)存
在線(xiàn)用戶(hù)數(shù) cup 內(nèi)存 I/O net.out
1W
10W
100W

7经瓷、如何基于webSocket實(shí)現(xiàn)一個(gè)分布式Im

7.1 說(shuō)明

  • 參考本項(xiàng)目源碼

  • gowebsocket v1.0.0 單機(jī)版Im系統(tǒng)

  • gowebsocket v2.0.0 分布式Im系統(tǒng)

  • 為了方便演示,IM系統(tǒng)和webSocket(acc)系統(tǒng)合并在一個(gè)系統(tǒng)中

  • IM系統(tǒng)接口:
    獲取全部在線(xiàn)的用戶(hù)洞难,查詢(xún)單前服務(wù)的全部用戶(hù)+集群中服務(wù)的全部用戶(hù)
    發(fā)送消息舆吮,這里采用的是http接口發(fā)送(微信網(wǎng)頁(yè)版發(fā)送消息也是http接口),這里考慮主要是兩點(diǎn):
    1.服務(wù)分離,讓acc系統(tǒng)盡量的簡(jiǎn)單一點(diǎn)色冀,不摻雜其它業(yè)務(wù)邏輯
    2.發(fā)送消息是走h(yuǎn)ttp接口潭袱,不使用webSocket連接,才用收和發(fā)送數(shù)據(jù)分離的方式锋恬,可以加快收發(fā)數(shù)據(jù)的效率

7.2 架構(gòu)

  • 項(xiàng)目啟動(dòng)注冊(cè)和用戶(hù)連接時(shí)序圖
用戶(hù)連接時(shí)序圖
  • 其它系統(tǒng)(IM屯换、任務(wù))向webSocket(acc)系統(tǒng)連接的用戶(hù)發(fā)送消息時(shí)序圖
分布是系統(tǒng)隨機(jī)給用戶(hù)發(fā)送消息

7.3 分布式系統(tǒng)部署

  • 用水平部署兩個(gè)項(xiàng)目(gowebsocket和gowebsocket1)演示分部署
  • 項(xiàng)目之間如何相互通訊:項(xiàng)目啟動(dòng)以后將項(xiàng)目Ip、rpcPort注冊(cè)到redis中伶氢,讓其它項(xiàng)目可以發(fā)現(xiàn)趟径,需要通訊的時(shí)候使用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

# 在啟動(dòng)項(xiàng)目
go run main.go 

  • gowebsocket1
# 將第一個(gè)項(xiàng)目拷貝一份
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

# 在啟動(dòng)第二個(gè)項(xiàng)目
go run main.go 
  • Nginx配置

在之前Nginx配置項(xiàng)中添加第二臺(tái)機(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
  • 重啟以后請(qǐng)求,驗(yàn)證是否符合預(yù)期:

查看請(qǐng)求是否落在兩個(gè)項(xiàng)目上
實(shí)驗(yàn)兩個(gè)用戶(hù)分別連接不同的項(xiàng)目(gowebsocket和gowebsocket1)是否也可以相互發(fā)送消息

  • 關(guān)于分布式部署

本項(xiàng)目只是演示了這個(gè)項(xiàng)目如何分布式部署癣防,以及分布式部署以后模塊如何進(jìn)行相互通訊
完全解決系統(tǒng)沒(méi)有單點(diǎn)的故障蜗巧,還需 Nginx集群、redis cluster等

8蕾盯、回顧和反思

8.1 在其它系統(tǒng)應(yīng)用

  • 本系統(tǒng)設(shè)計(jì)的初衷就是:和客戶(hù)端保持一個(gè)長(zhǎng)連接幕屹、對(duì)外部系統(tǒng)兩個(gè)接口(查詢(xún)用戶(hù)是否在線(xiàn)、給在線(xiàn)的用戶(hù)推送消息)级遭,實(shí)現(xiàn)業(yè)務(wù)的分離
  • 只有和業(yè)務(wù)分離可望拖,才可以供多個(gè)業(yè)務(wù)使用,而不是每個(gè)業(yè)務(wù)都建立一個(gè)長(zhǎng)連接

8.2 已經(jīng)實(shí)現(xiàn)的功能

  • gin log日志(請(qǐng)求日志+debug日志)
  • 讀取配置文件 完成
  • 定時(shí)腳本挫鸽,清理過(guò)期未心跳連接 完成
  • http接口说敏,獲取登錄、連接數(shù)量 完成
  • http接口丢郊,發(fā)送push盔沫、查詢(xún)有多少人在線(xiàn) 完成
  • grpc 程序內(nèi)部通訊,發(fā)送消息 完成
  • appIds 一個(gè)用戶(hù)在多個(gè)平臺(tái)登錄
  • 界面枫匾,把所有在線(xiàn)的人拉倒一個(gè)群里面架诞,發(fā)送消息 完成
  • 單聊、群聊 完成
  • 實(shí)現(xiàn)分布式干茉,水平擴(kuò)張 完成
  • 壓測(cè)腳本
  • 文檔整理
  • 文檔目錄谴忧、百萬(wàn)長(zhǎng)連接的實(shí)現(xiàn)、為什么要實(shí)現(xiàn)一個(gè)IM角虫、怎么實(shí)現(xiàn)一個(gè)Im
  • 架構(gòu)圖以及擴(kuò)展

IM實(shí)現(xiàn)細(xì)節(jié):

  • 定義文本消息結(jié)構(gòu) 完成
  • html發(fā)送文本消息 完成
  • 接口接收文本消息并發(fā)送給全體 完成
  • html接收到消息 顯示到界面 完成
  • 界面優(yōu)化 需要持續(xù)優(yōu)化
  • 有人加入以后廣播全體 完成
  • 定義加入聊天室的消息結(jié)構(gòu) 完成
  • 引入機(jī)器人 待定

8.2 需要完善沾谓、優(yōu)化

  • 登錄,使用微信登錄 獲取昵稱(chēng)上遥、頭像等
  • 有賬號(hào)系統(tǒng)搏屑、資料系統(tǒng)
  • 界面優(yōu)化、適配手機(jī)端
  • 消息 文本消息(支持表情)粉楚、圖片、語(yǔ)音、視頻消息
  • 微服務(wù)注冊(cè)模软、發(fā)現(xiàn)伟骨、熔斷等
  • 添加配置項(xiàng),單臺(tái)機(jī)器最大連接數(shù)量

8.3 總結(jié)

  • 雖然實(shí)現(xiàn)了一個(gè)分布式在聊天的IM燃异,但是有很多細(xì)節(jié)沒(méi)有處理(登錄沒(méi)有鑒權(quán)携狭、界面還待優(yōu)化等),但是可以通過(guò)這個(gè)示例可以了解到:通過(guò)WebSocket解決很多業(yè)務(wù)上需求
  • 本文雖然號(hào)稱(chēng)單臺(tái)機(jī)器能有百萬(wàn)長(zhǎng)連接(內(nèi)存上能滿(mǎn)足)回俐,但是實(shí)際在場(chǎng)景遠(yuǎn)比這個(gè)復(fù)雜(cpu有些壓力)逛腿,當(dāng)然了如果你有這么大的業(yè)務(wù)量可以購(gòu)買(mǎi)更多的機(jī)器更好的去支撐你的業(yè)務(wù),本程序只是演示如何在實(shí)際工作用使用webSocket.
  • 參考本文仅颇,你可以實(shí)現(xiàn)出來(lái)符合你需要的程序

9单默、參考文獻(xiàn)

維基百科 WebSocket

阮一峰 WebSocket教程

WebSocket協(xié)議:5分鐘從入門(mén)到精通

go-stress-testing 單臺(tái)機(jī)器100w連接壓測(cè)實(shí)戰(zhàn)

github 搜:link1st 查看項(xiàng)目 gowebsocket

https://github.com/link1st/gowebsocket

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市忘瓦,隨后出現(xiàn)的幾起案子搁廓,更是在濱河造成了極大的恐慌,老刑警劉巖耕皮,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件境蜕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡凌停,警方通過(guò)查閱死者的電腦和手機(jī)粱年,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)罚拟,“玉大人台诗,你說(shuō)我怎么就攤上這事≈凼妫” “怎么了拉庶?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)秃励。 經(jīng)常有香客問(wèn)我氏仗,道長(zhǎng),這世上最難降的妖魔是什么夺鲜? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任皆尔,我火速辦了婚禮,結(jié)果婚禮上币励,老公的妹妹穿的比我還像新娘慷蠕。我一直安慰自己,他們只是感情好食呻,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布流炕。 她就那樣靜靜地躺著澎现,像睡著了一般。 火紅的嫁衣襯著肌膚如雪每辟。 梳的紋絲不亂的頭發(fā)上剑辫,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音渠欺,去河邊找鬼妹蔽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛挠将,可吹牛的內(nèi)容都是我干的胳岂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼舔稀,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼乳丰!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起镶蹋,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤成艘,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后贺归,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體淆两,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年拂酣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了秋冰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡婶熬,死狀恐怖剑勾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赵颅,我是刑警寧澤虽另,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站饺谬,受9級(jí)特大地震影響捂刺,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜募寨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一族展、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拔鹰,春花似錦仪缸、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宾茂。三九已至,卻和暖如春锣尉,著一層夾襖步出監(jiān)牢的瞬間刻炒,已是汗流浹背决采。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工自沧, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人树瞭。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓拇厢,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親晒喷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子孝偎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-WebSo...
    敢夢(mèng)敢當(dāng)閱讀 8,909評(píng)論 0 50
  • 該文基于開(kāi)源項(xiàng)目分析,總結(jié)了IM相關(guān)的一些知識(shí)點(diǎn)凉敲,如何實(shí)現(xiàn)衣盾,以及針對(duì)客服業(yè)務(wù)需要補(bǔ)充的幾個(gè)點(diǎn)。開(kāi)源系統(tǒng)使用nett...
    猩猩福斯閱讀 11,313評(píng)論 2 62
  • WebSocket 機(jī)制 WebSocket 是 HTML5 一種新的協(xié)議爷抓。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信势决,能更...
    勇敢的_心_閱讀 2,259評(píng)論 0 4
  • WebSocket簡(jiǎn)介 談到Web實(shí)時(shí)推送,就不得不說(shuō)WebSocket蓝撇。在WebSocket出現(xiàn)之前果复,很多網(wǎng)站為...
    吧啦啦小湯圓閱讀 8,145評(píng)論 15 75
  • 許多人都對(duì)非洲有著這樣的誤解:黑人就是當(dāng)?shù)赝林兹司褪墙肭终卟巢F鋵?shí)虽抄,非洲有著世界上6大人種中的5個(gè),...
    夢(mèng)回默籬閱讀 513評(píng)論 0 0