直接用gorilla/websocket无法跨进程广播,因其*websocket.Conn仅存于本机内存,集群节点间无法共享;需借Redis Pub/Sub中转消息,各节点订阅后向本地存活连接投递,并用sync.Map+done通道安全遍历。

为什么直接用 gorilla/websocket 无法跨进程广播
因为 gorilla/websocket 的连接对象(*websocket.Conn)只存在于当前进程内存里,集群中其他节点根本拿不到这个实例。你调用 conn.WriteMessage() 只能发给本机的某个客户端,对其他机器上的连接完全无感——这不是 bug,是设计使然。
常见错误现象:本地测试一切正常,一上 Kubernetes 或多台服务器就“部分用户收不到消息”,日志里也看不到报错,纯靠抓包或客户端反馈才发现漏播。
- 别试图序列化
*websocket.Conn传给 Redis 或 MQ——它不是可序列化的结构体,会 panic - 别在 HTTP handler 里临时起 goroutine 往 conn 写数据后就 return,容易触发
write tcp: use of closed network connection - 广播逻辑必须和连接生命周期解耦:连接在哪台机器、何时断开,都该由统一的状态管理模块感知
用 Redis Pub/Sub 做跨节点消息中转最轻量
不需要引入 Kafka 或 NATS 这类重型组件,Redis 的 PUBLISH/SUBSCRIBE 足够支撑万级连接的广播吞吐。关键是要把“发消息”和“投递到 conn”拆成两步:所有节点往同一个 channel publish,每个节点自己订阅并负责把消息推给本机存活的连接。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
github.com/go-redis/redis/v9,避免老版本 v8 的 context 泄漏问题 - 订阅用
pubsub := rdb.Subscribe(ctx, "ws:global:broadcast"),不要用Subscribe后反复Receive(),要用pubsub.Channel()配合 range 循环消费 - 每条消息体建议带
room_id和exclude_conn_id字段,方便做房间广播或跳过自己(比如发信人不回显) - 务必设置
pubsub.ReceiveTimeout = 5 * time.Second,防止网络抖动导致 goroutine 卡死
如何安全地遍历本机所有活跃 WebSocket 连接
不能用全局 map + sync.RWMutex 粗暴存取,因为连接关闭时可能正在被广播 goroutine 读取,而 Close() 又要写 map,极易出现 concurrent map read/write panic。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
推荐做法是用 sync.Map 存 conn_id → *client,其中 *client 封装了 *websocket.Conn 和一个 done chan struct{}:
type client struct {
conn *websocket.Conn
done chan struct{}
}
<p>func (c *client) writePump(msg []byte) {
defer close(c.done)
for {
select {
case <-c.done:
return
default:
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
return // 自动清理,外部监听 done 即可移除
}
}
}
}
广播时遍历 sync.Map.Range(),对每个 *client select 判断 done 是否已关闭,没关才发;发完不等结果,避免阻塞。
连接 ID 必须全局唯一且可路由到具体节点
如果只用 uuid.NewString() 作为 conn_id,集群里就无法知道某连接在哪台机器上,自然没法做精准单播或踢人。需要让 ID 携带节点标识。
简单方案:启动时读取环境变量 NODE_ID(如 node-01),生成 conn_id 时拼接:fmt.Sprintf("%s:%s", os.Getenv("NODE_ID"), uuid.NewString())。
更健壮的做法是用 Redis 实现分布式注册:
- 连接建立后,用
SET ws:conn:{id} {node_id} EX 60注册,加NX防重 - 心跳时用
EXPIRE续期,断连或超时自动剔除 - 广播前先
KEYS ws:conn:*?不行——O(N) 扫描太伤。改用SCAN分批查,或直接依赖 Pub/Sub 全量广播(更常用)
真正容易被忽略的是连接重建场景:客户端重连时,旧 conn_id 未必及时失效,新连接要用新 id,同时服务端需主动通知其他节点“该 id 已迁移”,否则可能双写或丢消息。

















