发布时间:2026-09-22 04:32 更新时间:2026-09-22 04:32 阅读量:0
做聊天室、在线客服、消息推送或者行情看板的朋友,大概率踩过这个坑:本地直连后端 WebSocket 一切正常,一挂到 Nginx 反代后面,连接就开始莫名其妙地断,短则几秒,长则一两分钟,前端只能不停重连。多数情况下问题不在后端代码,而在代理层少传了几个头、或者某个超时参数用的是默认值。这篇文章把原理和配置拆开讲,照着改基本能解决。
WebSocket 的握手过程本质上是一个带 Upgrade: websocket 头的 HTTP 请求。浏览器发出请求后,Nginx 作为反向代理默认把它当成普通 HTTP 请求处理:它会照常转发请求行和大部分请求头,但 Upgrade、Connection 这类逐跳(hop-by-hop)头在 HTTP/1.1 规范里是不应该被代理无条件透传的,所以 Nginx 默认不会原样带过去。后端拿不到 Upgrade 头,就不会返回 101 Switching Protocols,连接只能退化成普通 HTTP 响应,前端自然连不上。
更隐蔽的一种情况是握手成功了,但连接过一会儿就断。这通常和两个因素有关:一是 proxy_read_timeout 默认 60 秒,如果这段时间内后端没有向下游发任何数据,Nginx 会主动掐断连接;二是后端或中间的负载均衡设备有自己的空闲超时。推送类应用往往长时间没有消息下行,正好撞在超时上。
推荐的做法是在 http 块里用 map 指令判断连接类型,再在 location 里条件化地设置 Connection 头。这样同一个 server 既能代理普通 HTTP,又能正确代理 WebSocket,不用为每种协议单独开端口。
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream ws_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 80;
server_name ws.example.com;
location /ws/ {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 10s;
proxy_buffering off;
}
}
}
几个点值得说明。map 的作用是:当请求头里带 Upgrade 时,$connection_upgrade 取 upgrade;普通请求取 close,避免长连接语义被错误放大。proxy_http_version 必须写成 1.1,HTTP/1.0 不支持 Upgrade 机制。proxy_buffering off 对实时性要求高的场景有用,能减少 Nginx 缓冲带来的延迟,但也会增加后端压力,消息量不大时开着也无妨,具体看业务形态。
超时值不要无脑写 3600s。如果后端和客户端都没有心跳,连接可能被中间的防火墙、云负载均衡静默回收,写再大的超时也没用。合理做法是让应用层每 30 秒左右发一次 ping/pong,超时值设成心跳周期的几倍即可。
配置改完先执行语法检查,再平滑重载,避免直接 restart 造成连接抖动:
nginx -t
nginx -s reload
接下来用命令行工具模拟一次握手,看返回码是不是 101。这里用 curl 带上握手所需的头部,注意 curl 本身不完整支持 WebSocket 会话,但用来验证握手阶段足够:
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
http://ws.example.com/ws/
如果返回 101 Switching Protocols,说明头部传递没问题;如果返回 200 或 400,基本可以断定 Upgrade 头没到位,回头检查 map 是否写在了 http 块内、location 是否覆盖了实际路径。要观察长连接是否被中途掐断,可以看 Nginx 的 error.log,超时断开会留下类似 upstream timed out 的记录;同时配合 ss -tnp 观察 ESTABLISHED 连接的数量变化,判断是客户端主动断开还是代理侧断开。不同版本 Nginx 的日志措辞略有差异,以实际输出为准。
还有两个容易忽略的坑。其一,如果 Nginx 前面还有一层云厂商的负载均衡,那一层通常也有空闲超时,需要一并调大或开启会话保持,否则只改 Nginx 不解决问题。其二,upstream 里配了 keepalive 之后,必须同时设置 proxy_set_header Connection "" 来清空该头,否则到后端的连接复用会失效——这一点和 WebSocket 的 Connection: upgrade 是两种不同场景,不要混用同一套头配置。
WebSocket 经 Nginx 反代断连,九成问题出在三个地方:Upgrade 与 Connection 头没有正确透传、proxy_read_timeout 用的默认值、应用层缺少心跳。把 map 变量、proxy_http_version 1.1 和合理的超时配好,再用 curl 验证一次握手返回码,基本就能定位。下一步建议给应用补上 ping/pong 心跳,并检查云负载均衡那一层的空闲超时设置,让整条链路的心跳周期保持一致,连接才会真正稳定。
| 📑 | 📅 |
|---|---|
| systemd timer 替代 crontab 实战:OnCalendar、随机延迟与失败重试 | 2026-09-22 |
| Docker容器时间漂移与crond定时任务错乱:TZ与宿主时间源协同 | 2026-09-21 |
| MySQL 授权与远程访问配置实战:user@host 匹配规则、bind-address 与防火墙三层放行检查 | 2026-09-21 |
| Nginx与PHP上传目录权限:www-data、umask与0777的坑 | 2026-09-21 |
| systemd-resolved 与 /etc/resolv.conf 冲突排查 | 2026-09-21 |
| 服务器被DDoS打满带宽:自查连接分布与限速缓解 | 2026-09-22 |
| Linux磁盘IO高却查不出:iotop与blkio限速实战 | 2026-09-22 |
| Nginx WebSocket 与 SSE 并存:proxy_buffering 冲突排查 | 2026-09-22 |
| MySQL备份文件损坏怎么验证:一致性校验与恢复演练 | 2026-09-22 |
| Linux 改完 fstab 重启起不来:UUID 混用与救援恢复 | 2026-09-22 |