使用Go語言建立WebSocket服務

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

Go Web 編程系列的每篇文章的源代碼都打了對應版本的軟件包,供你們參考。公衆號中回覆 gohttp10獲取本文源代碼

WebSocket介紹

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

  • 輕量級報頭減小了數據傳輸開銷。
  • 單個Web客戶端僅須要一個TCP鏈接。
  • WebSocket服務器能夠將數據推送到Web客戶端。

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

客戶端請求以下所示:github

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

這是服務器響應:web

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 協議的運行機制決定的,發送響應後即關閉鏈接)。websocket

所以,您須要使用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規範定義了的一個客戶機和服務器之間使用的特定幀格式。這是框架的位模式:

img{512x368}
圖:傳輸數據幀的位模式

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

// 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
  • 調用鏈接的WriteMessageReadMessage方法發送和接收消息。上面的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")
}

websocket.html裏咱們須要用JavaScript鏈接WebScoket服務進行收發消息,篇幅緣由我就只貼JS代碼了,完整的代碼經過本節的口令去公衆號就能獲取到下載連接。

<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,在輸入框中輸入任何消息都能再次回顯到瀏覽器中。

圖片

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

image-20200316142506287

總結

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

參考連接:

https://yalantis.com/blog/how...
https://www.gorillatoolkit.or...

前文回顧

深刻學習用Go編寫HTTP服務器

超詳細的Go模板庫應用指南

用Go語言建立靜態文件服務器

用SecureCookie實現客戶端Session管理

img

相關文章
相關標籤/搜索