WebSocket 協議 - 實戰

本文是 WebSocket 系列文章的第 2 篇,第 1 篇主要講述概念原理。本文從實戰角度介紹如何使用 WebSocket。

Part1 項目簡介

本文實戰項目來自 Mastering WebSockets With Go。實現了一個精簡版的 Web 聊天系統,前端採用 HTML+JS,後端用 Go 實現。注意: 本文對原項目做了一點 UI 顏色調整

在本地構建部署後效果如下,先要登錄後創建 WebSocket 連接,然後可以向聊天室發送信息。

Part2 關鍵 API

1 客戶端 API

下面是 javascript 創建 WebSocket 代碼,直接通過構造函數 new WebSocket 創建一個實例。

conn = new WebSocket("wss://" + document.location.host + "/ws?otp="+ otp);

            // Onopen
            conn.onopen = function (evt) {
                document.getElementById("connection-header").innerHTML = "Connected to Websocket: true";
            }

            conn.onclose = function(evt) {
                // Set disconnected
                document.getElementById("connection-header").innerHTML = "Connected to Websocket: false";
            }

            // Add a listener to the onmessage event
            conn.onmessage = function (evt) {
                console.log(evt);
                // parse websocket message as JSON
                const eventData = JSON.parse(evt.data);
                // Assign JSON data to new Event Object
                const event = Object.assign(new Event, eventData);
                // Let router manage message
                routeEvent(event);
            }

WebSocket 構造函數

WebSocket 接收一個 URL 參數,執行完下面語句後,就會與對應的 host 建立連接。

conn = new WebSocket("wss://" + document.location.host + "/ws?otp="+ otp)

onopen

WebSocket 實例對象的 onopen 屬性,在上面的連接建立成功後進行回調執行。

conn.onopen = function (evt) {
    document.getElementById("connection-header").innerHTML = "Connected to Websocket: true";
}

這裏會在連接成功後,頁面顯示 "Connected to Websocket: true"

onmessage

收到來自服務端數據後的回調函數, 處理邏輯在routeEvent.

conn.onmessage = function (evt) {
    console.log(evt);
    // parse websocket message as JSON
    const eventData = JSON.parse(evt.data);
    // Assign JSON data to new Event Object
    const event = Object.assign(new Event, eventData);
    // Let router manage message
    routeEvent(event);
}

routeEvent 先進行一些參數校驗,然後構造 messageEvent 丟給 appendChatMessage。

function routeEvent(event) {
    if (event.type === undefined) {
        alert("no 'type' field in event");
    }
    switch (event.type) {
        case "new_message":
        // Format payload
            const messageEvent = Object.assign(new NewMessageEvent, event.payload);
            appendChatMessage(messageEvent);
            break;
        default:
            alert("unsupported message type");
            break;
    }
}

真正顯示處理在appendChatMessage函數。在消息前加上日期時間,並拼接到聊天室之前內容的尾部。

function appendChatMessage(messageEvent) {
    var date = new Date(messageEvent.sent);
    // format message
    const formattedMsg = `${date.toLocaleString()}${messageEvent.message}`;
    // Append Message
    textarea = document.getElementById("chatmessages");
    textarea.innerHTML = textarea.innerHTML + "\n" + formattedMsg;
    textarea.scrollTop = textarea.scrollHeight;
}

onclose

WebSocket 連接關閉後的回調函數。在頁面上顯示 "Connected to Websocket: false"。

conn.onclose = function(evt) {
    // Set disconnected
    document.getElementById("connection-header").innerHTML = "Connected to Websocket: false";
}

send

send 方法向服務端發送數據。這裏把 payload 數據封裝成 Event json 格式發送給後端。

function sendEvent(eventName, payload) {
    // Create a event Object with a event named send_message
    const event = new Event(eventName, payload);
    // Format as JSON and send
    conn.send(JSON.stringify(event));
}

2 服務端 API

服務端採用的是開源的 github.com/gorilla/websocket。是用 Go 語言實現的 WebSocket 庫。該庫簡單易用、性能比較高。

Upgrade

調用 websocket.Upgrader 對象的 Upgrade 方法將普通的 HTTP 連接升級爲 WebSocket 連接。函數簽名如下, 直接將 http 的 w 和 r 傳遞給它即可。

func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error)

url 路由爲 / ws 即走 WebSocket 協議,可以看到 serveWS 中調用 conn, err := websocketUpgrader.Upgrade(w, r, nil)將 HTTP 請求升級爲 WebSocket.

func (m *Manager) serveWS(w http.ResponseWriter, r *http.Request) {
 otp := r.URL.Query().Get("otp")
 if otp == "" {
  w.WriteHeader(http.StatusUnauthorized)
  return
 }

 if !m.otps.VerifyOTP(otp) {
  w.WriteHeader(http.StatusUnauthorized)
  return
 }

 log.Println("New connection")
 conn, err := websocketUpgrader.Upgrade(w, r, nil)
 if err != nil {
  log.Println(err)
  return
 }
 client := NewClient(conn, m)
 m.addClient(client)
 go client.readMessages()
 go client.writeMessages()
}

ReadMessage

調用 ReadMessage 讀取客戶端發送的數據,該方法返回 3 個參數,第一個參數表示讀取的數據類型,第二參數是讀取到的數據,第三個參數爲 error 類型。

業務層面數據類型主要是 TextMessage 和 BinaryMessage,分別表示讀取到數據是文本數據還是二進制數據。

WriteMessage

調用 WriteMessage 向客戶端發送數據,需要傳入兩個參數,第一個參數表示數據類型,這裏傳輸的是文本數據。第二個參數傳數據內容。

Part3 總結

服務端用短短的幾百行代碼實現了一個完整的 WebSocket 服務器框架,實現結構圖如下,兼具安全性和擴展性,值得借鑑模仿。

以下幾點內容值得學習:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Y6WmPw8pjzcRFmcyNYzh7w