如今,在不刷新頁面的情況下發送消息並獲得即時響應在我們看來是理所當然的事情。但是曾幾何時,啟用實時功能對開發人員來說是一個真正的挑戰。開發社區在HTTP長輪詢(http long polling)和AJAX上走了很長一段路,但終於還是找到了一種構建真正的實時應用程式的解決方案。
該解決方案以WebSockets的形式出現,這使得在用戶瀏覽器和伺服器之間開啟一個交互式會話成為可能。WebSocket支持瀏覽器將消息發送到伺服器並接收事件驅動的響應,而不必使用長輪詢伺服器的方式去獲取響應。
就目前而言,WebSockets是構建實時應用程式的首選解決方案,包括在線遊戲,即時通訊程序,跟蹤應用程式等均在使用這一方案。本文將說明WebSockets的操作方式,並說明我們如何使用
構建WebSocket應用程式。我們還將比較最受歡迎的WebSocket庫,以便您可以根據選擇出最適合您的那個。
網絡套接字(network socket)與WebSocket
在Go中使用WebSockets之前,讓我們在網絡套接字和WebSockets之間劃清一條界限。
網絡套接字(或簡稱為套接字)充當內部端點,用於在同一計算機或同一網絡上的不同計算機上運行的應用程式之間交換數據。
套接字是Unix和Windows作業系統的關鍵部分,它們使開發人員更容易創建支持網絡的軟體。應用程式開發人員不可以直接在程序中包含套接字,而不是從頭開始構建網絡連接。由於網絡套接字可用於許多不同的網絡協議(如HTTP,FTP等),因此可以同時使用多個套接字。
套接字是通過一組函數調用創建和使用的,這些函數調用有時稱為套接字的應用程式編程接口(API)。正是由於這些函數調用,套接字可以像常規文件一樣被打開。
網絡套接字有如下幾種類型:
首先,讓我們弄清楚如何確保每個套接字都是唯一的。否則,您將無法建立可靠的溝通通道(channel)。
為每個進程(process)提供唯一的PID有助於解決本地問題。但是,這種方法不適用於網絡。要創建唯一的套接字,我們建議使用TCP / IP協議。使用TCP / IP,網絡層的IP位址在給定網絡內是唯一的,並且協議和埠在主機應用程式之間是唯一的。
TCP和UDP是用於主機之間通信的兩個主要協議。讓我們看看您的應用程式如何連接到TCP和UDP套接字。
為了建立TCP連接,Go客戶端使用net程序包中的DialTCP函數。DialTCP返回一個TCPConn對象。建立連接後,客戶端和伺服器開始交換數據:客戶端通過TCPConn向伺服器發送請求,伺服器解析請求並發送響應,TCPConn從伺服器接收響應。
圖:TCP Socket
該連接將持續保持有效,直到客戶端或伺服器將其關閉。創建連接的函數如下:
客戶端:
// 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
}
服務端:
// 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
}
與TCP套接字相反,使用UDP套接字,客戶端只是向伺服器發送數據報。沒有Accept函數,因為伺服器不需要接受連接,而只是等待數據報到達。
圖:UDP Socket
其他TCP函數都具有UDP對應的函數;只需在上述函數中將TCP替換為UDP。
客戶端:
// 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
}
服務端:
// 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連接提供全雙工通信通道。與HTTP相比,WebSocket不需要您發送請求即可獲得響應。它們允許雙向數據流,因此您只需等待伺服器響應即可。可用時,它將向您發送一條消息。
對於需要連續數據交換的服務(例如即時通訊程序,在線遊戲和實時交易系統),WebSockets是一個很好的解決方案。您可以在RFC 6455規範中找到有關WebSocket協議的完整信息。
WebSocket連接由瀏覽器請求發起,並由伺服器響應,之後連接就建立起來了。此過程通常稱為握手。WebSockets中的特殊標頭僅需要瀏覽器與伺服器之間的一次握手即可建立連接,該連接將在其整個生命周期內保持活動狀態。
WebSockets解決了許多實時Web開發的難題,與傳統的HTTP相比,它具有許多優點:
圖:WebSocket
WebSocket協議實現起來相對簡單。它使用HTTP協議進行初始握手。成功握手後,連接就建立起來了,並且WebSocket實質上使用原始TCP(raw 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.ResponseWriter編寫響應,因為一旦開始發送響應,它將關閉基礎TCP連接。
因此,您需要使用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"))
.....
}
「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庫列表
有幾個第三方庫可簡化開發人員的開發工作,並極大地促進使用WebSockets。
此WebSocket庫是標準庫的一部分。如RFC 6455規範中所述,它為WebSocket協議實現了客戶端和伺服器。它不需要安裝並且有很好的官方文檔。但是,另一方面,它仍然缺少其他WebSocket庫中可以找到的某些功能。/x/net/websocket軟體包中的Golang WebSocket實現不允許用戶以明確的方式重用連接之間的I/O緩衝區。
讓我們檢查一下STDLIB軟體包的工作方式。這是用於執行基本功能(如創建連接以及發送和接收消息)的代碼示例。
首先,要安裝和使用此庫,應將以下代碼行添加到您的:
import "golang.org/x/net/websocket"
客戶端:
// 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
}
.......
伺服器端:
// 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 Web工具包中的WebSocket軟體包擁有WebSocket協議的完整且經過測試的實現以及穩定的軟體包API。WebSocket軟體包文檔齊全,易於使用。您可以在Gorilla官方網站上找到文檔。
安裝
go get github.com/gorilla/websocket
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
}
.......
伺服器端:
// 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
}
.......
這個微小的WebSocket封裝具有強大的功能列表,例如零拷貝升級(zero-copy upgrade)和允許構建自定義數據包處理邏輯的低級API。GOBWAS在I/O期間不需要中間做額外分配操作。它還在wsutil軟體包中提供了圍繞API的高級包裝API和幫助API,使開發人員可以快速使用,而無需深入研究協議的內部。該庫具有靈活的API,但這是以可用性和清晰度為代價的。
可在GoDoc網站上找到文檔。您可以通過下面代碼行來安裝它:
go get github.com/gobwas/ws
客戶端:
// 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
}
.......
伺服器端:
// 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支持代理和子協議,用於發送和接收文本和二進位數據。開發人員還可以啟用或禁用SSL驗證。
您可以在GoDoc網站和項目的GitHub頁面上找到有關如何使用GOWebsockets的文檔和示例。通過添加以下代碼行來安裝軟體包:
go get github.com/sacOO7/gowebsocket
客戶端:
// 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
};
.......
伺服器端:
// 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
}
比較現有解決方案
我們已經描述了Go中使用最廣泛的四個WebSocket庫。下表包含這些工具的詳細比較。
圖 Websocket庫比較
為了更好地分析其性能,我們還進行了一些基準測試。結果如下:
除了這些工具之外,還有幾種替代實現可讓您構建強大的流處理解決方案。其中有:
流技術的不斷發展以及WebSockets等文檔較好的可用工具的存在,使開發人員可以輕鬆創建真正的實時應用程式。如果您需要使用WebSockets創建實時應用程式的建議或幫助,請給我們寫信。希望本教程對您有所幫助。
本文翻譯自《How to Use Websockets in Golang : Best Tools and Step-by-Step Guide》。
譯者:TonyBai