1. 程式人生 > 程式設計 >使用Go語言建立WebSocket服務的實現示例

使用Go語言建立WebSocket服務的實現示例

今天介紹如何用 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 相比,具有許多優點:

  1. 輕量級報頭減少了資料傳輸開銷。
  2. 單個Web客戶端僅需要一個TCP連線。
  3. WebSocket伺服器可以將資料推送到Web客戶端。

WebSocket協議實現起來相對簡單。它使用 HTTP 協議進行初始握手。握手成功後即建立連線, WebSocket 實質上使用原始 TCP 讀取/寫入資料。

使用Go語言建立WebSocket服務的實現示例

客戶端請求如下所示:

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

使用Go語言建立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: %s\n",conn.RemoteAddr(),string(msg))

		// 把訊息寫回客戶端,完成迴音
		if err = conn.WriteMessage(msgType,msg); err != nil {
			return
		}
	}
}
  • conn 變數的型別是 *websocket.Conn,websocket.Conn 型別用來表示 WebSocket 連線。伺服器應用程式從 HTTP 請求處理程式呼叫 Upgrader.Upgrade 方法以獲取 *websocket.Conn
  • 呼叫連線的 WriteMessageReadMessage 方法傳送和接收訊息。上面的 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,"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: Connected\n";
 };

 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 ,在輸入框中輸入任何訊息都能再次回顯到瀏覽器中。

使用Go語言建立WebSocket服務的實現示例

服務端則是把收到的訊息列印到終端中然後把呼叫 writeMessage 把訊息再回傳給客戶端,可以在終端中檢視到記錄。

使用Go語言建立WebSocket服務的實現示例 總結

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

參考連結:

https://yalantis.com/blog/how-to-build-websockets-in-go/

https://www.gorillatoolkit.org/pkg/websocket

到此這篇關於使用Go語言建立WebSocket服務的實現示例的文章就介紹到這了,更多相關Go語言建立WebSocket 內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!