gin本身不内置websocket支持,必须依赖gorilla/websocket完成连接升级和收发;用错upgrader配置或漏掉defer conn.close()会导致连接堆积、内存暴涨、goroutine泄漏。

直接说结论:Gin 本身不内置 WebSocket 支持,必须靠 gorilla/websocket 完成连接升级和收发;用错 Upgrader 配置或漏掉 defer conn.Close(),上线后会立刻出现连接堆积、内存暴涨、goroutine 泄漏。
为什么 Gin 的路由不能直接写 conn.ReadMessage()
Gin 的 Context 是 HTTP 生命周期绑定的,一旦响应写出(包括升级成功),c 就失效了。WebSocket 连接需要脱离 HTTP 上下文长期存活,所以必须在 Upgrade 后立刻把控制权交给原生 *websocket.Conn 对象,不能再依赖 c 做任何操作。
常见错误现象:
- 调用
c.Writer.Write()或c.JSON()报http: response.WriteHeader called multiple times - 连接建立后秒断,日志里反复出现
use of closed network connection
正确做法是:
- 只用
upgrader.Upgrade(c.Writer, c.Request, nil)升级一次 - 升级成功后,所有读写都走
conn.ReadMessage()/conn.WriteMessage() - 务必在函数开头加
defer conn.Close(),哪怕后面还有循环逻辑
CheckOrigin 在开发和生产环境怎么设才安全
CheckOrigin 不是可选项,是防止跨站 WebSocket 劫持的关键防线。开发阶段设为 return true 图省事,但上线前必须改掉。
典型错误配置:
-
CheckOrigin: func(r *http.Request) bool { return true }—— 生产环境等同于裸奔 -
CheckOrigin: func(r *http.Request) bool { return r.Header.Get("Origin") == "https://myapp.com" }—— 忽略端口、协议、子域名变化
推荐写法(支持多域名+端口):
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
switch origin {
case "https://app.example.com", "https://admin.example.com:8080":
return true
default:
return false
}
}
注意:Origin 头可被伪造,它只是第一道过滤,不能替代业务层身份校验。
单连接 echo 示例跑通了,但多人聊天为啥收不到消息
因为每个 conn 是独立对象,conn.WriteMessage() 只发给当前连接,不会广播。想实现群聊,必须自己维护连接池。
容易踩的坑:
- 用
map[*websocket.Conn]bool存连接,但没加锁 —— 并发读写 panic - 客户端断开时只删 map,没关
conn—— 文件描述符泄漏 - 广播时遍历 map 同时有新连接注册/断开 ——
fatal error: concurrent map iteration and map write
最小可行方案:
- 用
sync.RWMutex包裹 map 读写 - 注册/注销走 channel(如
register chan *Client),由一个 goroutine 统一处理 - 每个 client 持有独立
Send chan []byte,广播时往所有 channel 发,避免阻塞主循环
HTTPS 站点下 WebSocket 连接被浏览器拒绝
浏览器强制要求:HTTPS 页面只能建立 wss:// 连接,HTTP 页面只能用 ws://。如果后端只启了 HTTP 服务,前端却用 new WebSocket("wss://..."),控制台会报 ERR_CONNECTION_REFUSED 或 net::ERR_INSECURE_RESPONSE。
解决方案分两步:
- 后端启动 HTTPS:用
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", router),不是 Gin 的r.Run() - 前端 URL 必须匹配:Nginx 反向代理时,确保
proxy_pass https://backend,且 WebSocket Upgrade 头透传:
location /ws {
proxy_pass https://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
漏掉 Connection "upgrade" 这一行,Nginx 会当普通 HTTP 请求处理,后端收不到 Upgrade 请求头,upgrader.Upgrade() 直接失败。
最常被忽略的一点:WebSocket 连接生命周期远长于 HTTP,所有涉及数据库连接、缓存 client、JWT 解析的中间件,都不能复用 HTTP 请求那一套上下文传递方式;每个连接要单独做鉴权、绑定用户 ID,并在断开时清理资源。这不是“加个 WebSocket 路由”就能完事的事。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











