Spring Boot WebSocket本地连接失败的三大隐形关卡
1. 问题现场还原为什么“苍穹外卖”本地测试里WebSocket总连不上我第一次遇到这个情况是在给客户做“催单提醒”功能联调时。前端同事发来截图控制台红字刷屏——WebSocket connection to ws://localhost:8080/websocket/order failed后端日志里干干净净连握手请求都没进到Spring Boot的ServerEndpoint类里。更诡异的是打包成war丢到远程Tomcat上跑得好好的偏偏本地IDEIntelliJ启动的嵌入式Tomcat就是死活连不上。不是报404不是报500是直接Connection Refused——连TCP三次握手都卡在SYN_SENT阶段。这根本不是代码逻辑问题而是环境链路断了。后来排查清楚才发现本地开发模式下WebSocket连接失败90%以上不是因为ServerEndpoint写错了而是被三道隐形关卡拦住了前端请求路径错配、Nginx反向代理未透传WebSocket头、嵌入式Tomcat的WebSocket支持未显式启用。尤其“苍穹外卖”这类基于Spring Boot WebSocket实现订单实时推送的系统本地调试时最容易栽在这三个坑里。你可能觉得“不就是改个ws://地址吗加个ServerEndpoint注解就行”但现实是Spring Boot默认用的是Tomcat 9而Tomcat 9对WebSocket的支持需要两个前提一是Servlet容器必须启用WebSocket协议栈二是HTTP升级Upgrade请求头必须完整透传。本地开发时IDE启动的嵌入式Tomcat默认启用了WebSocket但一旦你加了Nginx做反向代理比如为了模拟生产环境或者前端用Vue CLI dev-server做了代理那Upgrade头就极大概率被过滤掉——而浏览器一旦收不到101 Switching Protocols响应连接立刻断开连日志都不会打。提示别急着翻源码或重写ServerEndpoint。先确认你的WebSocket URL是否真的指向了后端服务的真实端口。很多开发者把前端dev-server代理配置成/websocket→http://localhost:8080却忘了WebSocket协议必须用ws://而非http://且路径必须与ServerEndpoint的value完全一致包括斜杠。一个字符差就是Connection Refused。我试过最典型的错误配置前端写new WebSocket(ws://localhost:3000/websocket/order)后端ServerEndpoint(/websocket/order)但Nginx配置里只写了proxy_pass http://backend;没加proxy_http_version 1.1;和proxy_set_header Upgrade $http_upgrade;——结果就是前端永远卡在pending状态后端日志一片空白。这不是Bug是协议层的“失语”。所以解决这个问题的第一步不是改代码而是画一张本地调试的流量图浏览器 →WebSocket请求→ 前端dev-server可选→ Nginx可选→ Spring Boot嵌入式Tomcat → ServerEndpoint。每一道关卡都要验证是否放行了Upgrade: websocket和Connection: Upgrade这两个关键头。漏掉任何一环连接就断在半路。2. 根因定位四步法从TCP连接到Spring容器的全链路排查很多人一上来就查ServerEndpoint有没有扫描到或者看Configuration类里有没有注册ServerEndpointExporter。这方向没错但太早了。真正的排查顺序应该像网络工程师抓包一样从物理层往上推先确认TCP能通再看HTTP升级是否成功最后才查Spring容器是否加载了Endpoint。下面是我实测有效的四步定位法每一步都有对应命令和现象判断标准。2.1 第一步验证TCP端口可达性绕过所有HTTP层这是最基础也最容易被忽略的一步。很多开发者以为curl http://localhost:8080能返回首页就代表8080端口通但WebSocket用的是独立的TCP连接它不走HTTP GET而是发起一个带特殊头的HTTP Upgrade请求。所以必须用TCP层面的工具验证# 检查8080端口是否监听Linux/macOS lsof -i :8080 # 或者用netstat netstat -an | grep :8080 # Windows下用 netstat -ano | findstr :8080如果没输出说明Spring Boot根本没启动成功或者server.port被改成了其他值。这时候看IDE控制台最后一行是不是有Tomcat started on port(s): 8080 (http)。如果没有检查application.yml里是否误写了server.port: ${PORT:8080}但环境变量PORT没设导致端口为0——Tomcat会随机分配端口你连的8080其实是空的。注意Spring Boot 2.3默认禁用了Tomcat的AJP连接器但WebSocket依赖的是HTTP连接器。确保application.yml里没有server.tomcat.ajp.enabledtrue这种干扰配置它只影响AJP和WebSocket无关。2.2 第二步用curl模拟WebSocket Upgrade请求验证HTTP层握手TCP通了不代表HTTP升级能成功。我们用curl手动发一个Upgrade请求看后端是否返回101curl -i \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Version: 13 \ -H Sec-WebSocket-Key: $(openssl rand -base64 16) \ http://localhost:8080/websocket/order如果返回HTTP/1.1 101 Switching Protocols Connection: upgrade Upgrade: websocket ...恭喜HTTP层握手成功问题出在前端或Nginx。如果返回HTTP/1.1 404 Not Found ...说明ServerEndpoint(/websocket/order)没被扫描到或者路径写错了。检查ServerEndpoint类是否在Spring Boot主类的包扫描范围内或者是否漏了Component虽然ServerEndpoint本身是JSR-356标准注解但Spring Boot需要Component才能被Spring管理。如果返回HTTP/1.1 400 Bad Request ...常见于Sec-WebSocket-Key格式不对或者后端没处理WebSocket头。这时要确认Tomcat版本Spring Boot 2.2内嵌Tomcat 9.0.31默认支持WebSocket但如果你手动降级了Tomcat版本比如为了兼容老库就得检查pom.xml里tomcat-embed-websocket是否被排除。2.3 第三步抓包分析真实请求头定位Nginx/Proxy拦截点如果curl能101但浏览器连不上十有八九是中间代理Nginx或Vue CLI dev-server吃了Upgrade头。这时候必须抓包。我推荐用Chrome DevTools的Network标签页过滤WS类型点开失败的连接看Headers里的Request Headers必须存在Upgrade: websocket、Connection: Upgrade、Sec-WebSocket-Key、Sec-WebSocket-Version: 13如果缺了前两个说明代理层没透传针对不同代理场景场景A用了Nginx做反向代理检查Nginx配置必须包含这四行location /websocket/ { proxy_pass http://backend; proxy_http_version 1.1; # 关键必须是1.1HTTP/1.0不支持Upgrade proxy_set_header Upgrade $http_upgrade; # 关键透传Upgrade头 proxy_set_header Connection upgrade; # 关键透传Connection头 }漏掉任意一行Nginx就会把Upgrade请求当成普通HTTP请求转发后端Tomcat收不到升级指令自然返回400或404。场景BVue CLI dev-server代理vue.config.js里不能只写devServer: { proxy: { /websocket: { target: http://localhost:8080, changeOrigin: true, } } }必须显式开启WebSocket代理devServer: { proxy: { /websocket: { target: http://localhost:8080, changeOrigin: true, ws: true, // 关键必须设为true否则不代理WebSocket } } }2.4 第四步验证Spring容器是否加载了Endpoint终极代码层检查走到这一步如果前面都OK但还是连不上就要查Spring上下文了。在SpringBootApplication启动类里加个临时Bean打印所有注册的ServerEndpointComponent public class EndpointChecker { Autowired private ServletWebServerFactory servletWebServerFactory; PostConstruct public void checkEndpoints() { System.out.println( WebSocket Endpoint Check ); // 检查Tomcat是否启用了WebSocket支持 if (servletWebServerFactory instanceof TomcatServletWebServerFactory) { TomcatServletWebServerFactory factory (TomcatServletWebServerFactory) servletWebServerFactory; System.out.println(Tomcat WebSocket enabled: factory.getProtocolHandler().getUpgradeProtocol() ! null); } // 检查Spring是否扫描到了ServerEndpoint try { Class.forName(javax.websocket.server.ServerEndpoint); System.out.println(JSR-356 API available); } catch (ClassNotFoundException e) { System.out.println(JSR-356 API missing - check tomcat-embed-websocket dependency); } } }如果输出Tomcat WebSocket enabled: false说明内嵌Tomcat的WebSocket协议处理器没启用。这时要在application.yml里强制启用server: tomcat: protocol-header: x-forwarded-proto # 这行是关键确保Tomcat初始化时加载UpgradeProtocol additional-tomcat-connectors: - port: 8080 protocol: org.apache.coyote.http11.Http11NioProtocol3. ServerEndpoint深度配置不只是加个注解那么简单很多开发者以为只要在类上加个ServerEndpoint(/websocket/order)再写个OnOpen方法WebSocket就跑起来了。但在“苍穹外卖”这种高并发订单场景下这个注解背后藏着至少五个必须显式配置的细节否则上线后会出现连接数暴增、内存泄漏、消息乱序等问题。3.1 路径匹配规则斜杠是魔鬼绝对路径是铁律ServerEndpoint的value参数看着简单但它的匹配逻辑和Spring MVC完全不同。它匹配的是WebSocket URI的path部分且必须是绝对路径不支持Ant风格通配符。比如✅ 正确ServerEndpoint(/websocket/order)→ 匹配ws://host/websocket/order❌ 错误ServerEndpoint(websocket/order)→ 缺少开头斜杠Tomcat会尝试匹配/websocket/order但失败返回404❌ 错误ServerEndpoint(/websocket/**)→**不被支持启动时报IllegalArgumentException更隐蔽的问题是Context Path。如果你的Spring Boot应用设置了server.servlet.context-path/api那么WebSocket路径必须写成ServerEndpoint(/api/websocket/order)而不是/websocket/order。因为Tomcat的WebSocket映射是基于完整的请求URI不是相对路径。我踩过的坑本地开发时没设context-path测试环境设了/prod结果测试环境一直连不上。查日志发现No endpoint found for path [/websocket/order]其实应该写/prod/websocket/order。解决方案是统一用配置项动态生成Component public class WebSocketConfig { Value(${server.servlet.context-path:/}) private String contextPath; PostConstruct public void init() { // 动态注册Endpoint避免硬编码 String fullPath contextPath /websocket/order; System.out.println(WebSocket endpoint registered at: fullPath); } }3.2 Session管理别让每个连接都吃掉10MB内存WebSocket连接不像HTTP请求它是长连接Session对象会一直存活直到客户端关闭。ServerEndpoint默认的Session实现是Tomcat的WsSession它内部维护了一个很大的缓冲区默认8KB用于暂存未发送的消息。在“苍穹外卖”里一个骑手可能同时监听多个订单如果没做清理1000个连接就吃掉8MB内存加上心跳包、消息队列很容易OOM。必须显式配置Session超时和缓冲区大小ServerEndpoint( value /websocket/order, configurator CustomConfigurator.class // 自定义Configurator ) public class OrderWebSocket { // ... } // 自定义Configurator控制Session生命周期 public class CustomConfigurator extends ServerEndpointConfig.Configurator { Override public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { // 设置Session最大空闲时间毫秒 sec.getUserProperties().put(org.apache.tomcat.websocket.IO_TIMEOUT_MS, 30000L); // 设置发送缓冲区大小字节 sec.getUserProperties().put(org.apache.tomcat.websocket.SEND_BUFFER_SIZE_BYTES, 4096); } }实测心得把SEND_BUFFER_SIZE_BYTES从默认8192降到4096内存占用下降35%IO_TIMEOUT_MS设为30秒比默认的60秒更激进能更快释放僵尸连接。注意这个超时是“空闲超时”不是连接总时长不影响正常业务消息。3.3 并发模型OnMessage方法默认是单线程的OnMessage方法默认由Tomcat的WebSocket线程池串行执行。这意味着如果一个订单消息处理要200ms比如查数据库发短信那么同一Session的后续消息会排队等待。在高峰期一个骑手的连接可能积压几十条消息导致催单提醒延迟超过5秒。解决方案是启用异步处理OnMessage public void onMessage(Session session, String message) { // 提交到业务线程池立即返回 taskExecutor.submit(() - { try { processOrderMessage(session, message); } catch (Exception e) { // 记录异常但不阻塞WebSocket线程 log.error(Failed to process message, e); } }); } Bean public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 根据CPU核心数设 executor.setMaxPoolSize(50); executor.setQueueCapacity(1000); executor.setThreadNamePrefix(websocket-task-); return executor; }3.4 消息编解码JSON自动转换的陷阱很多教程教用OnMessage直接接收String然后Jackson手动解析。但Spring Boot提供了SendTo和MessageMapping可以自动序列化。不过ServerEndpoint原生不支持Spring的MessageMapping必须自己集成// 自定义MessageDecoder把JSON字符串转成OrderEvent对象 public class OrderEventDecoder implements MessageHandler.WholeString { private final ObjectMapper objectMapper new ObjectMapper(); Override public void handleMessage(String message) { try { OrderEvent event objectMapper.readValue(message, OrderEvent.class); // 处理事件 } catch (IOException e) { log.warn(Invalid JSON message, e); } } } // 在OnOpen里注册 OnOpen public void onOpen(Session session) { session.addMessageHandler(new OrderEventDecoder()); }关键经验不要在OnMessage里直接new ObjectMapper()它不是线程安全的。用Spring容器管理的ObjectMapperBean或者像上面那样用单例实例。3.5 安全加固别让WebSocket成为DDoS入口WebSocket连接建立后客户端可以持续发送消息没有任何频率限制。恶意用户可以伪造大量连接每秒发1000条消息直接打爆服务器CPU。必须加两道锁连接频控用Redis记录IP的连接次数1分钟内超过10次拒绝消息频控每个Session维护一个滑动窗口1秒内超过5条消息就断开OnOpen public void onOpen(Session session) { String ip getRemoteIp(session); String key ws:connect: ip; Long count redisTemplate.opsForValue().increment(key, 1); redisTemplate.expire(key, 60, TimeUnit.SECONDS); if (count 10) { session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, Too many connections)); return; } }4. Nginx与Tomcat协同配置生产环境必调的六个参数本地能跑通不代表生产环境没问题。“苍穹外卖”的生产架构通常是用户 → Nginx负载均衡SSL终止→ 多台Tomcat集群 → WebSocket服务。这时Nginx不仅是反向代理更是WebSocket连接的守门人。漏配任何一个参数都会导致连接在Nginx层就被重置。4.1 Nginx配置六行代码决定WebSocket生死这是经过线上压测验证的最小可行配置upstream websocket_backend { ip_hash; # 确保同一用户的WebSocket请求落到同一台Tomcat server 192.168.1.10:8080; server 192.168.1.11:8080; } server { listen 443 ssl; server_name api.cangqiong.com; # SSL配置省略... location /websocket/ { proxy_pass http://websocket_backend; proxy_http_version 1.1; # 必须HTTP/1.1才支持Upgrade proxy_set_header Upgrade $http_upgrade; # 必须透传Upgrade头 proxy_set_header Connection upgrade; # 必须透传Connection头 proxy_set_header Host $host; # 必须否则Tomcat日志显示host为空 proxy_read_timeout 86400; # 必须长连接超时设为24小时 } # 其他location... }重点解释proxy_read_timeout 86400Nginx默认proxy_read_timeout是60秒意思是如果60秒内后端没发数据Nginx就主动断开连接。WebSocket长连接需要这个值足够大否则Nginx会在空闲时断开导致前端不断重连。设为8640024小时是安全值实际可根据业务心跳间隔调整比如心跳30秒这里设为60秒即可。4.2 Tomcat集群Session共享WebSocket连接不能漂移WebSocket连接绑定的是单台Tomcat的内存Session。如果Nginx把同一个用户的请求轮询到不同Tomcat第二次连接就会创建新Session旧连接丢失导致消息收不到。解决方案只有两个方案A推荐Nginx ip_hash如上配置所示ip_hash保证同一IP的请求始终落到同一台Tomcat。缺点是无法应对用户IP变化比如移动网络切换但对“苍穹外卖”这种App端固定IP的场景很稳。方案BRedis广播Session复制用Spring Session Redis把WebSocket Session存到Redis并用Redis Pub/Sub广播消息。但要注意ServerEndpoint的Session对象不能直接序列化必须自定义序列化器Configuration EnableSpringHttpSession public class WebSocketSessionConfig { Bean public HttpSessionStrategy httpSessionStrategy() { return new HeaderHttpSessionStrategy(); // 用Header传递session id } }4.3 SSL/TLS优化Chrome 109的WebSocket加密要求Chrome 109开始强制要求wss://WebSocket Secure连接必须使用TLS 1.2且证书必须有效。如果Nginx用的是自签名证书或者TLS版本太低Chrome会直接拒绝连接控制台只显示net::ERR_SSL_VERSION_OR_CIPHER_MISMATCH。检查Nginx TLS配置ssl_protocols TLSv1.2 TLSv1.3; # 必须包含TLSv1.2 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; # 推荐加密套件 ssl_prefer_server_ciphers off;用openssl命令验证openssl s_client -connect api.cangqiong.com:443 -servername api.cangqiong.com # 查看输出里的Protocol和Cipher4.4 Tomcat Connector调优应对万级并发连接单台Tomcat默认最大连接数是200对“苍穹外卖”这种峰值10万订单的系统远远不够。必须修改server.xmlConnector port8080 protocolorg.apache.coyote.http11.Http11NioProtocol maxThreads1000 minSpareThreads100 maxConnections10000 connectionTimeout20000 keepAliveTimeout60000 acceptCount1000 compressionon compressionMinSize2048 noCompressionUserAgentsgozilla, traviata compressableMimeTypetext/html,text/xml,text/plain,application/json /关键参数maxConnections10000最大TCP连接数设为1万足够支撑5000个WebSocket连接每个连接占2个连接一个HTTP Upgrade一个WebSocketacceptCount1000当连接数达到maxConnections时新连接进入等待队列长度1000compressiononWebSocket消息通常是JSON压缩后体积减少60%节省带宽4.5 日志与监控快速定位线上WebSocket故障光靠Nginx access log看不出WebSocket问题。必须开启Tomcat的WebSocket详细日志在logging.properties里添加org.apache.tomcat.websocket.level FINE org.apache.coyote.http11.upgrade.level FINE这样就能看到每次Upgrade请求的完整流程FINE: Upgrading to WebSocket for request /websocket/order FINE: WebSocket session created: 0a1b2c3d-ef45-6789-abcd-ef0123456789 FINE: Sending text message to session 0a1b2c3d...再配合Prometheus Grafana监控三个核心指标tomcat_websocket_sessions_current当前WebSocket连接数tomcat_websocket_messages_received_total接收消息总数tomcat_websocket_messages_sent_total发送消息总数如果sessions_current突降说明Nginx或网络层有问题如果messages_received_total增长但messages_sent_total不涨说明后端处理阻塞。4.6 故障自愈Nginx主动健康检查防雪崩Nginx默认的upstream健康检查只检查HTTP 200但WebSocket服务可能HTTP接口正常返回200WebSocket却挂了比如内存溢出导致Upgrade失败。必须用自定义健康检查upstream websocket_backend { server 192.168.1.10:8080 max_fails3 fail_timeout30s; server 192.168.1.11:8080 max_fails3 fail_timeout30s; # 自定义健康检查每5秒用curl探测WebSocket Upgrade check interval5 rise2 fall3 timeout10 typehttp; check_http_send GET /health/ws HTTP/1.1\r\nHost: api.cangqiong.com\r\n\r\n; check_http_expect_alive http_2xx; }后端写个/health/ws接口用curl模拟Upgrade请求并返回200这样Nginx就能真正感知WebSocket服务是否存活。5. 前端避坑指南Vue/React里WebSocket的正确打开方式后端配置再完美前端写错了也是白搭。我在“苍穹外卖”项目里见过太多前端同学写的WebSocket代码表面能连上实则暗藏隐患重连机制失效、内存泄漏、消息乱序。下面是最精简可靠的Vue 3 Composition API写法。5.1 封装WebSocket类解决自动重连与状态管理不要直接new WebSocket()必须封装成可复用的Composable// composables/useWebSocket.ts import { ref, onUnmounted, watch } from vue interface WebSocketOptions { url: string heartbeat?: boolean reconnect?: boolean maxReconnectAttempts?: number } export function useWebSocket(options: WebSocketOptions) { const socket refWebSocket | null(null) const status refconnecting | open | closed | error(connecting) const messages refstring[]([]) const connect () { if (socket.value socket.value.readyState WebSocket.OPEN) return socket.value new WebSocket(options.url) socket.value.onopen () { status.value open console.log(WebSocket connected) // 发送心跳 if (options.heartbeat) startHeartbeat() } socket.value.onmessage (event) { messages.value.push(event.data) // 触发自定义事件供业务组件监听 window.dispatchEvent(new CustomEvent(websocket:message, { detail: event.data })) } socket.value.onclose (event) { status.value closed console.log(WebSocket closed, event.code, event.reason) // 自动重连 if (options.reconnect options.maxReconnectAttempts ! 0) { setTimeout(connect, 3000) // 3秒后重连 } } socket.value.onerror (error) { status.value error console.error(WebSocket error, error) } } const send (data: string) { if (socket.value?.readyState WebSocket.OPEN) { socket.value.send(data) } else { console.warn(WebSocket not ready, dropping message) } } const close () { if (socket.value) { socket.value.close() socket.value null } } // 心跳机制 let heartbeatTimer: NodeJS.Timeout | null null const startHeartbeat () { if (heartbeatTimer) clearInterval(heartbeatTimer) heartbeatTimer setInterval(() { if (socket.value?.readyState WebSocket.OPEN) { socket.value.send(JSON.stringify({ type: ping })) } }, 30000) // 30秒一次 } // 组件卸载时清理 onUnmounted(() { close() if (heartbeatTimer) clearInterval(heartbeatTimer) }) // 暴露API return { socket, status, messages, connect, send, close } }5.2 在组件中使用避免内存泄漏的三个要点script setup langts import { onMounted, onUnmounted, ref } from vue import { useWebSocket } from /composables/useWebSocket const { socket, status, messages, connect, send } useWebSocket({ url: wss://api.cangqiong.com/websocket/order, heartbeat: true, reconnect: true, maxReconnectAttempts: 5 }) // 1. 用watch监听status而不是在onopen里操作DOM watch(status, (newStatus) { if (newStatus open) { console.log(Ready to receive order updates) } }) // 2. 用window事件监听消息避免组件重复注册 const handleMessage (e: CustomEvent) { const data JSON.parse(e.detail) if (data.type order_update) { // 更新订单状态 } } onMounted(() { window.addEventListener(websocket:message, handleMessage) }) onUnmounted(() { window.removeEventListener(websocket:message, handleMessage) }) // 3. 发送消息前检查readyState const triggerRemind () { if (socket.value?.readyState WebSocket.OPEN) { send(JSON.stringify({ type: remind, orderId: 123456 })) } else { console.warn(WebSocket not ready, remind failed) } } /script关键经验不要在onmessage回调里直接更新组件state因为WebSocket可能在组件卸载后还在收消息导致updateon unmounted component警告。用window事件中转是最佳实践。send前必须检查readyState否则WebSocket is closed错误会静默吞掉消息。重连间隔要指数退避上面代码是固定3秒实际应改为Math.min(3000 * Math.pow(2, attempt), 30000)避免雪崩式重连。5.3 Postman调试技巧验证WebSocket服务是否真可用Postman 10.0支持WebSocket但默认不显示。调试步骤新建WebSocket请求URL填wss://api.cangqiong.com/websocket/order点击“Connect”观察状态栏是否变绿在Message框输入JSON如{type:test,data:hello}点Send查看Received Messages如果有回包说明服务正常如果连不上Postman会显示具体错误Error: unable to verify the first certificate→ SSL证书问题勾选“Disable SSL certificate verification”Error: WebSocket closed before handshake completed→ Nginx没透传Upgrade头检查proxy_http_version 1.1Error: Unexpected response from server→ 后端返回了404或400用curl复现5.4 移动端兼容性微信小程序与iOS Safari的特殊处理微信小程序不支持原生WebSocket必须用wx.connectSocket且域名必须备案// 小程序端 wx.connectSocket({ url: wss://api.cangqiong.com/websocket/order, success: () console.log(Connected), fail: (err) console.error(Connect failed, err), }) wx.onSocketMessage((res) { console.log(Received, res.data) })iOS Safari有个著名BugWebSocket连接后如果页面切到后台超过30秒连接会被系统杀死。解决方案是监听页面visibility状态document.addEventListener(visibilitychange, () { if (document.hidden) { // 页面切到后台记录时间 lastBackgroundTime Date.now() } else { // 页面回到前台检查连接是否还活着 if (socket.value?.readyState ! WebSocket.OPEN) { socket.value?.close() connect() // 重新连接 } } })6. 最终验证清单上线前必须跑完的十二项测试写完所有配置别急着上线。我整理了一份“苍穹外卖”WebSocket上线前的十二项必测清单每一项都对应一个真实线上事故。按顺序执行漏一项都可能引发客诉。序号测试项方法预期结果失败后果1TCP端口连通性telnet localhost 8080Connected后端服务未启动2HTTP Upgrade握手curl -i -H Upgrade: websocket -H Connection: Upgrade http://localhost:8080/websocket/order返回101 Switching ProtocolsNginx/Tomcat配置错误3前端WebSocket连接Chrome访问页面DevTools Network → WSStatus 101Connection Established前端URL或代理配置错误4消息收发双向前端send后端log查看是否收到后端session.getBasicRemote().sendText()前端onmessage是否触发双向消息1秒内到达Session未正确获取或发送缓冲区满5断网重连Chrome DevTools → Network → Offline再切回Online3秒内自动重连成功重连逻辑未生效或超时设置过长6高并发连接用wstest工具模拟1000个连接wstest -m echo -c 1000 -u ws://localhost:8080/websocket/order所有连接建立无超时Tomcat maxConnections不足7消息乱序测试前端连续send 10条消息后端按序处理并返回序号返回序号1,2,3...10OnMessage未异步处理线程阻塞8内存泄漏检测VisualVM连接Tomcat运行30分钟观察Old Gen内存内存平稳无持续增长Session未及时closeOOM风险9Nginx健康检查curl http://localhost:8080/health/ws返回200 OKNginx无法感知WebSocket故障10SSL证书验证openssl s_client -connect api.cangqiong.com:443Protocol: TLSv1.2, Verify return code: 0 (ok)iOS/Chrome 109无法连接11移动端真机测试iPhone Safari访问切后台再切回连接保持消息正常iOS后台连接被杀12日志完整性查看Tomcat logs/catalina.out有Upgrading to WebSocket和WebSocket session created日志无法定位线上问题