本文 AI 產出,尚未審核

Golang 網路編程 ── WebSocket 基礎

簡介

在即時互動的應用程式(即時聊天、線上遊戲、即時儀表板)中,WebSocket 已成為最常使用的雙向通訊協定。相較於傳統的 HTTP 請求/回應模式,WebSocket 只需要在連線建立時完成一次握手,之後即可在同一個 TCP 連線上進行全雙工的即時資料傳輸,降低了延遲與網路開銷。

對於使用 Golang 開發後端服務的工程師而言,標準函式庫提供了 net/http 以及第三方成熟的套件(如 gorilla/websocket)來輕鬆實作 WebSocket。掌握這些基礎概念與常見的實作模式,能讓你快速構建可靠的即時服務。


核心概念

1. WebSocket 握手 (Handshake)

WebSocket 的連線起始於一次 HTTP/1.1 的升級請求(Upgrade)。客戶端會送出 Upgrade: websocketConnection: Upgrade 以及 Sec-WebSocket-Key 等標頭,伺服器回應 101 Switching Protocols 並回傳 Sec-WebSocket-Accept,完成協定切換。

// 使用 gorilla/websocket 建立升級器
var upgrader = websocket.Upgrader{
    // 允許所有來源,正式環境請自行限制
    CheckOrigin: func(r *http.Request) bool { return true },
}

// HTTP 處理函式,負責完成握手
func wsHandler(w http.ResponseWriter, r *http.Request) {
    // Upgrade 會自動檢查握手標頭,若失敗會回傳錯誤
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("WebSocket 升級失敗: %v", err)
        return
    }
    defer conn.Close()
    // 交給後續的訊息處理函式
    handleConnection(conn)
}

2. 訊息類型與編碼

WebSocket 支援 文字 (Text)二進位 (Binary)關閉 (Close)Ping/Pong 四種訊息類型。最常見的是文字訊息,通常使用 JSON 編碼傳遞結構化資料;二進位則適合傳送檔案或影像。

type ChatMessage struct {
    Username string `json:"username"`
    Content  string `json:"content"`
    Time     int64  `json:"time"`
}

// 發送文字訊息(JSON 編碼)
func sendJSON(conn *websocket.Conn, msg ChatMessage) error {
    data, err := json.Marshal(msg)
    if err != nil {
        return err
    }
    // WriteMessage 會自動設定訊息類型為 TextMessage
    return conn.WriteMessage(websocket.TextMessage, data)
}

3. Ping / Pong 心跳機制

為避免長時間閒置的連線被 NAT 或防火牆切斷,伺服器(或客戶端)會定期發送 Ping,對方必須回應 Ponggorilla/websocket 內建自動回應 Pong,亦可自行設定讀寫超時。

// 設定讀寫超時與自動 Ping
func configureConn(conn *websocket.Conn) {
    // 每 30 秒送一次 Ping
    ticker := time.NewTicker(30 * time.Second)
    go func() {
        for range ticker.C {
            if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                log.Println("Ping 失敗:", err)
                return
            }
        }
    }()

    // 設定讀取超時,若 60 秒內未收到任何訊息就關閉連線
    conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    conn.SetPongHandler(func(appData string) error {
        // 收到 Pong 時延長讀取期限
        conn.SetReadDeadline(time.Now().Add(60 * time.Second))
        return nil
    })
}

4. Broadcast(廣播)與客戶端管理

多數即時應用需要把訊息 廣播 給所有已連線的客戶端。常見的做法是維護一個 hub(中心)結構,使用 chan 進行訊息分發,避免競爭條件。

type Hub struct {
    // 註冊/解除註冊的請求
    register   chan *websocket.Conn
    unregister chan *websocket.Conn
    // 需要廣播的訊息
    broadcast  chan []byte
    // 所有連線的集合
    clients    map[*websocket.Conn]bool
}

func newHub() *Hub {
    return &Hub{
        register:   make(chan *websocket.Conn),
        unregister: make(chan *websocket.Conn),
        broadcast:  make(chan []byte),
        clients:    make(map[*websocket.Conn]bool),
    }
}

// Hub 的主循環,負責管理客戶端與訊息分發
func (h *Hub) run() {
    for {
        select {
        case conn := <-h.register:
            h.clients[conn] = true
        case conn := <-h.unregister:
            if _, ok := h.clients[conn]; ok {
                delete(h.clients, conn)
                conn.Close()
            }
        case msg := <-h.broadcast:
            for conn := range h.clients {
                if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
                    // 若寫入失敗,視為斷線,直接移除
                    delete(h.clients, conn)
                    conn.Close()
                }
            }
        }
    }
}

5. 完整的 Echo 範例

下面是一個最小可執行的 Echo Server,展示從握手、訊息接收、回傳、心跳到關閉的完整流程。

package main

import (
    "log"
    "net/http"
    "time"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool { return true },
}

func wsEcho(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("升級失敗:", err)
        return
    }
    defer conn.Close()

    // 設定心跳
    ticker := time.NewTicker(20 * time.Second)
    defer ticker.Stop()

    // 讀取迴圈
    go func() {
        for {
            _, msg, err := conn.ReadMessage()
            if err != nil {
                log.Println("讀取失敗:", err)
                return
            }
            // 原封不動回傳
            if err = conn.WriteMessage(websocket.TextMessage, msg); err != nil {
                log.Println("寫入失敗:", err)
                return
            }
        }
    }()

    // Ping 迴圈
    for range ticker.C {
        if err = conn.WriteMessage(websocket.PingMessage, nil); err != nil {
            log.Println("Ping 失敗:", err)
            return
        }
    }
}

func main() {
    http.HandleFunc("/ws", wsEcho)
    log.Println("WebSocket server listening on :8080/ws")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

常見陷阱與最佳實踐

常見問題 可能原因 解決方式
連線建立後立即斷線 握手失敗、CheckOrigin 限制、TLS 設定錯誤 確認瀏覽器或客戶端送出的 Origin,在開發階段可暫時允許所有來源;正式環境請明確列白。
訊息遺失或順序錯亂 多協程同時寫入同一 *websocket.Conn 不要 在多個 goroutine 直接呼叫 WriteMessage,使用 單一寫入協程sync.Mutex 包裝。
心跳失效導致斷線 未設定讀取期限或未回應 Pong 使用 SetReadDeadline + SetPongHandler,或自行實作 Ping/Pong 機制。
記憶體洩漏 客戶端斷線未從 hub 中移除 unregister 時務必 delete 連線並 Close(),避免持續佔用資源。
大量客戶端時的廣播效能 逐一寫入每個連線,阻塞主 goroutine 使用 緩衝通道工作池,將寫入動作分派給獨立的 writer goroutine。

最佳實踐

  1. 使用 hub 中央管理:讓所有連線的註冊/解除與訊息分發集中處理,降低競爭條件。
  2. 限制訊息大小conn.SetReadLimit(maxBytes) 可防止惡意客戶端發送巨量資料。
  3. TLS 加密:在公開網路上務必使用 wss://(TLS)保護資料。
  4. 日誌與監控:記錄連線建立、斷開、錯誤碼,配合 Prometheus / Grafana 監控連線數與延遲。
  5. Graceful Shutdown:在服務關閉前,先關閉 hub、通知所有客戶端,避免突斷。

實際應用場景

場景 為什麼選擇 WebSocket 可能的 Go 實作
即時聊天系統 需要雙向即時訊息、低延遲 使用 hub 進行訊息廣播,配合 JWT 驗證身分。
多人協作編輯 多人同時編輯同一文件,變更必須即時同步 每個編輯事件以 JSON 傳遞,使用 Binary 傳送差分檔案。
線上遊戲 高頻率的狀態更新(位置、動作) 使用 Binary 訊息減少封包大小,結合 protobuf 序列化。
即時儀表板 後端推送即時統計、圖表資料 伺服器端每秒推送一次 JSON,前端使用 Chart.js 動態更新。
IoT 裝置遠端控制 裝置需要即時接收指令,同時回報狀態 透過 WebSocket 建立長連線,避免頻繁的 HTTP 請求。

總結

WebSocket 為即時雙向通訊提供了 輕量、低延遲 的解決方案。使用 Golang 時,我們可以藉由 gorilla/websocket 快速完成握手、訊息編碼、心跳管理與廣播機制。掌握以下要點,即可在實務專案中安全、有效地運用 WebSocket:

  • 正確處理握手與 Origin 驗證。
  • 使用 hub 集中管理連線,避免競爭條件。
  • 為每條連線設定 讀寫超時Ping/Pong 心跳。
  • 限制訊息大小、使用 Mutex 或單寫入協程保證 thread‑safe。
  • 在正式環境啟用 TLS、做好日誌與監控。

透過本文的概念與範例,你已具備在 Go 生態系統中構建可靠 WebSocket 服務的基礎,接下來可以依照各自的業務需求,擴充認證、壓縮、訊息分片等進階功能,打造真正符合產業需求的即時應用。祝開發順利!