使用Go語言創建WebSocket服務
- 2020 年 3 月 26 日
- 筆記
今天介紹如何用Go
語言創建WebSocket
服務,文章的前兩部分簡要介紹了WebSocket
協議以及用Go
標準庫如何創建WebSocket
服務。第三部分實踐環節我們使用了gorilla/websocket
庫幫助我們快速構建WebSocket
服務,它幫封裝了使用Go
標準庫實現WebSocket
服務相關的基礎邏輯,讓我們能從繁瑣的底層程式碼中解脫出來,根據業務需求快速構建WebSocket
服務。
Go Web 編程系列的每篇文章的源程式碼都打了對應版本的軟體包,供大家參考。公眾號中回復
gohttp10
獲取本文源程式碼
WebSocket介紹
WebSocket
通訊協議通過單個TCP
連接提供全雙工通訊通道。與HTTP
相比,WebSocket
不需要你為了獲得響應而發送請求。它允許雙向數據流,因此您只需等待伺服器發送的消息即可。當Websocket
可用時,它將向您發送一條消息。對於需要連續數據交換的服務(例如即時通訊程式,在線遊戲和實時交易系統),WebSocket
是一個很好的解決方案。 WebSocket
連接由瀏覽器請求,並由伺服器響應,然後建立連接,此過程通常稱為握手。 WebSocket
中的特殊標頭僅需要瀏覽器與伺服器之間的一次握手即可建立連接,該連接將在其整個生命周期內保持活動狀態。 WebSocket
解決了許多實時Web
開發的難題,並且與傳統的HTTP
相比,具有許多優點:
- 輕量級報頭減少了數據傳輸開銷。
- 單個
Web
客戶端僅需要一個TCP
連接。 WebSocket
伺服器可以將數據推送到Web
客戶端。
WebSocket協議實現起來相對簡單。它使用HTTP
協議進行初始握手。握手成功後即建立連接,WebSocket
實質上使用原始TCP
讀取/寫入數據。

客戶端請求如下所示:
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規範定義了一個客戶機和伺服器之間使用的特定幀格式。這是框架的位模式:

圖:傳輸數據幀的位模式
使用以下程式碼對客戶端有效負載進行解碼:
// 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: %sn", 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") }
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: Connectedn"; }; 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
把消息再回傳給客戶端,可以在終端中查看到記錄。

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