网页能打开,聊天消息、远程终端或实时通知却一直转圈?先在浏览器开发者工具的 Network 面板筛选 WS:**连接从未建立,和建立后一段时间断开,是两类问题。**先记录握手状态与断开时间,再改 Nginx 配置。
本文针对 Linux VPS 上常见的 HTTP/1.1 Upgrade 反代链路:浏览器 → HTTPS/WSS 入口 → Nginx → 本机应用。示例假设后端监听 127.0.0.1:3000,路径为 /ws/;域名、端口和路径都要按实际应用替换。
HTTP/1.1 的 WebSocket 升级成功通常返回 101 Switching Protocols,之后才开始传输 WebSocket 帧,协议结构见 RFC 6455。不要把普通 HTTP 200 当成握手成功。
| 现象 | 优先检查 | 下一步 |
|---|---|---|
| 浏览器提示 Mixed Content,尚无握手 | HTTPS 页面使用了 ws:// | 使用正确的 wss:// 地址 |
| 握手返回 200 或 404 | 请求打到首页、错误路由或路径被改写 | 核对最终 URL 与 proxy_pass |
| 返回 301/302 | 域名、斜杠、HTTPS 或登录跳转 | 直接请求正确的最终端点 |
| 返回 400/426 | Upgrade 头、协议版本或应用要求 | 对照代理与应用日志 |
| 返回 401/403 | 登录状态、Origin、子协议或 WAF | 找到具体拒绝请求的一层 |
| 返回 502 | 后端未启动、监听地址或端口错误 | 先确认 Nginx 能连接后端 |
| 先 101,空闲一段时间后断开 | 读取超时、心跳或前置代理限制 | 记录间隔,检查每一层超时 |
同一状态码可能来自不同组件。把请求发生时间、端点路径和应用日志对齐,比只看浏览器一句 connection failed 更有用。包含 Cookie、令牌的完整握手不要公开粘贴。
生产网页通过 HTTPS 打开时,应连接有效证书保护的 wss:// 端点,而不是公网 ws://。MDN 客户端指南说明了安全连接与客户端事件的用法。
可在实际前端页面的开发者工具中观察:
const ws = new WebSocket("wss://ws.example.com/ws/");
ws.addEventListener("open", () => console.log("connected"));
ws.addEventListener("message", event => console.log("received", event.data));
ws.addEventListener("close", event => console.log("closed", event.code, event.wasClean));
ws.addEventListener("error", () => console.log("check Network and server logs"));
代码只是诊断示例。真实服务如果需要登录、子协议或专用客户端,应保留原来的认证流程;不要把后台密钥写进浏览器控制台。普通浏览器 WebSocket 构造器也不能像 fetch 一样随意设置 Authorization 请求头。
证书域名必须匹配端点。网页本身 HTTPS 正常,不代表另一个 WebSocket 子域的证书、解析和回源也正常。若握手绕进登录页或循环跳转,可继续看 网站重定向排查。
NGINX WebSocket 文档指出,Upgrade 和 Connection 是逐跳头,反向代理需要显式传递升级意图。
先把下面的 map 放在 http {} 内,位于 server {} 外;如果项目已有等价 map,复用它,不要重复声明同名变量:
map $http_upgrade $ws_connection_upgrade {
default upgrade;
'' close;
}
再把下面的 location 合并到已有 HTTPS server 中,保留原证书和其他站点配置:
location /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $ws_connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
此示例显式指定后端 HTTP/1.1,并在普通请求没有 Upgrade 时使用 Connection: close。不要对整个网站硬塞 Connection: upgrade。这里的 90 秒是示例值,后面仍要结合心跳与前置网关调整。
路径尤其容易弄错:此处 proxy_pass 没有末尾斜杠,所以 /ws/echo 到后端仍是 /ws/echo。加上斜杠可能替换 location 前缀,见 NGINX proxy_pass 定义。如果应用实际只接受 /socket,不能靠照抄 /ws/ 就让两边匹配。
上层还有负载均衡器时,$scheme 代表 Nginx 当前收到的协议,未必等于浏览器外部协议。按真实的可信代理链传递协议,避免应用生成错误地址;不要无条件信任公网客户端自报的转发头。
修改前备份站点配置。检查实际生效文件,语法通过后再加载:
sudo nginx -t
sudo systemctl reload nginx
这组配置用于普通 Nginx。面板、Ingress 或容器内 Nginx 可能从别处生成配置,应该修改它们真正采用的来源。
下面发送一个不含私密凭据的握手请求。Sec-WebSocket-Key 使用协议文档中的示例值,只用于诊断:
curl --http1.1 -i --max-time 5 'https://ws.example.com/ws/' \
-H 'Origin: https://app.example.com' \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=='
检查是否返回 101、Upgrade、Connection 和正确的 Sec-WebSocket-Accept。命令限定 5 秒,连接成功保持打开时也可能在到时后报 curl 超时,不能仅凭最后一行就认定握手失败。不要用 -L 隐藏跳转链,也不要用 -k 的成功证明证书配置正常。
curl 的这次请求只帮助观察握手,不等于验证了完整消息协议。再用原应用客户端发送、接收一条真实消息,并检查重连后业务是否恢复。
Socket.IO 还包含自己的路径、版本、心跳和消息协议,不能把它当成任意原生 WebSocket 服务。使用对应客户端,并对照 Socket.IO 反代文档核对路径与超时。
proxy_read_timeout 文档说明,超时针对两次读取之间的间隔,而不是连接的总寿命。后端长期没有数据,代理可能断开;因此“每次大约同样的空闲时长后掉线”比“网络不好”更具体。
协议级 Ping/Pong 与应用自己的 JSON 心跳不是同一个东西。浏览器原生 WebSocket API 没有直接发送协议 Ping 的方法;可由服务端发送 Ping,或按应用既有协议设计有回包的心跳。只让客户端不断发数据、后端从不回应,不一定能重置代理的后端读取计时。
先核对心跳周期、允许的响应等待时间和整条链路最短的空闲限制,再设置代理超时。不要直接拉到几天来掩盖后端卡死;还要考虑无效连接占用资源。Socket.IO 等框架应按其实际配置计算,不能照抄别人的默认秒数。
若经过 Cloudflare,也要检查对应域名的 WebSocket 设置和安全规则。Cloudflare 官方说明提醒,基础设施更新等情况可能中断已有连接,客户端仍需具备合理的重连机制。只看源站 Nginx 超时不够。
原生 WebSocket 不是给 fetch 加几个 Allow-Origin 响应头就能修好的请求。服务端仍应校验握手 Origin 和身份,不能为了让连接成功就放行所有来源。涉及普通 HTTP API 的跨域问题,可以分别对照 CORS 排查指南。
握手 403 时,检查后端允许的前端来源、登录 Cookie 是否发送、应用需要的子协议和前置安全网关。逐层比较响应,不要把正常身份验证全部关闭。
浏览器显示关闭码 1006 时,只能说明连接异常关闭、没有收到正常关闭帧;它不是服务端通过关闭帧发送的业务错误码,见 MDN CloseEvent code。继续查看最后一条消息时间、后端重启/OOM、代理日志和 CDN 事件,不能仅凭 1006 判断是防火墙或 Nginx。
先确认握手确实升级成功,路径、Origin 和身份信息符合应用要求;再验证消息能够双向传输;最后让连接跨过预期空闲周期,并主动断网恢复,检查重连与会话续接。
本文已在本机隔离回环环境中,用现有 Nginx 1.19.6 和模拟后端完成 6 项检查:握手与转发头、文本帧回传、普通 HTTP 请求、拒绝来源、错误路径、错误版本。其中普通请求返回 200,拒绝来源返回 403,错误路径返回 404,错误版本返回 426。测试验证了握手后的一个文本帧往返,没有验证 TLS、CDN、浏览器、长时间空闲或真实登录会话。先定位失败发生在建立前还是建立后,再修改对应层的配置。
