WebSocket 与 Gin 集成要点(升级/心跳)
一句话结论:Gin 不内置 WebSocket,常规做法是在 Gin handler 里用 gorilla/websocket 这类库完成 HTTP→WS 的 101 升级(gin 的 ResponseWriter 实现了 http.Hijacker,可被直接升级);升级后原 *gin.Context 不能再用于响应。要点是:升级前完成鉴权与同源校验、升级后“一个读 goroutine + 一个写 goroutine”且写必须串行、用 ping/pong + 读写超时做心跳保活与断线检测、代理层透传 Upgrade 头。
要点
- 升级:upgrader.Upgrade(c.Writer, c.Request, nil);升级前校验 Origin(CheckOrigin)与子协议,做登录鉴权(读 cookie/header/token)。
- 浏览器 WebSocket API 无法自定义 Header:token 只能走 query 参数、cookie 或 Sec-WebSocket-Protocol 子协议,所以“先鉴权后升级”顺序很重要。
- 读写并发:一个连接同时最多一个 Reader、一个 Writer;写侧多协程要收敛到一个 channel + writePump(gorilla 官方 chat 示例即此模式),否则需加写锁。
- 心跳:服务端定期发 Ping,客户端回 Pong;服务端 SetReadDeadline 并在 SetPongHandler 里续期,超时即认为死链并 Close。
- 反向代理(nginx)必须透传 Upgrade/Connection 头,否则升级成不了 101。
- 资源治理:连接数上限、单连接消息大小(SetReadLimit)、消息频率限制、退出时关闭连接并清理在线表,防连接与 goroutine 泄漏。
升级与心跳骨架
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true }, // 生产按来源校验
}
r.GET("/ws", func(c *gin.Context) {
token := c.Query("token") // 先鉴权
if !authOK(token) { c.JSON(401, gin.H{"message": "unauthorized"}); return }
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) // 之后勿再用 c
if err != nil { return }
go readPump(conn) // 读 + SetPongHandler 续期
go writePump(conn) // 写 channel,串行发送
})
// 心跳:每 pingPeriod 发 Ping,读侧超时即关闭
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error {
return conn.SetReadDeadline(time.Now().Add(pongWait))
})
追问记忆点
- 追问:为什么升级后不能再用 gin.Context?——连接已被 Hijack 接管,之后读写都走 net.Conn/websocket.Conn,再写 c 会 panic 或无效。
- 追问:nginx 代理下 WebSocket 断线频繁是什么原因?——多半是没配 Upgrade 头透传,或代理 idle timeout 比心跳周期短。
- 追问:心跳只发 ping 够吗?——不够,还要配 Pong 处理与读超时,ping 只是探测,超时关闭才能回收死连接。
- 记忆点:升级(Hijack/101)→ 鉴权前置 → 读写单飞 + 心跳超时 → 代理透传 Upgrade 头 → 限流与连接治理。