ARTICLE DETAIL

资讯详情

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

Spring Boot集成WebSocket实时推送:从轮询到分布式实战

Spring Boot集成WebSocket实时推送:从轮询到分布式实战 去年接了一个上门烹饪预约平台的需求核心功能之一是用户下单后厨师端要实时收到新订单提醒同时用户在网页上能看到厨师的接单状态变化。一开始我直接用 HTTP 轮询每隔两秒请求一次订单状态接口结果接口压力大得离谱高峰期 Tomcat 线程池差点被打满用户体验还差——订单到了好几秒才有反应。后来换了 WebSocket问题一下清爽了。这篇就用实际项目里的踩坑经验把 Spring Boot 集成 WebSocket 的思路、代码、部署细节一次讲透覆盖从单体应用到多实例部署的完整链路。这篇内容适合谁后端开发做实时推送功能、前端同事对接长连接、还有那些要在自己项目里集成 WebSocket 但被各种断连、超时、代理问题折磨的人。如果你是第一次接触 WebSocket看完能直接写出一套能跑的功能如果你已经踩过坑里面有几节排障思路应该能帮上忙。1. 为什么实时通信选了 WebSocketHTTP 轮询的痛点与协议演进1.1 从 HTTP 半双工到全双工的刚需场景HTTP 协议本身是“请求-响应”模式客户端不发请求服务端就不能主动推送数据。这个设计在普通网页浏览场景下没问题但到了实时通信场景就特别别扭。就拿预约平台来说厨师端需要实时知道有没有新订单。如果用 HTTP 轮询那就得让客户端定时去问服务端“有单吗有单吗”服务端即便没有新数据也要回一个“没有”没数据时的每次请求都是纯浪费。而且 HTTP 每次请求都要带完整的请求头三次握手四次挥手来回折腾高频轮询时网络开销非常大。当时我统计了一下500 个在线客户端、2 秒轮询一次每秒就是 250 个请求大部分还没新数据。如果把轮询改成 1 秒一次服务端压力直接翻倍。WebSocket 解决的正是这个问题——一次握手建立连接后服务端和客户端都可以随时主动发数据真正实现了全双工通信。1.2 WebSocket 协议工作流程和核心优势WebSocket 并不是什么黑魔法它本质上是 HTTP 协议的一次升级Upgrade。客户端发起一个带Upgrade: websocket头的 HTTP 请求服务端返回101 Switching Protocols之后双方就在这个 TCP 连接上直接收发帧数据了。这个机制带来几个关键优势连接建立后不再有 HTTP 请求头开销传输效率高服务端可以主动推送数据不需要客户端轮询一个 TCP 连接支持双向通信资源占用远低于 HTTP 轮询消息支持文本和二进制格式灵活性高但从底层原理看WebSocket 和 HTTP 不同它需要保活机制。TCP 连接空闲太久会被中间设备比如 Nginx、云服务商的网关回收所以生产环境里必须考虑心跳保活这个后面细说。1.3 常见传输方案对比轮询、SSE 与 WebSocket实际做技术选型时并不一定非要用 WebSocket。我梳理了一个对比表方案通信方向实时性连接开销适用场景HTTP 轮询客户端拉取取决于轮询间隔每次请求都要握手低频、对实时性要求不高的状态查询SSEServer-Sent Events服务端单向推送实时单个长连接服务端通知类场景如系统公告、消息提醒WebSocket双向实时通信实时单个长连接需要双向交互的场景如聊天、协同编辑、实时订单推送SSE 其实是个被低估的方案如果只是服务端单向推数据SSE 更轻量、自动重连也更省心。但预约平台里客户端要上报状态、厨师要回复操作指令需要双向通信所以最终选了 WebSocket。2. Spring Boot 集成 WebSocket 的两种主流姿势2.1 原生 WebSocket 端点ServerEndpoint的轻量实现Spring Boot 集成 WebSocket 有两条路线第一条是直接用 Java 自带的 JSR-356 规范也就是ServerEndpoint注解端点的方式。先说代码结构。建一个 WebSocket 配置类把ServerEndpointExporter声明成 BeanConfiguration public class WebSocketConfig { Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }然后定义端点类用ServerEndpoint指定路径比如/websocket/orderComponent ServerEndpoint(/websocket/order) Slf4j public class OrderWebSocketEndpoint { private static CopyOnWriteArraySetSession sessions new CopyOnWriteArraySet(); OnOpen public void onOpen(Session session) { sessions.add(session); log.info(新连接接入当前连接数{}, sessions.size()); } OnClose public void onClose(Session session) { sessions.remove(session); log.info(连接关闭当前连接数{}, sessions.size()); } OnMessage public void onMessage(String message, Session session) { log.info(收到客户端消息{}, message); } OnError public void onError(Session session, Throwable error) { log.error(连接异常, error); } public static void sendToAll(String message) { for (Session session : sessions) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error(发送消息失败, e); } } } }这套方式最大的好处是简单——不需要额外依赖Spring Boot 的spring-boot-starter-websocket里面已经包含了相关支持。但要注意ServerEndpoint端点对象默认是多例的也就是说每个连接都会创建一个端点实例所以静态成员变量之类的东西要小心连接会话集合必须用线程安全的CopyOnWriteArraySet。2.2 基于 STOMP 的消息代理模式第二条路线是 Spring 家族主推的 STOMP 协议方式。STOMP 是一个简单的文本消息协议它在 WebSocket 之上定义了一套消息格式支持“订阅-发布”模型。简单理解WebSocket 是管道STOMP 是管道里跑的报文规范。配置类长这样Configuration EnableWebSocketMessageBroker public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 客户端订阅消息的前缀即服务端推送给客户端的地址 registry.enableSimpleBroker(/topic, /queue); // 客户端发送消息的前缀如 /app/sendMessage registry.setApplicationDestinationPrefixes(/app); } Override public void registerStompEndpoints(StompEndpointRegistry registry) { // WebSocket 握手端点前端连接这个地址 registry.addEndpoint(/ws).withSockJS(); } }发送消息时用SimpMessagingTemplate把消息推送到指定订阅地址Service public class OrderNotifyService { Resource private SimpMessagingTemplate messagingTemplate; public void notifyNewOrder(Long orderId) { MapString, Object payload new HashMap(); payload.put(orderId, orderId); payload.put(type, NEW_ORDER); // 推送到所有订阅了 /topic/order 的客户端 messagingTemplate.convertAndSend(/topic/order, payload); } }STOMP 模式的好处是支持按主题订阅天然就有“房间”的概念适合业务上需要区分频道的场景。比如预约平台里可以把订单消息发到/topic/order把系统公告发到/topic/notice客户端按需订阅。2.3 集成方式选型什么场景用哪种如果你要做的功能只是“服务端主动推送消息给所有连接”比如大屏数据刷新、系统公告那用ServerEndpoint就够了代码量最小也不容易出幺蛾子。如果你的业务里有很多消息类型、要区分用户、要实现“点对点”推送或者前端团队习惯用 STOMP.js 那套生态那 STOMP 模式更合适。它自带消息路由用户可以用订阅地址做细粒度隔离比如/queue/order/{userId}。我当时做的预约平台需求比较简单核心就是“有新单推送到所有在线厨师”和“用户端接收接单状态”所以选了原生ServerEndpoint连前端都只需要一个原生 WebSocket 对象就能搞定省了一堆依赖。但如果后续要加“按用户定向推送”原生端点就得自己维护每个连接对应的用户标识反而麻烦。选型时建议想清楚业务未来半年的演进方向。3. 手把手实现一个实时消息推送服务3.1 环境准备与依赖引入我用的是 Spring Boot 2.7 版本JDK 8Maven 项目。引入 WebSocket 依赖只需要一个 starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency这个 starter 会自动把 WebSocket 相关的容器初始化好不需要额外的版本号跟着 Spring Boot 父依赖走就行。有些老项目还在用 Spring Boot 2.1集成方式也差不多。如果是 Spring Boot 2.6 以上的版本要注意处理 SpringFox 3.0.0 的兼容问题后面单独讲。3.2 后端核心代码连接管理、会话存储与消息推送真正的项目里ServerEndpoint那个最简单的写法是不够的。因为你要能主动向指定用户推送消息光有一个连接集合不够还得把连接和用户关联起来。我的方案是客户端握手时在 URL 后面带上认证令牌比如/websocket/order?tokenxxx服务端在OnOpen里解析 token拿到用户 ID然后把 Session 存入 ConcurrentHashMap。Component ServerEndpoint(/websocket/order) Slf4j public class OrderWebSocketEndpoint { // 模拟用户标识生产环境从 token 解析 private static final ConcurrentHashMapLong, Session USER_SESSION_MAP new ConcurrentHashMap(); private Long userId; OnOpen public void onOpen(Session session) { String queryString session.getQueryString(); // 解析 token得出 userId this.userId parseUserId(queryString); USER_SESSION_MAP.put(userId, session); log.info(用户 {} 接入当前在线人数{}, userId, USER_SESSION_MAP.size()); } OnClose public void onClose(Session session) { USER_SESSION_MAP.remove(userId); log.info(用户 {} 断开当前在线人数{}, userId, USER_SESSION_MAP.size()); } OnMessage public void onMessage(String message, Session session) { log.info(用户 {} 发送消息{}, userId, message); } OnError public void onError(Session session, Throwable error) { log.error(用户 {} 连接异常, userId, error); } /** * 向指定用户推送消息 */ public static void sendToUser(Long targetUserId, String message) { Session session USER_SESSION_MAP.get(targetUserId); if (session null) { log.warn(用户 {} 不在线消息丢弃{}, targetUserId, message); return; } try { synchronized (session) { session.getBasicRemote().sendText(message); } } catch (IOException e) { log.error(向用户 {} 推送消息失败, targetUserId, e); } } /** * 向全部在线用户广播 */ public static void broadcast(String message) { USER_SESSION_MAP.values().forEach(session - { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error(广播消息失败, e); } }); } private Long parseUserId(String queryString) { // 简单起见这里写死一个模拟逻辑生产环境解析 token return 10001L; } }这里有几个关键设计点第一ConcurrentHashMap必须用静态的因为ServerEndpoint端点实例是多例的每个连接一个实例不用静态就存不住东西。第二发送消息时加了synchronized (session)锁。这是因为 WebSocket 的BasicRemote在并发发送时可能会抛异常多个线程同时给同一个 Session 发消息需要串行化。如果你用的是AsyncRemote可以不用这个锁但AsyncRemote又要自己处理发送完成的回调各有利弊。第三用户不在线时直接丢弃消息这在业务里要斟酌。预约平台里订单消息丢不得所以我后来改成了“离线则落库待推”等用户重新连接后拉取未读消息。这块可以结合自己的业务场景决定。3.3 前端接入JavaScript 连接与消息处理前端方面原生 WebSocket 就够了代码非常简单const socket new WebSocket(ws://localhost:8080/websocket/order?tokenxxx); socket.onopen function () { console.log(WebSocket 连接已建立); // 可以发送一条验证消息给服务端 socket.send(JSON.stringify({type: AUTH, token: xxx})); }; socket.onmessage function (event) { const message JSON.parse(event.data); console.log(收到服务端消息, message); // 根据 message.type 分发处理 if (message.type NEW_ORDER) { // 渲染新订单 } else if (message.type ORDER_ACCEPTED) { // 更新订单状态 } }; socket.onclose function () { console.log(连接关闭准备重连); // 断线重连逻辑 }; socket.onerror function (error) { console.error(WebSocket 错误, error); };注意开发环境用ws://生产环境如果用 HTTPS前端就要用wss://否则浏览器会拦截混用请求。这也是个容易忽略的细节——不 HTTPS 的页面访问ws://没问题但 HTTPS 页面访问ws://就会被限流甚至直接拒绝。3.4 心跳保活与断线重连设计WebSocket 连接空闲久了会被中间层回收这个是最容易踩的坑之一。我遇到过一两次客户端明明没断但服务器推消息死活推不过去。后来排查发现是连接被服务端关闭了但客户端不知道。解决办法是设计心跳机制。客户端每隔 30 秒发一个ping消息服务端收到后回一个pong如果客户端连续 3 次没收到pong就主动断开重连。服务端也要做兜底比如 60 秒没收到任何消息就关闭连接。前端心跳逻辑let heartbeatTimer null; function startHeartbeat() { heartbeatTimer setInterval(() { if (socket.readyState WebSocket.OPEN) { socket.send(JSON.stringify({type: PING})); } }, 30000); } // 收到任何消息都说明连接还活着重置计时器 socket.onmessage function (event) { const message JSON.parse(event.data); if (message.type PONG) { return; } // 处理其他业务消息 }; socket.onclose function () { clearInterval(heartbeatTimer); // 断线重连指数退避 reconnect(); }; let reconnectAttempts 0; function reconnect() { setTimeout(() { reconnectAttempts; initWebSocket(); }, Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)); }重连用指数退避策略1 秒、2 秒、4 秒……最长 30 秒避免断网时客户端疯狂重连把服务端打挂。服务端对应处理PING消息并回PONGOnMessage public void onMessage(String message, Session session) { if (PING.equals(message)) { try { session.getBasicRemote().sendText(PONG); } catch (IOException e) { log.error(发送心跳响应失败, e); } return; } // 处理业务消息 }有些小伙伴还会用session.setMaxIdleTimeout()来设置服务端的空闲超时时间比如session.setMaxIdleTimeout(60000)意思是 60 秒没数据就自动断开。这个配置要根据心跳间隔来设置要比心跳间隔大否则会误杀正常连接。4. 实战中常见的坑与排查技巧4.1 握手 404 或握手失败路径和拦截器问题WebSocket 集成后第一步就是测握手。我见过最多的问题是请求打到/websocket/order返回 404。排查步骤确认ServerEndpoint的路径是否带了项目上下文路径。如果你的应用配置了server.servlet.context-path/api前端 WebSocket 地址就要写成/api/websocket/order。确认ServerEndpointExporterBean 是否被正确扫描。Spring Boot 项目里主启动类和配置类不在同一包时容易出现 Bean 没被注册的问题。确认拦截器有没有挡掉握手请求。如果项目里有 Spring MVC 拦截器注意放行 WebSocket 握手路径。另外部分云环境比如某些负载均衡器或网关默认不支持 HTTP Upgrade 请求需要手动放行这也是生产环境部署时要小心的点。4.2 “stream disconnected before completion: websocket closed by server before res”解决实录这个报错是热词里出现频率很高的一个不少同事遇到这个错误时一脸懵。字面意思是“在流完成之前连接已断开服务端在响应完成前关闭了 WebSocket”。我遇到过的场景是后端收到客户端消息后处理业务逻辑时间比较长超过了几十秒然后客户端就报了这个错。根因有两类第一类是服务端消息处理太慢客户端的读超时先触发了或者服务端主动关闭了连接。WebSocket 虽然是长连接但这并不意味着服务端可以慢吞吞地处理消息。客户端通常都有自己的超时设置长时间没有响应就会断开。第二类是连接被 Nginx 或网关的超时机制回收。Nginx 默认proxy_read_timeout是 60 秒如果服务端 60 秒内没有往这个连接上写任何数据Nginx 就会主动断开。我的解决办法客户端收到消息后立即返回把耗时操作丢到线程池异步处理让 WebSocket 连接及时获得响应。调大proxy_read_timeout和proxy_send_timeout。加强心跳让连接保持活跃。proxy_read_timeout 300s; proxy_send_timeout 300s;这个问题的核心思路是WebSocket 连接要“活”着就要不断有数据流动不管是业务数据还是心跳数据。4.3 Spring Boot 2.6 与 SpringFox 3.0.0 的兼容问题这个坑和 WebSocket 没有直接关系但我在集成过程中踩到了因为项目里同时用了 Swagger 做接口文档。Spring Boot 2.6 之后Spring MVC 的路径匹配策略从AntPathMatcher改成了PathPatternParser而 SpringFox 3.0.0 老版本不兼容这个变化启动时会报java.lang.IllegalStateException: Failed to introspect Class ... Caused by: java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.combine(...)最简单的解决方案在application.yml里把路径匹配策略改回去。spring: mvc: pathmatch: matching-strategy: ant_path_matcher如果你不想改全局配置换成springdoc-openapi也可以但项目里如果已经有很多 Swagger 注解迁移成本也不小。我当时为了省事直接改了匹配策略。注意这个配置是 Spring Boot 2.6 才有的2.6 以下版本不用管。4.4 连接数上限与内存溢出问题WebSocket 长连接多了以后服务端文件描述符和内存都是瓶颈。每个 WebSocket 连接在 Java 中对应一个Session对象底层是 Socket默认每个连接会占用一定的内存缓冲。我在压测的时候发现单机 2000 个连接时服务还能扛住到 5000 个时开始频繁 GC后来调大了 JVM 堆内存才勉强撑住。生产环境要注意几点操作系统文件描述符上限要调整默认 1024 肯定不够。Spring Boot 内嵌 Tomcat 最大线程数要调大WebSocket 消息处理也占用 Tomcat 线程。session.setMaxTextMessageBufferSize()和session.setMaxBinaryMessageBufferSize()可以限制单条消息大小防止超大消息打爆内存。Tomcat 线程池配置server: tomcat: threads: max: 200 max-connections: 10000 accept-count: 200max-connections是 Tomcat 能接受的最大连接数这个和 WebSocket 连接数直接相关。如果跑在默认配置下线上连接数一上去就报“无法分配连接”那大概率就是这里的问题。5. 生产部署与分布式扩展5.1 Nginx 反向代理 WebSocket 的配置要点开发环境直连没问题上了生产环境基本都是 Nginx 反代到后端服务。Nginx 支持 WebSocket 反向代理但必须显式配置 Upgrade 头否则连接建立不起来。map $http_upgrade $connection_upgrade { default upgrade; close; } upstream backend_ws { server 127.0.0.1:8080 weight1; keepalive 32; } server { listen 80; server_name example.com; location /websocket/ { proxy_pass http://backend_ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $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 300s; proxy_send_timeout 300s; } }重点是proxy_set_header Upgrade $http_upgrade和proxy_set_header Connection $connection_upgrade这两行缺一不可。proxy_read_timeout和proxy_send_timeout我调成了 300 秒再加上前面说的客户端心跳连接基本上不会被 Nginx 掐断。还有一个细节如果 WebSocket 握手请求带有 Cookie 或鉴权 Header需要确认 Nginx 默认的转发策略不会把 Header 丢掉。默认情况下Host、Connection这类头会被 Nginx 重写其他自定义 Header 一般会透传但要注意下划线开头的 Header 默认会被忽略可以在http块里加underscores_in_headers on;。5.2 Spring Boot 打包部署开发环境命令行运行和 Tomcat 部署Spring Boot 项目本身是内嵌 Tomcat 的打包成 jar 之后直接命令行运行java -jar app.jar --server.port8080开发环境想在命令行直接跑也可以mvn spring-boot:run。这些都是常规操作但有个点要注意如果你打算把 Spring Boot 项目打 war 包丢到外部 Tomcat 里跑WebSocket 的支持方式和内嵌 Tomcat 略有不同。外置 Tomcat 部署时ServerEndpointExporter这种 Bean 可能不用注册因为外部容器会自己管理端点。Spring 官方文档的意思是内嵌容器需要手动用 Bean 注册端点外置容器则不用。但如果配置类留着也没关系只要不重复注册就好。我实际部署中发现同一个ServerEndpoint路径在外置 Tomcat 下偶尔会报“路径冲突”排查下来就是端点被注册了两次。5.3 多实例部署的会话共享Redis Pub/Sub 与 Redis Stream前面讲的连接管理都是基于单机内存的。如果服务有多个实例用户的 WebSocket 连接落在实例 A但业务逻辑跑在实例 BB 想推送消息给这个用户怎么办这就是分布式 WebSocket 的经典问题连接不共享消息要跨节点路由。最简单可靠的方案是借助 Redis。场景分类实时性要求高、消息量不大用 Redis Pub/Sub消息量大、需要消费确认或重放用 Redis Stream我先说 Pub/Sub 方案。所有实例启动时订阅同一个频道比如ws:order:notify当某个实例要向用户推送消息时先把这个消息发布到 Redis 频道所有实例都收到然后各自检查“这个用户是不是在我这里”在谁那里谁就真正推送。Service public class RedisNotifyService { Resource private StringRedisTemplate stringRedisTemplate; public void publish(Long userId, String message) { stringRedisTemplate.convertAndSend(ws:order:notify, userId | message); } }监听端Component public class WsNotifySubscriber { Resource private OrderWebSocketEndpoint endpoint; EventListener(ApplicationReadyEvent.class) public void subscribe() { // 伪代码实际用 RedisMessageListenerContainer } }这个方案最大的坑是Pub/Sub 消息是即发即弃的如果某个实例在消息发布那一刻挂了消息就丢了。所以对可靠性要求高的推送要用 Redis Stream 或者本地先落库。Redis Stream 的方式大致流程消息先写入 Stream每个实例用XREADGROUP去消费消费到消息后判断目标用户是否在自己节点上在自己节点就推送不在就 ACK 掉。这样即使某个实例临时不可用消息还在 Stream 里其他实例可以接管。说说订阅实现。这个也是热搜词里大家常问的点Spring Boot 里“拉取队列消息”并不像 JMS 那样一个注解搞定。最常用的封装是RedisMessageListenerContainer它本质上还是注册了一个监听器订阅 Redis 频道然后回调你的方法。如果你想用 Stream 协议手动XREAD拉取缓存里的StreamOperations也可以搞但还要自己处理消费组和 pending 列表业务复杂度高不少。我个人的经验是推送场景用 Pub/Sub可靠性要求高的场景直接上 Stream 消费组别在 Pub/Sub 上强行做补偿方案最后只会发现“补偿逻辑比业务逻辑还复杂”。5.4 结合业务场景烹饪预约系统、婚庆预约平台里的实时通信前面一直在聊技术最后举两个实际业务场景把 WebSocket 的应用串起来。第一个是上门烹饪预约系统。用户预约厨师上门核心链路是用户下单 - 平台推送给附近厨师 - 厨师抢单/接单 - 用户收到接单状态 - 厨师上门打卡 - 服务完成结算。这条链路里WebSocket 的身影出现在用户下单后平台实时推送新订单给在线的空闲厨师厨师接单后用户端马上收到状态变化不用刷新页面服务过程中厨师可以给用户发送文字通知比如“已出发”第二个是婚庆服务预约平台。这类平台的业务特点是参与角色多新人、婚庆顾问、策划师、服务商。比如一个套餐被新人拍下了顾问和服务商都要实时得到通知协调档期。用ServerEndpoint可以灵活地给不同角色推送不同消息或者用一个连接配合消息类型字段区分业务比 HTTP 轮询的体验好太多。WebSocket 在这些场景里解决的核心问题是一样的让状态变化即刻触达所有相关方。不用每个人靠刷新页面来同步信息这带来的体验提升是质的尤其是移动端网络差的时候轮询频繁断连长连接反而更省电省流量。6. 再聊几个实操中的细节6.1 消息内容建议统一为 JSON 并包含消息类型字段一开始我偷懒服务端直接推字符串“新订单来了”前端拿到后也不知道该怎样处理。后来所有消息统一成 JSON{ type: NEW_ORDER, timestamp: 1700000000000, data: { orderId: 123456, customerName: 张三 } }前端拿到消息先看type再分发逻辑便于扩展。data里放业务数据timestamp用于排查消息延迟。6.2 生产环境记得做连接数监控和日志分级WebSocket 连接数是个关键指标我踩过无人值守的坑晚上凌晨 2 点服务重启连接全部断开没有自动重连机制的客户端就完全“失联”了第二天早上用户投诉订单提醒没收到。所以生产环境一定要监控当前在线连接数掉到 0 就要告警连接建立/关闭速率异常波动往往是网络或服务问题消息推送成功率这个需要业务埋点日志方面连接建立和断开用 info 级别记录用户 ID 和时间业务消息推送失败用 error 级别记录完整消息内容心跳日志用 debug不然一天的 PING/PONG 日志能占好几 GB 磁盘。6.3 安全WebSocket 的鉴权和消息校验WebSocket 长连接建立后如果鉴权有问题等于给攻击者开了一扇长期窗口。我的实践是握手时通过token参数或 Header 传递登录凭证服务端校验通过才建立连接连接建立后定期校验 token 有效性比如每 10 分钟用户权限变了就主动断开对消息内容限长防止有人故意发超大消息消耗服务端内存需要 HTTPS/WSS 的场景要上wss://避免消息被中间人窃听配置消息大小限制OnOpen public void onOpen(Session session) { session.setMaxTextMessageBufferSize(64 * 1024); // 64KB session.setMaxBinaryMessageBufferSize(64 * 1024); // 64KB session.setMaxIdleTimeout(120000); // 2分钟空闲超时 }setMaxIdleTimeout这个参数要和客户端心跳配合好。心跳 30 秒一次空闲超时 120 秒留足了余量又不会让死连接长期占着资源。6.4 关于 Spring Boot 中间件部署和 WebSocket 前置网关的整合经验有的项目会把 WebSocket 服务部署在 API 网关之后网关负责鉴权、限流、转发。这种架构下要注意网关层通常会把 WebSocket 升级请求当成普通 HTTP 请求来处理如果网关不支持 WebSocket 协议代理升级就会失败。如果用了某些中间件转发需要确认它是否支持长连接透传。这个从“spring boot 中间件部署”和“chales 监听 websocket”这两个热搜词能看出不少人卡在工具链调试上。排查这种问题推荐先用浏览器开发者工具看 Network 面板的 WS 标签能看到握手请求和每一条收发帧。再用curl做一次握手模拟测试确认后端服务本身没问题就能逐步定位到是不是网关或代理的问题。curl -i -N \ -H Connection: Upgrade \ -H Upgrade: websocket \ -H Sec-WebSocket-Version: 13 \ -H Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw \ http://localhost:8080/websocket/order如果返回101 Switching Protocols说明后端 WebSocket 没问题问题就出在前置链路上。最后再分享一个真实的体会WebSocket 本身并不难难的是把连接生命周期管理好。我从一开始的简单 Demo到后来踩了 Nginx 超时、集群会话共享、离线消息补偿这些坑之后才真正理解长连接应用和普通 HTTP 接口在设计上的区别——HTTP 接口是一次性的出错了重试就行WebSocket 是一直活着的你得时刻关心它是不是真的活着、消息是不是真的送达了。希望这篇实战记录能帮你少走几步弯路。
返回列表