拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Nginx 配置 WebSocket 转发:握手原理、负载均衡与常见踩坑

1. 为什么要单独考虑 WebSocket 转发升级握手与普通 HTTP 的差异1.1 从一次“浏览器控制台报错”说起我之前接过一个在线客服系统的项目前端用 WebSocket 做实时聊天后端接口全都通着接口文档也检查过好几遍但浏览器里只要建立连接就报WebSocket connection to ws://xxx/xxx failed: Error during WebSocket handshake: Unexpected response code: 404。当时第一反应是后端路由写错了结果后端日志里压根没看到这条请求说明请求根本没到达后端服务或者到达的方式不对。后来我逐条检查了 Nginx 的配置发现问题出在转发时把Upgrade头弄丢了。Nginx 默认情况下不会把客户端的Connection和Upgrade头原样透传给后端而后端拿到一个没有升级标识的普通 HTTP 请求自然返回 404。这个问题如果不懂 WebSocket 握手的原理排查起来非常绕因为后端看起来毫不知情前端看起来也完全正常中间那一层 Nginx 就是“黑盒”。这篇文章我会把 Nginx 配置转发 WebSocket 功能的所有细节拆开讲清楚从最基础的握手原理、最简可用配置到生产环境的多路由转发、负载均衡、WSS 加密链路、常见踩坑和验证方法。如果你正准备给实时聊天、消息推送、协同编辑这类服务接入 Nginx或者已经被“连接秒断”“握手失败”折磨过这篇应该能把根因和解决方案都讲透。1.2 握手升级的本质101 状态码与 Connection/Upgrade 头要理解 Nginx 该怎么配就得先弄明白 WebSocket 连接到底是怎么建立的。WebSocket 并不是凭空创建一条 TCP 连接而是借用了 HTTP 的握手通道。客户端先发一个普通的 HTTP GET 请求但在请求头里夹带三个关键字段GET /ws HTTP/1.1 Host: example.com Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13服务器收到这个请求后如果同意切换协议会返回101 Switching Protocols状态码并在响应头里带上Sec-WebSocket-Accept。浏览器确认 101 之后这条 TCP 连接就从 HTTP 协议切换成了 WebSocket 帧协议之后双方就可以随时向对方推送数据了。这里的关键点在于唯一合法的 WebSocket 握手成功状态码就是 101。如果 Nginx 在转发过程中把Connection: Upgrade或Upgrade: websocket这两个头改掉或丢弃后端就会把请求当成普通 HTTP 请求处理返回 200、404、502 这些常规状态码浏览器收到非 101 的响应直接判定握手失败。我常用的一个类比这就像你去银行办业务前面排队取号是 HTTP但你想走的 VIP 快速通道需要额外出示一张“升级凭证”。Nginx 的角色就像大堂保安默认做法是把所有客户的“升级凭证”都没收只按普通客户放行。你要做的就是让保安把这张凭证原样递给柜台。1.3 Nginx 默认行为为什么会让 WebSocket 失败Nginx 作为反向代理时它的默认配置偏向“稳”而不是“全”。具体到 WebSocket 场景有两个默认行为直接导致握手失败默认使用 HTTP/1.0 协议与上游后端通信而 HTTP/1.0 不支持Upgrade机制。默认会把请求头里的Connection字段重写成closeUpgrade头也不会自动透传。这两个默认行为叠加在一起WebSocket 请求经过 Nginx 之后后端看到的只是一个普普通通的 GET 请求。所以配置 WebSocket 转发的核心就三件事让 Nginx 与后端通信时使用 HTTP/1.1 协议。把客户端请求里的Upgrade头透传给后端。把Connection头设置为upgrade或根据是否升级请求动态设置。原理说清楚之后下面直接上配置实例。2. 最简可用配置从零搭建一个能跑通的 WS 代理2.1 环境假定与基础结构我这里假定一个最常见的场景后端服务跑在127.0.0.1:8080提供 WebSocket 接口路径是/ws。前端页面通过ws://你的域名/ws来连接。Nginx 在 80 端口统一接入其他 API 路径也有但我们只关心/ws这一条链路的转发配置。整体链路是这样的浏览器 → ws://example.com/ws → Nginx (80端口) → 127.0.0.1:8080/ws2.2 第一版配置逐行拆解看一个最基础、能跑通的配置server { listen 80; server_name example.com; location /ws { 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_read_timeout 60s; proxy_send_timeout 60s; } }逐行解释一下这些配置没有一个多余的proxy_pass http://127.0.0.1:8080;指定后端地址。这里没有带 URI所以 Nginx 会把客户端原始的/ws路径原样转发给后端。如果写成proxy_pass http://127.0.0.1:8080/;路径拼接规则会变容易出错后面会展开说。proxy_http_version 1.1;这是最重要的配置之一。Nginx 默认用 HTTP/1.0 与后端通信而 HTTP/1.0 根本没有Upgrade机制的概念。显式声明 1.1后端才能正常识别升级请求。proxy_set_header Upgrade $http_upgrade;把客户端传过来的Upgrade头原样透传给后端。$http_upgrade是 Nginx 内置变量代表客户端请求头里Upgrade字段的值如果客户端请求没有这个字段它就是空字符串。proxy_set_header Connection upgrade;这是第二个核心配置。把 Nginx 与后端之间的Connection头固定设置为upgrade告诉后端这条连接是升级连接。要注意这个配置会影响 Nginx 与后端之间的连接管理它已经超出了普通 HTTP 代理的“透传”范畴属于对上游的“主动改写”。proxy_set_header Host $host;把原始域名头传给后端。有些后端会基于 Host 做域名鉴权或路由分发不传可能返回 403 或跳错。proxy_set_header X-Real-IP $remote_addr;把客户端真实 IP 传给后端。如果不传后端看到的来源 IP 全是 Nginx 的日志排查和限流都不准。proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;追加客户端 IP 到 XFF 链上。$proxy_add_x_forwarded_for会把客户端来路 XFF如果有与$remote_addr拼接保证最后的 IP 是直连 Nginx 的地址。proxy_read_timeout 60s;和proxy_send_timeout 60s;设置与后端之间的读写超时。默认值就是 60 秒。对 WebSocket 这种长连接来说60 秒通常太短如果两端超过 60 秒没有数据交互Nginx 会主动断开。后面专门有一节讲调优。这个最简配置足够让握手跑通。但先别急着上生产后面几个地方还有不少坑。2.3 验证配置是否生效的三种手段配置写完后先执行nginx -t检查语法然后nginx -s reload重载。接下来怎么验证方法一浏览器控制台打开前端页面切到 Network 标签过滤出ws开头的请求查看状态码。正常情况应当显示 101。如果失败浏览器控制台通常会直接输出类似WebSocket connection to ws://... failed的报错。这是最直观的方式。方法二curl 手工模拟握手curl -i -N \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ \ -H Sec-WebSocket-Version: 13 \ http://example.com/ws正常的响应长这样HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: upgrade Sec-WebSocket-Accept: ...如果返回 504、502 或 404说明配置或后端有问题。curl 只能验证握手不能真正跑业务数据但定位握手问题已经够了。方法三Nginx 访问日志101状态码会直接出现在 access log 里。配置好之后用tail -f access.log实时观察。如果日志里全是 200 或 404说明握手没有成功。这里有个细节WebSocket 连接建立之后长连接期间不会再产生新的 HTTP 访问日志所以不要以为日志停了就是出问题了。3. 生产环境必须处理的问题路径隔离、负载均衡与 upstream3.1 多服务路径转发一个 Nginx 代理多个 WS 服务实际项目里一个 Nginx 往往要同时代理 REST API、WebSocket、Socket.IO 等多个服务。这时候location的匹配规则就很重要。最常见的组织方式是这样upstream api_backend { server 10.0.0.1:8081; } upstream ws_backend { ip_hash; server 10.0.0.1:8080; server 10.0.0.2:8080; } server { listen 80; server_name example.com; # 普通 HTTP API location /api/ { proxy_pass http://api_backend; proxy_set_header Host $host; } # WebSocket 路径 location /ws { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } # Socket.IO 路径 location /socket.io/ { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; } }这里有个容易被忽略的坑location /ws和location /ws/看起来只差一个斜杠匹配范围完全不同。location /ws能匹配/ws、/ws/abc也会匹配/wssomething。location /ws/只匹配以/ws/开头的路径不匹配/ws本身。如果你的前端入口是ws://example.com/ws不带尾斜杠用location /ws更稳。如果入口统一是/ws/xxx这种带斜杠的形式用location /ws/可以避免误伤/ws_static之类无关路径。我见过一个案例有人在/ws下挂了静态资源结果/wss的请求也被这个 location 拦截排查了半天才发现是斜杠匹配的问题。3.2 upstream 负载均衡与 ip_hash 的选择当后端 WebSocket 服务部署了多实例就需要配置 upstream 做负载均衡。这里必须想清楚一个问题WebSocket 和普通 HTTP 的负载均衡思路不一样。普通 HTTP 请求是短连接每次请求打到哪个后端无所谓因为无状态或者状态在数据库里。WebSocket 是长连接一个客户端建立连接后整个会话期间的数据都走同一条 TCP 连接。如果客户端断线重连下一次握手打到另一个后端节点而新节点上没有该客户端的状态比如聊天室房间路由表存在某个节点的内存里业务就会出问题。所以 WebSocket 的 upstream 我一般建议加ip_hashupstream ws_backend { ip_hash; server 10.0.0.1:8080; server 10.0.0.2:8080; server 10.0.0.3:8080; }ip_hash能保证同一个来源 IP 的请求始终被分配到同一个后端节点。对于大多数中小型实时应用这是性价比最高的粘性会话方案不需要额外引入 Redis 共享会话不需要在网关层做复杂的一致性哈希一行配置解决问题。当然如果你的业务状态做了全量共享比如用 Redis Pub/Sub 广播消息、用数据库做会话状态存储那就不用关心粘性问题可以放心使用默认的轮询策略。要不要粘性会话取决于你的业务设计不要盲目照抄。另外补充一个被动健康检查的配置max_fails和fail_timeout。upstream ws_backend { ip_hash; server 10.0.0.1:8080 max_fails3 fail_timeout30s; server 10.0.0.2:8080 max_fails3 fail_timeout30s; }这个配置的含义是30 秒内如果向后端转发请求失败 3 次就把该节点标记为不可用30 秒后再重新尝试探测。对 WebSocket 这种长连接场景被动健康检查的灵敏度有限因为只要连接建立成功后续的帧数据传输不会触发失败计数。要做到更实时的节点摘除还是得依赖应用层的健康检查接口或者更完善的网关方案。3.3 转发头部完整规范X-Real-IP、X-Forwarded-For 与 Host头部透传这块容易被低估但生产环境里后排的好多诡异问题都跟头有关。我常用的完整头部转发配置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;这里有个陷阱X-Forwarded-For要用$proxy_add_x_forwarded_for不要直接$http_x_forwarded_for。前者会把客户端已有的 XFF 链和当前$remote_addr拼接起来保证服务端取 XFF 最后一跳时拿到的一定是真实客户端 IP除非前面还有一层 CDN 或 LB。如果后端解析时取的是 XFF 的第一个值就可能被伪造建议后端改成取最后一个值。X-Forwarded-Proto也值得重视。后端如果同时承接 HTTP 和 HTTPS 请求可能会根据这个头来判断当前请求是走明文还是加密从而生成正确的回调地址或做重定向。不传这个头后端就不知道用户原来是通过 HTTPS 访问的重定向出来的地址可能是http://浏览器直接提示不安全。在 WebSocket 场景这些头在前几次握手请求里就会传给后端后端起手就能拿到客户端的真实来源信息。对做登录鉴权、限流、审计的系统来说这一步非常重要。4. WSS 加密链路与子协议浏览器安全策略下的正确姿势4.1 ws:// 与 wss:// 的转换关系如果你的前端页面是通过 HTTPS 打开的浏览器默认会禁止该页面向ws://明文 WebSocket 服务发起连接这属于混合内容Mixed Content限制。解决思路是在 Nginx 层做 TLS 终结浏览器 →wss://example.com/ws→ Nginx 解密 → 将明文ws://转发给后端。配置 SSL 证书后server 块长这样server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com.crt; ssl_certificate_key /etc/nginx/ssl/example.com.key; location /ws { 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_pass用的是http://而不是https://因为 Nginx 和后端之间走的是内网明文TLS 只在前端这一侧终止。如果你需要 Nginx 和后端之间也走 TLS才改用proxy_pass https://并加上证书相关配置。如果你不想维护两套 server 块可以考虑 80 跳 443 的写法server { listen 80; server_name example.com; return 301 https://$host$request_uri; }生产环境我建议直接把 80 全部 301 到 443配合 HSTS 头让浏览器记住这个站点只走 HTTPS。这样用户手输ws://连接时浏览器会自动升级成wss://减少一层踩坑概率。4.2 Sec-WebSocket-Protocol 子协议转发有些 WebSocket 后端会使用子协议Subprotocol来区分消息格式或版本。典型例子是 GraphQL 使用的graphql-ws或者自定义的chat.v1。客户端在握手请求里带上Sec-WebSocket-Protocol: graphql-ws, chat.v1后端选择一个它支持的协议在响应头里返回Sec-WebSocket-Protocol: graphql-ws浏览器会校验响应头里的子协议是否在客户端请求的列表里如果不在握手直接失败。好消息是Nginx 默认会透传未知请求头Sec-WebSocket-Protocol通常不需要额外配置。但有一个例外如果某些精简版 Nginx 镜像或者自定义配置里用proxy_set_header构建了白名单只放行固定头那就需要显式加上proxy_set_header Sec-WebSocket-Protocol $http_sec_websocket_protocol;这里要注意一个细节如果你在前端指定了子协议但后端没有返回对应的Sec-WebSocket-Protocol响应头浏览器会判定握手失败。这种情况 Nginx 配置再对也没用问题在后端逻辑。排查时可以用 curl 加上-H Sec-WebSocket-Protocol: chat.v1来测试后端是否正确回选了协议。4.3 浏览器混合内容限制下的处理混合内容限制是浏览器层面的安全策略HTTPS 页面不能发起非 HTTPS 的子资源请求。WebSocket 连接ws://会被当成“被动混合内容”直接拦截。但如果你直接访问的是 HTTP 页面页面里发起ws://连接是允许的。所以问题的根源在于你提供的是 HTTPS 页面却希望浏览器连明文ws://。这是浏览器有意为之的安全机制不属于配置 bug。解决方案有三种Nginx 层做 TLS 终结前端统一使用wss://最标准、最推荐的方式也是本文 4.1 节描述的方案。后端直接支持 TLSNginx 用proxy_pass https://后端地址做代理。这种方式配置成本高且 Nginx 与后端之间的证书管理复杂一般不建议。开发环境临时绕过让前端从https://改成http://访问页面这样ws://就能用了。这只适合本地开发不适合生产。生产环境没有任何理由不做 WSSLets Encrypt 都能免费签发证书别省这一步。我在实际项目里见过因为跳过了 HTTPS 导致线上页面报“不安全”然后用户跑光的例子代价远比配置证书要高。5. 踩坑实录超时断开、连接秒断、Nginx 版本差异5.1 最常见问题proxy_read_timeout 引起的频繁断开我排查过的 WebSocket 问题里出现频率最高的就是这个连接能建立但只要客户端与后端之间超过一定时间没有消息来往连接就被掐断。原因在于 Nginx 的proxy_read_timeout默认值是 60 秒它控制的是 Nginx 从后端读取数据的超时时间。WebSocket 连接建立后TCP 连接上如果长时间没有数据传输Nginx 会认为连接不活跃主动断开。问题在于不是所有业务都会在 60 秒内持续发送数据。比如推送服务用户可能在半小时内都没收到任何消息。解决方法很简单调大超时时间proxy_read_timeout 3600s; proxy_send_timeout 3600s;但这里有个权衡超时时间设得过大那些真正已死的连接比如客户端网络中断但 TCP 没感知会长时间占用 Nginx 和后端的连接资源。所以更合理的方案是应用层设计心跳机制客户端定时发送 ping 帧服务端返回 pong 帧保证连接始终有数据流动同时把 Nginx 超时设成一个比心跳间隔大几倍的值比如心跳 30 秒Nginx 超时设 120 秒左右。这样无论从哪个层面看连接都是“活跃”的不会触发误杀。5.2 连接“秒断”的另一个原因防火墙或中间网络设备还有一种“秒断”情况连接建立后几秒或几十秒就被断开但后端和 Nginx 日志里都看不到明确报错。这种情况我在一个金融类项目里遇到过。用户反馈 WebSocket 连接总是 20 多秒断一次查遍 Nginx 和后端配置都没发现问题。后来在网络运维那边查到公司的出口防火墙上有一条 TCP 空闲连接超时策略空闲 30 秒就清理连接状态。为什么 30 秒空闲就会被清理因为客户端没有发心跳连接上有 30 秒没有任何 TCP 数据包防火墙认为这条连接已经不再使用就把状态表项清了。之后任何一方再发数据防火墙直接丢弃连接就“卡死”了。这个问题的根治方案还是应用层心跳。无论你怎么调 Nginx 超时都绕不过网络中间设备的空闲策略。我在实际项目中把心跳间隔设为 30~45 秒这个频率既能保持连接活跃又不至于占用太多带宽。如果心跳包也不起作用某些环境可能只识别 HTTP 层的心跳可以再配合 TCP keepalive 参数调整。5.3 Nginx 版本和编译模块对 WS 支持的影响Nginx 对 WebSocket 的支持是渐进式增强的不是所有版本都具备相同能力。Nginx 1.3.13 之后Upgrade头转发才被正式支持。如果你的服务器还在用远古版本即使配置写对了也不会有 101 响应。Nginx 1.13.10 开始支持 gRPC虽然和 WebSocket 没有直接关系但说明 Nginx 的升级代理能力一直在增强。某些第三方编译版 Nginx比如 OpenResty默认集成了 Lua如果你在 Nginx 层写了 Lua 请求拦截脚本有可能把Upgrade请求误判成普通请求直接拦截这种情况下要检查 Lua 脚本的逻辑。建议直接使用官方源的 Nginx 1.20 版本或者通过系统包管理器安装较新版本。不要守着老版本不放很多奇怪的握手问题在升级后自动消失。另一个容易混淆的点是--with-stream模块。如果你的配置用的是 Nginx 四层 TCP 转发stream块而不是http块那么 WebSocket 是不需要设置 Upgrade 头的因为四层转发直接把 TCP 流量整个透传Nginx 根本不解析应用层协议。有人混淆了这两种模式在http块里对照文档去查流模块配置自然找不到对应项。5.4 连接数限制和 worker 配置对长连接的影响WebSocket 是长连接一个客户端握一次手这条 TCP 连接会一直占用着。这意味着同样规模的在线用户数WebSocket 服务占用的文件描述符和内存远高于普通 HTTP 服务。Nginx 层面有两个参数需要检查worker_processes auto; worker_connections 10240;worker_connections表示每个 worker 进程能同时处理的最大连接数。注意一次 WebSocket 请求在 Nginx 内部会占用两条连接客户端 → Nginx 一条Nginx → 后端一条所以实际可支撑的客户端数大约等于worker_processes × worker_connections / 2。几千并发在线对 WebSocket 来说已经不算小规模了。另外一个常见问题是系统层面的文件描述符限制。之前遇到过一次too many open files的报错排查下来是 Linux 对 Nginx 进程的ulimit -n限制太低默认可能只有 1024高并发下一下子就打满了。解决方式是修改/etc/security/limits.confnginx soft nofile 65535 nginx hard nofile 65535改完记得重启 Nginx 进程不是 reload然后执行cat /proc/$(pgrep nginx)/limits确认实际生效值。内存方面每个 WebSocket 连接占用的内存在 KB 级别几千并发问题不大。但如果你在 Nginx 层开了大量缓冲区或者启用了某些消耗内存的模块内存增长会很明显这个要结合压测结果来调。6. 掌握验证与监控手段长连接服务上线后的体检方法6.1 用 curl 手工模拟 WebSocket 握手上面 2.3 已经给了基础版 curl 命令这里再补充一个带子协议的版本curl -i -N \ -H Host: example.com \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ \ -H Sec-WebSocket-Version: 13 \ -H Sec-WebSocket-Protocol: chat.v1 \ http://example.com/ws这个命令看着简单实际调试时能帮你快速分层定位问题如果返回 404说明 Nginx 路径匹配不对或后端本身没有这个路由。如果返回 502说明 Nginx 连不上后端检查 upstream 配置和后端端口。如果返回 504说明 Nginx 与后端的超时时间太短或者后端 hang 住了。如果返回 403多半是 Host 头不对或后端有 IP 白名单限制。一个细节Sec-WebSocket-Key是一个 Base64 编码的 16 字节随机值格式不挑剔只要是个合法 Base64 字符串即可。Sec-WebSocket-Version目前主流是 13现代浏览器都固定这个值。6.2 从访问日志中观测 101 状态码WebSocket 服务上线后建议给相应接口单独定义一种日志格式避免被普通 HTTP 接口的日志淹没log_format ws_log $remote_addr [$time_local] $request $status upstream_addr$upstream_addr request_time$request_time;然后在 server 块里给 WebSocket 的 location 单独设置 access_loglocation /ws { ... access_log /var/log/nginx/ws_access.log ws_log; }排查时的常用命令grep 101 /var/log/nginx/ws_access.log | tail -50如果一整天都没有 101 之外的连接状态说明转发正常。如果出现大量 500、502优先看后端错误日志。如果出现大量 499表示客户端主动断开这在 WebSocket 场景下是正常的用户刷新页面、切后台、断网重连都会导致 499。偶尔出现正常但某个接口大量 499 就需要关注是不是前端连接管理逻辑有问题。6.3 浏览器 DevTools 与命令行工具配合使用浏览器 DevTools 的 Network 面板里WebSocket 连接点开后能看到握手详情和帧数据。Messages 标签页可以实时查看收发消息排查协议问题时非常直观。命令行工具方面websocat比 curl 更适合做 WebSocket 全链路测试。它能像 netcat 一样建立连接后通过 stdin/stdout 收发消息websocat ws://example.com/ws输入{type:ping}之类的 JSON就能看到后端返回内容。这对验证“连接建起来之后的数据收发是否正常”特别重要因为 curl 只能验证握手不能真正跑业务数据。如果你更习惯 Python 环境可以用websockets库写测试脚本import asyncio import websockets async def test(): async with websockets.connect( wss://example.com/ws, subprotocols[chat.v1] ) as ws: await ws.send({\type\:\ping\}) msg await ws.recv() print(f收到: {msg}) asyncio.run(test())这种脚本容易集成到自动化回归测试里每次发版前跑一遍省心很多。7. 长期维护 WebSocket 服务中积累的几条配置经验走到这里WebSocket 转发的核心逻辑基本都覆盖了。最后分享几条我在长期维护实时服务过程中沉淀下来的配置习惯照着做能少踩很多坑。第一把复杂的业务逻辑收敛在 Nginx 之外。Nginx 的定位是流量入口和负载均衡不要在里面塞太多业务判断。以前我在 Nginx 层写过一段 Lua 脚本用来校验 Token后来升级版本时 Lua 库兼容性出问题服务直接不可用。WebSocket 握手阶段的鉴权更推荐在后端业务代码里做让后端在返回 101 之前校验 Token不合法就返回 401 或 403浏览器会直接报握手失败未鉴权的长连接根本建不起来。这样既安全又便于维护。第二证书续期一定要自动化。证书过期当天WebSocket 连接会大面积失败这是一个非常典型的线上事故。建议配置 crontab 让 certbot 自动续期并在续期完成后自动执行nginx -s reload。早期我手动续期证书时忘记 reload Nginx新证书没被加载用户那边全部握手失败教训很深刻。现在这条链路上已经不需要人工介入了。第三调参前先想清楚业务的心跳机制。Nginx 的proxy_read_timeout、proxy_send_timeout、TCP keepalive、客户端心跳 这四个参数需要协同设计不能匹马单枪地调大某一个。我的建议是客户端心跳 30~45 秒Nginx 超时设成心跳间隔的 2~4 倍TCP keepalive 放低一点作为兜底。这样既保证连接活跃又不至于因为 Nginx 超时误杀正常连接。第四留一份能复现问题的测试脚本。不管是 curl 命令还是 websocat 脚本都要放进项目的 CI 里或者至少留在 wiki 里。我之前处理过一个线上问题等环境都复原不了就是因为当时没有保留完整的复现命令。后来我把 WebSocket 握手验证的 curl 和 Python 脚本都收进仓库再遇到问题可以直接在预发环境跑一遍排查效率高了很多。以上这些经验是我熬了好几个半夜才慢慢积累下来的。WebSocket 和普通 HTTP 代理的区别真的不复杂核心抓住握手升级、HTTP/1.1、头透传和超时调优这几个关键点大部分问题都能自己定位到根因。你踩过的那些坑大概率也都在这个范围里。最后再分享一个小技巧如果你的前端项目用的是 Socket.IO服务端也是 Socket.IO 官方库那 Nginx 配置时除了 Upgrade 头和 Connection 头还要重点确保proxy_set_header Connection upgrade与超时参数是配套调优的同时让 Socket.IO 的path在前后端保持一致。否则会出现连接反复横跳的诡异现象。把这些基础原理吃透之后无论以后换什么实时通信框架都能在 Nginx 这一层快速定位问题到底出在传输层还是应用层。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门