最近帮朋友排查一个WebSocket连接问题,后端服务明明运行正常,但通过Nginx访问就报400错误。这个问题其实非常典型,很多开发者第一次用Nginx代理WebSocket都会遇到。就像我第一次踩坑时一样,明明本地测试一切正常,上线就出问题,特别容易让人抓狂。
WebSocket协议有个特点,它建立连接时需要先完成一个特殊的HTTP握手过程。简单来说,客户端会发送一个带有Upgrade头的HTTP请求,告诉服务器"我要升级到WebSocket协议"。如果这个握手过程被Nginx拦截或修改,就会导致后端服务收不到正确的协议升级请求,直接返回400 Bad Request错误。
这里有个生活化的类比:想象你要去银行办理业务,先取号排队(HTTP请求),轮到你了突然说要升级成VIP服务(WebSocket)。如果大堂经理(Nginx)没把这句话传达给柜员(后端服务),柜员就会一脸懵:"这人到底要办什么业务?"于是直接拒绝服务。
遇到400错误时,第一步应该是查看Nginx的错误日志。在Ubuntu系统上,日志通常位于/var/log/nginx/error.log。用这个命令可以实时查看最新日志:
bash复制tail -f /var/log/nginx/error.log
典型的WebSocket握手失败日志会显示:
code复制[error] 1234#1234: *5678 upstream prematurely closed connection while reading response header from upstream
这表示Nginx和后端的连接被异常关闭了。更关键的是要检查客户端收到的响应头,可以用Chrome开发者工具的Network面板查看。正常的WebSocket连接应该返回状态码101 Switching Protocols,如果看到400就说明握手失败了。
我遇到过最典型的配置缺失是这三个参数:
nginx复制proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
这三个就像是一组密匙,缺一不可:
曾经有个生产环境的问题,团队花了三天时间排查,最后发现是因为配置文件中多了一个不起眼的"proxy_set_header Connection "";" 把升级头给覆盖了。这种细节特别容易忽略。
对于只有一个WebSocket服务的情况,建议这样配置:
nginx复制server {
listen 80;
server_name ws.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
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_connect_timeout 7d;
proxy_read_timeout 7d;
proxy_send_timeout 7d;
}
}
注意这里的超时设置我直接给了7天,因为WebSocket通常是长连接。之前有次设置了60秒,结果连接老是莫名其妙断开,排查了好久才发现是这个原因。
很多应用需要同时支持HTTP和WebSocket,比如前端页面和实时消息服务共存的情况。这时候可以这样配置:
nginx复制server {
listen 80;
server_name app.yourdomain.com;
# 全局WebSocket支持
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
location / {
# 普通HTTP请求
proxy_pass http://frontend;
}
location /socket.io/ {
# WebSocket服务
proxy_pass http://socket-backend;
# 特别设置长超时
proxy_read_timeout 86400s;
}
}
这种配置下,/socket.io/路径下的请求会走WebSocket协议,其他请求保持HTTP。注意Connection头这里用了变量$connection_upgrade,这是Nginx的一个技巧,可以自动处理连接状态。
当常规方法无法定位问题时,可以祭出网络分析神器tcpdump。这个命令可以抓取Nginx和后端之间的流量:
bash复制tcpdump -i lo -A -n 'port 3000'
通过分析原始数据包,你能看到:
有次我用这个方法发现Nginx把WebSocket请求误认为是普通HTTP请求,原因是location匹配规则写得太宽泛,把/socket路径也捕获到了普通HTTP路由里。
WebSocket服务对性能要求较高,这几个参数可以优化:
nginx复制# 提高缓冲区大小
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
# 禁用缓冲以获得实时性
proxy_buffering off;
# 保持连接活跃
proxy_headers_hash_max_size 1024;
proxy_headers_hash_bucket_size 128;
特别是proxy_buffering off这一项,对于实时性要求高的应用特别重要。之前有个股票行情推送系统,开启缓冲后延迟高达3秒,关闭后降到毫秒级。
如果使用HTTPS/WSS,配置需要稍作调整:
nginx复制server {
listen 443 ssl;
server_name secure.ws.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# HTTPS需要额外设置
proxy_set_header X-Forwarded-Proto $scheme;
}
}
这里容易踩的坑是忘记设置X-Forwarded-Proto头,导致后端无法识别原始协议是HTTPS。我曾经因此浪费了半天时间,因为后端服务根据这个头来决定是否启用安全策略。
当使用Nginx做WebSocket负载均衡时,需要特别注意:
nginx复制upstream websocket_cluster {
ip_hash; # 保持会话粘性
server ws1.example.com;
server ws2.example.com;
}
server {
location / {
proxy_pass http://websocket_cluster;
# 标准WebSocket配置...
# 特别设置用于负载均衡
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500;
}
}
关键点是ip_hash指令,它能保证同一个客户端的请求总是转发到同一台后端服务器。WebSocket是长连接,如果不保持会话粘性会导致连接中断。去年我们线上就出现过因为没配置ip_hash,用户频繁掉线的问题。