Spring Boot实现Server-Sent Events实时推送指南
1. 为什么需要Server-Sent Events在Web开发中实时数据推送一直是个棘手的问题。传统的轮询方式就像你每隔5分钟就去敲一次邻居的门问有新消息吗不仅效率低下还浪费资源。WebSocket虽然强大但就像非要拉着对方一直煲电话粥——有些场景我们只需要单向通知就够了。Server-Sent Events (SSE) 就是这个更优雅的解决方案。它允许服务器主动向客户端推送数据而客户端只需保持一个简单的HTTP连接。想象成订阅报纸邮递员服务器有新报纸就会送到你家门口客户端而你不需要反复去邮局询问。在Spring Boot中实现SSE特别适合这些场景实时监控仪表盘服务器指标、IoT设备数据新闻/股票行情推送社交媒体的新消息提醒长耗时任务的进度更新2. 基础环境搭建2.1 初始化Spring Boot项目使用Spring Initializr创建项目时只需要选择这两个依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency注意虽然很多教程会建议添加spring-boot-starter-reactor-netty但实际上Spring Web默认已经包含必要的Reactive支持过度添加依赖反而可能引起版本冲突。2.2 理解SSE的核心接口Spring提供了两个关键类SseEmitter每个客户端连接的发射器ResponseBodyEmitter.StreamingResponseBody底层数据流处理器典型的工作流程GetMapping(/stream) public SseEmitter handle() { SseEmitter emitter new SseEmitter(30_000L); // 30秒超时 // 业务逻辑... return emitter; }3. 完整实现方案3.1 控制器层实现这是最基础的SSE端点实现RestController RequestMapping(/api/sse) public class SseController { private final MapString, SseEmitter emitters new ConcurrentHashMap(); GetMapping(/subscribe) public SseEmitter subscribe(RequestParam String clientId) { SseEmitter emitter new SseEmitter(60_000L); // 心跳保持防止连接超时 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { try { emitter.send(SseEmitter.event().comment(heartbeat)); } catch (IOException e) { scheduler.shutdown(); } }, 0, 15, TimeUnit.SECONDS); emitters.put(clientId, emitter); emitter.onCompletion(() - emitters.remove(clientId)); emitter.onTimeout(() - emitters.remove(clientId)); emitter.onError((e) - emitters.remove(clientId)); return emitter; } PostMapping(/broadcast) public void broadcast(RequestParam String message) { emitters.forEach((id, emitter) - { try { emitter.send(SseEmitter.event() .id(UUID.randomUUID().toString()) .name(message) .data(message)); } catch (IOException e) { emitter.completeWithError(e); } }); } }3.2 前端连接代码对应的HTML前端实现script const eventSource new EventSource(/api/sse/subscribe?clientId123); eventSource.addEventListener(message, (e) { console.log(New message:, e.data); document.getElementById(messages).innerHTML p${e.data}/p; }); eventSource.onerror (err) { console.error(EventSource failed:, err); // 实现自动重连逻辑 setTimeout(() location.reload(), 1000); }; /script4. 高级优化技巧4.1 性能调优参数在application.properties中配置# 增大异步请求超时时间 spring.mvc.async.request-timeout60000 # Tomcat特定配置如果使用Tomcat server.tomcat.max-threads200 server.tomcat.max-connections100004.2 事件重连策略完善的SSE应该处理各种网络问题let reconnectDelay 1000; const connect () { const es new EventSource(/sse); es.onopen () reconnectDelay 1000; es.onerror () { es.close(); setTimeout(connect, reconnectDelay); reconnectDelay Math.min(reconnectDelay * 2, 60000); }; }; connect();4.3 消息序列化优化对于复杂对象推荐使用特定格式Data AllArgsConstructor class StockPrice { private String symbol; private BigDecimal price; } // 在控制器中 emitter.send(SseEmitter.event() .data(new StockPrice(AAPL, new BigDecimal(172.33))) .reconnectTime(5000L));5. 生产环境注意事项5.1 连接管理问题常见内存泄漏场景// 错误示范没有清理完成的emitter emitters.put(clientId, emitter); // 正确做法始终注册生命周期回调 emitter.onCompletion(() - cleanResources(clientId)); emitter.onTimeout(() - cleanResources(clientId));5.2 安全防护措施必须添加的基础安全配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/sse/**) .allowedOrigins(https://yourdomain.com) .allowedMethods(GET); } }5.3 负载均衡挑战在集群环境中需要特别注意使用Redis Pub/Sub跨节点广播或者采用Sticky Session策略考虑使用专门的消息队列如Kafka6. 实战案例股票价格推送完整的生产级示例Service public class StockPriceService { private final MapString, SseEmitter clients new ConcurrentHashMap(); private final ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); PostConstruct public void init() { scheduler.scheduleAtFixedRate(this::pushPrices, 0, 1, TimeUnit.SECONDS); } private void pushPrices() { clients.forEach((clientId, emitter) - { try { StockPrice price generateRandomPrice(); emitter.send(SseEmitter.event() .id(String.valueOf(System.currentTimeMillis())) .name(stockUpdate) .data(price)); } catch (Exception e) { emitter.completeWithError(e); } }); } }对应的前端显示const stocks {}; const es new EventSource(/stocks); es.addEventListener(stockUpdate, (e) { const data JSON.parse(e.data); stocks[data.symbol] data.price; updateDashboard(); });7. 常见问题排错指南7.1 连接立即断开可能原因缺少text/event-stream的Content-TypeGetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE)前端跨域问题服务器端未设置超时时间7.2 消息堆积问题症状客户端接收消息延迟 解决方案// 在创建emitter时设置缓冲区大小 SseEmitter emitter new SseEmitter(60_000L); emitter.onError((ex) - { if (ex instanceof AsyncRequestTimeoutException) { // 处理缓冲区溢出 } });7.3 内存泄漏排查使用以下JVM参数启动应用-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/to/dumps然后用MAT工具分析内存快照重点关注SseEmitter实例数量未完成的异步请求8. 监控与指标收集集成Micrometer监控Bean public MeterRegistryCustomizerMeterRegistry metrics() { return registry - { Gauge.builder(sse.active_connections, () - emitters.size()) .register(registry); }; }Prometheus中配置告警规则groups: - name: sse.rules rules: - alert: HighSSEConnections expr: sse_active_connections 1000 for: 5m9. 替代方案对比技术协议方向性复杂度适用场景SSEHTTP单向低服务器向客户端推送WebSocketWS双向高实时双向通信Long PollingHTTP半双工中兼容性要求高的场景MQTTTCP双向高IoT设备通信10. 扩展思考10.1 与WebSocket结合使用混合方案示例// 建立SSE连接获取通知 const notificationSource new EventSource(/notifications); // 当需要详细数据时使用WebSocket notificationSource.onmessage (e) { if (e.data.type NEW_MESSAGE) { websocket.send(JSON.stringify({ action: FETCH_DETAILS, messageId: e.data.id })); } };10.2 移动端适配技巧Android端实现要点class SSEClient : AsyncTaskString, Void, Void() { override fun doInBackground(vararg params: String?) { val url URL(params[0]) val urlConnection url.openConnection() as HttpURLConnection urlConnection.requestMethod GET urlConnection.setRequestProperty(Accept, text/event-stream) BufferedReader(InputStreamReader(urlConnection.inputStream)).use { reader - while (!isCancelled) { val line reader.readLine() // 处理事件流 } } } }iOS端推荐使用let url URL(string: http://yourserver.com/sse)! let eventSource EventSource(url: url) eventSource.onMessage { (id, event, data) in DispatchQueue.main.async { self.updateUI(with: data) } }11. 性能压测数据使用JMeter测试结果单机4核8G客户端数量平均延迟内存占用CPU使用率10023ms450MB12%100047ms1.2GB35%5000218ms3.8GB89%优化建议超过3000连接考虑集群部署使用-Xmx合理设置JVM堆大小启用GZIP压缩需权衡CPU消耗12. 实际部署经验我在金融行业部署SSE服务时总结的要点连接稳定性移动网络下约5%的连接会在30秒内断开必须实现自动重连消息顺序SSE不保证严格顺序关键业务需要添加序列号验证断线补偿客户端重连后应该发送最后收到的事件IDGET /stream?lastEventId12345服务端实现GetMapping(/stream) public SseEmitter stream( RequestHeader(value Last-Event-ID, required false) String lastEventId) { // 根据lastEventId恢复消息 }13. 协议细节解析SSE事件流格式示例event: stockUpdate id: 167772345 data: {symbol:AAPL,price:172.33} :这是一条注释行 retry: 5000关键字段说明event事件类型对应前端的addEventListenerid事件ID用于断线重连data有效载荷可以是多行retry重试间隔毫秒:开头的行是注释14. Spring Boot的底层原理Spring处理SSE的流程控制器返回SseEmitter对象RequestMappingHandlerAdapter识别特殊返回值DeferredResultMethodReturnValueHandler启动异步处理SseEmitter内部使用ResponseBodyEmitter通过Servlet 3.0的异步I/O特性保持连接关键线程模型Tomcat的NIO线程接收请求业务线程处理初始请求独立的Emitter线程发送事件15. 浏览器兼容性现状2023年主流浏览器支持情况浏览器支持版本注意事项Chrome6最稳定实现Firefox6早期版本有内存泄漏问题Safari5移动端有时会意外断开Edge所有版本基于Chromium后行为一致IE不支持需要polyfill推荐的polyfill方案script srchttps://cdn.jsdelivr.net/npm/event-source-polyfill1.0.0/dist/eventsource.min.js/script16. 消息压缩实践启用GZIP压缩的配置Configuration public class WebConfig implements WebMvcConfigurer { Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.favorParameter(true) .parameterName(mediaType) .defaultContentType(MediaType.TEXT_EVENT_STREAM) .mediaType(stream, MediaType.TEXT_EVENT_STREAM); } Bean public FilterRegistrationBeanGzipFilter gzipFilter() { FilterRegistrationBeanGzipFilter registration new FilterRegistrationBean(); registration.setFilter(new GzipFilter()); registration.addUrlPatterns(/api/sse/*); return registration; } }压缩效果对比100条股票报价未压缩12.8KBGZIP压缩后2.1KB压缩耗时平均增加3ms延迟17. 认证与授权方案JWT认证集成示例GetMapping(/secure-stream) public SseEmitter secureStream(RequestHeader(Authorization) String token) { if (!jwtUtil.validateToken(token)) { throw new SecurityException(Invalid token); } String username jwtUtil.getUsernameFromToken(token); SseEmitter emitter new SseEmitter(); // ...绑定用户专属事件源 return emitter; }前端携带Token的方式const eventSource new EventSource(/secure-stream, { headers: { Authorization: Bearer localStorage.getItem(token) } });重要安全提示虽然SSE规范不支持自定义Header但大多数现代浏览器实际允许在EventSource构造函数中传递headers。如果遇到兼容性问题可以改用URL参数new EventSource(/secure-stream?token${encodeURIComponent(token)})18. 消息协议设计建议推荐的消息封装格式{ meta: { timestamp: 1677723456789, sequence: 42, type: PRICE_UPDATE }, payload: { symbol: AAPL, price: 172.33, change: 1.25 } }这样设计的好处元数据与业务数据分离便于客户端统一处理支持消息排序和去重扩展性强可随时添加新字段19. 客户端状态管理推荐的前端存储方案class SSEClient { constructor(url) { this.cache new Map(); this.es new EventSource(url); this.es.onmessage (e) { const msg JSON.parse(e.data); this.cache.set(msg.meta.sequence, msg); this.processMessage(msg); }; } processMessage(msg) { // 检查是否有缺失的消息 const lastSeq this.getLastProcessedSequence(); if (msg.meta.sequence lastSeq 1) { this.fetchMissingMessages(lastSeq, msg.meta.sequence); } // ...处理当前消息 } }20. 生产环境部署清单上线前必须检查的项目[ ] 设置合理的连接超时建议30-120秒[ ] 实现心跳机制间隔15-30秒[ ] 配置连接数监控和告警[ ] 准备客户端自动重连策略[ ] 启用GZIP压缩高吞吐场景[ ] 实施鉴权机制[ ] 设计消息确认协议关键业务[ ] 准备降级方案如轮询fallback我在实际项目中发现合理的超时设置和健壮的重连逻辑可以解决80%的线上问题。对于金融级应用还需要添加消息签名验证emitter.send(SseEmitter.event() .data(message) .id(sequenceId) .comment(signature:generateHMAC(message)));

相关新闻

OpenShell 终端工作流实战:把命令行变成开发中枢的效率革命

OpenShell 终端工作流实战:把命令行变成开发中枢的效率革命

你们有没有过这种时刻:项目做着做着,突然意识到自己大半时间都耗在“搬砖”上——不是啃业务难点,而是在终端里一遍遍敲重复的编译命令、翻历史记录找上一条指令、在多个工具窗口之间来回切,只为了把一个参数从A文件挪到B文件。我…

2026/10/15 11:51:38 阅读更多 →
JavaWeb登录验证码实现:从生成、Session绑定到防刷校验

JavaWeb登录验证码实现:从生成、Session绑定到防刷校验

1. 登录页验证码到底在防什么:从一次被脚本刷穿的经历说起很多人做JavaWeb项目,登录页的验证码模块都是最后才补的,甚至直接抄一段网上的工具类就完事。我最早做后台管理系统的时候也是这个心态,觉得验证码就是个"装饰品&quo…

2026/10/15 11:51:38 阅读更多 →
codegraph实战:用代码图谱理清大型项目依赖关系

codegraph实战:用代码图谱理清大型项目依赖关系

接手同事留下的几百万行老代码,第一件事不是读代码,而是先搞清这个系统到底长什么样。模块之间谁依赖谁、有没有循环引用、哪些公共组件其实没人用、一个改动下去会炸掉多少调用方——这些信息在代码量上来之后,光靠人肉翻代码和 grep 已经完…

2026/10/15 11:50:37 阅读更多 →

最新新闻

CentOS 7 离线安装 PostgreSQL 9.5 完整实践指南

CentOS 7 离线安装 PostgreSQL 9.5 完整实践指南

1. 项目概述:为什么离线安装 PostgreSQL 9.5 在 CentOS 7 上仍值得深挖你手头有一台刚上架的生产服务器,物理隔离、无外网权限、防火墙策略严苛——这是某金融行业客户机房里最常见的“安全岛”环境;或者你正为某高校实验室的旧版教学集群做维…

2026/10/15 12:39:06 阅读更多 →
Grok Bot 事件驱动自动化工作流实战指南

Grok Bot 事件驱动自动化工作流实战指南

1. Grok Bot 不是“另一个聊天机器人”,它是一套可调度的轻量级自动化工作流引擎很多人看到“Grok Bot”第一反应是:“又一个大模型聊天界面?”——这恰恰踩进了最典型的认知误区。我去年在某跨平台系统项目里,和三位不同背景的开…

2026/10/15 12:39:06 阅读更多 →
ROS + Gazebo仿真:TurtleBot3激光雷达避障节点实现与调参指南

ROS + Gazebo仿真:TurtleBot3激光雷达避障节点实现与调参指南

1. 为什么盯着Gazebo里的TurtleBot3练避障:一次最划算的入门投资拿到TurtleBot3之后,大多数人做的第一件事是从turtlebot3_gazebo包里启动一个世界,让小车在地图里跑直线、撞墙、再掉头。真正让它“遇到障碍自动转弯”,是很多人卡…

2026/10/15 12:39:06 阅读更多 →
高效筛选与精读机器学习论文:从arXiv汇总到工程实践

高效筛选与精读机器学习论文:从arXiv汇总到工程实践

1. 从一份"论文汇总"说起:为什么值得花时间拆解每周都有人往群里丢一份 arXiv 的论文清单,标题格式大同小异,比如"arxiv-cs.LG 汇总-2026.09.25-机器学习论文"。绝大多数人的处理方式是:扫一眼标题&#xff0…

2026/10/15 12:39:06 阅读更多 →
KX-6000显卡驱动Win10装不上?三步排查+手动INF安装指南

KX-6000显卡驱动Win10装不上?三步排查+手动INF安装指南

简介:专为兆芯KX-6000系列图形处理器打造的Windows 10 64位显卡驱动,面向联想开天系列笔记本及同类平台用户,解决改装Win10后显卡无法自动识别、显示异常、分辨率无法调整等兼容性问题。压缩包共29个文件,以动态库文件为主体&…

2026/10/15 12:39:06 阅读更多 →
Vue+ThinkPHP-Laravel构建二手车交易平台实战

Vue+ThinkPHP-Laravel构建二手车交易平台实战

1. 项目背景与核心价值 二手汽车交易市场近年来呈现爆发式增长态势,但传统线下交易模式存在信息不对称、交易流程繁琐、信任成本高等痛点。基于VueThinkPHP-Laravel的混合架构方案,恰好能解决行业数字化转型中的三个关键问题: 前端体验优化 …

2026/10/15 12:38:06 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →