Nginx转发WebSocket:握手与掉线排查
聊天、实时日志或控制面板的网页能打开,实时数据却不更新,要单独检查 WebSocket。普通 HTTP 请求成功,不能证明协议升级和后续长连接正常。
本文以 Ubuntu 24.04 的系统 Nginx 为例:应用在同机 127.0.0.1:3000提供原生 WebSocket,实际端点为 /ws,公网域名为 app.example.com。请替换端口、路径和域名。Socket.IO 等使用自己的协议及路径,不能直接当作这里的原生接口测试。面板接管 Nginx 的站点应在对应配置入口修改,避免维护两份配置。
第一关:后端本身能不能完成握手
先在服务器终端直接请求后端。这里的固定 Key 只是协议测试值,不是登录密码:
curl -q --noproxy '*' --http1.1 -i -N --max-time 5 \
-H 'Host: app.example.com' \
-H 'Origin: https://app.example.com' \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: YmFvbGV5dW4td3MtdGVzdA==' \
'http://127.0.0.1:3000/ws'
本例的 HTTP/1.1 升级成功时,应先看到 101 Switching Protocols及相应升级头。连接保持打开后,curl 可能在 5 秒到达上限时报超时,这与一开始就连不上不同。该命令只看握手,不能证明消息收发和业务可用。
接口要求登录、特定 Origin 或子协议时,缺少条件可能得到 401、403、400。按应用真实客户端核对这些条件,不为测试关闭认证,也不要把真实 Cookie、令牌贴进公开命令记录。本机握手失败时先处理应用、路径和访问要求,不急着改 Nginx。
第二关:给正确的路径转发协议升级
先保存当前站点配置。在 Nginx 的 http层放置下面的映射;它不属于 server 或 location 内部。已有同名变量定义时核对并复用,不重复声明:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
再在实际提供 HTTPS 的目标 server 中加入或调整这一段。下面只匹配 /ws端点,不接管其他网站路径:
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 $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 75s;
}
Upgrade 和 Connection 需要显式传给后端;这里显式使用 HTTP/1.1,避免不同版本默认值差异。proxy_pass没有附加 URI,后端仍收到 /ws。如果应用实际使用 /socket/,要一起调整匹配和检查地址,不靠追加斜杠碰运气。
本例由当前 Nginx 结束 HTTPS,应用只应信任这个明确的代理来源。前面另有 CDN 或负载均衡时,需要重新核对协议头和可信代理范围,不能直接相信任意客户端传来的转发头。云侧无需额外公开 3000 端口。
sudo nginx -t && sudo systemctl reload nginx
配置检查失败便停止,处理报错后再重载。宝塔管理的站点可参照宝塔反向代理教程,不要另行启动一套系统 Nginx。
从公网入口再测一次
HTTPS 页面应连接 wss://app.example.com/ws。下面用 HTTPS 发起同一套升级请求,检查证书和公网转发;不添加 -k跳过证书验证:
curl -q --http1.1 -i -N --max-time 5 \
-H 'Origin: https://app.example.com' \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: YmFvbGV5dW4td3MtdGVzdA==' \
'https://app.example.com/ws'
| 结果 | 接下来查什么 |
|---|---|
| 200,内容是网页HTML | 可能进入首页、错误站点或前端回退,而不是WebSocket端点。 |
| 301、302、307、308 | 核对协议、主域名和末尾斜杠,直接使用最终正确的WebSocket地址。 |
| 400、426 | 检查升级头、协议版本、子协议及应用的具体拒绝原因。 |
| 502 | 回到本机监听与握手,并检查目标地址和应用日志。 |
| 101后固定间隔断开 | 检查各层空闲超时、应用心跳及服务是否重启。 |
握手成功后,才检查空闲与心跳
proxy_read_timeout衡量的是从后端连续两次读取数据之间的等待时间,并非整条连接的总寿命。本例设为 75 秒。应用应按合理间隔向客户端发送心跳并检查应答;仅有客户端发包、后端长期不返回数据,不能保证这个读取超时被刷新。
前置代理、应用本身和网络设备也可能有不同的空闲限制。记录断开的时间、状态和关闭原因,结合站点日志比较,不把超时无限加大当成通用修复。WebSocket 的 Origin 校验与普通 fetch 的 CORS 预检不同,增加一组允许跨域头不能代替应用的 Origin 和身份检查。
最后用真实应用完成一次双向消息收发,空闲超过原先容易掉线的时段,再检查心跳、恢复连接和登录状态。看到 101 只是握手验收,不能代替这一步。