ARTICLE DETAIL

资讯详情

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

SpringBoot WebSocket 升级 wss 完整指南:Nginx 配置与心跳保活

SpringBoot WebSocket 升级 wss 完整指南:Nginx 配置与心跳保活 简介面向使用Spring Boot 2.1框架的开发者提供一套完整的WebSocket安全访问配置示例。它围绕一个具体项目展开重点解决在已有HTTPS基础上为wss安全协议启用访问的问题帮助读者掌握从生成SSL证书、配置Tomcat连接器、注册WebSocket处理器到前端使用wss地址建立连接并收发消息的完整链路。内容包含服务端处理逻辑示例、前端JavaScript客户端代码、Maven工程配置以及相关的证书文件覆盖依赖引入、握手通信、消息回显等关键环节适用于聊天室、实时数据推送等需要安全长连接的场景。整个压缩包共66个文件大小仅61KB核心文件以Java源码、XML配置和jks证书为主同时附带Git仓库元数据目录结构清晰便于对照项目工程理解各项配置的作用。已有1.3万余人学习该资源适合有一定Spring Boot基础、希望快速为实时通信加上HTTPS安全通道的开发者参考。1. 从 ws 到 wssSpringBoot WebSocket 为什么不能停在裸连下午三点线上环境突然一批用户反馈推送不更新我打开浏览器控制台看到一行报错Mixed Content: The page at https://... was loaded over HTTPS, but attempted to connect to an insecure WebSocket endpoint ws://...。那一刻我就知道又是 wss 没配好。SpringBoot WebSocket 在内网调通太容易了ws://localhost:8080/ws一连就上但一旦前端页面走 HTTPS浏览器会直接拦掉所有明文 WebSocket 连接连握手都不发。这篇就是把ws升级成wss的完整落地过程从 SpringBoot 服务端证书装载、Nginx 反向代理、JS 客户端心跳到上线前验证每一步都带参数和排错记录适合正在把 WebSocket 从开发环境往生产环境搬的从业者。2. SpringBoot 服务端起 wss内嵌容器、证书装载与端点注册2.1 wss 在 SpringBoot 里的完整链路谁握手、谁加解密、谁转发先说清楚 wss 的本质。wss 并不是 WebSocket 协议新增了什么能力它就是把 WebSocket 握手和消息帧整体放进 TLS 加密隧道里。浏览器侧做的事是先走一次 HTTPS 的 TLS 握手然后在加密通道里发起 HTTP Upgrade 请求升级到 WebSocket。理解这一点很重要因为整个 wss 配置其实是在回答两个问题TLS 在哪一层终结WebSocket 的 Upgrade 请求如何到达 SpringBoot 的WebSocketHandler常见部署有三种链路。第一种是 SpringBoot 内嵌 Tomcat 直接挂证书浏览器直连wss://host:8443/wsTLS 和 WebSocket 都在同一个端口处理。第二种是 Nginx 在 443 端口终结 TLS然后通过 HTTP/1.1 Upgrade 把连接转发给内网 SpringBoot 的ws://127.0.0.1:8080/ws这是生产环境最常见的模型。第三种是内网穿透场景frp 服务端挂着域名证书frpc 把流量转发到内网 SpringBoot浏览器看到的是wss://domain/ws穿透隧道里跑的是裸 ws。这三种模型的共同点是浏览器永远只认 HTTPS 页面下的 wss任何ws://都会被当混合内容拦掉。区别在于证书挂在哪一层、TLS 在哪一层终结。很多新手在第一种模型下用自签名证书调通了换到第二种模型就翻车多半是因为不清楚「浏览器到 Nginx 是 wssNginx 到 SpringBoot 是 ws」这个分段关系于是在 Nginx 里配了 SSL 后又要求 SpringBoot 也走 HTTPS结果证书重复加载、端口错乱。先把这个链路图刻在脑子里后面的配置才不会玄学式试错。部署模型TLS 终结层SpringBoot 侧监听浏览器访问地址内嵌容器直挂证书SpringBoot/Tomcatwss://host:8443/wswssNginx 反向代理Nginx 443ws://127.0.0.1:8080/wswssfrp 内网穿透frps 公网端口ws://127.0.0.1:8080/wswss2.2 直接让 SpringBoot 挂证书一段能跑通的完整配置如果你的场景是内网工具、测试环境、或者是给固定小团队用的管理后台不想引入 Nginx可以直接让 SpringBoot 内嵌容器挂证书。先准备证书。我一般用 keytool 生成自签名证书做本地验证命令如下keytool -genkeypair -alias websocket-demo \ -keyalg RSA -keysize 2048 \ -storetype PKCS12 \ -keystore websocket-demo.p12 \ -validity 3650 \ -dname CNlocalhost, OUDev, OExample, LCity, STState, CCN这个命令生成一个 PKCS12 格式的证书库文件websocket-demo.p12-validity 3650是 10 年有效期-dname里CNlocalhost要和你实际访问的域名一致浏览器做域名校验时就是拿这个字段比对地址栏的。-storetype PKCS12是为了让 Java 和 Nginx 都能直接读取。然后把证书信息写进 SpringBoot 的application.ymlserver: port: 8443 ssl: enabled: true key-store: classpath:websocket-demo.p12 key-store-type: PKCS12 key-store-password: changeit key-alias: websocket-demoserver.ssl.key-store指向 classpath 下的 p12 文件key-store-password对应生成时设置的密码key-alias要跟 keytool 里的 alias 一致。这里最容易被忽略的是密码问题keytool 交互过程中会要求设置 keystore 密码和 key 密码SpringBoot 的key-store-password读的是 keystore 密码如果两个密码设得不一样启动时会报keystore password was incorrect。再注册 WebSocket 端点。新建配置类实现WebSocketConfigurerConfiguration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new DemoWebSocketHandler(), /ws) .setAllowedOrigins(*); } }registry.addHandler的第一个参数是你自己的TextWebSocketHandler实现类第二个参数/ws是客户端连接的路径。setAllowedOrigins(*)表示允许任意来源握手生产环境建议收紧成具体域名列表比如setAllowedOrigins(https://example.com)否则等于允许任何网页往你的 WebSocket 端点发起连接。这段配置跑起来后浏览器连wss://localhost:8443/ws就能握手成功。但注意自签名证书会让浏览器弹「您的连接不是私密连接」需要手动点「高级 → 继续前往」。这只适合开发联调不适合对外业务原因不只是体验更关键的是自签名证书无法被客户端代码里的证书校验逻辑信任很多语言的原生 WebSocket 客户端会因为证书链不完整而直接握手失败。2.3 生产环境为什么不建议直接挂Nginx 终结 TLS 的选型理由上面这套直接挂证书的方案能跑通但我一般只在两种场景用一是纯内网工具二是给前端联调用的一体化工程。生产环境我强烈建议把 TLS 终结在 Nginx理由有三个。第一证书轮换和维护不该绑在 Java 进程上。SpringBoot 应用重启一次的成本不低而证书每年或每 90 天要换一次如果证书维护要依赖重启应用发布窗口就成了瓶颈。Nginx 可以nginx -s reload平滑加载新证书对业务零影响。第二Nginx 在 TLS 之外还能补上 WebSocket 场景需要的基础防护proxy_read_timeout超时控制、client_max_body_size限制、upstream 健康检查这些在 SpringBoot 里做要么集成额外组件要么自己造轮子。第三多应用共享 443 端口时需要 SNI 区分域名Nginx 一个 server 块一个证书就能搞定SpringBoot 内嵌容器做 SNI 就比较绕。还有一点是关于 HTTP/2 的Nginx 上开了 HTTP/2 不影响 WebSocket但要注意后端转发必须是proxy_http_version 1.1因为 WebSocket 握手是基于 HTTP/1.1 的 Upgrade 机制HTTP/2 的 Upgrade 语义完全不同。这个细节在下一章配置里会重点讲很多人在 Nginx 上开了 HTTP/2 后 WebSocket 连接失败就是因为后端转发用了默认的 HTTP/1.0Connection: Upgrade头被剥离了。3. 用 Nginx 把 ws 升级成 wss配置模板与参数拆解3.1 Nginx 代理 wss 的配置模板Upgrade 头与 http/1.1 缺一不可生产环境最常用的是 Nginx 终结 TLS再把 WebSocket 流量转发给内网 SpringBoot。我先给一份能直接用的模板再拆每一行的作用。upstream websocket_backend { server 127.0.0.1:8080; keepalive 32; } server { listen 443 ssl http2; server_name ws.example.com; ssl_certificate /etc/nginx/certs/ws.example.com_fullchain.pem; ssl_certificate_key /etc/nginx/certs/ws.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; location /ws { proxy_pass http://websocket_backend; 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 75s; proxy_send_timeout 75s; } }这段配置的关键在location /ws块。proxy_http_version 1.1是必须的Nginx 默认向后端转发用的是 HTTP/1.0而 HTTP/1.0 没有 Upgrade 语义。proxy_set_header Upgrade $http_upgrade把浏览器发来的 Upgrade 头原样传给后端Connection upgrade则显式告诉后端要建立升级连接。这两个头少任何一个SpringBoot 那边的握手都会失败返回 400 或 426。proxy_read_timeout 75s是我配置里特别标注的。Nginx 的proxy_read_timeout默认是 60s意思是 60 秒内后端没返回数据就断开连接。WebSocket 空闲时没有任何数据流动如果前端不做心跳连接会在 60 秒左右被 Nginx 单方面掐断表现就是「隔一会儿就掉线一次」。我会把proxy_read_timeout和proxy_send_timeout调到 75s比前端心跳间隔 30s 的两倍还长留出足够余量这个参数博弈在后面避坑章还会再讲。keepalive 32在 upstream 里是让 Nginx 与后端 SpringBoot 之间保持长连接复用减少频繁建连的开销。它不影响 WebSocket 本身但能降低高并发下后端 TIME_WAIT 连接的数量。3.2 证书、443 端口和浏览器信任链让 wss 一打开就绿锁证书是 wss 里仅次于握手头的第二大坑。很多人在本地用curl -k https://...调试通过就以为证书没问题结果浏览器一连就报ERR_CERT_INVALID原因多半是证书链不完整。我用letsencrypt签发的证书举例/etc/letsencrypt/live/ws.example.com/目录下会生成这几个文件文件作用Nginx 里对应的配置项fullchain.pem站点证书 中间证书的完整链ssl_certificateprivkey.pem私钥权限必须收紧ssl_certificate_keycert.pem仅站点证书不含中间证书单独用会触发链不完整ssl_certificate必须填fullchain.pem不能填cert.pem否则浏览器会因为没有中间证书而无法构建出完整的信任链。ssl_certificate_key对应privkey.pem文件权限建议设为600并确保 Nginx 进程用户可读。ssl_protocols我一般只开 TLSv1.2 和 TLSv1.3TLSv1.0/1.1 在 2020 年后已被主流浏览器标记为不安全开了反而可能被安全扫描工具报漏洞。一个容易忽略的细节是端口。listen 443 ssl http2意味着浏览器要用标准的wss://ws.example.com/ws访问不带端口。但如果你把 listen 改成8443前端 URL 必须写成wss://ws.example.com:8443/ws不写端口就默认走 443 或 80连接必然失败。这个「443 不用写、非 443 必须写」的规则是所有部署方式通用的。3.3 frp 隧道场景frps 挂证书、frpc 转发内网 ws如果你的 SpringBoot 服务跑在 NAT 后面的内网机器上没有公网 IP又想用wss://域名对外提供 WebSocket 服务常见做法是用 frp 做反向穿透。这个场景下证书要挂在 frps服务端因为浏览器直连的是 frps 的公网端口。frps 侧配置里开启 HTTPS 虚拟端口# frps.ini bindPort 7000 vhostHTTPSPort 443 subdomainHost ws.example.com tlsMode truevhostHTTPSPort 443让 frps 在公网 443 端口监听 TLS 流量tlsMode true表示 frps 自己终结 TLS。证书通过tls_cert_file和tls_key_file指定路径指向 fullchain 和 privkey。内网机器上的 frpc 配置则简单得多# frpc.ini [websocket] type tcp localIP 127.0.0.1 localPort 8080 remotePort 7000注意这段 frpc 配置转发的是裸 TCP 流量给内网 SpringBoot 的8080端口也就是说 frps 把 TLS 解掉后隧道里跑的是ws://127.0.0.1:8080/ws跟 Nginx 模型一样。浏览器访问wss://ws.example.com/ws时frps 按接收到的 HTTP Upgrade 头把流量路由到匹配的 frpc。有个坑是subdomainHost要和证书域名一致否则 frps 的虚拟主机路由会因为 Host 不匹配直接返回 404。我一般会让 frps 只做端口转发TLS 终结和域名路由都交给前面再挂一层 Nginx这样证书轮换和访问日志都集中在 NGINX 这一层frp 本身不用频繁动。4. 浏览器与 JS 客户端wss 握手、心跳与重连策略4.1 WebSocket 的 JS 客户端动态协议与连接参数服务端和代理都通了前端如果还写死ws://也白搭。我在生产环境要求前端统一用动态协议判断不要写死。核心代码就这几行function createWebSocket(urlPath) { const protocol location.protocol https: ? wss:// : ws://; const wsUrl protocol location.host urlPath; const ws new WebSocket(wsUrl); ws.onopen function () { console.log([ws] connected:, wsUrl); // 连接建立后启动心跳 startHeartbeat(ws); }; ws.onclose function (event) { console.warn([ws] closed, code:, event.code, reason:, event.reason); // 关闭后按策略重连 scheduleReconnect(wsUrl); }; ws.onerror function (error) { console.error([ws] error:, error); // onerror 之后通常跟着 onclose不要在 error 里重复重连 }; return ws; }location.protocol判断是这一小节的核心逻辑页面是 HTTPS 时自动用wss://是 HTTP 时用ws://这样在联调和生产环境之间切换不用改代码。location.host带了域名和端口省去手动拼接端口导致连错的问题。onerror里不重连是我有意设计的因为浏览器实际行为是onerror后必触发onclose如果两处都写重连逻辑会造成一次断线触发两次连接请求后端会看到大量重复握手。一个容易忽略的细节是使用new WebSocket(wsUrl)时如果服务端返回了非 101 的响应码浏览器不会走onerror而是直接触发onclose并且event.code会是一个非 1000 的值。所以排查问题时onclose里的event.code比onerror更有参考价值。4.2 心跳机制实现前后端配合的保活方案WebSocket 的掉线问题一半是代理超时一半是网络中间设备把空闲连接当垃圾回收了。心跳是解决这两个问题的唯一有效手段。我在前端实现的心跳逻辑是一个「30 秒一次探测、连续 3 次无响应判死」的机制function startHeartbeat(ws) { const heartbeatInterval 30000; // 30s 发一次心跳 const maxMissingPong 3; // 连续 3 次没收到 pong 就判定连接不可用 let missingPongCount 0; let heartbeatTimer null; const sendPing function () { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: ping, timestamp: Date.now() })); } }; // 收到任何消息都认为连接是活的重置连续丢失计数 ws.onmessage function (event) { const data JSON.parse(event.data); if (data.type pong) { missingPongCount 0; } }; heartbeatTimer setInterval(function () { missingPongCount; if (missingPongCount maxMissingPong) { console.warn([ws] heartbeat timeout, closing connection); ws.close(); clearInterval(heartbeatTimer); return; } sendPing(); }, heartbeatInterval); }这段代码的思路是前端每 30 秒发一条ping消息并把计数器加 1只要收到任何后端消息就把计数器清零当计数器累计到 3也就是 90 秒内一条消息都没收到就主动ws.close()触发重连。这里要理解「收到任何消息都清零」的含义——不一定要收到 pong后端推送的业务消息也能证明连接活着所以pong和业务消息在心跳逻辑里等价。有个细节值得注意ws.onmessage在这个函数里重新赋值了而你在外层创建的 WebSocket 实例很可能也注册过别的onmessage处理器。安排代码时要把心跳的onmessage和业务消息处理合并成一个分发函数别互相覆盖否则会出现「业务能收到消息但心跳逻辑完全不工作」的隐蔽 bug。4.3 SpringBoot 服务端怎么配合心跳空闲超时与异常断开服务端不能只等客户端来心跳自己也得有兜底。SpringBoot 的TextWebSocketHandler里我一般会做三件事设置会话空闲超时、处理BinaryMessage之外的异常消息、在连接关闭时清理资源。Component public class DemoWebSocketHandler extends TextWebSocketHandler { Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { session.setMaxIdleTimeout(120000L); // 连接建立时记录会话用于后续的服务端主动推送 SessionRegistry.add(session.getId(), session); } Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); // 心跳消息直接回 pong不进入业务逻辑 if (ping.equals(payload) || payload.contains(\type\:\ping\)) { session.sendMessage(new TextMessage({\type\:\pong\})); return; } // 业务消息处理... } Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { SessionRegistry.remove(session.getId()); } }setMaxIdleTimeout(120000L)的单位是毫秒120 秒是 SpringBoot 服务端的空闲保底即使前端因为某种原因没发心跳服务端也能在 120 秒后主动回收死连接。这个值我会设得比前端心跳周期30s大得多目的是让服务端兜底而不是跟前端抢着断。handleTextMessage里ping的匹配我用了两种判断一种是纯文本ping另一种是 JSON 格式里带type:ping。因为前端的ws.send(JSON.stringify({ type: ping }))实际发的是 JSON 字符串服务端拿到的payload是{type:ping,timestamp:...}如果你只判断equals(ping)心跳就永远不会命中。服务端主动推送时还有个常见的坑session.sendMessage会抛IOException。如果连接已经被对端关闭但是服务端会话还没触发afterConnectionClosed发消息就会异常。我在SessionRegistry的工具方法里会捕获这个异常并主动关闭会话而不是让它冒泡到 Spring 的消息处理器里把线程搞乱。5. 避坑与常见问题排查从握手 403 到绿地变灰5.1 握手阶段前端报 403后端却没有任何日志现象浏览器控制台显示WebSocket connection to wss://... failed: Error during WebSocket handshake: Unexpected response code: 403但 SpringBoot 端的日志里完全没有握手请求记录。原因403 几乎都是握手请求根本没到后端而是被 Nginx 的proxy_pass路径映射挡掉了。最常见的一种情况是后端端点是/ws而前端访问的是/ws/或者 Nginx location 写的是/ws/代理到后端时带了额外路径。另一种情况是setAllowedOrigins没配好但那种会在后端日志里留下Handshake failed due to invalid Origin header记录很好区分。解决先在 Nginx 上开 access_log 确认请求有没有到 Nginx再开 upstream 日志确认有没有转发到后端。路径问题用最简单的方式排查把proxy_pass http://websocket_backend;改成proxy_pass http://websocket_backend/ws;同时前端 URL 保持wss://domain/ws。如果改了就能通说明是路径两层叠加了。另一个快速验证是把setAllowedOrigins(*)临时放开如果 403 消失就是 Origin 校验问题再用具体域名替换回来。5.2 混合内容拦截https 页面里的 ws:// 被浏览器静默拒绝现象页面在https://example.com/admin下正常打开WebSocket 状态一直是CONNECTING控制台报Mixed Content网络面板里那条 ws 请求被浏览器直接标为失败。原因浏览器安全策略规定HTTPS 页面里的所有子资源请求必须走 HTTPS明文 WebSocket 属于降级内容直接拦截。这个拦截发生在网络层之前所以服务端和 Nginx 日志里什么都查不到看起来像连接被黑洞吞了。解决按 4.1 节的location.protocol动态判断来拼wsUrl不要在前端代码里写死。如果你接手的是个老项目全局搜一下ws://字符串把硬编码的地方全部替换。还有一种隐蔽情况后端返回的推送数据里有把资源地址写死为http://的逻辑前端拿到后拼接出ws://这种要连后端一起排查。5.3 证书链不完整curl 能过、浏览器过不了现象用curl -k https://ws.example.com能拿到响应curl --cacert ca.pem https://ws.example.com也正常但浏览器访问 wss 报ERR_CERT_INVALID展开详情提示「证书链不正确」。原因curl -k跳过了证书校验所以必然能通--cacert是你手动指定了信任锚点说明服务端把证书发出来了。真正的区别在于浏览器只会用系统信任库去验证证书链如果服务端没把中间证书一起下发浏览器无法从站点证书回溯到根证书。这对用云厂商免费证书、Lets Encrypt 的用户尤其常见他们经常只把域名证书文件传上去漏了中间证书。解决Nginx 的ssl_certificate必须配置fullchain.pem而不是cert.pem。验证方法是用 openssl 模拟服务端下发证书链的过程openssl s_client -connect ws.example.com:443 -servername ws.example.com -showcerts /dev/null 2/dev/null | grep -E s:|i:输出的证书链里至少要看到两行s:是你的站点证书紧跟着的i:是中间证书的签发者再往上应该还有一层根证书。如果只有一行s:就结束了说明中间证书缺失。另一个常用检查是用https://crt.sh或本地openssl verify -untrusted intermediate.pem cert.pem验证。5.4 60 秒必断心跳和 Nginx 超时在博弈现象WebSocket 连接能正常建立消息也能收发但每 60 秒左右必定掉线一次掉线后前端重连又能恢复如此反复。原因这是一个非常经典的参数博弈。Nginxproxy_read_timeout默认值是 60s意思是只要 60 秒内从后端读不到任何数据连接就被 Nginx 掐断。如果你的前端心跳间隔刚好是 60s 或者更长心跳在断开后才发出去自然收不到 pong前端又触发重连现象就成了「周期性秒断」。解决两件事要同时做缺一不可。第一前端心跳周期必须明显小于 Nginx 超时时间比如心跳 30s、超时 75s。第二Nginx 的proxy_read_timeout和proxy_send_timeout要手动调大到 75s 或更长不要依赖默认值。还要注意 SpringBoot 侧的setMaxIdleTimeout也要大于心跳周期我一般设 120s。这里没有银弹超时时间必须满足不等式前端心跳间隔 × 2 Nginx 超时时间 SpringBoot 空闲超时。5.5 frp 隧道公网端口通、内网端口不通现象frpc 显示连接成功浏览器访问wss://domain/ws时直接超时或拒绝连接但内网直接访问ws://127.0.0.1:8080/ws完全正常。原因frp 的vhostHTTPSPort和bindPort是两个不同的端口前者是给外部 HTTPS/WebSocket 流量用的入口后者是 frpc 回连 frps 的控制通道。新手经常只配置了bindPort 7000忘了vhostHTTPSPort 443或者把两者的值填反了。还有一种情况是云厂商安全组只放开了7000没放开443。解决先确认安全组和主机防火墙对vhostHTTPSPort端口放行。然后在公网机器上用 telnet 或 nc 测端口连通性nc -zv ws.example.com 443通了之后再用openssl s_client -connect ws.example.com:443 -servername ws.example.com看 frps 是否正常下发证书。最后确认 frpc 里remotePort对应的type tcp的转发规则确保 localPort 指向 SpringBoot 的 8080。这一整套查下来99% 的问题集中在端口没放行和证书没挂对。6. 上线前验证一条龙从证书链到真实消息回显wss 配置完别急着发版我有一套固定的验证路径从证书链到真实消息回显每一条都跑过再放量。第一步验证证书链。用openssl s_client -connect ws.example.com:443 -servername ws.example.com确认服务端下发的证书链完整同时看Protocol字段是不是 TLSv1.2 或 TLSv1.3。第二步用wscat测真实握手wscat -c wss://ws.example.com/ws能进入交互模式就说明握手通了输入一条{type:ping}应该能收到{type:pong}。第三步看 SpringBoot 日志确认afterConnectionEstablished和handleTextMessage都被触发。第四步回到浏览器控制台打开网络面板刷新页面确认 WebSocket 请求状态是 101并切到 Console 观察心跳有没有周期输出。验证项命令/方法预期结果证书链openssl s_client -connect host:443 -showcerts至少两级证书链无 verify error握手wscat -c wss://domain/ws进入交互模式ping 有 pong 回显后端日志观察afterConnectionEstablished日志有握手建立记录无异常堆栈心跳链路浏览器 console 观察 30s 周期心跳定时发出无断连重连最后我想起一次真实教训某次上线前我偷懒验证到「能用 wscat 连上」就收工了结果第二天线上反馈推送延迟。查了半天发现是后端某个拦截器在握手路径上做了 ip 白名单wscat 从本机连能过线上用户的 IP 全被拦。从那以后我每次上线前都强制走一遍「证书链 → wscat 握手 → 后端日志 → 浏览器真实域名」四步验证而且至少找一个非本机的网络环境测最后一步。wss 配通不难难的是把所有链路环节的假设都验证一遍别让「本地能连」骗了。希望帮到你。本文还有配套的精品资源点击获取
返回列表