使用 Go 語言創建 WebSocket 服務

感謝參考原文-http://bjbsair.com/2020-04-01/tech-info/18504.html

今天介紹如何用 Go 語言創建 WebSocket 服務,文章的前兩部分簡要介紹了 WebSocket 協議以及用 Go 標準庫如何創建 WebSocket 服務。第三部分實踐環節我們使用了 gorilla/websocket 庫幫助我們快速構建 WebSocket 服務,它幫封裝了使用 Go 標準庫實現 WebSocket 服務相關的基礎邏輯,讓我們能從繁瑣的底層代碼中解脫出來,根據業務需求快速構建 WebSocket 服務。

WebSocket介紹

WebSocket 通信協議通過單個 TCP 連接提供全雙工通信通道。與 HTTP 相比, WebSocket 不需要你爲了獲得響應而發送請求。它允許雙向數據流,因此您只需等待服務器發送的消息即可。當 Websocket 可用時,它將向您發送一條消息。對於需要連續數據交換的服務(例如即時通訊程序,在線遊戲和實時交易系統), WebSocket 是一個很好的解決方案。 WebSocket 連接由瀏覽器請求,並由服務器響應,然後建立連接,此過程通常稱爲握手。 WebSocket 中的特殊標頭僅需要瀏覽器與服務器之間的一次握手即可建立連接,該連接將在其整個生命週期內保持活動狀態。 WebSocket 解決了許多實時 Web 開發的難題,並且與傳統的 HTTP 相比,具有許多優點:

  • 輕量級報頭減少了數據傳輸開銷。
  • 單個 Web 客戶端僅需要一個 TCP 連接。
  • WebSocket 服務器可以將數據推送到 Web 客戶端。

WebSocket協議實現起來相對簡單。它使用 HTTP 協議進行初始握手。握手成功後即建立連接, WebSocket 實質上使用原始 TCP 讀取/寫入數據。

使用 Go 語言創建 WebSocket 服務

客戶端請求如下所示:

GET /chat HTTP/1.1  
    Host: server.example.com  
    Upgrade: websocket  
    Connection: Upgrade  
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==  
    Sec-WebSocket-Protocol: chat, superchat  
    Sec-WebSocket-Version: 13  
    Origin: http://example.com

這是服務器響應:

HTTP/1.1 101 Switching Protocols  
    Upgrade: websocket  
    Connection: Upgrade  
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=  
    Sec-WebSocket-Protocol: chat

如何在Go中創建WebSocket應用

要基於Go 語言內置的 net/http 庫編寫 WebSocket 服務器,你需要:

  • 發起握手
  • 從客戶端接收數據幀
  • 發送數據幀給客戶端
  • 關閉握手

發起握手

首先,讓我們創建一個帶有 WebSocket 端點的 HTTP 處理程序:

// HTTP server with WebSocket endpoint  
func Server() {  
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {  
            ws, err := NewHandler(w, r)  
            if err != nil {  
                 // handle error  
            }  
            if err = ws.Handshake(); err != nil {  
                // handle error  
            }  
        …

然後初始化 WebSocket 結構。

初始握手請求始終來自客戶端。服務器確定了 WebSocket 請求後,需要使用握手響應進行回覆。

請記住,你無法使用 http.ResponseWriter 編寫響應,因爲一旦開始發送響應,它將關閉其基礎的 TCP 連接(這是 HTTP 協議的運行機制決定的,發送響應後即關閉連接)。

因此,您需要使用 HTTP 劫持( hijack )。通過劫持,可以接管基礎的 TCP 連接處理程序和 bufio.Writer 。這使可以在不關閉 TCP 連接的情況下讀取和寫入數據。

// NewHandler initializes a new handler  
func NewHandler(w http.ResponseWriter, req *http.Request) (*WS, error) {  
        hj, ok := w.(http.Hijacker)  
        if !ok {  
            // handle error  
        }                  .....  
}

要完成握手,服務器必須使用適當的頭進行響應。

// Handshake creates a handshake header  
    func (ws *WS) Handshake() error {  

        hash := func(key string) string {  
            h := sha1.New()  
            h.Write([]byte(key))  
            h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))  

        return base64.StdEncoding.EncodeToString(h.Sum(nil))  
        }(ws.header.Get("Sec-WebSocket-Key"))  
      .....  
}

客戶端發起 WebSocket 連接請求時用的 Sec-WebSocket-key 是隨機生成的,並且是Base64編碼的。接受請求後,服務器需要將此密鑰附加到固定字符串。假設祕鑰是 x3JJHMbDL1EzLkh9GBhXDw== 。在這個例子中,可以使用 SHA-1 計算二進制值,並使用 Base64 對其進行編碼。得到 HSmrc0sMlYUkAGmm5OPpG2HaGWk= 。然後使用它作爲 Sec-WebSocket-Accept 響應頭的值。

傳輸數據幀

握手成功完成後,您的應用程序可以從客戶端讀取數據或向客戶端寫入數據。WebSocket規範定義了一個客戶機和服務器之間使用的特定幀格式。這是框架的位模式:

使用 Go 語言創建 WebSocket 服務

圖:傳輸數據幀的位模式

使用以下代碼對客戶端有效負載進行解碼:

// Recv receives data and returns a Frame  
    func (ws *WS) Recv() (frame Frame, _ error) {  
        frame = Frame{}  
        head, err := ws.read(2)  
        if err != nil {  
            // handle error  
        }

反過來,這些代碼行允許對數據進行編碼:

// Send sends a Frame  
    func (ws *WS) Send(fr Frame) error {  
        // make a slice of bytes of length 2  
        data := make([]byte, 2)  

        // Save fragmentation & opcode information in the first byte  
        data[0] = 0x80 | fr.Opcode  
        if fr.IsFragment {  
            data[0] &= 0x7F  
        }  
        .....

關閉握手

當各方之一發送狀態爲關閉的關閉幀作爲有效負載時,握手將關閉。可選 的 ,發送關閉幀的一方可以在有效載荷中發送關閉原因。 如果關閉是由客戶端發起的,則服務器應發送相應的關閉幀作爲響應。

// Close sends a close frame and closes the TCP connection  
func (ws *Ws) Close() error {  
    f := Frame{}  
    f.Opcode = 8  
    f.Length = 2  
    f.Payload = make([]byte, 2)  
    binary.BigEndian.PutUint16(f.Payload, ws.status)  
    if err := ws.Send(f); err != nil {  
        return err  
    }  
    return ws.conn.Close()  
}

使用第三方庫快速構建WebSocket服務

通過上面的章節可以看到用 Go 自帶的 net/http 庫實現 WebSocket 服務還是太複雜了。好在有很多對 WebSocket 支持良好的第三方庫,能減少我們很多底層的編碼工作。這裏我們使用 gorilla web toolkit 家族的另外一個庫 gorilla/websocket 來實現我們的 WebSocket 服務,構建一個簡單的 Echo 服務( echo 意思是迴音,就是客戶端發什麼,服務端再把消息發回給客戶端)。

我們在 http_demo 項目的 handler 目錄下新建一個 ws 子目錄用來存放 WebSocket 服務相關的路由對應的請求處理程序。

增加兩個路由:

  • /ws/echo echo 應用的 WebSocket 服務的路由。
  • /ws/echo_display echo 應用的客戶端頁面的路由。

創建WebSocket服務端

// handler/ws/echo.go  
package ws  

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

var upgrader = websocket.Upgrader{  
    ReadBufferSize:  1024,  
    WriteBufferSize: 1024,  
}  

func EchoMessage(w http.ResponseWriter, r *http.Request) {  
    conn, _ := upgrader.Upgrade(w, r, nil) // 實際應用時記得做錯誤處理  

    for {  
        // 讀取客戶端的消息  
        msgType, msg, err := conn.ReadMessage()  
        if err != nil {  
            return  
        }  

        // 把消息打印到標準輸出  
        fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg))  

        // 把消息寫回客戶端,完成迴音  
        if err = conn.WriteMessage(msgType, msg); err != nil {  
            return  
        }  
    }  
}
  • conn 變量的類型是 websocket.Conn , websocket.Conn 類型用來表示 WebSocket 連接。服務器應用程序從 HTTP 請求處理程序調用 Upgrader.Upgrade 方法以獲取 websocket.Conn
  • 調用連接的 WriteMessage 和 ReadMessage 方法發送和接收消息。上面的 msg 接收到後在下面又回傳給了客戶端。 msg 的類型是 []byte 。

創建WebSocket客戶端

前端頁面路由對應的請求處理程序如下,直接返回 views/websockets.html 給到瀏覽器渲染頁面即可。

// handler/ws/echo_display.go  
package ws  

import "net/http"  

func DisplayEcho(w http.ResponseWriter, r *http.Request) {  
    http.ServeFile(w, r, "views/websockets.html")  
}
<form>  
    <input id="input" type="text" />  
    <button onclick="send()">Send</button>  
    <pre id="output"></pre>  
</form>  
...  
<script>    var input = document.getElementById("input");  
    var output = document.getElementById("output");  
    var socket = new WebSocket("ws://localhost:8000/ws/echo");  

    socket.onopen = function () {  
        output.innerHTML += "Status: Connected\n";  
    };  

    socket.onmessage = function (e) {  
        output.innerHTML += "Server: " + e.data + "\n";  
    };  

    function send() {  
        socket.send(input.value);  
        input.value = "";  
    }</script>  
...

註冊路由

服務端和客戶端的程序都準備好後,我們按照之前約定好的路徑爲他們註冊路由和對應的請求處理程序:

// router/router.go  
func RegisterRoutes(r *mux.Router) {  
    ...  
    wsRouter := r.PathPrefix("/ws").Subrouter()  
    wsRouter.HandleFunc("/echo", ws.EchoMessage)  
    wsRouter.HandleFunc("/echo_display", ws.DisplayEcho)  
}

測試驗證

重啓服務後訪問 http://localhost:8000/ws/echo_display ,在輸入框中輸入任何消息都能再次回顯到瀏覽器中。

使用 Go 語言創建 WebSocket 服務

圖片

服務端則是把收到的消息打印到終端中然後把調用 writeMessage 把消息再回傳給客戶端,可以在終端中查看到記錄。

使用 Go 語言創建 WebSocket 服務

總結

WebSocket 在現在更新頻繁的應用中使用非常廣泛,進行 WebSocket 編程也是我們需要掌握的一項必備技能。文章的實踐練習稍微簡單了一些,也沒有做錯誤和安全性檢查。主要是爲了講清楚大概的流程。關於 gorilla/websocket 更多的細節在使用時還需要查看官方文檔才行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章