尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Spring Boot实时推送方案对比:短轮询、长轮询、SSE与WebSocket实战

发布时间:2026/9/8 0:11:13

资讯中心
01
ARTICLE

Spring Boot实时推送方案对比:短轮询、长轮询、SSE与WebSocket实战

Spring Boot实时推送方案对比:短轮询、长轮询、SSE与WebSocket实战
做 Web 开发这几年实时推送永远是一个绕不开的话题。用户在线聊天、后台任务进度、消息中心红点、股票行情刷新背后都是同一件事服务端有状态变化得第一时间让客户端知道。但 HTTP 协议天生是“请求-响应”模式客户端不发请求服务端就没办法主动开口。Spring Boot 工程里常见的实时推送方案我总结下来无非四种短轮询、长轮询、SSEServer-Sent Events和 WebSocket。这篇文章不打算罗列概念而是用三个我在项目里实际落地的场景把技术选型、核心代码、常见坑一次讲清楚。如果你想系统了解 Spring Boot 实时推送的套路或者正在纠结自己的场景该用哪种方案这篇应该能帮你省不少时间。1. 实时推送方案选型先看场景再看技术1.1 四种主流实时推送方案原理拆解先讲原理因为选型选错了后面写再多代码都是返工。短轮询是最朴素的方案。客户端开个定时器每 3 秒或 5 秒发一次请求服务端每次返回当前状态。真实场景里如果任务执行需要 10 秒客户端可能已经发了三次无意义的请求前两次拿到的都是“还没好”。反过来如果服务端在第 4 秒就完成了客户端也得等到第 6 秒才能查出来。结果就是实时性差服务端有 2 秒以上的感知延迟同时大量无数据变化的请求白白消耗了 Tomcat 线程和带宽。短轮询唯一的优势是没有任何技术门槛所有 Web 开发同学拿到就能写浏览器兼容性也最好。长轮询比短轮询聪明一点。客户端发起请求后服务端不立即返回而是把这个请求挂起来比如挂 30 秒这期间业务数据一旦就绪立刻把响应写回去如果 30 秒内一直没数据就返回一个超时标志客户端收到后立刻重新发起下一次请求。用这种方式数据到达和响应返回之间的延迟基本能控制在毫秒级同时请求次数比短轮询少得多。但代价是服务端需要维护大量挂起状态的请求且对请求线程的释放时机有要求搞不好就会占用连接资源。SSE全称 Server-Sent Events是 HTML5 规范里的东西。客户端通过 EventSource 发起一个 HTTP 请求服务端接受请求后保持连接不关闭然后持续把数据以文本流的方式推给客户端。它是单向的也就是只能服务端推送、客户端接收不需要客户端回消息。浏览器原生支持断线自动重连服务端还可以通过事件 ID 帮客户端做消息续传这一点在很多场景里非常实用。WebSocket是真正意义上的全双工长连接。首次握手走 HTTP然后升级为 WebSocket 协议之后服务端和客户端可以随时互相发消息。它的实时性是四种方案里最高的双向能力也是其他三种方案不具备的。缺点是实现和运维成本更高连接保活、心跳、鉴权、跨域、集群消息路由都要自己处理网关和代理服务器还得专门做协议升级的兼容配置。1.2 如何快速判断该用哪种方案我自己的项目经验里选型基本按照下面这个思路判断需要双向交互比如聊天、协同编辑、多人白板选 WebSocket这是唯一真正支持双向的长连接方案。只需要服务端单方向推送数据比如通知提醒、行情刷新、日志流优先选 SSE。代码量和复杂度远低于 WebSocket还自带重连和事件 ID 机制。项目环境很老浏览器兼容性要求极高或者设备厂商的 SDK 只支持普通 HTTP 请求才有可能回退到长轮询。任务状态的查询频率本身不高比如 5 到 10 秒轮询一次就能接受直接用短轮询都行没必要为了一次实时体验引入复杂框架。这里给一个对比表格方便你以后快速决策维度短轮询长轮询SSEWebSocket通信方向客户端主动请求请求挂起后被动响应服务端单向推送双向实时通信实时性取决于轮询间隔毫秒级毫秒级毫秒级浏览器兼容性全部全部现代浏览器现代浏览器服务端资源占用高无效请求多中挂起连接占资源低低实现复杂度低中低到中中到高典型场景非敏感状态查询低版本兼容环境通知、行情、日志聊天、协作选型这块我踩过一个很深的坑有一个内部项目要用实时推送做数据看板我上来就上了 WebSocket后来发现所有页面都只需要收数据没有一次需要往服务端发消息。用 WebSocket 白白维护了一套连接管理、心跳、断线重连的代码后来重构换成 SSE代码少了将近一半稳定性反而更高。所以选型的第一步永远是问自己这个场景数据流向到底是不是双向的。2. 案例一WebSocket 实现网页版聊天室2.1 依赖与基础配置先看一个最常见的场景网页版聊天室。用户进入页面能看到其他人上线、发送的消息自己也能发消息所有人即时看到。这类场景必须用 WebSocket因为每一次“发送”和“接收”都是双向的数据流动SSE 只支持单向根本做不到。Spring Boot 使用 WebSocket 非常方便引入一个 starter 就带齐了服务端和客户端的支持。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency然后实现一个配置类把 WebSocket 的处理器注册到指定路径上。Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatWebSocketHandler(), /chat) .setAllowedOrigins(*); } }这里有个细节setAllowedOrigins(*)在前后端分离的项目里经常是必须的。如果不设置跨域请求会被浏览器拦截前端一直报连接失败但后端日志里又看不到明显报错。生产环境不建议直接放开*最好配置成具体的线上域名列表避免被恶意站点拉一条 WebSocket 连接做坏事。Spring 的 WebSocket 支持两种写法。一种是上面这种面向 Handler 的风格也是 Spring 官方主推、嵌入 WebSocket 原生 API 最少的方式。另一种是使用ServerEndpoint注解需要额外注入一个ServerEndpointExporter的 Bean写起来更像传统 Java WebSocket 的写法。如果你的项目里没有历史包袱我建议用第一种因为 Spring 封装得更彻底和 Spring Security、Spring MVC 的集成也更好做。2.2 服务端消息处理器聊天室的核心逻辑都在TextWebSocketHandler的子类里。我把完整实现贴出来注释里写清楚了每个方法的作用。Slf4j public class ChatWebSocketHandler extends TextWebSocketHandler { // 使用 ConcurrentHashMap 保存所有在线会话 private static final MapString, WebSocketSession SESSIONS new ConcurrentHashMap(); /** * 用户建立连接后把他加到在线列表并广播上线通知 */ Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { SESSIONS.put(session.getId(), session); log.info(用户 {} 加入聊天室当前在线 {}, session.getId(), SESSIONS.size()); session.sendMessage(new TextMessage(欢迎加入聊天室当前在线人数 SESSIONS.size())); broadcast(用户 session.getId() 加入了聊天室); } /** * 收到用户消息后做简单加工再广播给所有人 */ Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { String payload message.getPayload(); log.info(收到来自 {} 的消息{}, session.getId(), payload); broadcast([ session.getId() ] payload); } /** * 连接关闭后移除会话并广播离开通知 */ Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { SESSIONS.remove(session.getId()); log.info(用户 {} 离开聊天室当前在线 {}, session.getId(), SESSIONS.size()); broadcast(用户 session.getId() 离开了聊天室); } /** * 传输异常时关闭连接并清理会话 */ Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { log.error(WebSocket 传输异常sessionId{}, session.getId(), exception); SESSIONS.remove(session.getId()); if (session.isOpen()) { session.close(CloseStatus.SERVER_ERROR); } } private void broadcast(String content) { TextMessage message new TextMessage(content); SESSIONS.values().forEach(session - { try { if (session.isOpen()) { sendMessageSafely(session, message); } } catch (Exception e) { log.error(广播消息失败sessionId{}, session.getId(), e); } }); } }这里非常容易踩一个并发坑多个消息同时要发给同一个WebSocketSession时会抛出The remote endpoint was in state [TEXT_FULL_WRITING]。原因很简单长连接上没有消息锁两个线程同时往同一个连接写数据底层 Channel 就乱套了。生产中不要直接调用session.sendMessage我会给 session 包一层ConcurrentWebSocketSessionDecorator或者像上面代码一样在发送消息时加一个synchronized (session)。前者是 Spring 官方提供的线程安全会话包装器建议优先用这个WebSocketSession safeSession new ConcurrentWebSocketSessionDecorator(session, 10000, 64 * 1024);第二个参数是发送时阻塞的超时时间10 秒第三个参数是缓冲区上限64KB。超过限制时连接会被强制关闭避免内存被慢消费者拖垮。2.3 前端实现与连接管理浏览器端的 WebSocket 客户端代码不复杂但断线重连的逻辑必须写。项目上线后最常见的投诉就是“页面放着不动过一会儿消息就不来了”八成是连接被服务端或中间网络设备悄悄断掉而前端没有重连机制。!DOCTYPE html html langzh head meta charsetUTF-8 title聊天室/title /head body h3Spring Boot WebSocket 聊天室/h3 div idchatBox styleheight: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px;/div input idmsgInput typetext stylewidth: 300px; placeholder输入消息后回车发送 button onclicksendMessage()发送/button script let ws; function connect() { ws new WebSocket(ws:// location.host /chat); ws.onopen function () { console.log(WebSocket 连接已建立); }; ws.onmessage function (event) { appendLine(event.data); }; ws.onclose function () { console.log(连接已关闭3 秒后重连); setTimeout(connect, 3000); }; ws.onerror function (e) { console.error(WebSocket 错误, e); // onerror 之后一般会触发 onclose所以这里不用重复重连 }; } function appendLine(text) { const div document.createElement(div); div.textContent text; document.getElementById(chatBox).appendChild(div); } function sendMessage() { const input document.getElementById(msgInput); if (input.value ws ws.readyState WebSocket.OPEN) { ws.send(input.value); input.value ; } } connect(); /script /body /html实际项目中断线重连建议加上指数退避策略第一次重连等 1 秒失败后等 2 秒、4 秒、8 秒再往上封顶到 30 秒或 60 秒。否则服务端发布重启时几百个客户端同时疯狂重连会直接把服务端打挂。另外如果连接经过了 Nginx记得服务端和客户端都要有心跳机制保活这部分我在第 5 节里详细展开。3. 案例二SSE 实现服务端主动通知3.1 为什么这里选 SSE 而不是 WebSocket第二个案例场景是消息中心。用户登录系统后如果被分配了新任务、收到新的审批单页面顶部的红点要立刻亮起来。这类场景的特点是数据只从服务端流向客户端客户端不需要给服务端回任何实时消息而且连接的并发量可能很大。这个场景如果上 WebSocket等于杀鸡用牛刀。SSE 的优势非常明显协议就是普通 HTTP服务端实现简单前端一个EventSource对象就能订阅。自带断线重连功能。EventSource 连接断开后浏览器会自动重连你不用像 WebSocket 那样自己写重连逻辑。支持自定义事件名也支持使用事件 ID 做断点续传。复杂消息场景下很好用。服务端资源占用比 WebSocket 管理一堆 session 更轻。当然SSE 的局限也必须说清楚。它是单向的客户端不能通过同一个连接往服务端发消息EventSource 只支持 GET 请求也不能自定义请求头所以鉴权信息一般只能放在 URL 参数或者 Cookie 里。另外HTTP/1.1 下浏览器对同一域名的连接数有限制一般最多开 6 个 SSE 连接如果一个页面同时订阅多路事件流要注意控制数量。3.2 SseEmitter 核心用法Spring MVC 中 SSE 的核心类是SseEmitter。控制器收到订阅请求后创建一个 SseEmitter 返回给 Spring 框架这个连接就会被保持住后续任意线程都可以调用emitter.send()把数据写回客户端。先看订阅端口的实现Slf4j RestController RequestMapping(/api/notify) public class NotifyController { // userId - SseEmitter维护每个用户当前的订阅连接 private final MapString, SseEmitter emitterMap new ConcurrentHashMap(); /** * 用户订阅消息通知流 */ GetMapping(value /subscribe, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter subscribe(RequestParam String userId) { // 这里设置 60 秒超时只是为了演示真实场景可设为 0L 表示不超时 SseEmitter emitter new SseEmitter(60_000L); emitterMap.put(userId, emitter); emitter.onCompletion(() - { log.info(SSE 连接正常结束移除 userId{}, userId); emitterMap.remove(userId); }); emitter.onTimeout(() - { log.info(SSE 连接超时移除 userId{}, userId); emitter.complete(); emitterMap.remove(userId); }); emitter.onError(e - { log.error(SSE 连接异常移除 userId{}, userId, e); emitterMap.remove(userId); }); return emitter; } /** * 给指定用户推送一条消息模拟业务触发 */ PostMapping(/send) public void send(RequestParam String userId, RequestBody String message) { SseEmitter emitter emitterMap.get(userId); if (emitter null) { log.warn(用户 {} 未建立 SSE 连接, userId); return; } try { emitter.send(SseEmitter.event() .id(UUID.randomUUID().toString()) .name(message) .data(message)); } catch (IOException e) { log.error(向用户 {} 推送消息失败, userId, e); emitterMap.remove(userId); } } }需要注意几个核心点。SseEmitter的构造参数是超时时间单位毫秒。传0L表示永不超时但生产环境不建议一刀切设成 0。如果客户端异常断开而服务端没有及时发现emitter 会一直留在emitterMap里造成连接和内存泄漏。我更推荐的做法是设置一个合理超时时间然后配合心跳机制探测死连接。比如设定 60 秒业务数据超过 60 秒没有推送时就发送一条注释行: heartbeat来维持连接同时也可以验证客户端是否还活着。SseEmitter.event()返回一个SseEventBuilder可以连缀设置事件 ID、事件名、数据内容。.id()是可选但推荐使用的浏览器重连时会自动把 Last-Event-ID 带到服务器服务端接收后可以决定从哪个消息开始续推。.name()指定事件名前端用addEventListener(message)监听的就是这个名字如果没设置 name默认走onmessage。最后emitter.send()之后不需要也不能手动关闭连接。连接的生命周期由服务端控制如果想主动结束推送调用emitter.complete()客户端会收到正常的流结束信号。在真实项目里emitterMap不建议像我示例里这么简单粗暴地使用。多端登录同一用户会产生多条连接更合理的维护结构应该是MapString, ListSseEmitter推送时遍历这用户的全部分组设备。如果你有 Redis Pub/Sub 或者 Kafka 在系统里推送接口也经常是消费消息队列里的消息再调用emitter.send()这样集群中的任意节点都能把消息推给连接到其他节点的客户端。3.3 客户端 EventSource 接入前端用 EventSource 非常省心浏览器内置不需要额外引入库。const source new EventSource(/api/notify/subscribe?userId10001); source.addEventListener(open, function () { console.log(SSE 连接已打开); }); source.addEventListener(message, function (event) { console.log(收到消息, event.data); // 更新页面上的红点或通知列表 }); source.addEventListener(error, function (e) { if (e.eventPhase EventSource.CLOSED) { console.log(SSE 连接关闭浏览器会自动重连); } });EventSource 的自动重连是浏览器帮我们做好的重连间隔默认几秒可以通过服务器返回的retry:字段调整。如果需要带 Cookie 或者 URL 参数做鉴权直接在拼接 URL 时加参数就行比如上面的userId10001。用 curl 调试 SSE 接口很方便能直接看到服务端输出的流式内容curl -N http://localhost:8080/api/notify/subscribe?userId10001-N参数表示禁用缓冲让 curl 一条条实时打印服务端返回的数据。这种调试方式在排查“到底服务端有没有推送”的问题时特别高效。4. 案例三长轮询实现任务状态查询4.1 长轮询的工作流程第三个案例回到日常开发里最烦人的问题后台任务执行状态查询。用户提交一个 Excel 导出请求或者触发一个数据清洗任务服务端执行可能要花十几秒甚至更久。这种场景如果用短轮询每 3 秒问一次“好了吗”服务端接口压力不小用 WebSocket 又显得大材小用尤其是不需要跟客户端有任何后续交互。长轮询是这里的折中方案客户端发起请求服务端把请求挂起任务完成或超时后再返回结果客户端拿到结果处理后立刻发起下一次请求。长轮询的核心思想是“挂了等消息”而不是“定时问一次”。这样做的好处是任务完成和客户端感知到结果之间的延迟被压缩到极小请求次数也降到了最低。4.2 DeferredResult 挂起请求Spring MVC 的异步功能里DeferredResult就是为这种场景准备的。它的工作方式很巧妙控制器直接返回一个DeferredResultServlet 容器立刻释放当前请求线程回去处理其他请求等业务线程在后台把结果算好调用setResult()时容器再把结果写回客户端。整个挂起过程不占用 Tomcat 工作线程这是它和Thread.sleep阻塞线程最大的区别。Slf4j RestController RequestMapping(/api/task) public class TaskController { private final MapString, DeferredResultString requestMap new ConcurrentHashMap(); private final MapString, String taskStatusMap new ConcurrentHashMap(); /** * 客户端长轮询任务状态 */ GetMapping(/v1/status/{taskId}) public DeferredResultString getStatus(PathVariable String taskId) { // 先查一次内存状态如果已经完成了直接返回结果 String currentStatus taskStatusMap.get(taskId); if (currentStatus ! null !RUNNING.equals(currentStatus)) { return immediateResult({\status\:\ currentStatus \}); } // 挂起 30 秒超时返回 RUNNING客户端收到后会立刻发起下一次请求 DeferredResultString deferredResult new DeferredResult(30_000L, {\status\:\RUNNING\}); requestMap.put(taskId, deferredResult); deferredResult.onCompletion(() - { log.info(taskId{} 的请求处理完成, taskId); requestMap.remove(taskId); }); deferredResult.onTimeout(() - { log.warn(taskId{} 的请求等待超时返回 RUNNING, taskId); requestMap.remove(taskId); }); return deferredResult; } /** * 业务代码在后台任务执行完毕后调用唤醒等待的请求 */ public void completeTask(String taskId, String finalStatus) { taskStatusMap.put(taskId, finalStatus); DeferredResultString deferredResult requestMap.get(taskId); if (deferredResult ! null) { deferredResult.setResult({\status\:\ finalStatus \}); } } private DeferredResultString immediateResult(String json) { DeferredResultString result new DeferredResult(); result.setResult(json); return result; } }这里有几个实现细节要特别留意。超时时间的设置。我一般选 30 秒原因有两个一是大多数后端任务在 30 秒内都应该有中间状态或最终状态可返回二是即使请求超时返回客户端立刻重新发起体验上几乎无感。如果超时时间设到 60 秒以上客户端网络层可能先超时断开反而容易出问题。DeferredResult的构造器第二参数是超时后的默认返回值。当请求挂起达到 30 秒时Spring 会把默认值写回客户端同时触发onTimeout回调。注意超时后任务如果才完成setResult()会抛出IllegalStateException业务代码里要捕获这个异常否则会把异常顶到调用链上。直接返回RUNNING的设计是长轮询的精髓。客户端拿到 RUNNING 就知道任务还没完成马上发起下一轮请求服务端拿到同一任务的新请求时会再次挂起等待。这样任务完成和客户端感知之间的时间差最多就是一次网络 round-trip 的时间远比短轮询的固定间隔实时。4.3 前端不断重新请求前端的长轮询代码比服务端看着更简单核心思想就是一个递归链式的 fetchasync function pollTaskStatus(taskId) { try { const resp await fetch(/api/task/v1/status/${taskId}); const data await resp.json(); if (data.status SUCCESS) { console.log(任务已完成); renderResult(data); return; } // 拿到 RUNNING立刻发起下一轮请求 pollTaskStatus(taskId); } catch (e) { // 网络异常或接口超时延迟 1 秒后重新发起 setTimeout(() pollTaskStatus(taskId), 1000); } }这里注意前端请求的默认超时时间可能跟服务端的 30 秒对不上。比如浏览器、axios 默认超时只有 10 秒的话服务端还挂着呢客户端这边早就报错了。所以使用长轮询时要么把客户端请求超时时间设置得比服务端DeferredResult超时时间长一点要么在 catch 分支里做重试。建议两种都做显式设置客户端 timeout 为 35 秒catch 分支继续重试双保险。5. 实战踩坑记录与排查技巧5.1 WebSocket 连接断开与心跳机制WebSocket 最典型的坑就是连接“悄悄断掉”。客户端网络切换、服务器重启、中间防火墙清理空闲连接都会导致连接断开。但断开这件事不一定能被双方感知到有时候会一直保持一个半开连接状态服务端往这个连接上写数据才发现已经写不通了。解决方法是加心跳。服务端每隔 30 秒发一次 ping 帧或业务层心跳消息客户端收到后回一个 pong 帧或者客户端自己定期发一个{type:heartbeat}的空消息。如果连续几次心跳没有收到对方的回应就主动断开重连。Spring 里没有开箱即用的心跳定时器你可以自己注入一个TaskScheduler在afterConnectionEstablished时给每个 session 安排一个定时任务向连接写入心跳消息同时在消息处理器里过滤掉心跳类型的数据不要把它当业务消息广播。还有注意 Nginx 对 WebSocket 的空闲超时。默认配置下Nginx 大约 60 秒没有数据传输就会断开连接。所以不要只靠应用层心跳Nginx 的proxy_read_timeout也要同步调大比如设成 75 秒比心跳间隔大就行。5.2 Nginx 代理与集群部署的坑WebSocket 走 Nginx 代理时第一件事就是配置升级头和读写超时。完整配置如下location /chat { proxy_pass http://backend_servers; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 75s; proxy_send_timeout 75s; }重点是Upgrade和Connection upgrade两行没有它们后端拿到的还是普通 HTTP 请求WebSocket 握手会一直失败。SSE 走 Nginx 时坑又不一样。Nginx 默认开了缓冲会把后端推送的数据攒一批才转发给客户端结果前端一看数据半天才冒出来一次实时性全没了。解决方法是关闭该路径的缓冲location /api/notify { proxy_pass http://backend_servers; proxy_buffering off; proxy_cache off; proxy_read_timeout 3600s; }或者在后端响应头里加X-Accel-Buffering: no这是 Nginx 专门用来控制是否缓冲响应体的头Spring 可以放在拦截器或过滤器里统一添加。如果服务端是集群部署WebSocket 和 SSE 都要考虑连接粘滞。用户第一次连到 A 节点下一次请求被负载均衡到 B 节点B 节点没有他的连接推送就丢了。解决思路有两个一是用 Redis Pub/Sub 或消息中间件广播推送事件所有节点都消费再查询本地连接是否存在二是用 Nginxip_hash做简单的会话保持但从运维角度长期维护还是前者更稳。5.3 SSE 与长轮询的隐藏坑SSE 使用中我踩过最深的一次坑是连接数打满。线上有个页面同时打开了六个 SSE 订阅用户多开几个标签页浏览器直接没法访问该域名的其他资源了。这是因为 HTTP/1.1 对同一域名的连接数限制是 6 个SSE 连接本身一直不释放把连接池占满了。要彻底解决要么上 HTTP/2多路复用要么减少 SSE 连接数把一批推送合并到一个事件流里用事件名区分消息类型。SSE 的连接泄漏也容易发生客户端页面直接关闭浏览器不会通知服务端服务端那条 SSE 连接还一直开着直到下一次 send 时才可能抛 IOException。这就是为什么生产环境一定要配合心跳写探测消息。我习惯每 30 秒用 SseEmitter 发送一行注释: heartbeat一旦发现抛出 IOException立刻调用onError里的清理逻辑移除连接。长轮询的坑主要在并发和状态一致性。如果同一任务的多个请求同时挂起任务完成时setResult只能触发一次其他请求会一直挂到超时才返回 RUNNING客户端就会多做几轮空轮询。因此服务端最好维护“一个人只挂一个请求”的策略比如用putIfAbsent代替put或者把旧请求的setResult提前触发。另一个坑是任务执行完毕后如果服务端把taskStatusMap当成临时缓存而不清理时间长了内存会一直涨。我给这个 Map 设置了定时清理任务启动一个延迟线程定期扫描超过一天没有更新的键值对。调试方面再分享几个小技巧SSE 用curl -N能流式看数据WebSocket 直接浏览器控制台new WebSocket(ws://localhost:8080/chat)就能手动测试交互长轮询用curl -v能看到请求一直挂住、直到服务端返回响应的时间线一次就能判断服务端的挂起时间是否按预期工作。最后再分享一点我的个人体会。实时推送方案没有绝对的好坏最重要的是匹配场景。如果需求只是“服务端有数据变了页面要及时刷新”SSE 往往是最舒服的如果要求双向交互只能 WebSocket如果被老旧浏览器和网络环境卡住了长轮询也能体面地兜底。动手写代码之前先把数据流方向、并发量、浏览器环境、中间代理这几件事想清楚方案自然就浮出来了。我在实际项目里见过太多一上来就上 WebSocket结果连需求都说不清楚的情况做技术还是要克制一点能用简单方案解决的就不要为了技术而技术。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。