Spring Boot 整合 SSE, http长连接

1. 什么是 SSE? (30秒)

SSE (Server-Sent Events) 是一种允许服务器通过 HTTP 连接主动向客户端发送实时更新的技术。

  • 特点:基于 HTTP,使用简单,单向通信(服务器 -> 客户端),自动重连。

  • 对比 WebSocket:WebSocket 是双向的,更复杂;SSE 是单向的,更轻量,适用于通知、日志流、实时数据更新等场景。


2. 核心依赖与配置 (30秒)

Spring Boot 从 2.2.x 版本开始提供了对 SSE 的专用支持,主要包含在 spring-boot-starter-web 中,无需引入额外依赖

确保你的 pom.xml 中有:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

3. 三步编写代码 (3分钟)

第一步:创建控制器 (Controller)

创建一个 @RestController,并定义一个方法来产生 SSE 流。

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;@RestController
public class SseController {// 用于保存所有连接的 SseEmitter,可以根据用户ID等关键字进行存储private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();/*** 用于客户端连接 SSE* @param clientId 客户端标识,用于区分不同客户端* @return SseEmitter*/@GetMapping(path = "/sse/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public SseEmitter connect(@RequestParam String clientId) {// 设置超时时间,0表示永不超时。可以根据需要设置,例如 30_000L (30秒)SseEmitter emitter = new SseEmitter(0L);// 注册回调函数,当连接完成或出错时,从Map中移除这个Emitteremitter.onCompletion(() -> EMITTER_MAP.remove(clientId));emitter.onError((e) -> EMITTER_MAP.remove(clientId));emitter.onTimeout(() -> EMITTER_MAP.remove(clientId));// 将新的 emitter 存入 MapEMITTER_MAP.put(clientId, emitter);// 可选:发送一个初始连接成功的事件try {emitter.send(SseEmitter.event().name("INIT") // 事件名称,可选.data("连接成功 for: " + clientId) // 事件数据.id("1") // 事件ID,可选,用于重连.reconnectTime(5000)); // 重连时间,可选} catch (IOException e) {e.printStackTrace();}return emitter;}
}
第二步:创建发送消息的方法

在同一个 Controller 中,添加一个 API 来模拟向特定客户端发送消息。

    /*** 向指定客户端发送消息*/@GetMapping("/sse/send")public String sendMessage(@RequestParam String clientId, @RequestParam String message) {SseEmitter emitter = EMITTER_MAP.get(clientId);if (emitter != null) {try {// 构建并发送事件emitter.send(SseEmitter.event().name("MESSAGE") // 事件类型.data(message)   // 事件数据.id("msg-id-" + System.currentTimeMillis())); // ID} catch (IOException e) {// 发送失败,移除 emitterEMITTER_MAP.remove(clientId);return "发送失败,客户端可能已断开";}return "发送成功 to: " + clientId;}return "客户端不存在";}
第三步:编写前端页面进行测试 (1分钟)

在 src/main/resources/static 目录下创建一个 sse-demo.html 文件。

<!DOCTYPE html>
<html>
<head><title>SSE Demo</title>
</head>
<body><h1>SSE 客户端测试</h1><label for="clientId">客户端ID: </label><input type="text" id="clientId" value="test-client-1"><button onclick="connectSSE()">连接SSE</button><button onclick="closeSSE()">断开连接</button><hr><label for="message">要发送的消息: </label><input type="text" id="message" value="Hello SSE!"><button onclick="sendMessage()">发送消息</button><hr><h3>收到的事件:</h3><div id="messages"></div><script>let eventSource;function connectSSE() {const clientId = document.getElementById('clientId').value;// 断开现有连接if (eventSource) {eventSource.close();}// 建立新的 SSE 连接eventSource = new EventSource(`/sse/connect?clientId=${clientId}`);// 监听通用消息(没有指定 event name 的消息)eventSource.onmessage = function (event) {appendMessage(`[message]: ${event.data}`);};// 监听特定名称的事件 (例如:MESSAGE)eventSource.addEventListener("MESSAGE", function (event) {appendMessage(`[MESSAGE]: ${event.data}`);});// 监听特定名称的事件 (例如:INIT)eventSource.addEventListener("INIT", function (event) {appendMessage(`[INIT]: ${event.data}`);});eventSource.onerror = function (err) {console.error("SSE error:", err);appendMessage('[错误] 连接出错');};}function closeSSE() {if (eventSource) {eventSource.close();appendMessage('[信息] 连接已关闭');eventSource = null;}}function sendMessage() {const clientId = document.getElementById('clientId').value;const message = document.getElementById('message').value;fetch(`/sse/send?clientId=${clientId}&message=${encodeURIComponent(message)}`).then(response => response.text()).then(data => console.log(data));}function appendMessage(text) {const messageDiv = document.getElementById('messages');const p = document.createElement('p');p.textContent = `${new Date().toLocaleTimeString()}: ${text}`;messageDiv.appendChild(p);}</script>
</body>
</html>

4. 运行与测试 (1分钟)

  1. 启动应用:运行你的 Spring Boot 应用。

  2. 打开页面:访问 http://localhost:8080/sse-demo.html

  3. 进行测试

    • 输入一个客户端 ID(如 user1),点击 “连接SSE”。前端会收到 [INIT] 事件。

    • 在另一个浏览器标签页或使用 Postman 访问 :  http://localhost:8080/sse/send?clientId=user1&message=你好!

    • 观察第一个标签页,会立即收到 [MESSAGE]: 你好! 的消息。

总结

  • 核心对象SseEmitter

  • 关键注解@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)

  • 流程

    1. 客户端连接 /sse/connect,服务端创建并保存 SseEmitter

    2. 服务端通过 emitter.send() 主动推送消息。

    3. 客户端通过 EventSource API 监听和处理消息。

    4. 连接结束时,服务端需要清理 SseEmitter(通过回调函数)。

现在你已经掌握了 Spring Boot 整合 SSE 的基本方法!在实际项目中,你可能需要将其与业务逻辑、身份认证(如 JWT)以及更强大的连接管理(如使用数据库或 Redis 存储 emitter)相结合。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/95057.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/95057.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

类和反射的机制

一、类1.类的生命周期1. 类的编译&#xff1a;通过 javac 命令将 .java 源文件编译成 .class 字节码文件。 2. 类的加载&#xff1a;类加载器&#xff08;ClassLoader&#xff09;将 .class 文件从硬盘加载到内存&#xff0c;形成“类对象”&#xff0c;包括加载、链接、初始化…

【论文笔记】VGGT-从2D感知3D:pose估计+稠密重建+点跟踪

VGG组联合Meta改进了dust3r&#xff0c;输入图片&#xff0c;输出对应的一系列3D属性&#xff0c;被CVPR2025收录&#xff01;1.abstract我们提出了VGGT&#xff0c;一种前馈神经网络&#xff0c;能够直接从场景的一个、几个或数百个视角推断出所有关键的3D属性&#xff0c;包括…

idea2025.2中maven编译中文乱码

问题描述&#xff1a;使用idea2025.2编译器中maven编译java文件后中文出现乱码情况解决方案&#xff1a;添加指令&#xff1a; JAVA_TOOL_OPTIONS-Dfile.encodingUTF-8 在下图位置注意&#xff1a;再次编译时&#xff0c;可以在原本文件中小范围修改一点内容&#xff08;打个…

【适度精简】Windows 7 旗舰版-emmy精简系统

Windows 7旗舰版曾是非常受欢迎的操作系统&#xff0c;但随着时间推移和技术发展&#xff0c;其在一些场景下暴露出了诸多问题&#xff0c;适度精简的Windows 7旗舰版正是为解决这些问题而出现&#xff0c;以下是从用户软件痛点角度对其背景和作用的分析。 添加图片注释&#x…

数据分析编程第七步:分析与预测

7.1 销售趋势分析利用历史销售数据统计月销售额&#xff0c;计算季节化因子&#xff0c;获取去季节化销售数据&#xff0c;然后进行线性拟合&#xff0c;最后预测接下来的某个月的销售额。第一步&#xff1a;读数&#xff0c;统计月销售额A1file(“sales.csv”).importtc(order…

【web3】十分钟了解web3是什么?

十分钟了解web3是什么?Web3的核心概念区块链与去中心化智能合约加密货币与代币去中心化应用&#xff08;DApps&#xff09;钱包与身份验证DAO&#xff08;去中心化自治组织&#xff09;Web3 国内产品Web3 国际产品Web3 基础设施Web3 应用场景技术实现特点挑战与未来Web3的核心…

联合体和枚举——嵌入式学习笔记

目录 前言 一、联合体&#xff08;共用体&#xff09; 1、基本概念 2、初始化和引用 &#xff08;1&#xff09;初始化 &#xff08;2&#xff09;引用 二、枚举 前言 在C语言的编程世界中&#xff0c;我们早已熟悉了结构体struct这种能将不同数据类型捆绑在一起的“打包…

SRE命令行兵器谱之思想篇:像SRE一样思考——命令行不只是工具,更是你的战友

SRE命令行兵器谱之思想篇:像SRE一样思考——命令行不只是工具,更是你的战友 欢迎来到《SRE命令行兵器谱》系列。在深入研究 grep, lsof, tcpdump 这些强大“兵器”的细节之前,我们必须先回答一个更重要的问题: 一个SRE(网站可靠性工程师)在黑色的终端窗口前,脑子里想的…

STL库——list(类模拟实现)

ʕ • ᴥ • ʔ づ♡ど &#x1f389; 欢迎点赞支持&#x1f389; 个人主页&#xff1a;励志不掉头发的内向程序员&#xff1b; 专栏主页&#xff1a;C语言&#xff1b; 文章目录 前言 一、基本框架 二、构造函数 三、析构函数 四、赋值重载 五、增删查改 5.1、push_front/pus…

在PowerPoint和WPS演示让蝴蝶一直跳8字舞

如何让PPT中插入的对象按指定的轨迹运动并且一直“停不下来”&#xff1f;简单三步&#xff1a;①插入对象、②设置路径动画、③设置动画重复。本文以蝴蝶图片一直跳8字舞为例进行实际操作讲解&#xff0c;PowerPoint和WPS演示都一样操作&#xff0c;本文以WPS演示进行讲解。第…

并发编程——06 JUC并发同步工具类的应用实战

0 常用并发同步工具类的真实应用场景JDK 提供了比synchronized更加高级的各种同步工具&#xff0c;包括ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等&#xff0c;可以实现更加丰富的多线程操作&#xff1b;1 ReentrantLock&#xff08;可重入的占用锁&#xff0…

Apple登录接入记录

Apple文档——通过 Apple 登录 使用入门 - 通过 Apple 登录 - Apple Developer Apple文档——设计要求——登录通过 Apple 登录 | Apple Developer Documentation 插件github版——apple-signin-unity&#xff08;README 中为接入步骤&#xff09; GitHub - lupidan/apple-…

【小程序-慕尚花坊04】网络请求并发与loading

网络请求并发与loading一&#xff0c;网络请求并发与loading1&#xff0c;并发处理1.1&#xff0c;异步实现方式2.2&#xff0c;Promise.all异步方式封装2&#xff0c;loading加载2.1&#xff0c;loading的基本使用2.2&#xff0c;loading与并发结合案例2.3&#xff0c;loading…

CentOS 7 升级 OpenSSH 10.0p2 完整教程(含 Telnet 备份)

&#x1f539; CentOS 7 升级 OpenSSH 10.0p2 完整教程&#xff08;含 Telnet 备份&#xff09; 注意&#xff1a;为了避免升级 SSH 时无法远程登录&#xff0c;建议先启用 Telnet 服务 作为备用连接方式。 CentOS 7 默认 OpenSSH 版本是 7.x&#xff0c;升级到 10.0p2 需要 源…

aragfw9.dll aqnky-ef.dll aqua dock.dll apscon~1.dll apropdll.dll app_web_yqnqasrp.dll app_web_

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

rabbitMQ延时队列实现,怎么保证消息的幂等

一、RabbitMQ 延时队列实现方式 基于 TTL&#xff08;Time-To-Live&#xff09; 死信队列&#xff08;Dead Letter Queue&#xff09; 这是最常用的实现方式&#xff0c;核心思路是&#xff1a; (1)消息设置过期时间&#xff08;TTL&#xff09; (2)消息过期后进入绑定的死信队…

前沿技术观察:从AI 时代到量子计算的下一站

前沿技术观察&#xff1a;从AI 时代到量子计算的下一站&#x1f680; 技术的浪潮一波接一波&#xff0c;从 人工智能 到 区块链&#xff0c;再到 边缘计算、元宇宙、量子计算&#xff0c;这些前沿技术正在深刻影响我们的生活与产业格局。 对于开发者和技术爱好者来说&#xff0…

通过Kubernetes安装mysql5服务

以下是清晰、结构化的操作流程优化说明&#xff0c;按步骤梳理从部署到配置持久化、暴露服务的完整过程&#xff1a;一、基础部署&#xff1a;快速验证 MySQL 可用性创建有状态工作负载进入 KubeSphere 项目 → 工作负载 → 有状态副本集 → 创建&#xff0c;选择 通过镜像创建…

【mysql】SQL 中 IS 与 = 的区别:一个 NULL 值引发的思考

SQL 中 IS 与 的区别&#xff1a;一个 NULL 值引发的思考为什么查询结果总是少一条数据&#xff1f;可能是 NULL 在捣鬼在 SQL 查询中&#xff0c;很多开发者都曾遇到过这样的困惑&#xff1a;明明看起来正确的查询语句&#xff0c;返回的结果却总是与预期不符。这往往是因为没…

openGauss笔记

1、安装 直接用docker安装 2、国产化 符合国产化要求 3、客户端 3.1 dbeaver 社区版本&#xff08;25.1.4&#xff09;即可&#xff0c;驱动建议用离线版本&#xff0c;在官网下载最新的&#xff0c;然后在驱动管理里面进行添加本地的jar 3.1.1 驱动配置3.1.2 依赖 需要java版本…