用 Nginx 反向代理调试 WebSocket 连接的常见配置问题
最近在折腾一个带实时推送的小项目,后端 WebSocket 直连一切正常,一挂到 Nginx 反向代理后面就各种抽风:要么握手直接失败,要么连上了过一分钟准断。翻了半天日志才发现,这玩意儿的坑其实就那么几个,但每一个都挺隐蔽的,不踩一遍真不知道。下面把这次踩过的坑整理一下,都是实测可用的配置,省得下次再对着报错一脸懵。
最经典的坑:Upgrade 头没透传
WebSocket 握手本质上是靠 HTTP 的 Upgrade 和 Connection 这两个头来完成的,而 Nginx 反代默认是不把这俩头传给后端的。所以你直连后端能通、走代理就 400 或者一直 pending,八成就是这个原因。
最简单的写法是直接写死:
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
但这样有个小问题,这条 location 里所有请求都会带上 Connection: upgrade,哪怕人家只是个普通 HTTP 请求。更标准的做法是用一个 map,根据客户端有没有带 Upgrade 头来决定传什么:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
然后 proxy_set_header Connection $connection_upgrade; 就行了。注意 map 得放在 http 块里,塞进 server 里面 Nginx 会直接报错说这里不允许 map,别问我怎么知道的。
连上了但一分钟就断:proxy_read_timeout
这个坑更阴。表现是这样的:握手成功了,页面也能收到消息,但只要中间 60 秒没有数据往来,连接就悄悄断了,前端也不一定报错,就是再也收不到推送了。原因是 Nginx 默认的 proxy_read_timeout 是 60 秒,对普通 HTTP 请求来说足够了,但对 WebSocket 这种经常长时间空闲的连接就太短了。
proxy_read_timeout 300s;
proxy_send_timeout 300s;
另一个思路是在应用层加心跳,让连接上时不时有点数据流动。我个人的建议是两个都做,超时别设太夸张,几分钟就行,主要靠心跳保活,不然半死不活的连接会一直挂着白白占资源。
别忘了 proxy_http_version 1.1
这个其实应该算第一个坑的一部分,但值得单独拎出来说。Nginx 反代默认用 HTTP/1.0 跟后端通信,而 WebSocket 握手要求 HTTP/1.1。少了 proxy_http_version 1.1; 这行,Upgrade 头写得再对也没用。我第一次配的时候就光顾着加 Upgrade 头,死活握手不成功,抓包看了半天才发现后端收到的是 HTTP/1.0 的请求,当场无语。
HTTPS 下用 wss 的注意事项
前端页面是 HTTPS 的话,浏览器是不允许连 ws:// 的,会直接报 mixed content 错误,必须走 wss://。好在 wss 并不需要后端做什么,SSL 终结在 Nginx 这一层就行,后端还是普通 ws:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
另外还有个常见的低级错误:页面是 https 但代码里把地址写死成了 ws://,本地能跑,一上线就挂。这种问题排查起来反而最费时间,因为你下意识会以为是代理的问题。
其他零碎的小坑
- 有些后端框架会校验 Host,记得加
proxy_set_header Host $host;,不然可能莫名收到 400。 - location 路径拼接:
location /ws/配proxy_pass http://127.0.0.1:8000;(末尾不带路径)会把/ws/原样传给后端;写成proxy_pass http://127.0.0.1:8000/;则会把/ws/前缀去掉。后端路由对不上的时候先检查一下这个。 - 如果中间还套了 CDN 或者别的代理,得确认人家支持 WebSocket,不然你本地 Nginx 配得再对也白搭。
浏览器控制台那个 "WebSocket connection failed" 基本不会告诉你具体原因,真正有用的信息在 Nginx 的 error.log 和后端日志里,排查的时候两边对着看,效率高很多。
最后贴一份完整可用的配置
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
改完记得先 nginx -t 验证一下语法,没问题再 nginx -s reload。不然配置写错了 reload 会静默失败,老配置还在跑,你会一直以为是自己没保存生效,又是白折腾一场。
总的来说,WebSocket 走 Nginx 代理的问题,九成以上都能归结为三件事:Upgrade 头没透传、HTTP 版本不对、读超时太短。下次遇到的时候先抓包看看握手请求长什么样,再对比后端实际收到了什么,基本就能定位了,希望你能少熬点夜。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。