gorilla/websocket保活需手动配置ping/pong:服务端必须设置ponghandler重置读超时,ping需独立goroutine调writecontrol并设写超时,读超时须每次readmessage后重置,且值要小于nginx等中间件空闲超时。

gorilla/websocket 的 Ping/Pong 保活必须手动配全,只设 SetKeepAlive(true) 或依赖浏览器自动心跳,连接大概率在 60 秒左右被 Nginx 静默断开——这不是代码 bug,是机制没对齐。
服务端必须显式注册 PongHandler 并重置读超时
默认情况下 gorilla/websocket 收到客户端 Ping 不会自动回 Pong,也不会更新连接状态。Nginx 看不到响应,空闲超时后直接 FIN 断连。
-
conn.SetPongHandler必须设置,哪怕只做一件事:conn.SetReadDeadline(time.Now().Add(pongWait)) - 不要在 handler 里做日志、DB 查询等耗时操作,它运行在 reader goroutine 中,卡住就整条连接挂死
- 如果业务层自己维护心跳计时器(如
c.HeartbeatTime),这里也得同步更新
发送 Ping 必须用独立 goroutine + WriteControl + 写超时
WriteMessage 不支持控制帧,必须用 WriteControl(websocket.PingMessage, nil, time.Now().Add(writeWait));且不能塞进主读循环,否则读卡住时心跳就停摆。
- 启动独立 goroutine:用
time.NewTicker(pingPeriod),别用time.AfterFunc递归调用(易 drift) - 每次
WriteControl前必须设conn.SetWriteDeadline,推荐5 * time.Second,超时立即conn.Close() -
pingPeriod建议设为pongWait * 0.9(例如pongWait = 55 * time.Second→pingPeriod = 25 * time.Second) - ticker 要绑定连接生命周期:连接关闭时必须
ticker.Stop(),否则 goroutine 泄漏
读超时不是设一次,而是每次 ReadMessage 后立刻重置
SetReadDeadline 是绝对时间点,不自动刷新。很多人设一次 60 秒后就不管了,结果心跳刚到、deadline 已过,下一次 ReadMessage 直接返回 i/o timeout。
- 每次成功
ReadMessage后(无论收到业务数据还是 Ping),都得立刻调conn.SetReadDeadline(time.Now().Add(pongWait)) - 这个值必须比中间设备的空闲超时更短:Nginx 默认
proxy_read_timeout 60s,你就得 ≤ 55s;ALB 默认 60s,你也得调低 - 别把
ReadDeadline和WriteDeadline设成同一个值,写超时建议单独设为10 * time.Second防卡死
连接关闭时 goroutine 泄漏比内存泄漏更致命
每个 WebSocket 连接至少对应 reader + writer + ping goroutine。一旦连接因网络闪断、客户端强杀进程关闭,而清理逻辑没覆盖 io.EOF、websocket.CloseAbnormalClosure、net.OpError,这些 goroutine 就永远卡在 [IO wait] 状态,无法 GC。
- reader 和 ping goroutine 共享同一
*websocket.Conn,但conn.Close()是并发安全的;关键是谁来触发、如何通知其他 goroutine 退出 - 推荐用
sync.Once+chan struct{}统一关闭信号:任一环节出错(写失败、读超时、解析错误),都走once.Do(closeFunc) - 务必检查错误类型:
os.SyscallError含ETIMEDOUT、ECONNRESET,net.ErrClosed,这些都要立即关连接并退出 goroutine
真正容易被忽略的,是读超时的刷新时机和中间件 timeout 的数值对齐——这两处一错,心跳逻辑写得再漂亮也白搭。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











