拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

WebSocket wss 配置实战:Nginx 反向代理与生产环境落地

WebSocket wss 配置实战:Nginx 反向代理与生产环境落地 简介这份资源面向使用 Spring Boot 2.1 开发实时通信功能的 Java 后端开发者聚焦于将 WebSocket 从普通 ws 升级为基于 SSL/TLS 的 wss 安全访问解决 HTTPS 环境下长连接无法正常建立、证书配置繁琐等常见问题。压缩包共 66 个文件约 61KB以 java 源码、xml 配置、properties 属性文件为主另含 jks 证书文件、mvnw 构建脚本及少量前端 sample 与 index 页面覆盖服务端配置、证书生成与客户端连接示例。资源围绕 SSL 证书生成、Tomcat 连接器与端口重定向、WebSocketConfigurer 注册处理器、前端 wss 地址调用等环节展开读者可据此搭建一套可运行的加密长连接 Demo理解双向通信与安全传输的配合方式并掌握自签名证书的生成与排错思路。目前已有 13109 人学习下载适合需要为聊天室、实时推送等场景补齐 wss 配置能力的开发者参考。1. WebSocket 配置 wss 访问从 ws 到加密长连接的落地路径本地开发时ws://localhost:8080/ws跑得飞起一上测试环境换成域名就报Mixed Content浏览器控制台红字一片前端同学跑过来问“是不是后端挂了”。这种场景我遇到过太多次问题往往不在业务代码而在 WebSocket 从ws升级到wss这一层没配对。wss本质是 WebSocket over TLS和 HTTPS 共用同一套证书体系与握手逻辑只是协议标识从ws变成wss、默认端口从 80 变成 443。它解决的是明文传输被中间人窃听和篡改的问题也是现代浏览器在 HTTPS 页面里允许建立长连接的前提。这篇内容面向正在做实时推送、需要把 WebSocket 推到生产环境的后端和运维同学从反向代理配置讲到应用层参数把每一步能复现的命令和踩过的坑都摊开说。2. 先搞清楚 wss 握手到底多做了什么2.1 ws 与 wss 的握手差异普通ws握手就是一次 HTTP Upgrade 请求客户端发Connection: Upgrade和Upgrade: websocket服务端返回 101 状态码之后这条 TCP 连接就变成全双工通道。wss在这之前多了一层 TLS 握手客户端先和服务器完成证书校验、密钥协商建立加密通道然后才在这个通道里跑同样的 HTTP Upgrade。也就是说wss的握手报文本身是加密的中间设备看不到Upgrade头这也是为什么有些老旧的代理或防火墙会直接掐断wss连接——它不认识加密流量里的协议升级。从抓包角度看ws的握手包在 Wireshark 里能直接看到GET /ws HTTP/1.1而wss只能看到 TLS 的Client Hello和Server Hello后面的 HTTP 内容全是密文。这个差异决定了排查思路ws连不上先看 HTTP 状态码wss连不上先看 TLS 握手有没有完成。2.2 为什么生产环境必须上 wss浏览器对混合内容的限制越来越严。HTTPS 页面里发起ws://请求Chrome 从 81 版本开始直接拦截控制台报Mixed Content: The page at https://... was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint。这不是可选项是硬性阻断。另外很多公司内网和云厂商的负载均衡默认只放行 44380 端口要么被封要么被重定向ws根本出不去。从安全角度ws传输的业务数据——用户 ID、消息内容、token——全是明文同网络下抓包就能还原。wss把整条通道加密配合证书校验还能防域名劫持。所以只要不是纯内网调试wss就是必选项。2.3 证书从哪来、怎么配wss依赖 TLS 证书证书来源常见三种云厂商签发的免费 DV 证书、Lets Encrypt 自动续期、企业内 CA 签发。云厂商证书一般给.pem和.key两个文件Nginx 里用ssl_certificate和ssl_certificate_key指向即可。Lets Encrypt 用 certbot 签发后证书在/etc/letsencrypt/live/域名/下fullchain.pem是证书链privkey.pem是私钥。注意证书链必须完整只配站点证书不配中间 CA部分客户端会报unable to verify the first certificate。用openssl s_client -connect 域名:443 -showcerts能看到服务端实际下发的证书链。3. Nginx 反向代理把 ws 升级成 wss 的完整配置3.1 最小可用配置与逐行解释最常见的落地方式是在 Nginx 上终止 TLS然后反向代理到后端的ws服务。下面这份配置我用了很多次直接抄改域名和端口就能跑# 定义后端 WebSocket 服务地址用 upstream 方便做多实例 upstream ws_backend { server 127.0.0.1:8080; keepalive 64; # 保持长连接减少后端握手开销 } server { listen 443 ssl; server_name ws.example.com; # 证书配置fullchain 包含站点证书和中间 CA ssl_certificate /etc/nginx/cert/fullchain.pem; ssl_certificate_key /etc/nginx/cert/privkey.pem; # 只保留 TLS1.2 和 1.3老协议有已知漏洞 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location /ws { # 把原始 Host 和客户端 IP 透传给后端 proxy_pass http://ws_backend; proxy_http_version 1.1; # 这两个头是 WebSocket 升级的关键缺一不可 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; # 长连接超时设置默认 60s 会被断开 proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }proxy_http_version 1.1必须显式写Nginx 默认用 1.0而 WebSocket 升级依赖 1.1 的Upgrade机制。Connection upgrade这个头如果写成Connection $connection_upgrade配合 map 使用能兼容普通 HTTP 请求但只做 WebSocket 时直接写死upgrade更省事。proxy_read_timeout是血泪经验默认 60 秒没数据就断心跳间隔设成 30 秒以内才安全或者直接把超时拉到 3600 秒。3.2 用 map 兼容普通请求的写法如果同一个 location 既要处理 WebSocket 又要处理普通 HTTP硬写Connection upgrade会让普通请求也带上 upgrade 头可能触发后端异常。标准做法是在http块里加一个 map# 根据请求头里有没有 upgrade 动态决定 Connection 的值 map $http_upgrade $connection_upgrade { default upgrade; close; }然后 location 里改成proxy_set_header Connection $connection_upgrade;。这样普通请求的Connection是closeWebSocket 请求是upgrade互不干扰。这个 map 必须放在http块放在server块里 Nginx 会报语法错误。3.3 验证配置是否生效改完配置先nginx -t检查语法再nginx -s reload平滑重载。验证分两步先用curl看 TLS 握手和 HTTP 响应再用 WebSocket 客户端测实际升级。# 检查 TLS 握手和证书链-I 只取响应头 curl -I https://ws.example.com/ws # 用 openssl 看证书链是否完整 openssl s_client -connect ws.example.com:443 -showcerts /dev/null 2/dev/null | grep -E s:|i: # 用 wscat 测 WebSocket 升级-c 指定连接地址 npx wscat -c wss://ws.example.com/wscurl返回 101 或 400 都说明 TLS 层通了400 通常是后端没正确处理 upgrade 头。wscat能连上并显示Connected就说明整条链路通了。如果wscat报Unexpected server response: 502去看 Nginx 的error.log一般是后端服务没起来或者 upstream 地址写错。4. 应用层怎么接住 wssSpring Boot 与 Django 两种常见实现4.1 Spring Boot 整合 WebSocket 并暴露 wssSpring Boot 里用spring-boot-starter-websocket起一个 WebSocket 服务默认监听wsTLS 由 Nginx 终止应用层不用改。但如果要让 Spring Boot 自己处理 TLS需要配置SslContext。常见做法是前者应用只跑wsNginx 做wss入口。Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // 注册处理器setAllowedOrigins 限制跨域来源 registry.addHandler(new MyWebSocketHandler(), /ws) .setAllowedOrigins(https://ws.example.com); } }setAllowedOrigins不配的话生产环境跨域会被拦。如果前端域名和 WebSocket 域名不同这里必须显式列出。后端处理消息时TextMessage和BinaryMessage分开处理心跳消息建议用PingMessage而不是自定义文本框架层能自动回Pong。4.2 Django Channels 实现后台数据推送Django 做 WebSocket 推送一般用 Channels配合 Daphne 或 Uvicorn 跑 ASGI。Channels 的routing.py里定义消费者consumers.py里写推送逻辑。后台有数据时通过 channel layer 推给前端# consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class PushConsumer(AsyncWebsocketConsumer): async def connect(self): # 每个连接加入同一个组方便广播 self.group_name push_group await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def push_message(self, event): # 收到组消息后转发给前端 await self.send(text_datajson.dumps(event[data]))后台有数据时调用channel_layer.group_send(push_group, {type: push_message, data: {...}})就能推给所有连接。type字段对应消费者里的方法名写错会静默失败这是新手最容易踩的坑。Django 侧同样只跑wswss交给 Nginx。4.3 前端连接 wss 的写法与重连前端new WebSocket(wss://ws.example.com/ws)即可但生产环境必须加重连和心跳否则网络抖动后连接断了没人知道let ws; let heartbeatTimer; function connect() { ws new WebSocket(wss://ws.example.com/ws); ws.onopen () { // 每 25 秒发一次心跳小于 Nginx 的 60s 超时 heartbeatTimer setInterval(() ws.send(JSON.stringify({ type: ping })), 25000); }; ws.onclose () { clearInterval(heartbeatTimer); // 断线后 3 秒重连避免频繁重试打爆服务端 setTimeout(connect, 3000); }; ws.onerror (err) console.error(ws error, err); } connect();心跳间隔要小于 Nginx 的proxy_read_timeout否则连接会被代理层静默断开前端onclose触发重连看起来就是“每隔一分钟断一次”的玄学问题。5. wss 配置里最容易翻车的几个地方5.1 现象浏览器报 Mixed Contentwss 请求被拦截原因页面是 HTTPS但 WebSocket 地址写成了ws://。浏览器把ws视为不安全内容直接阻断控制台报Mixed Content。解决把前端连接地址改成wss://并确认 Nginx 443 端口配置正确。如果后端返回的地址是动态拼接的检查拼接逻辑里协议部分有没有根据window.location.protocol判断。5.2 现象wscat 能连上浏览器连不上报 403原因后端setAllowedOrigins没配前端域名或者 Nginx 没透传Origin头。浏览器发 WebSocket 握手时会带Origin服务端校验不通过就返回 403。解决Spring Boot 里setAllowedOrigins加上前端域名Nginx 里确认proxy_set_header Origin $http_origin;有透传。Django Channels 里用AllowedHostsOriginValidator时也要把域名加进去。5.3 现象连接建立后 60 秒左右自动断开原因Nginxproxy_read_timeout默认 60 秒期间没有数据往来就断开。心跳间隔大于 60 秒时必然触发。解决把proxy_read_timeout和proxy_send_timeout调到 3600s同时前端心跳间隔设成 25 到 30 秒。两个方向都要调只调一个还是会断。5.4 现象TLS 握手失败报证书不受信任原因证书链不完整或者证书域名和访问域名不匹配。自签证书在浏览器里也会报NET::ERR_CERT_AUTHORITY_INVALID。解决用openssl s_client检查证书链确保fullchain.pem包含中间 CA。域名不匹配就重新签发对应域名的证书。自签证书只适合内网测试生产环境用受信任 CA 签发的证书。5.5 现象Nginx 报 502后端日志无请求原因proxy_pass指向的 upstream 地址不通或者后端只监听了127.0.0.1而 Nginx 在另一台机器。也可能是 SELinux 阻止了 Nginx 的网络连接。解决在 Nginx 机器上curl后端地址确认连通性检查后端监听地址是不是0.0.0.0SELinux 环境下setsebool -P httpd_can_network_connect 1。6. 用 wss 做实时推送时我固定下来的几个习惯第一个习惯是心跳和超时永远成对调。Nginx 的proxy_read_timeout设 3600s前端心跳设 25s后端如果也有空闲超时比如 Tomcat 的connectionTimeout一并调到大于心跳间隔。这三个值只要有一个小于心跳间隔连接就会在某个不确定的时刻断掉排查起来非常费劲。我一般会在配置注释里写清楚这三个值的依赖关系下次改的时候不至于漏掉。第二个习惯是上线前用wscat和浏览器双端验证。wscat验证的是 TLS 和升级链路浏览器验证的是Origin校验和混合内容策略两者覆盖的失败面不同。只测一个经常会漏比如wscat不带Origin头后端setAllowedOrigins配错了它照样能连上但浏览器就连不上。第三个习惯是给 WebSocket 连接加唯一标识和日志。后端在connect时生成一个connIddisconnect时打印connId和关闭码。关闭码 1006 通常是非正常断开1000 是正常关闭1008 是策略拒绝。有了这个日志用户报“消息收不到”时能快速定位是连接没建立还是建立后断了。第四个习惯是证书到期监控。Lets Encrypt 证书 90 天到期certbot 自动续期偶尔会因为端口占用或 DNS 问题失败。我一般会在续期后加一个nginx -s reload的 hook再用一个定时任务检查证书剩余天数少于 15 天就告警。证书过期导致wss全挂这种事故一旦发生就是全量用户受影响比单个接口挂掉严重得多。最后一个习惯是压测时关注文件描述符和内存。WebSocket 是长连接每个连接占一个 fd默认ulimit -n是 1024上万连接直接打满。Nginx 的worker_connections和系统ulimit都要调后端服务的连接池和线程模型也要评估。我见过一次线上事故连接数上来后 fd 耗尽新连接全部失败但老连接还活着表现就是“部分用户能用部分用户不能用”排查了半天才定位到 fd 限制。这些参数没有银弹只能根据实际连接数和机器规格压测后确定。希望帮到你。本文还有配套的精品资源点击获取
返回列表