Go實現基於WebSocket的彈幕服務

ImClive發表於2018-11-05

拉模式和推模式

拉模式

1、資料更新頻率低,則大多數請求是無效的
2、線上使用者量多,則服務端的查詢負載高
3、定時輪詢拉取,實時性低

推模式

1、僅在資料更新時才需要推送
2、需要維護大量的線上長連線
3、資料更新後可以立即推送

基於webSocket推送

1、瀏覽器支援的socket程式設計,輕鬆維持服務端長連線
2、基於TCP可靠傳輸之上的協議,無需開發者關心通訊細節
3、提供了高度抽象的程式設計介面,業務開發成本較低

webSocket協議與互動

通訊流程

客戶端->upgrade->服務端
客戶端<-switching<-服務端
客戶端->message->服務端
客戶端<-message<-服務端

實現http服務端

1、webSocket是http協議upgrade而來
2、使用http標準庫快速實現空介面:/ws

webSocket握手

1、使用webSocket.Upgrader完成協議握手,得到webSocket長連線
2、操作webSocket api,讀取客戶端訊息,然後原樣傳送回去

封裝webSocket

缺乏工程化設計

1、其他程式碼模組,無法直接操作webSocket連線
2、webSocket連線非執行緒安全,併發讀/寫需要同步手段

隱藏細節,封裝api

1、封裝Connection結構,隱藏webSocket底層連線
2、封裝Connection的api,提供Send/Read/Close等執行緒安全介面

api原理(channel是執行緒安全的)

1、SendMessage將訊息投遞到out channel
2、ReadMessage從in channel讀取訊息

內部原理

1、啟動讀協程,迴圈讀取webSocket,將訊息投遞到in channel
2、啟動寫協程,迴圈讀取out channel,將訊息寫給webSocket

// server.go
package main

import (
    "net/http"
    "github.com/gorilla/websocket"
    "./impl"
    "time"
)

var (
    upgrader = websocket.Upgrader{
        //允許跨域
        CheckOrigin: func(r *http.Request) bool {
            return true
        },
    }
)

func wsHandler(w http.ResponseWriter, r *http.Request) {
    var (
        wsConn *websocket.Conn
        err error
        conn *impl.Connection
        data []byte
    )

    //Upgrade:websocket
    if wsConn, err = upgrader.Upgrade(w, r, nil); err != nil {
        return
    }
    if conn, err = impl.InitConnection(wsConn); err != nil {
        goto ERR
    }

    go func() {
        var (
            err error
        )
        for {
            if err =conn.WriteMessage([]byte("heartbeat")); err != nil {
                return
            }
            time.Sleep(1 * time.Second)
        }
    }()

    for {
        if data, err = conn.ReadMessage(); err != nil {
            goto ERR
        }
        if err = conn.WriteMessage(data); err != nil {
            goto ERR
        }

    }

    ERR:
        //關閉連線
        conn.Close()
}

func main() {
    //http:localhost:7777/ws
    http.HandleFunc("/ws", wsHandler)
    http.ListenAndServe("0.0.0.0:7777", nil)
}
// connection.go
package impl

import (
    "github.com/gorilla/websocket"
    "sync"
    "github.com/influxdata/platform/kit/errors"
)

var once sync.Once

type Connection struct {
    wsConn *websocket.Conn
    inChan chan []byte
    outChan chan []byte
    closeChan chan byte
    isClosed bool
    mutex sync.Mutex
}

func InitConnection(wsConn *websocket.Conn) (conn *Connection, err error) {
    conn = &Connection{
        wsConn:wsConn,
        inChan:make(chan []byte, 1000),
        outChan:make(chan []byte, 1000),
        closeChan:make(chan byte, 1),
    }

    //啟動讀協程
    go conn.readLoop()

    //啟動寫協程
    go conn.writeLoop()

    return
}

//API
func (conn *Connection) ReadMessage() (data []byte, err error) {
    select {
    case data = <- conn.inChan:
    case <- conn.closeChan:
        err = errors.New("connection is closed")
    }
    return
}

func (conn *Connection) WriteMessage(data []byte) (err error) {
    select {
    case conn.outChan <- data:
    case <- conn.closeChan:
        err = errors.New("connection is closed")
    }
    return
}

func (conn *Connection) Close() {
    // 執行緒安全的close,可重入
    conn.wsConn.Close()
    conn.mutex.Lock()
    if !conn.isClosed {
        close(conn.closeChan)
        conn.isClosed = true
    }
    conn.mutex.Unlock()
}

//內部實現
func (conn *Connection) readLoop() {
    var (
        data []byte
        err error
    )
    for {
        if _, data, err = conn.wsConn.ReadMessage(); err != nil {
            goto ERR
        }

        //阻塞在這裡,等待inChan有空位置
        //但是如果writeLoop連線關閉了,這邊無法得知
        //conn.inChan <- data

        select {
        case conn.inChan <- data:
        case <-conn.closeChan:
            //closeChan關閉的時候,會進入此分支
            goto ERR
        }
    }
    ERR:
        conn.Close()
}

func (conn *Connection) writeLoop() {
    var (
        data []byte
        err error
    )
    for {
        select {
        case data = <- conn.outChan:
        case <- conn.closeChan:
            goto ERR

        }

        if err = conn.wsConn.WriteMessage(websocket.TextMessage, data); err != nil {
            goto ERR
        }
        conn.outChan <- data
    }
    ERR:
        conn.Close()
}

相關文章