如今,在不刷新頁面的情況下發送消息并獲得即時響應在我們看來是理所當然的事情。但是曾幾何時,啟用實時功能對開發人員來說是一個真正的挑戰。開發社區在HTTP長輪詢(http long polling)和AJAX上走了很長一段路,但終于還是找到了一種構建真正的實時應用程序的解決方案。
該解決方案以WebSockets的形式出現,這使得在用戶瀏覽器和服務器之間開啟一個交互式會話成為可能。WebSocket支持瀏覽器將消息發送到服務器并接收事件驅動的響應,而不必使用長輪詢服務器的方式去獲取響應。
就目前而言,WebSockets是構建實時應用程序的首選解決方案,包括在線游戲,即時通訊程序,跟蹤應用程序等均在使用這一方案。本文將說明WebSockets的操作方式,并說明我們如何使用
Go語言
構建WebSocket應用程序。我們還將比較最受歡迎的WebSocket庫,以便您可以根據選擇出最適合您的那個。
網絡套接字(network socket)與WebSocket
在Go中使用WebSockets之前,讓我們在網絡套接字和WebSockets之間劃清一條界限。
網絡套接字
網絡套接字(或簡稱為套接字)充當內部端點,用于在同一計算機或同一網絡上的不同計算機上運行的應用程序之間交換數據。
套接字是Unix和windows操作系統的關鍵部分,它們使開發人員更容易創建支持網絡的軟件。應用程序開發人員不可以直接在程序中包含套接字,而不是從頭開始構建網絡連接。由于網絡套接字可用于許多不同的網絡協議(如HTTP,FTP等),因此可以同時使用多個套接字。
套接字是通過一組函數調用創建和使用的,這些函數調用有時稱為套接字的應用程序編程接口(API)。正是由于這些函數調用,套接字可以像常規文件一樣被打開。
網絡套接字有如下幾種類型:
- 數據報套接字(SOCK_DGRAM),也稱為無連接套接字,使用用戶數據報協議(UDP)。數據報套接字支持雙向消息流并保留記錄邊界。
- 流套接字(SOCK_STREAM),也稱為面向連接的套接字,使用傳輸控制協議(TCP),流控制傳輸協議(SCTP)或數據報擁塞控制協議(DCCP)。這些套接字提供了沒有記錄邊界的雙向,可靠,有序且無重復的數據流。
- 原始套接字(或原始IP套接字)通常在路由器和其他網絡設備中可用。這些套接字通常是面向數據報的,盡管它們的確切特性取決于協議提供的接口。大多數應用程序不使用原始套接字。提供它們是為了支持新的通信協議的開發,并提供對現有協議更深層設施的訪問。
套接字通信
首先,讓我們弄清楚如何確保每個套接字都是唯一的。否則,您將無法建立可靠的溝通通道(channel)。
為每個進程(process)提供唯一的PID有助于解決本地問題。但是,這種方法不適用于網絡。要創建唯一的套接字,我們建議使用TCP / IP協議。使用TCP / IP,網絡層的IP地址在給定網絡內是唯一的,并且協議和端口在主機應用程序之間是唯一的。
TCP和UDP是用于主機之間通信的兩個主要協議。讓我們看看您的應用程序如何連接到TCP和UDP套接字。
- 連接到TCP套接字
為了建立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 }
- 連接到UDP套接字
與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相比,它具有許多優點:
- 輕量級報頭減少了數據傳輸開銷。
- 單個Web客戶端僅需要一個TCP連接。
- WebSocket服務器可以將數據推送到Web客戶端。
圖: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。
- STDLIB(golang.org/x/net/websocket)
此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
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 } .......
- GOBWAS
這個微小的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
該工具提供了廣泛的易于使用的功能。它允許并發控制,數據壓縮和設置請求標頭。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庫比較
為了更好地分析其性能,我們還進行了一些基準測試。結果如下:
- 如您所見,GOBWAS與其他庫相比具有明顯的優勢。每個操作分配的內存更少,每個分配使用的內存和時間更少。另外,它的I/O分配為零。此外,GOBWAS還具有創建WebSocket客戶端與服務器的交互并接收消息片段所需的所有方法。您也可以使用它輕松地使用TCP套接字。
- 如果您真的不喜歡GOBWAS,則可以使用Gorilla。它非常簡單,幾乎具有所有相同的功能。您也可以使用STDLIB,但由于它缺少許多必要的功能,并且在生產中表現不佳,而且正如您在基準測試中所看到的那樣,它的性能較弱。GOWebsocket與STDLIB大致相同。但是,如果您需要快速構建原型或MVP,則它可能是一個合理的選擇。
除了這些工具之外,還有幾種替代實現可讓您構建強大的流處理解決方案。其中有:
- go-socket.io
- Apache Thrift
- gRPC
- package rpc
流技術的不斷發展以及WebSockets等文檔較好的可用工具的存在,使開發人員可以輕松創建真正的實時應用程序。如果您需要使用WebSockets創建實時應用程序的建議或幫助,請給我們寫信。希望本教程對您有所幫助。
本文翻譯自《How to Use Websockets in Golang : Best Tools and Step-by-Step Guide》。
譯者:TonyBai