【大模型应用】SSE 详细分析和 Java 项目使用 SSE
第一部分:SSE 深度解析
1. 什么是 SSE?
SSE 的全称是 Server-Sent Events,即服务器发送事件。它是一种基于HTTP的轻量级技术标准,允许服务器在建立一次连接后,主动向客户端(通常是Web浏览器)持续推送数据。
- 核心思想:建立一个长期存在的单向连接(从服务器到客户端),服务器可以随时通过这个连接发送一系列由特定格式文本组成的事件流(Event Stream)。
- 要解决的问题:
- 实时性需求:在需要服务器主动向客户端推送更新(如新闻推送、实时监控数据、股价变动、任务进度更新、AI生成过程中的Token流式输出)的场景下,替代低效的“客户端轮询(Polling)”。
- ** simplicity**:与更复杂的双向通信协议WebSocket相比,SSE更简单易用,因为它基于标准的HTTP协议,无需额外的协议升级握手。
- 自动重连:内置连接失败后自动重试的机制。
2. 为什么 SSE 如此重要?
SSE是构建现代实时Web应用的关键技术之一,因为它:
- 标准协议:是W3C的正式标准,被所有现代浏览器(包括移动端浏览器)原生支持,无需额外的客户端库。
- 简单轻量:基于HTTP/HTTPS,易于理解、实现和调试。服务器端无需处理复杂的协议逻辑。
- 单向高效:完美适用于服务器向客户端推送数据的场景,避免了WebSocket的双向复杂性开销。
- 内置容错:客户端自动处理连接中断和重连。
第二部分:SSE 底层架构深度分析
1. 协议细节
SSE通信本质上是一个长时间保持的HTTP响应。服务器需要设置特定的HTTP头,并且响应体遵循特定的文本格式。
a. HTTP 头设置
服务器必须在响应中设置以下头信息,以告知客户端这是一个事件流:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
text/event-stream:这是最重要的头,它定义了媒体类型为事件流。no-cache:确保响应不被缓存。keep-alive:指示连接应该保持打开状态。
b. 数据格式
响应体是一个简单的文本流,由不同字段行组成,每行以一个换行符\n结尾。字段包括:
event:: (可选)事件类型标识符。客户端可以根据不同的事件类型绑定不同的监听器。data:: (必需)消息的数据内容。一行数据可以包含多个data:行,它们会被连接起来作为一个数据体,换行符会被保留。id:: (可选)事件的ID。用于重连时,客户端可以通过Last-Event-ID头告知服务器最后一个接收到的事件ID,从而实现数据的续传。retry:: (可选)重连时间间隔(毫秒)。建议客户端在连接断开后等待多长时间 before attempting to reconnect。:: (可选)注释行,会被客户端忽略,可用于发送心跳包保持连接活跃。
示例流:
event: status
data: {"progress": 50}
data: This is a message
data: that spans two lines.
id: 12345
event: message
data: Hello, world!
: This is a comment
2. 连接生命周期与管理
a. 建立连接
- 客户端使用标准的JavaScript
EventSourceAPI向一个特定的URL发起HTTP请求。 - 服务器识别该请求,设置正确的SSE头,并保持HTTP响应流打开。
b. 推送数据
- 服务器在需要的时候,将格式化的文本(如上所述)写入响应输出流。
- 每条消息后必须跟随一个双换行符
\n\n来表示一条完整消息的结束。 - 客户端
EventSource会解析流入的数据,触发对应的事件(如onmessage,onerror或自定义事件)。
c. 保持连接与心跳
- 为了防止代理或防火墙超时断开空闲连接,服务器可以定期发送注释行(
: heartbeat\n)作为“心跳包”。
d. 处理断开与重连
- 如果连接意外断开(网络问题、服务器重启),客户端的
EventSource对象会自动尝试重新建立连接。 - 重连时,客户端会在请求头中带上最后一次接收到的事件的
id(Last-Event-ID)。 - 服务器可以读取这个头,从而决定从哪个点开始继续发送数据,避免数据丢失或重复。
第三部分:Java 项目使用 SSE 的方式方法
在Java生态中,有多种方式可以实现SSE服务器端。
方案一:使用 Servlet API 实现(底层、核心)
这是最基础的方式,让你完全控制整个流程。
-
核心步骤:
- 在Servlet的
doGet或doPost方法中处理请求。 - 设置正确的SSE HTTP头。
- 获取响应的
PrintWriter或OutputStream。 - 在一个循环中,持续向输出流写入格式化的SSE消息。
- 妥善处理异常和连接关闭,确保资源被正确释放。
- 在Servlet的
-
示例代码:
@WebServlet("/sse/events") public class SseServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) { // 1. 设置SSE标准头 response.setContentType("text/event-stream"); response.setCharacterEncoding("UTF-8"); response.setHeader("Cache-Control", "no-cache"); response.setHeader("Connection", "keep-alive"); response.setHeader("Access-Control-Allow-Origin", "*"); // 处理CORS try (PrintWriter writer = response.getWriter()) { // 2. 模拟持续发送事件 for (int i = 0; i < 10; i++) { // 构建SSE格式消息 String event = "event: progress\n" + "id: " + i + "\n" + "data: {\"percentage\": " + (i * 10) + \"}\n" + "retry: 3000\n\n"; // 双换行符结束一条消息 writer.write(event); writer.flush(); // 立即刷新缓冲区,确保数据发送到客户端 // 模拟一些延迟 Thread.sleep(1000); } // 可以发送一个结束事件 writer.write("event: end\ndata: Stream completed\n\n"); writer.flush(); } catch (IOException | InterruptedException e) { // 处理异常,通常意味着客户端已断开连接 log.info("Client likely disconnected: " + e.getMessage()); } } }注意事项:
- 异步处理:上面的例子是同步的,会阻塞Servlet线程。在生产环境中,你应该使用异步Servlet(
asyncSupported = true)并将任务提交到单独的线程池,避免耗尽Web容器的线程。 - 连接管理:需要维护一个所有活跃客户端连接的集合(如
ConcurrentHashMap),以便在业务事件发生时(如数据库变更、消息队列到来),能够遍历所有连接并向它们推送数据。
- 异步处理:上面的例子是同步的,会阻塞Servlet线程。在生产环境中,你应该使用异步Servlet(
方案二:使用 Spring Framework(推荐、高效)
Spring Framework(特别是Spring MVC和Spring WebFlux)对SSE提供了一流的支持,极大地简化了开发。
a. Spring MVC (Servlet Stack)
Spring MVC提供了SseEmitter类,它封装了异步请求处理和SSE格式化的复杂性。
@RestController
@RequestMapping("/sse")
public class SseController {
// 存储所有活跃的Emitter,用于广播
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
@GetMapping(path = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribe() {
// 设置超时时间(例如0表示永不超时,或者根据需要设置)
SseEmitter emitter = new SseEmitter(0L); // 0L for no timeout
String emitterId = UUID.randomUUID().toString();
emitters.put(emitterId, emitter);
// 处理完成或错误时的清理逻辑
emitter.onCompletion(() -> emitters.remove(emitterId));
emitter.onTimeout(() -> emitters.remove(emitterId));
emitter.onError((e) -> {
log.error("Error on SseEmitter", e);
emitters.remove(emitterId);
});
// 可以立即发送一个欢迎消息
try {
emitter.send(SseEmitter.event()
.name("welcome")
.data("Connected successfully! ID: " + emitterId));
} catch (IOException e) {
emitter.completeWithError(e);
}
return emitter;
}
// 一个业务方法,用于向所有连接的客户端广播消息
public void broadcastMessage(String message) {
emitters.forEach((id, emitter) -> {
try {
emitter.send(SseEmitter.event()
.name("notification")
.data(message));
} catch (IOException e) {
// 发送失败,移除该emitter
emitter.completeWithError(e);
emitters.remove(id);
}
});
}
}
b. Spring WebFlux (Reactive Stack)
如果你使用响应式编程模型,Spring WebFlux提供了更优雅、更强大的解决方案——Flux。它可以自然地表示一个事件流。
@RestController
@RequestMapping("/sse/reactive")
public class ReactiveSseController {
// 生成一个无限流,每秒推送一次
@GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamEvents() {
return Flux.interval(Duration.ofSeconds(1))
.map(sequence -> ServerSentEvent.<String>builder()
.id(String.valueOf(sequence))
.event("periodic-event")
.data("SSE Message #" + sequence + " at " + Instant.now())
.build());
}
// 从响应式数据源(如Kafka, R2DBC)获取数据并推送
@GetMapping(path = "/database-changes")
public Flux<ServerSentEvent<ChangeEvent>> streamDatabaseChanges() {
return myReactiveDatabaseChangeListener.getFluxOfChanges()
.map(change -> ServerSentEvent.builder()
.data(change)
.build());
}
}
Spring方案的优势:
- 简化异步处理:
SseEmitter和Flux自动处理异步和线程管理。 - 内置格式化和错误处理:无需手动拼接SSE字符串。
- 与Spring生态无缝集成:可以轻松地与Spring Security(添加认证)、Spring Events(监听应用内事件并推送)等集成。
架构建议与最佳实践
- 认证与授权:SSE端点也是API,需要保护。可以使用Spring Security等在建立连接前进行认证(例如通过URL参数或Token头)。
- 性能与可扩展性:
- 每个SSE连接都是一个长期的HTTP连接,会消耗服务器资源(内存、文件描述符)。
- 对于高并发场景,需要调整Web容器(Tomcat、Netty)的最大连接数和线程池配置。
- 考虑使用响应式编程(WebFlux),因为它基于NIO,可以用更少的线程处理更多的连接,天生适合这种长连接场景。
- 网关与代理:确保反向代理(如Nginx)配置了合适的超时时间(
proxy_read_timeout需要设置得很长或关闭)。 - 客户端实现:
- 前端使用标准的
EventSourceAPI非常简单。
const eventSource = new EventSource('/api/sse/subscribe'); eventSource.onmessage = (event) => { console.log('Generic message:', event.data); }; eventSource.addEventListener('progress', (event) => { const data = JSON.parse(event.data); updateProgressBar(data.percentage); }); eventSource.addEventListener('end', () => { eventSource.close(); console.log('Stream ended.'); }); eventSource.onerror = (error) => { console.error('EventSource failed:', error); }; - 前端使用标准的
总结
| 方面 | 方案一:纯Servlet | 方案二:Spring (MVC/WebFlux) |
|---|---|---|
| 复杂度 | 高,需要手动处理所有细节 | 低,框架提供高级抽象 |
| 控制力 | 完全控制 | 较高,遵循框架约定 |
| 异步处理 | 需手动管理线程池和异步Servlet | 框架自动管理(SseEmitter/Flux) |
| 集成度 | 低 | 高,与Spring生态无缝集成 |
| 适用场景 | 遗留项目、需要极致定制 | 绝大多数新的Spring Boot项目 |
对于绝大多数Java项目,强烈推荐使用Spring框架(特别是WebFlux)来实现SSE。它极大地降低了开发难度,提高了代码的可维护性和可扩展性,让你能专注于业务逻辑而非协议细节。SSE是构建实时数据推送功能的强大而简单的工具,在AI应用开发中,它非常适合用于向客户端流式传输AI模型的生成结果。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120688

浙公网安备 33010602011771号