go語言websocket-工具與指南
翻譯自:How to Use Websockets in Golang : Best Tools and Step-by-Step Guide
在不重新整理頁面的情況下發送訊息並獲得即時響應是我們認為理所當然的事情。但在過去,實現實時功能對開發人員來說是一個真正的挑戰。開發人員社群已經從HTTP長輪詢和AJAX走了很長一段路,最終找到了構建真正實時應用程式的解決方案。
這個解決方案以WebSockets的形式出現,它使得在使用者的瀏覽器和伺服器之間開啟互動會話成為可能。WebSockets允許瀏覽器向伺服器傳送訊息並接收事件驅動的響應,而無需輪詢伺服器以獲得響應。
目前,WebSockets是構建實時應用程式的首選解決方案:線上遊戲、即時訊息、跟蹤應用程式等等。本指南解釋了WebSocket的工作原理,並展示瞭如何用Go程式語言構建WebSocket應用程式。我們還比較了最流行的WebSocket庫,以便您可以根據自己的需要選擇最佳的WebSocket庫。
網路套接字
網路套接字,或簡單地稱為套接字,作為內部端點,用於在執行在同一臺計算機或執行在同一網路上的不同計算機上的應用程式之間交換資料。
套接字是Unix和基於windows的作業系統的關鍵部分,它們使開發人員更容易建立支援網路的軟體。應用程式開發人員可以在他們的程式中包含套接字,而不是從頭開始構建網路連線。由於網路套接字用於許多不同的網路協議(HTTP、FTP等),所以可以同時使用多個套接字。
套接字與一組函式呼叫一起建立和使用,這些函式呼叫有時稱為套接字的應用程式程式設計介面(API)。由於函式呼叫,套接字可以像普通檔案一樣開啟。
有幾種型別的網路套接字:
資料報套接字(SOCK_DGRAM),也稱為無連線套接字,使用使用者資料報協議(UDP)。資料報套接字支援訊息的雙向流並保留記錄邊界。
流套接字(SOCK_STREAM),也稱為面向連線的套接字,使用傳輸控制協議(TCP)、流控制傳輸協議(SCTP)或資料報擁塞控制協議(DCCP)。這些套接字提供了雙向的、可靠的、有序的、不重複的資料流,沒有記錄邊界。
原始套接字(或原始IP套接字)通常在路由器和其他網路裝置中可用。這些套接字通常是面向資料流的,儘管它們的確切特徵取決於協議提供的介面。大多數應用程式不使用原始套接字。提供它們是為了支援新通訊協議的開發,並提供對現有協議中更深奧的設施的訪問。
套接字通訊
首先,讓我們看看如何確保每個套接字都是惟一的。如果沒有,你就無法建立一個可靠的溝通渠道。
給每個程序一個獨特的PID有助於處理本地問題。但是這種方法不能在網路上工作。要建立一個惟一的套接字,我們建議使用TCP/IP協議。使用TCP/IP,網路層的IP地址在給定的網路中是惟一的,協議和埠在主機應用程式中也是惟一的。
TCP和UDP是主機之間通訊的兩種主要協議。讓我們看看您的應用程式如何連線到TCP和UDP套接字。
連線到TCP套接字
為了建立TCP連線,Go客戶機使用網路包中的DialTCP函式。返回一個TCPConn物件。建立連線後,客戶機和伺服器開始交換資料:客戶機透過TCPConn向伺服器傳送請求,伺服器解析請求併發送響應,TCPConn接收來自伺服器的響應。
此連線在客戶機或伺服器關閉之前一直有效。建立連線的功能如下:
Client side:
// init
tcpAddr, err := net。ResolveTCPAddr(resolver, serverAddr)
if err != nil {
// handle error
}
conn, err := net。DialTCP(network, nil, tcpAddr)
if err != nil {
// handle error
}
// send message
_, err = conn。Write({message})
if err != nil {
// handle error
}
// receive message
var buf [{buffSize}]byte _, err := conn。Read(buf[0:])
if err != nil {
// handle error
}
Server side:
// init
tcpAddr, err := net。ResolveTCPAddr(resolver, serverAddr)
if err != nil {
// handle error
}
listener, err := net。ListenTCP(“tcp”, tcpAddr)
if err != nil {
// handle error
}
// listen for an incoming connection
conn, err := listener。Accept()
if err != nil {
// handle error
}
// send message
if _, err := conn。Write({message}); err != nil {
// handle error
}
// receive message
buf := make([]byte, 512)
n, err := conn。Read(buf[0:])
if err != nil {
// handle error
}
連線到UDP套接字
與TCP套接字相反,使用UDP套接字,客戶機只向伺服器傳送一個數據報。沒有Accept函式,因為伺服器不需要接受連線,只需要等待資料報到達。
其他TCP函式有UDP對應的函式;在上面的函式中,只需用UDP替換TCP。
Client side:
// init
raddr, err := net。ResolveUDPAddr(“udp”, address)
if err != nil {
// handle error
}
conn, err := net。DialUDP(“udp”, nil, raddr)
if err != nil {
// handle error
} ……。
// send message
buffer := make([]byte, maxBufferSize)
n, addr, err := conn。ReadFrom(buffer)
if err != nil {
// handle error
} ……。
// receive message
buffer := make([]byte, maxBufferSize)
n, err = conn。WriteTo(buffer[:n], addr)
if err != nil {
// handle error
}
Server side:
// init
udpAddr, err := net。ResolveUDPAddr(resolver, serverAddr)
if err != nil {
// handle error
}
conn, err := net。ListenUDP(“udp”, udpAddr)
if err != nil {
// handle error
} ……。
// send message
buffer := make([]byte, maxBufferSize)
n, addr, err := conn。ReadFromUDP(buffer)
if err != nil {
// handle error
} ……。
// receive message
buffer := make([]byte, maxBufferSize)
n, err = conn。WriteToUDP(buffer[:n], addr)
if err != nil {
// handle error
}
什麼是WebSocket
WebSocket通訊協議透過一個TCP連線提供一個全雙工通訊通道。與HTTPs相反,WebSockets不需要您傳送請求來獲得響應。它們允許雙向資料流,因此您可以等待伺服器響應。它會在你有空的時候給你發信息。
WebSockets對於需要持續資料交換的服務是一個很好的解決方案——例如,即時通訊、線上遊戲和實時交易系統。您可以在RFC 6455規範中找到關於WebSocket協議的完整資訊。
瀏覽器請求WebSocket連線,伺服器響應WebSocket連線,然後建立連線。這個過程通常稱為握手。WebSockets中特殊型別的頭只需要在瀏覽器和伺服器之間進行一次握手,就可以建立一個在整個生命週期內都保持活動的連線。
WebSockets解決了實時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應用
要基於net/http庫編寫一個簡單的WebSocket echo伺服器,需要:
發起一個握手
從客戶端接收資料幀
向客戶端傳送資料幀
關閉握手
首先,讓我們建立一個帶有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編寫響應。因為一旦您開始傳送響應,它將關閉底層TCP連線。
所以需要使用HTTP劫持。劫持允許您接管底層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”)) 。。。。。
}
“Sec-WebSocket-key”是隨機生成的,採用base64編碼。伺服器需要在接受請求後將此鍵附加到固定字串。假設您有x3JJHMbDL1EzLkh9GBhXDw== key。在本例中,可以使用SHA-1計算二進位制值,並使用Base64對其進行編碼。你會得到HSmrc0sMlYUkAGmm5OPpG2HaGWk =。將此值用作Sec-WebSocket-Accept響應頭的值。
傳輸資料幀
當握手成功完成後,您的應用程式可以從客戶機讀寫資料。WebSocket規範定義了在客戶機和伺服器之間使用的特定框架格式。這是一個小模式的框架:
使用以下程式碼解碼客戶端負載(payload):
// 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庫列表
有幾個第三方庫可以簡化開發人員的工作,並極大地促進WebSockets的使用。
STDLIB (x/net/websocket)
這個WebSocket庫是標準庫的一部分。它為WebSocket協議實現了客戶機和伺服器,如RFC 6455規範中所述。它不需要安裝,並且有很好的官方文件。但另一方面,它仍然缺乏一些可以在其他WebSocket庫中找到的特性。/x/net/ WebSocket包中的Golang WebSocket實現不允許使用者以一種清晰的方式在連線之間重用I/O緩衝區。
讓我們檢查一下STDLIB包是如何工作的。下面是執行基本功能的程式碼示例,比如建立連線、傳送和接收訊息。
首先,要安裝和使用這個庫,你應該把這行程式碼新增到你的:
import “
http://
golang。org/x/net/websoc
ket
”
Client side:
// create connection // schema can be ws:// or wss:// // host, port – WebSocket server
conn, err := websocket。Dial(“{schema}://{host}:{port}”, “”, op。Origin) 、
if err != nil {
// handle error
}
defer conn。Close() ……。
// send message
if err = websocket。JSON。Send(conn, {message}); err != nil {
// handle error
} ……。
// receive message // messageType initializes some type of message
message := messageType{}
if err := websocket。JSON。Receive(conn, &message); err != nil {
// handle error
} ……。
Server side:
// Initialize WebSocket handler + server
mux := http。NewServeMux()
mux。Handle(“/”, websocket。Handler(func(conn *websocket。Conn) { func() { for {
// do something, receive, send, etc。
}
} ……。
// receive message
// messageType initializes some type of message
message := messageType{}
if err := websocket。JSON。Receive(conn, &message); err != nil {
// handle error
} ……。
// send message
if err := websocket。JSON。Send(conn, message); err != nil {
// handle error
} ……。。
Gorilla
Gorilla web toolkit中的WebSocket包擁有完整且經過測試的WebSocket協議實現,以及一個穩定的包API。WebSocket包有良好的文件記錄,易於使用。您可以在Gorilla官方網站上找到相關文件。
Installation
go get
http://
github。com/gorilla/webs
ocket
Examples of code
Client side:
// init
// schema – can be ws:// or wss://
// host, port – WebSocket server
u := url。URL{ Scheme: {schema}, Host: {host}:{port}, Path: “/”, }
c, _, err := websocket。DefaultDialer。Dial(u。String(), nil)
if err != nil {
// handle error
} ……。
// send message
err := c。WriteMessage(websocket。TextMessage, {message})
if err != nil {
// handle error
} ……。
// receive message
_, message, err := c。ReadMessage()
if err != nil {
// handle error
} ……。
Server side:
// init
u := websocket。Upgrader{}
c, err := u。Upgrade(w, r, nil)
if err != nil {
// handle error
} ……。
// receive message
messageType, message, err := c。ReadMessage()
if err != nil {
// handle error
} ……。
// send message
err = c。WriteMessage(messageType, {message})
if err != nil {
// handle error
} ……。
GOBWAS
這個小小的WebSocket 包有一系列功能強大的特性,比如零複製升級和一個底層API,它允許構建自定義包處理邏輯。
GOBWAS在I/O期間不需要中間分配。它還擁有圍繞wsutil包中的API的高階包裝器和助手,允許開發人員快速啟動,而無需深入研究協議的內部。這個庫有一個靈活的API,但這是以可用性和清晰性為代價的。
文件可以在GoDoc網站上找到。你可以安裝它包括以下程式碼行:
go get
http://
github。com/gobwas/ws
Client side:
// init
// schema – can be ws or wss
// host, port – ws server
conn, _, _, err := ws。DefaultDialer。Dial(ctx, {schema}://{host}:{port})
if err != nil {
// handle error
} ……。
// send message
err = wsutil。WriteClientMessage(conn, ws。OpText, {message})
if err != nil { // handle error } ……。
// receive message
msg, _, err := wsutil。ReadServerData(conn)
if err != nil { // handle error } ……。
Server side:
// init
listener, err := net。Listen(“tcp”, op。Port)
if err != nil { // handle error }
conn, err := listener。Accept()
if err != nil { // handle error }
upgrader := ws。Upgrader{}
if _, err = upgrader。Upgrade(conn); err != nil { // handle error } ……。
// receive message
for {
reader := wsutil。NewReader(conn, ws。StateServerSide)
_, err := reader。NextFrame()
if err != nil { // handle error }
data, err := ioutil。ReadAll(reader)
if err != nil { // handle error } ……。
} ……。
// send message
msg := “new server message”
if err := wsutil。WriteServerText(conn, {message}); err != nil { // handle error } ……。
GOWebsockets
這個工具提供了廣泛的易於使用的特性。它允許併發控制、資料壓縮和設定請求頭。GoWebsockets支援用於傳送和接收文字和二進位制資料的代理和子協議。開發人員還可以啟用或禁用SSL驗證。
您可以在GoDoc網站和專案的GitHub頁面上找到關於如何使用GOWebsockets的文件和示例。透過新增以下程式碼行安裝包:
go get
http://
github。com/sacOO7/goweb
socket
Client side:
// init
// schema – can be ws or wss
// host, port – ws server
socket := gowebsocket。New({schema}://{host}:{port})
socket。Connect() ……。
// send message
socket。SendText({message}) or socket。SendBinary({message}) ……。
// receive message
socket。OnTextMessage = func(message string, socket gowebsocket。Socket) { // hande received message }; or socket。OnBinaryMessage = func(data [] byte, socket gowebsocket。Socket) { // hande received message }; ……。
Server side:
// init
// schema – can be ws or wss
// host, port – ws server
conn, _, _, err := ws。DefaultDialer。Dial(ctx, {schema}://{host}:{port})
if err != nil { // handle error } ……。
// send message
err = wsutil。WriteClientMessage(conn, ws。OpText, {message})
if err != nil { // handle error } ……。
// receive message
msg, _, err := wsutil。ReadServerData(conn)
if err != nil { // handle error }
以上我們描述了Golang中使用最廣泛的四個WebSocket庫。下表包含了這些工具的詳細比較。
為了更好地分析它們的效能,我們還進行了一些基準測試。研究結果如下:
正如您所看到的,GOBWAS比其他庫具有明顯的優勢。它每個操作的分配更少,每個分配使用的記憶體和時間也更少。另外,它的I/O分配為零。此外,GOBWAS提供了建立WebSocket客戶機-伺服器互動和接收訊息片段所需的所有方法。您還可以使用它輕鬆地使用TCP套接字。
如果你真的不喜歡GOBWAS,你可以用Gorilla。它非常簡單,幾乎具有所有相同的功能。您也可以使用STDLIB,但是它在生產環境中沒有那麼好,因為它缺乏許多必要的特性,而且,正如您在基準測試中看到的,它提供的效能較差。GOWebsocket與STDLIB基本相同。但是如果你需要快速構建一個原型或者MVP,這是一個合理的選擇。
除了這些工具之外,還有一些替代實現允許您構建強大的流解決方案。其中包括:
go-socket。io :
https://
github。com/googollee/go
-socket。io
Apache Thrift :
https://
thrift。apache。org/tutor
ial/go
gRPC :
https://
grpc。io/
Package rpc :
https://
golang。org/pkg/net/rpc/
流媒體技術的不斷髮展,以及諸如WebSockets等文件良好的工具的可用性,使得開發人員可以很容易地建立真正的實時應用程式。