引言:为什么 WebSocket 需要反向代理
随着即时通讯、在线客服、实时推送、协同编辑等功能的普及,越来越多的个人站长开始在网站上使用 WebSocket 技术。WebSocket 让服务器可以主动向浏览器推送数据,实现真正的双向实时通信,这是传统 HTTP 轮询和长轮询难以做到的。但是,直接在 Nginx 后面部署 WebSocket 服务,常常会遇到连接失败、频繁断开、无法升级协议等问题。很多人第一次配置时都会踩坑,本篇文章就从一个草根站长的视角,把 Nginx 反向代理 WebSocket 的原理、配置、调优和排错过程完整地梳理一遍,希望能帮你少走弯路。
一、WebSocket 与普通 HTTP 请求的本质区别
要理解反向代理配置,先要明白 WebSocket 和普通 HTTP 请求有什么不同。普通 HTTP 请求是"一问一答"模式:浏览器发一个请求,服务器返回一个响应,然后这个 TCP 连接要么关闭,要么被复用处理下一个请求。整个过程是无状态的,服务器无法主动向浏览器推送消息。
WebSocket 则完全不同。它通过 HTTP 协议发起一次握手请求,请求头里带有 Upgrade: websocket 和 Connection: Upgrade 两个关键字段,服务器如果同意升级,就返回 101 Switching Protocols 状态码。从这一刻起,这个 TCP 连接就从 HTTP 协议切换成了 WebSocket 协议,变成一条全双工的长连接,浏览器和服务器可以随时互相发送数据帧,直到某一方主动关闭。
正因为 WebSocket 连接是长时间保持的,它和普通 HTTP 连接在代理层面有本质区别:普通 HTTP 请求可能在几毫秒内就完成,而一条 WebSocket 连接可能存活几十分钟甚至几天。Nginx 默认的代理超时时间、缓冲设置、连接复用逻辑,都是为短连接设计的,如果不做调整,WebSocket 连接很快就会被 Nginx 掐断,表现出来就是"连上一会儿就断开"或者"根本连不上"。
二、Nginx 反向代理 WebSocket 的核心原理
Nginx 从 1.3.13 版本开始就正式支持 WebSocket 反向代理,原理其实并不复杂。当浏览器把握手请求发给 Nginx 时,Nginx 需要做两件事:第一,把握手请求里关键的 Upgrade 和 Connection 请求头原样转发给后端 WebSocket 服务器;第二,在后端返回 101 之后,Nginx 不再把这条连接当作普通 HTTP 连接处理,而是进入隧道模式,在浏览器和后端之间双向透传数据帧。
这里最关键的就是那两个请求头。HTTP 规范规定,Connection 头是一个"逐跳"(hop-by-hop)头,意思是它只对当前这一跳有效,代理默认不会把它转发给下一跳。Nginx 在代理普通请求时会主动剥离 Connection 头,这正是 WebSocket 握手失败最常见的原因。解决办法是显式告诉 Nginx 要转发哪些头:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";第一行把浏览器请求里的 Upgrade 头的值取出来,赋值给 $http_upgrade 变量;第二行把 Connection 头强制设置为 upgrade。这两行配置是所有 WebSocket 反代方案的地基,缺了任何一个,握手都会失败。
三、最基础的 WebSocket 反向代理配置
假设你的 WebSocket 服务跑在本机 8080 端口,域名为 ws.example.com,那么一份最简配置是这样的:
server {
listen 80;
server_name ws.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header 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_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}注意其中 proxy_http_version 1.1 这一行。WebSocket 的协议升级依赖 HTTP/1.1 的 Upgrade 机制,而 Nginx 向上游发起请求时默认使用 HTTP/1.0,HTTP/1.0 不支持 Upgrade,所以必须显式改成 1.1,否则后端永远不会返回 101。这是第二个高频踩坑点。
四、完整生产配置模板
上面那份配置能跑通,但离生产环境还有距离。真实场景下我们通常要同时处理静态资源和 WebSocket 路径、启用 TLS、限制超时、记录真实客户端 IP,一份更完整的配置模板如下:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream ws_backend {
server 127.0.0.1:8080;
server 127.0.0.1:8081;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name ws.example.com;
ssl_certificate /etc/nginx/ssl/ws.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/ws.example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
# 心跳与超时:WebSocket 空闲连接也要保持
proxy_connect_timeout 10s;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
location /ws/ {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $connection_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_set_header X-Forwarded-Proto https;
# 限制单 IP 并发连接,防止被恶意刷连接
limit_conn conn_per_ip 10;
}
location / {
# 静态资源与普通页面走这里
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}
}这里用 map 指令根据请求里有没有 Upgrade 头来动态决定 Connection 头的值:有升级请求就透传 upgrade,没有就保持 close,这样同一个 server 块既能代理 WebSocket 又能代理普通 HTTP,互不干扰。limit_conn 可以限制每个 IP 同时建立的连接数,防止有人用脚本开成千上万条连接把你的服务拖垮。
五、WSS:给 WebSocket 加上 TLS 加密
如果网站已经全站 HTTPS,那么 WebSocket 也必须走加密通道,即 WSS(wss:// 协议)。浏览器安全策略规定:HTTPS 页面只能发起 wss:// 连接,发起 ws:// 会被直接拦截。所以只要你的站点开了 HTTPS,WebSocket 就必须配 TLS。
好消息是配置 WSS 不需要动 WebSocket 服务本身。Nginx 负责终止 TLS,然后把解密后的明文 WebSocket 流量转发给后端的 ws:// 服务,后端完全无感知。你只需要像上面的模板一样,在 listen 443 ssl 的 server 块里配置证书,前端连接地址从 ws://ws.example.com/ws/ 改成 wss://ws.example.com/ws/ 即可。证书可以用 Let's Encrypt 免费申请,配合 certbot 自动续期,成本为零。
有一个细节值得注意:配置了 TLS 之后,如果后端程序需要判断客户端是否走 HTTPS,它读取 X-Forwarded-Proto 头即可,但前提是 Nginx 设置了这一行,而且后端程序信任这个头。如果后端是 Node.js 的 ws 库或者 Python 的 websockets 库,通常还需要显式开启"信任代理头"的选项,否则它们会把 X-Forwarded-For 当作真实来源 IP,导致限流和风控逻辑全部失效。
六、多个 WebSocket 后端:负载均衡与会话保持
当单机撑不住海量长连接时,就需要多台后端服务器做负载均衡。Nginx 的 upstream 模块天然支持多后端轮询,但 WebSocket 场景有一个特殊问题:连接一旦建立,浏览器和后端就绑定了,如果负载均衡策略把同一条连接上的后续数据帧转发到另一台后端,逻辑立刻就乱套了。
解决思路是会话保持(sticky session)。Nginx 官方推荐用 ip_hash 策略,让同一个客户端 IP 的请求永远落在同一台后端上:
upstream ws_backend {
ip_hash;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}ip_hash 的优点是配置简单、无需额外模块,缺点是当客户端通过代理池访问时,所有流量可能被哈希到同一台机器,造成倾斜;而且某台后端宕机后,哈希到它的客户端会全部掉线重连。更精细的方案是使用第三方 sticky 模块,通过 cookie 实现会话保持,但需要自己编译 Nginx,对个人站长来说成本略高,一般 ip_hash 就够用了。
另外一个与负载均衡强相关的坑是:WebSocket 连接数非常高时,Nginx 默认的 upstream keepalive 连接池设置(注意这个 keepalive 是 upstream 块里的指令,和心跳无关)会影响性能。合理的值通常是 16 到 64 之间,太小会导致频繁建立上游连接,太大则浪费内存。
七、心跳保活与超时参数调优
WebSocket 连接是长连接,但网络链路中可能存在各种中间设备(防火墙、NAT 网关、运营商设备),它们会默默清理长时间没有数据传输的空闲连接。结果就是:客户端以为连接还在,实际上链路早就断了,等服务器真正推数据时才发现连接已死,表现为"消息突然收不到"。
解决这个问题有两条腿走路。第一,应用层做心跳:客户端每隔一段时间(比如 30 秒)发送一个 ping 帧,服务器回复 pong 帧,让链路一直有数据流动。第二,Nginx 层调整超时:proxy_read_timeout 和 proxy_send_timeout 默认只有 60 秒,空闲超过 60 秒 Nginx 就会主动断开连接,这显然不能满足长连接需求,生产环境一般调到 3600 秒甚至更长,让应用层的心跳机制来决定连接的真实生死:
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;这里的原则是:超时时间应该大于心跳间隔的若干倍。比如心跳 30 秒一次,超时设 3600 秒,即使偶尔丢几个心跳包也不会误杀连接;而如果后端进程真的卡死不再响应,应用层会通过连续收不到 pong 来主动重连,形成闭环。
八、常见问题排查
配置完 WebSocket 反代后,最常遇到的几个问题以及排查思路如下。
第一,浏览器控制台报 400 Bad Request 或者 "Error during WebSocket handshake"。几乎可以肯定是 Upgrade 或 Connection 头没配好,或者 proxy_http_version 还是默认的 1.0。逐个核对第三节列出的三行核心配置即可。
第二,握手成功但连接十几秒后自动断开。优先检查 Nginx 的 proxy_read_timeout 和 proxy_send_timeout,如果还是默认 60 秒,连接必然被掐断,改成 3600s 再试。
第三,后端能收到连接但客户端一直收不到消息。这种情况要检查 Nginx 是否启用了 proxy_buffering。WebSocket 流量是双向的实时数据流,Nginx 默认的缓冲机制会先把上游数据攒起来再发给客户端,对实时性有致命影响。虽然 Nginx 对协议升级后的连接会自动禁用缓冲,但稳妥起见可以显式加上 proxy_buffering off; 并关闭 gzip 对 WebSocket 路径的影响。
第四,用 curl 测试时看到 502 Bad Gateway。这通常是后端服务没起来、端口写错,或者后端监听了 IPv6 的 ::1 而 Nginx 连的是 127.0.0.1。用 curl 直接访问后端地址,逐步缩小范围。
第五,无法获取客户端真实 IP。检查 X-Real-IP 和 X-Forwarded-For 是否配置,以及后端程序是否读取了正确的头字段,Node.js 需要 app.set('trust proxy', true) 之类的开关。
九、安全与性能建议
WebSocket 反代在生产环境还应该注意以下几点。安全方面:第一,限制连接频率和并发数,前面提到的 limit_conn 指令很有用,还可以配合 limit_req 限制握手请求的速率,防止握手风暴;第二,鉴权不要只靠连接地址,因为 WebSocket 一旦建立就无法再修改请求头,常见的做法是在握手阶段携带 token,后端在 101 之前校验,校验失败直接返回 403;第三,如果 WebSocket 服务本身没有鉴权能力,可以考虑把它放在独立域名下,与主站隔离,减少攻击面。
性能方面:第一,把 WebSocket 路径与静态资源路径分开,静态资源走 Nginx 的静态文件模块或者 CDN,不要让 WebSocket 代理拖慢普通页面;第二,为 worker 进程数、worker_connections 等基础参数预留余量,每条 WebSocket 连接都要占用一个连接槽位,海量长连接场景下默认的 1024 连接数很快就会打满;第三,开启 access_log 的缓冲,或者把 WebSocket 路径的日志级别调低,因为长连接会产生大量访问日志,写满磁盘只是时间问题。
总结
Nginx 反向代理 WebSocket 的核心就三件事:转发 Upgrade 和 Connection 头、把上游协议版本设为 HTTP/1.1、把超时时间从短连接思维调整成长连接思维。这三件事做对了,WebSocket 就能稳稳地跑在 Nginx 后面。在此基础上再根据业务需要补充 TLS、负载均衡、心跳、安全限制等能力,就构成了一套完整的生产方案。希望这篇文章能帮你一次配置成功,少踩几个坑。