← 中文资源

简体中文版本

Gorilla WebSocket 房间广播中心

用中心化 Hub 管理 WebSocket 房间:注册与注销走通道串行化,广播按房间分发,发送缓冲满的掉队者被移除。

阅读英文原文

示例说明

每连接一个 goroutine 直接互发消息会随连接数平方级增长锁竞争。Hub 模式把状态变更收敛到单事件循环:注册、注销、广播三类事件全部经通道排队,处理天然串行无需细粒度锁。每客户端带缓冲的 send 通道把网络写入与广播解耦——写慢的客户端只是缓冲堆积,不会拖住广播循环。

技术要点

  • 通道串行化消除状态竞争
  • 房间映射支持多租户隔离
  • 缓冲满即移除慢消费者

代码解读

run 的 select 三分支对应三类事件:注册加入房间、注销清理并关闭 send 通道、广播遍历房间内全部客户端非阻塞投递——default 分支意味着缓冲满的客户端被异步注销。serveWS 升级连接后启动读写两个协程,读协程刷新读超时兼当断线探测。

使用建议

读超时配 ping 心跳维持真实活跃检测。房间数量要治理防泄漏——空房间已即时删除。横向扩展时房间状态需外置到 Redis。

代码示例

package hub

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

	"github.com/gorilla/websocket"
)

type Client struct{ conn *websocket.Conn; room string; send chan []byte }

type Hub struct {
	mu         sync.RWMutex
	rooms      map[string]map[*Client]struct{}
	register   chan *Client
	unregister chan *Client
	broadcast  chan message
}

type message struct{ room string; data []byte }

func newHub() *Hub {
	return &Hub{
		rooms:      map[string]map[*Client]struct{}{},
		register:   make(chan *Client),
		unregister: make(chan *Client),
		broadcast:  make(chan message, 64),
	}
}

func (h *Hub) run() {
	for {
		select {
		case c := <-h.register:
			h.mu.Lock()
			if h.rooms[c.room] == nil {
				h.rooms[c.room] = map[*Client]struct{}{}
			}
			h.rooms[c.room][c] = struct{}{}
			h.mu.Unlock()
		case c := <-h.unregister:
			h.mu.Lock()
			if room, ok := h.rooms[c.room]; ok {
				if _, alive := room[c]; alive {
					delete(room, c)
					close(c.send)
				}
				if len(room) == 0 {
					delete(h.rooms, c.room)
				}
			}
			h.mu.Unlock()
		case msg := <-h.broadcast:
			h.mu.RLock()
			for c := range h.rooms[msg.room] {
				select {
				case c.send <- msg.data:
				default: // 缓冲满:掉队者被移除
					go h.unregister <- c
				}
			}
			h.mu.RUnlock()
		}
	}
}

var upgrader = websocket.Upgrader{ReadBufferSize: 1024, WriteBufferSize: 1024}

func (h *Hub) serveWS(w http.ResponseWriter, r *http.Request, room string) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("upgrade:", err)
		return
	}
	client := &Client{conn: conn, room: room, send: make(chan []byte, 16)}
	h.register <- client

	go func() {
		defer func() { h.unregister <- client }()
		for {
			conn.SetReadDeadline(time.Now().Add(90 * time.Second))
			if _, _, err := conn.ReadMessage(); err != nil {
				return
			}
		}
	}()

	go func() {
		for data := range client.send {
			if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
				return
			}
		}
	}()
}