必须显式校验origin,生产环境严禁return true或不配置checkorigin;需用精确白名单比对(如https://app.example.com),禁用前缀匹配;同时应设置readbuffersize和writebuffersize为65536以降低gc压力。

gorilla/websocket.Upgrader 配置必须显式校验 Origin
生产环境不设 CheckOrigin 或直接返回 true,等于把 WebSocket 接口暴露给任意域名——攻击者可伪造 Origin 发起连接,劫持用户会话或发起 CSRF 式广播。这不是“开发阶段才管”的事,而是上线前必须堵死的入口。
正确做法是白名单校验,比如只允许前端部署在 https://app.example.com 和 https://staging.app.example.com:
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return origin == "https://app.example.com" ||
origin == "https://staging.app.example.com"
},
}
- 不要用正则或字符串前缀匹配(如
strings.HasPrefix(origin, "https://example.com")),易被绕过 - 若前端走 CDN 或多子域,建议统一提取 Host 或从 JWT claim 中解析可信 domain
- 开发时可用
os.Getenv("ENV") == "dev"区分环境,但禁止硬编码return true
ReadBufferSize / WriteBufferSize 不设默认值会放大内存压力
gorilla/websocket 默认 ReadBufferSize 和 WriteBufferSize 均为 0,触发 runtime 按需分配小缓冲区(通常 4KB 以下)。高并发下每连接都 malloc 小块内存,GC 压力陡增,且频繁拷贝导致 CPU 占用飙升。
实测中,设为 65536(64KB)能显著降低分配频次和 GC pause:
var upgrader = websocket.Upgrader{
ReadBufferSize: 65536,
WriteBufferSize: 65536,
}
- 若业务消息普遍
- 缓冲区大小不影响协议帧解析逻辑,只影响底层
io.ReadFull和bufio.Writer行为 - 别盲目设过大(如 1MB),单连接内存占用翻倍,连接数上万时 OOM 风险剧增
Upgrade 后必须立刻设 ReadDeadline / WriteDeadline
没设 deadline 的 *websocket.Conn 在 ReadMessage() 或 WriteMessage() 上可能永久阻塞——网络抖动、客户端假死、NAT 超时都会让 goroutine 卡住,最终耗尽 stack 内存或触发调度器饥饿。
典型错误写法:升级完就开 goroutine 读消息,却不设超时:
// ❌ 危险
go func() {
for {
_, msg, err := conn.ReadMessage()
if err != nil { /* 忽略错误处理 */ }
// ...
}
}()
正确姿势是每次读/写前重置 deadline(尤其读操作):
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
_, msg, err := conn.ReadMessage()
if err != nil {
if !websocket.IsCloseError(err, websocket.CloseAbnormalClosure) {
log.Printf("read error: %v", err)
}
return
}
// 处理消息后,再次设置下一轮读超时
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
-
SetReadDeadline必须在每次ReadMessage前调用,不能只设一次 -
SetWriteDeadline可在发送前设,但广播场景建议按消息粒度设(避免单条慢写拖垮整组连接) - 别依赖
PingHandler自动保活——它只响应 Ping,不解决读阻塞问题
连接管理别用全局 map + RWMutex 抗压
当连接数突破 2000,用 map[*Client]bool 加一把 sync.RWMutex 管理连接,锁竞争会成为明显瓶颈:广播时遍历 map 需读锁,注册/注销需写锁,QPS 直线下降。
更可行的方案是分片 + 无锁通道:
- 按 client ID hash 分 16 或 32 个
ConnectionShard,每个 shard 独立锁 - 广播改用 fan-out 模式:消息先发到中心
broadcastchannel,再由各 shard goroutine 分发到本片连接 - 连接注销走异步队列,避免 write lock 阻塞高频读操作
关键不是“要不要用 map”,而是“能不能避免所有连接共享同一把锁”。单机扛 5000+ 连接没问题,但想冲 1w+,分片或 ring buffer 是绕不开的一步。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











