gorilla/websocket是实现多房间websocket服务的唯一合理选择,因其封装了帧解析、心跳、并发读写等rfc 6455全流程能力;必须为每个房间维护独立clients sync.map,配专属写协程与channel,禁止全局map广播或直接writemessage。

gorilla/websocket 必须用于多房间,标准库不行
Go 标准库 net/http 只提供 Upgrade 方法,不处理帧解析、心跳、连接生命周期管理——你得自己实现 RFC 6455 全流程,实际项目中没人这么干。用 gorilla/websocket 是唯一合理选择,它封装了并发读写、错误恢复、ping/pong 自动响应等关键能力。
常见错误:http: response.WriteHeader on hijacked connection,本质是误在已升级的连接上调用了 WriteHeader 或 Write。正确路径只有:用 upgrader.Upgrade() 拿到 *websocket.Conn,之后所有通信只走它。
-
CheckOrigin函数必须设置,开发时可临时返回true,上线必须校验 Referer 或 Origin -
SetReadDeadline和SetWriteDeadline建议设为 30 秒以上,避免网络抖动触发误断连 - 关闭连接前,先发
websocket.CloseMessage,再调conn.Close(),否则客户端可能收不到关闭通知
每个房间必须独立维护 clients sync.Map,不能共用全局 map
房间隔离不是靠“过滤消息”,而是靠“路由前就限定连接集合”。把所有 *websocket.Conn 塞进一个全局 map 然后广播,结果就是 A 房间的消息被 B 房间用户收到——这是最常踩的坑。
每个房间应是一个独立结构体,至少含:id string、clients sync.Map(key 为 *websocket.Conn,value 可为 struct{})、mu sync.RWMutex(用于安全遍历)。
- 客户端连接 URL 带
room=xxx参数,服务端用r.URL.Query().Get("room")提取,再查或创建对应房间实例 - 禁止在
conn.ReadMessage()循环里直接调conn.WriteMessage()给他人——这会阻塞读协程,且无法应对部分连接卡死 - 房间广播时,只把消息 send 到每个 client 的专属
writeCh chan []byte,不直接调WriteMessage()
每个 *websocket.Conn 必须配专属写协程,否则 panic: concurrent write
*websocket.Conn.WriteMessage() 不是并发安全的。多个 goroutine 同时调用它(比如广播时遍历所有 conn 并写),只要其中某个 conn 正在读(如处理心跳),就会触发 panic: concurrent write to websocket connection。
解决方式是给每个连接配一个写协程 + channel:
- client 结构体里定义
writeCh chan []byte,容量建议 16~32(太小易丢,太大占内存) - 写协程用
defer conn.Close()+recover()包裹,防止 panic 导致连接泄漏 - 连接关闭时必须
close(client.writeCh)并 stop 写协程,否则 goroutine 和 channel 都会泄漏 - 广播时用
select { case client.writeCh 非阻塞发送,失败则清理该 client
token 绑定 room/user,消息字段必须服务端注入
前端传的 room、username 字段完全不可信。攻击者改个 URL 或 WebSocket 消息就能伪造身份、跳转房间、冒充管理员。
正确做法是:HTTP 升级前走一次 /join?token=xxx&room=xxx 接口签发短期 token(含 room_id、user_id、exp),升级时带上 token,服务端解析并绑定身份到 conn 上下文。
- 客户端第一帧必须是认证消息,服务端验证通过才允许加入房间;否则直接
conn.Close() - 后续消息用 JSON 格式,但只信任服务端注入字段:
room_id(来自 token)、user_id(同上)、content(客户端提供,长度限制 ≤512 字节) - 禁止客户端在消息里传
room、to、from等路由字段——这些全由服务端根据 token 和上下文决定 - 空闲房间需定时检查:当最后一个 client 断开,立刻从全局
rooms map[string]*Room中 delete 对应房间,否则内存持续增长
房间清理逻辑容易被忽略:它必须发生在 unregister 的真正执行点(即 hub goroutine 中),且要加锁判断 clients 数量是否归零——这里竞争条件多,不 careful 就漏删或误删。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











