Golang 官方認可的 websocket 庫-gorilla/websocket

wentao發表於2021-07-15

推薦理由

Golang 官方標準庫實現的 websocket 在功能上有些欠缺,本次介紹的 gorilla/websocket 庫,是Gorilla出品的速度快、質量高,並且被廣泛使用的 websocket 庫,很好的彌補了標準庫功能上的欠缺。另外 Gorilla Web toolkit 包含多個實用的 HTTP 應用相關的工具庫,感興趣可以到官網主頁https://www.gorillatoolkit.org自取。

功能介紹

gorilla/websocket 庫是 RFC 6455 定義的 websocket 協議的一種實現,在資料收發方面,提供 Data Messages、Control Messages 兩類 message 粒度的讀寫 API;效能方面,提供 Buffers 和 Compression 的相關配置選項;安全方面,可通過 CheckOrigin 來控制是否支援跨域。

gorilla/websocket 庫和官方實現的對比

摘自 gorilla GitHub 主頁

github.com/gorilla golang.org/x/net
RFC 6455 Features
Passes Autobahn Test Suite Yes No
Receive fragmented message Yes No, see note 1
Send close message Yes No
Send pings and receive pongs Yes No
Get the type of a received data message Yes Yes, see note 2
Other Features
Compression Extensions Experimental No
Read message using io.Reader Yes No, see note 3
Write message using io.WriteCloser Yes No, see note 3

Notes:

  1. Large messages are fragmented in Chrome's new WebSocket implementation.
  2. The application can get the type of a received data message by implementing a Codec marshal function.
  3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries. Read returns when the input buffer is full or a frame boundary is encountered. Each call to Write sends a single frame message. The Gorilla io.Reader and io.WriteCloser operate on a single WebSocket message.

使用指南

安裝

go get github.com/gorilla/websocket

基礎示例

下面以一個簡單的 echo 來說明 gorilla/websocket 庫基本使用。

client 程式碼:

package main

import (
    "flag"
    "log"
    "net/url"
    "os"
    "os/signal"
    "time"

    "github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:8080", "http service address")

func main() {
    flag.Parse()
    log.SetFlags(0)

    // 用來接收命令列的終止訊號
    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt)

    // 和服務端建立連線
    u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"}
    log.Printf("connecting to %s", u.String())

    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    defer c.Close()

    done := make(chan struct{})

    go func() {
        defer close(done)
        for {
            // 從接收服務端message
            _, message, err := c.ReadMessage()
            if err != nil {
                log.Println("read:", err)
                return
            }
            log.Printf("recv: %s", message)
        }
    }()

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-done:
            return
        case t := <-ticker.C:
            // 向服務端傳送message
            err := c.WriteMessage(websocket.TextMessage, []byte(t.String()))
            if err != nil {
                log.Println("write:", err)
                return
            }
        case <-interrupt:
            log.Println("interrupt")

            // 收到命令列終止訊號,通過傳送close message關閉連線。
            err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
            if err != nil {
                log.Println("write close:", err)
                return
            }
            // 收到接收協程完成的訊號或者超時,退出
            select {
            case <-done:
            case <-time.After(time.Second):
            }
            return
        }
    }
}

server 程式碼:

package main

import (
    "flag"
    "html/template"
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:8080", "http service address")

var upgrader = websocket.Upgrader{}

func echo(w http.ResponseWriter, r *http.Request) {
    // 完成和Client HTTP >>> WebSocket的協議升級
    c, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("upgrade:", err)
        return
    }
    defer c.Close()
    for {
        // 接收客戶端message
        mt, message, err := c.ReadMessage()
        if err != nil {
            log.Println("read:", err)
            break
        }
        log.Printf("recv: %s", message)
        // 向客戶端傳送message
        err = c.WriteMessage(mt, message)
        if err != nil {
            log.Println("write:", err)
            break
        }
    }
}

func main() {
    flag.Parse()
    log.SetFlags(0)
    http.HandleFunc("/echo", echo)
    log.Fatal(http.ListenAndServe(*addr, nil))
}

更多示例可以參考 https://github.com/gorilla/websocket/tree/master/examples

總結

gorilla/websocket 庫是 websocket 協議的一種實現,相比標準庫的實現,封裝了協議細節,使用者關注 message 粒度的 API 即可,但需要注意 message 的讀寫 API 非併發安全,使用時注意不要多個協程併發呼叫。

參考資料

  1. https://github.com/gorilla/websocket
更多原創文章乾貨分享,請關注公眾號
  • Golang 官方認可的 websocket 庫-gorilla/websocket
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章