小Cの已经记不起来的博客

用 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://,本地能跑,一上线就挂。这种问题排查起来反而最费时间,因为你下意识会以为是代理的问题。

其他零碎的小坑

浏览器控制台那个 "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 版本不对、读超时太短。下次遇到的时候先抓包看看握手请求长什么样,再对比后端实际收到了什么,基本就能定位了,希望你能少熬点夜。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放