websocket在gin中必须用get路由,因握手依赖upgrade头;nginx需透传upgrade/connection头并设http/1.1;连接池须用互斥锁保护map;服务端需主动ping防超时断连。

WebSocket连接在Gin里必须用GET路由升级
WebSocket协议握手阶段本质是HTTP请求,只有GET方法能触发Upgrade: websocket头。如果你误用POST或PUT写路由,upgrader.Upgrade()会直接返回405 Method Not Allowed错误,且不报具体原因——这是新手最常卡住的第一步。
实操建议:
- 路由必须显式声明为
r.GET("/ws", wsHandler),不能用r.Any()或r.POST() - 前端连接URL必须是
ws://或wss://开头,不能是http://;否则浏览器会拒绝升级 -
upgrader.CheckOrigin默认拒绝跨域,开发时可临时设为return true,但上线前务必改成白名单校验,例如strings.HasSuffix(r.Host, ".yourdomain.com")
Nginx反向代理必须透传Upgrade和Connection头
如果Nginx没配对,WebSocket连接会在握手后立刻断开,现象是前端onopen触发但几秒内掉线,服务端日志可能只显示broken pipe或无任何错误。根本原因是Nginx默认把WebSocket当普通HTTP处理,关闭了长连接通道。
关键配置项(放在location /ws块内):
-
proxy_http_version 1.1:强制使用HTTP/1.1(Upgrade机制依赖于此) -
proxy_set_header Upgrade $http_upgrade:把客户端的Upgrade: websocket头透传过去 -
proxy_set_header Connection "upgrade":覆盖Nginx默认的Connection: close,告诉它别关连接 - 去掉
proxy_buffering on或显式设为off:避免Nginx缓存未完成的消息帧
连接池管理必须用指针+map+互斥锁,不能裸存*websocket.Conn
直接用map[string]*websocket.Conn存连接看似简单,但在并发读写时极易panic:goroutine A正在遍历map,goroutine B调用conn.Close()并delete该key,触发fatal error: concurrent map iteration and map write。
安全做法:
- 用
sync.RWMutex包裹map读写,所有store/load/delete操作都加锁 - map的value不要存
*websocket.Conn本身,改存封装结构体指针(如*Client),里面包含conn、userID、lastPing等字段 - 关闭连接时,先调用
conn.Close(),再从map中删除,最后close(sendChan)(如果用了channel) - 务必在
defer里做清理:比如defer deleteClient(id),而不是只靠read loop里的break
心跳检测必须服务端主动发ping,不能只依赖客户端
浏览器WebSocket API不保证自动发ping,而Nginx默认60秒无数据就断连(proxy_read_timeout)。如果只等客户端发ping,连接大概率被中间设备静默断开,且服务端无法感知。
正确实现方式:
- 在
Connection结构体里启动独立pingTicker,每25s调用一次conn.WriteMessage(websocket.PingMessage, nil) - 设置
upgrader.PongWait = 60 * time.Second,并启用SetPongHandler:收到pong时重置lastPong时间 - 在read loop里检查
time.Since(lastPong) > 90*time.Second,超时则break并清理连接 - 注意:
PingMessage内容必须为空字节,gorilla/websocket会自动处理,别传字符串
真正难的不是写通第一个连接,而是让成百上千个连接在高并发下不泄漏、不panic、不被中间件切断。每个map访问、每次conn.WriteMessage、每个Nginx超时参数,都得对着真实压测结果调。











