根本原因是nginx默认60秒proxy_read_timeout主动关闭空闲websocket连接;需配置proxy_read_timeout、proxy_send_timeout、三个关键header(upgrade/connection/http/1.1)及go服务端主动心跳。
go websocket 服务在 nginx 反向代理下频繁断连,根本原因不是代码问题,而是 nginx 默认把长连接当“空闲 http 连接”给主动关了。
为什么 proxy_read_timeout 设成 60 秒就会断连
Nginx 的 proxy_read_timeout 控制的是「从 upstream(Go 服务)读取响应的超时时间」。对 WebSocket 来说,握手成功后就没有“HTTP 响应”了,整个连接进入数据帧双向传输阶段。Nginx 如果在该时间内没从 Go 服务收到任何数据(比如心跳、消息),就认为上游挂了,直接关闭连接——客户端立刻收到 EOF 或 WebSocket is closed 错误。
- 默认值是
60,这就是为什么“静默 60 秒必断”成为最常见现象 - 设成
3600或86400只是治标;真正稳定需配合proxy_send_timeout和心跳机制 - 注意:这个 timeout 不影响客户端发包,只影响 Nginx 等待后端发包的时间
必须配置的三个 header:Upgrade、Connection、HTTP/1.1
Go 的 gorilla/websocket 或 gobwas/ws 都严格校验握手头。Nginx 若未透传或重写关键字段,Go 服务会直接拒绝升级,返回 400 Bad Request 或静默关闭,浏览器控制台报 Handshake failed due to invalid Upgrade header。
-
proxy_http_version 1.1:WebSocket 强制要求 HTTP/1.1,Nginx 默认可能用 1.0 转发 -
proxy_set_header Upgrade $http_upgrade:把客户端带的Upgrade: websocket原样传给 Go -
proxy_set_header Connection $connection_upgrade:不能硬写"upgrade",必须用map动态判断,否则非 WebSocket 请求(如普通 AJAX)会被错误 upgrade
对应必须在 http 块里加:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
Go 服务端要不要发心跳?必须发,且要早于 Nginx 超时
Nginx 不会主动发 ping,它只被动等数据。如果 Go 服务不发心跳,仅靠客户端单方面 ping,Nginx 可能因 proxy_read_timeout 触发断连(它收不到服务端回的 pong)。
- 推荐在 Go 服务中启用
websocket.DefaultWriteWait = 30 * time.Second,并每 25 秒主动conn.WriteMessage(websocket.PingMessage, nil) - 同时设
conn.SetPongHandler处理客户端 ping,避免连接被误判为僵死 - 客户端也要配对应心跳间隔(比如 30s ping,超时 45s 断连),和 Nginx 的
proxy_read_timeout形成梯度:Go 心跳 proxy_read_timeout
HTTPS 场景下容易漏掉的 SSL 相关配置
用 wss:// 时,若 Nginx 终止 SSL,但没透传原始协议信息,Go 服务可能生成 ws:// 的重定向链接或拒绝连接。
- 务必加
proxy_set_header X-Forwarded-Proto $scheme,让 Go 知道外层是 HTTPS - 如果 Go 服务依赖
Host头做路由或 CORS 判断,补上proxy_set_header Host $host - SSL 证书链不全、OCSP stapling 失败也可能导致握手卡在 TLS 层,表现为“连接无响应”,而非明确错误
真正棘手的从来不是配置项本身,而是这些参数之间的协同节奏:header 决定能否握手,timeout 决定能活多久,心跳决定是否被误杀。少一个环节,连接就在某个深夜悄无声息地断掉。











