Golang 網路編程 ── WebSocket 基礎
簡介
在即時互動的應用程式(即時聊天、線上遊戲、即時儀表板)中,WebSocket 已成為最常使用的雙向通訊協定。相較於傳統的 HTTP 請求/回應模式,WebSocket 只需要在連線建立時完成一次握手,之後即可在同一個 TCP 連線上進行全雙工的即時資料傳輸,降低了延遲與網路開銷。
對於使用 Golang 開發後端服務的工程師而言,標準函式庫提供了 net/http 以及第三方成熟的套件(如 gorilla/websocket)來輕鬆實作 WebSocket。掌握這些基礎概念與常見的實作模式,能讓你快速構建可靠的即時服務。
核心概念
1. WebSocket 握手 (Handshake)
WebSocket 的連線起始於一次 HTTP/1.1 的升級請求(Upgrade)。客戶端會送出 Upgrade: websocket、Connection: 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,對方必須回應 Pong。gorilla/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。 |
最佳實踐
- 使用 hub 中央管理:讓所有連線的註冊/解除與訊息分發集中處理,降低競爭條件。
- 限制訊息大小:
conn.SetReadLimit(maxBytes)可防止惡意客戶端發送巨量資料。 - TLS 加密:在公開網路上務必使用
wss://(TLS)保護資料。 - 日誌與監控:記錄連線建立、斷開、錯誤碼,配合 Prometheus / Grafana 監控連線數與延遲。
- 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 服務的基礎,接下來可以依照各自的業務需求,擴充認證、壓縮、訊息分片等進階功能,打造真正符合產業需求的即時應用。祝開發順利!