今天介紹如何用 Go
語(yǔ)言創(chuàng)建 WebSocket
服務(wù),文章的前兩部分簡(jiǎn)要介紹了 WebSocket
協(xié)議以及用 Go
標(biāo)準(zhǔn)庫(kù)如何創(chuàng)建 WebSocket
服務(wù)。第三部分實(shí)踐環(huán)節(jié)我們使用了 gorilla/websocket
庫(kù)幫助我們快速構(gòu)建 WebSocket
服務(wù),它幫封裝了使用 Go
標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn) WebSocket
服務(wù)相關(guān)的基礎(chǔ)邏輯,讓我們能從繁瑣的底層代碼中解脫出來(lái),根據(jù)業(yè)務(wù)需求快速構(gòu)建 WebSocket
服務(wù)。
Go Web 編程系列的每篇文章的源代碼都打了對(duì)應(yīng)版本的軟件包,供大家參考。公眾號(hào)中回復(fù) gohttp10
獲取本文源代碼
WebSocket介紹
WebSocket
通信協(xié)議通過(guò)單個(gè) TCP
連接提供全雙工通信通道。與 HTTP
相比, WebSocket
不需要你為了獲得響應(yīng)而發(fā)送請(qǐng)求。它允許雙向數(shù)據(jù)流,因此您只需等待服務(wù)器發(fā)送的消息即可。當(dāng) Websocket
可用時(shí),它將向您發(fā)送一條消息。 對(duì)于需要連續(xù)數(shù)據(jù)交換的服務(wù)(例如即時(shí)通訊程序,在線(xiàn)游戲和實(shí)時(shí)交易系統(tǒng)), WebSocket
是一個(gè)很好的解決方案。 WebSocket
連接由瀏覽器請(qǐng)求,并由服務(wù)器響應(yīng),然后建立連接,此過(guò)程通常稱(chēng)為握手。 WebSocket
中的特殊標(biāo)頭僅需要瀏覽器與服務(wù)器之間的一次握手即可建立連接,該連接將在其整個(gè)生命周期內(nèi)保持活動(dòng)狀態(tài)。 WebSocket
解決了許多實(shí)時(shí) Web
開(kāi)發(fā)的難題,并且與傳統(tǒng)的 HTTP
相比,具有許多優(yōu)點(diǎn):
- 輕量級(jí)報(bào)頭減少了數(shù)據(jù)傳輸開(kāi)銷(xiāo)。
- 單個(gè)Web客戶(hù)端僅需要一個(gè)TCP連接。
- WebSocket服務(wù)器可以將數(shù)據(jù)推送到Web客戶(hù)端。
WebSocket協(xié)議實(shí)現(xiàn)起來(lái)相對(duì)簡(jiǎn)單。它使用 HTTP
協(xié)議進(jìn)行初始握手。握手成功后即建立連接, WebSocket
實(shí)質(zhì)上使用原始 TCP
讀取/寫(xiě)入數(shù)據(jù)。

客戶(hù)端請(qǐng)求如下所示:
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
這是服務(wù)器響應(yīng):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
如何在Go中創(chuàng)建WebSocket應(yīng)用
要基于Go 語(yǔ)言?xún)?nèi)置的 net/http
庫(kù)編寫(xiě) WebSocket
服務(wù)器,你需要:
- 發(fā)起握手
- 從客戶(hù)端接收數(shù)據(jù)幀
- 發(fā)送數(shù)據(jù)幀給客戶(hù)端
- 關(guān)閉握手
發(fā)起握手
首先,讓我們創(chuàng)建一個(gè)帶有 WebSocket
端點(diǎn)的 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
結(jié)構(gòu)。
初始握手請(qǐng)求始終來(lái)自客戶(hù)端。服務(wù)器確定了 WebSocket
請(qǐng)求后,需要使用握手響應(yīng)進(jìn)行回復(fù)。
請(qǐng)記住,你無(wú)法使用 http.ResponseWriter
編寫(xiě)響應(yīng),因?yàn)橐坏╅_(kāi)始發(fā)送響應(yīng),它將關(guān)閉其基礎(chǔ)的 TCP
連接(這是 HTTP
協(xié)議的運(yùn)行機(jī)制決定的,發(fā)送響應(yīng)后即關(guān)閉連接)。
因此,您需要使用 HTTP
劫持( hijack
)。通過(guò)劫持,可以接管基礎(chǔ)的 TCP
連接處理程序和 bufio.Writer
。這使可以在不關(guān)閉 TCP
連接的情況下讀取和寫(xiě)入數(shù)據(jù)。
// NewHandler initializes a new handler
func NewHandler(w http.ResponseWriter, req *http.Request) (*WS, error) {
hj, ok := w.(http.Hijacker)
if !ok {
// handle error
} .....
}
要完成握手,服務(wù)器必須使用適當(dāng)?shù)念^進(jìn)行響應(yīng)。
// 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"))
.....
}
客戶(hù)端發(fā)起 WebSocket
連接請(qǐng)求時(shí)用的 Sec-WebSocket-key
是隨機(jī)生成的,并且是Base64編碼的。接受請(qǐng)求后,服務(wù)器需要將此密鑰附加到固定字符串。假設(shè)秘鑰是 x3JJHMbDL1EzLkh9GBhXDw==
。在這個(gè)例子中,可以使用 SHA-1
計(jì)算二進(jìn)制值,并使用 Base64
對(duì)其進(jìn)行編碼。得到 HSmrc0sMlYUkAGmm5OPpG2HaGWk=
。然后使用它作為 Sec-WebSocket-Accept
響應(yīng)頭的值。
傳輸數(shù)據(jù)幀
握手成功完成后,您的應(yīng)用程序可以從客戶(hù)端讀取數(shù)據(jù)或向客戶(hù)端寫(xiě)入數(shù)據(jù)。WebSocket規(guī)范 定義了的一個(gè)客戶(hù)機(jī)和服務(wù)器之間使用的特定幀格式。這是框架的位模式:

圖:傳輸數(shù)據(jù)幀的位模式
使用以下代碼對(duì)客戶(hù)端有效負(fù)載進(jìn)行解碼:
// 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
}
反過(guò)來(lái),這些代碼行允許對(duì)數(shù)據(jù)進(jìn)行編碼:
// 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
}
.....
關(guān)閉握手
當(dāng)各方之一發(fā)送狀態(tài)為關(guān)閉的關(guān)閉幀作為有效負(fù)載時(shí),握手將關(guān)閉。可選的,發(fā)送關(guān)閉幀的一方可以在有效載荷中發(fā)送關(guān)閉原因。如果關(guān)閉是由客戶(hù)端發(fā)起的,則服務(wù)器應(yīng)發(fā)送相應(yīng)的關(guān)閉幀作為響應(yīng)。
// 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()
}
使用第三方庫(kù)快速構(gòu)建WebSocket服務(wù)
通過(guò)上面的章節(jié)可以看到用 Go
自帶的 net/http
庫(kù)實(shí)現(xiàn) WebSocket
服務(wù)還是太復(fù)雜了。好在有很多對(duì) WebSocket
支持良好的第三方庫(kù),能減少我們很多底層的編碼工作。這里我們使用 gorilla web toolkit
家族的另外一個(gè)庫(kù) gorilla/websocket
來(lái)實(shí)現(xiàn)我們的 WebSocket
服務(wù),構(gòu)建一個(gè)簡(jiǎn)單的 Echo
服務(wù)( echo
意思是回音,就是客戶(hù)端發(fā)什么,服務(wù)端再把消息發(fā)回給客戶(hù)端)。
我們?cè)?http_demo
項(xiàng)目的 handler
目錄下新建一個(gè) ws
子目錄用來(lái)存放 WebSocket
服務(wù)相關(guān)的路由對(duì)應(yīng)的請(qǐng)求處理程序。
增加兩個(gè)路由:
/ws/echo
echo
應(yīng)用的WebSocket 服務(wù)的路由
/ws/echo_display
echo
應(yīng)用的客戶(hù)端頁(yè)面的路由。 創(chuàng)建WebSocket服務(wù)端
// 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) // 實(shí)際應(yīng)用時(shí)記得做錯(cuò)誤處理
for {
// 讀取客戶(hù)端的消息
msgType, msg, err := conn.ReadMessage()
if err != nil {
return
}
// 把消息打印到標(biāo)準(zhǔn)輸出
fmt.Printf("%s sent: %s\n", conn.RemoteAddr(), string(msg))
// 把消息寫(xiě)回客戶(hù)端,完成回音
if err = conn.WriteMessage(msgType, msg); err != nil {
return
}
}
}
conn
變量的類(lèi)型是 *websocket.Conn
, websocket.Conn
類(lèi)型用來(lái)表示 WebSocket
連接。服務(wù)器應(yīng)用程序從 HTTP
請(qǐng)求處理程序調(diào)用 Upgrader.Upgrade
方法以獲取 *websocket.Conn
- 調(diào)用連接的
WriteMessage
和 ReadMessage
方法發(fā)送和接收消息。上面的 msg
接收到后在下面又回傳給了客戶(hù)端。 msg
的類(lèi)型是 []byte
。
創(chuàng)建WebSocket客戶(hù)端
前端頁(yè)面路由對(duì)應(yīng)的請(qǐng)求處理程序如下,直接返回 views/websockets.html
給到瀏覽器渲染頁(yè)面即可。
// 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
服務(wù)進(jìn)行收發(fā)消息,篇幅原因我就只貼 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>
...
注冊(cè)路由
服務(wù)端和客戶(hù)端的程序都準(zhǔn)備好后,我們按照之前約定好的路徑為他們注冊(cè)路由和對(duì)應(yīng)的請(qǐng)求處理程序:
// router/router.go
func RegisterRoutes(r *mux.Router) {
...
wsRouter := r.PathPrefix("/ws").Subrouter()
wsRouter.HandleFunc("/echo", ws.EchoMessage)
wsRouter.HandleFunc("/echo_display", ws.DisplayEcho)
}
測(cè)試驗(yàn)證
重啟服務(wù)后訪(fǎng)問(wèn) http://localhost:8000/ws/echo_display
,在輸入框中輸入任何消息都能再次回顯到瀏覽器中。

服務(wù)端則是把收到的消息打印到終端中然后把調(diào)用 writeMessage
把消息再回傳給客戶(hù)端,可以在終端中查看到記錄。
總結(jié)
WebSocket
在現(xiàn)在更新頻繁的應(yīng)用中使用非常廣泛,進(jìn)行 WebSocket
編程也是我們需要掌握的一項(xiàng)必備技能。文章的實(shí)踐練習(xí)稍微簡(jiǎn)單了一些,也沒(méi)有做錯(cuò)誤和安全性檢查。主要是為了講清楚大概的流程。關(guān)于 gorilla/websocket
更多的細(xì)節(jié)在使用時(shí)還需要查看官方文檔才行。
參考鏈接:
https://yalantis.com/blog/how-to-build-websockets-in-go/
https://www.gorillatoolkit.org/pkg/websocket
到此這篇關(guān)于使用Go語(yǔ)言創(chuàng)建WebSocket服務(wù)的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)Go語(yǔ)言創(chuàng)建WebSocket 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
您可能感興趣的文章:- 利用Go語(yǔ)言搭建WebSocket服務(wù)端方法示例
- Go 實(shí)現(xiàn)百萬(wàn)WebSocket連接的方法示例
- 利用 Go 語(yǔ)言編寫(xiě)一個(gè)簡(jiǎn)單的 WebSocket 推送服務(wù)
- go的websocket實(shí)現(xiàn)原理與用法詳解