gorilla/websocket 连接不能直接广播,因为其 *websocket.conn 是单对单长连接,无内置广播能力;需手动维护连接集合并发安全地逐个发送,并做好连接状态检查、写超时、错误隔离与房间分组管理。

为什么 gorilla/websocket 的连接不能直接广播
因为 WebSocket 连接是单对单的 TCP 长连接,*websocket.Conn 本身不带广播能力。你调用 conn.WriteMessage() 只能发给这一个客户端。想群发,得自己维护连接集合,并逐个写——但必须注意并发安全和连接状态(比如对方已断开却还在尝试写,会 panic)。
常见错误现象:write tcp: use of closed network connection 或 panic: send on closed channel,基本都源于没检查连接是否活跃就盲目遍历发送。
- 永远用
conn.SetWriteDeadline()配合conn.WriteMessage(),否则阻塞写可能卡死 goroutine - 不要把
*websocket.Conn直接存进全局 map 并裸奔读写——用sync.Map或加sync.RWMutex - 每个连接应配一个单独的读/写 goroutine,读协程负责监听
conn.ReadMessage(),写协程从 channel 拉消息发出去
用 map[string]map[*websocket.Conn]bool 实现房间分组
房间本质就是一组活跃连接的集合。用两层 map 是最直白的做法:外层 key 是房间名(如 "lobby" 或 "room-123"),内层是连接指针到 bool 的映射(只用作存在性判断,不存值)。
关键点在于进出房间的原子性:加入时要同时注册连接、启动写协程;退出时要从所有房间移除、关闭写 channel、删 map 条目。漏掉任意一步都可能导致内存泄漏或 panic。
- 进房前先检查连接是否还活着:
conn.WriteMessage(websocket.PingMessage, nil)轻量探测 - 出房操作必须加锁,且要先从 map 删除,再关 channel——反过来容易导致写协程往已关闭 channel 发送
- 房间名建议限制字符集(如只允许字母、数字、短横线),避免被恶意构造为
../../etc/passwd类路径穿越(虽然这里不涉及文件,但习惯要养)
广播时如何避免“写失败拖垮整个房间”
一个连接写失败(比如网络中断),不应该让其他正常连接等它超时。理想做法是:对每个连接独立做写操作 + recover + 清理,不相互影响。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
典型反模式是用一个 for 循环遍历房间所有连接,中间某个 conn.WriteMessage() 阻塞或 panic,整个广播就停了。正确做法是并发写,但控制 goroutine 数量(别每条消息起 100 个)。
- 用
sync.WaitGroup等待所有写完成,但每个写逻辑包一层defer func(){recover()}() - 写之前设
conn.SetWriteDeadline(time.Now().Add(5 * time.Second)),超时即跳过该连接 - 写失败后立即从房间 map 中删除该连接,并调用
conn.Close()(如果还没关) - 别用
go conn.WriteMessage(...)无节制启协程——用带缓冲的 worker channel 控制并发度,比如最多 10 个并发写
客户端怎么通知服务端“我要进哪个房间”
WebSocket 没有内置路由,所以房间切换必须靠自定义协议。常见做法是在首次连接后,客户端立刻发一条 JSON 消息,例如:{"type":"join","room":"game-42"}。服务端解析后执行入房逻辑。
不能依赖 URL 路径分房(如 /ws?room=abc),因为浏览器 new WebSocket() 不支持动态 query 参数传递给服务端处理逻辑(Go 的 http.ServeHTTP 虽能拿到,但 gorilla/websocket 升级后就断开了请求上下文)。
- 服务端收到消息后,必须校验
room字段是否存在、长度是否合理(防爆破)、是否含非法字符 - 同一个连接重复 join 同一个房间,应幂等处理(不报错,也不重复添加)
- 建议在连接结构体里加字段记录当前所在房间(
currentRoom string),方便后续踢人、迁移等操作 - 广播时区分“全服广播”和“房间广播”:前者遍历所有连接,后者只遍历指定房间的 map
广播和房间的边界其实很薄——核心就三件事:连接生命周期管理、房间映射的增删查、写操作的容错控制。最容易被忽略的是连接状态检查时机:不是只在刚连上时做一次,而要在每次写之前都轻量探测,或者依赖 SetPingHandler 定期心跳反馈。否则看似在线的连接,实际早已哑火。










