响应式编程 之 SseEmitter

Spring MVC(基于 Servlet 的传统 Web 框架) 中,若需实现异步、流式、非阻塞(相对而言)的 HTTP 响应,可以使用 Spring 提供的两个核心类:

  • SseEmitter:专用于 Server-Sent Events (SSE) 场景
  • ResponseBodyEmitter:通用的异步响应流,支持任意格式(JSON、文本、自定义等)

虽然它们都基于 Servlet 3.0+ 的 异步处理机制(AsyncContext),但在用途、协议、数据格式和客户端兼容性上有显著区别。

下面从 原理 → 区别 → 使用示例 → 注意事项 四个维度详细解析。


一、底层原理:Servlet 异步处理

1. 传统同步模型的问题

@GetMapping("/slow")
public String slow() {
    Thread.sleep(5000); // 阻塞 Tomcat 线程 5 秒!
    return "done";
}
  • 每个请求占用一个 Tomcat 线程,高并发时线程耗尽。

2. 异步模型(AsyncContext)

  • 调用 request.startAsync() 后,立即释放容器线程
  • 后续由业务线程写入响应(通过 AsyncContext.getResponse())。
  • Spring 封装为 ResponseBodyEmitter / SseEmitter,简化使用。

关键点
这些 emitter 不是真正的非阻塞 I/O(如 Netty),而是“释放请求线程 + 异步写响应”,仍基于 Servlet 容器(Tomcat/Jetty)。


二、SseEmitter vs ResponseBodyEmitter 对比

特性 SseEmitter ResponseBodyEmitter
用途 专用于 SSE(Server-Sent Events) 通用异步响应流(任意格式)
Content-Type 固定为 text/event-stream 可自定义(如 application/json, text/plain
数据格式 自动封装为 SSE 格式(data: ...\n\n 原始对象,由 HttpMessageConverter 序列化
客户端 必须用 EventSource(浏览器) 任意 HTTP 客户端(如 fetch, curl
事件支持 支持 id, event, retry 等字段 无事件概念,纯数据流
超时控制 构造时指定(毫秒),默认 30 秒 同左
完成方式 complete(), completeWithError() 同左
典型场景 实时通知、日志推送 文件下载、大结果集分块返回

三、使用示例详解

示例 1:SseEmitter —— 实现 SSE 推送

@RestController
public class SseController {

    private final Set<SseEmitter> emitters = 
        Collections.synchronizedSet(new HashSet<>());

    @GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter handleSse() {
        SseEmitter emitter = new SseEmitter(60_000L); // 60秒超时

        // 注册生命周期回调
        emitter.onCompletion(() -> emitters.remove(emitter));
        emitter.onTimeout(() -> {
            emitters.remove(emitter);
            emitter.complete();
        });
        emitter.onError(e -> {
            emitters.remove(emitter);
            // 日志记录异常
        });

        emitters.add(emitter);

        // 异步发送数据(避免阻塞请求线程)
        CompletableFuture.runAsync(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    // 发送标准 SSE 事件
                    emitter.send(SseEmitter.event()
                        .id(String.valueOf(i))
                        .name("update")      // 事件类型
                        .data("Message-" + i)
                        .reconnectTime(3000) // 建议重连时间(毫秒)
                    );
                    Thread.sleep(1000);
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }

    // 广播消息给所有连接
    @PostMapping("/broadcast")
    public void broadcast(@RequestBody String msg) {
        emitters.forEach(emitter -> {
            try {
                emitter.send(msg);
            } catch (IOException e) {
                emitters.remove(emitter);
            }
        });
    }
}

前端调用:

const es = new EventSource('/sse');
es.addEventListener('update', event => {
    console.log('收到 update 事件:', event.data);
});
es.onerror = () => es.close();

示例 2:ResponseBodyEmitter —— 通用流式响应

@RestController
public class StreamController {

    @GetMapping(value = "/stream-json", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseBodyEmitter streamJson() {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter();

        CompletableFuture.runAsync(() -> {
            try {
                // 发送多个 JSON 对象(注意:这不是合法 JSON 数组!)
                emitter.send(Map.of("step", 1, "status", "start"));
                Thread.sleep(1000);
                emitter.send(Map.of("step", 2, "status", "processing"));
                Thread.sleep(1000);
                emitter.send(Map.of("step", 3, "status", "done"));
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }

    // 流式文件下载(逐块发送)
    @GetMapping("/download")
    public ResponseBodyEmitter downloadFile() {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter();

        CompletableFuture.runAsync(() -> {
            try (InputStream is = new FileInputStream("large-file.zip")) {
                byte[] buffer = new byte[8192];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    emitter.send(buffer, 0, len); // 发送原始字节
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }
}

客户端调用(JavaScript):

// 注意:这不是标准 JSON,需逐行解析(类似 NDJSON)
const response = await fetch('/stream-json');
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const chunk = decoder.decode(value);
    console.log('Received chunk:', chunk); // 可能是 {"step":1,...}{"step":2,...}
}

⚠️ 重要ResponseBodyEmitter 发送多个对象时,不会自动加逗号或换行!需自行设计协议(如每条 JSON 后加 \n)。


四、关键注意事项

1. 必须异步处理

  • 在 Controller 方法中直接 emitter.send() 会阻塞请求线程。
  • 务必使用 CompletableFuture, @Async, 或线程池提交任务。

2. 超时与资源泄漏

  • 默认超时 30 秒(可通过构造函数设置)。
  • 必须监听 onTimeout() / onCompletion() / onError() 并清理资源(如从集合中移除)。

3. 线程安全

  • SseEmitterResponseBodyEmitter 不是线程安全的
  • 不要多个线程同时调用 send()

4. 错误处理

  • 客户端断开连接时,send() 会抛 IOException
  • 必须捕获并调用 completeWithError(),否则连接会卡住。

5. 与 WebFlux 的区别

  • 这些 emitter 仅适用于 Spring MVC(Servlet)
  • 在 WebFlux 中应使用 Flux<ServerSentEvent>Flux<T> 直接返回。

五、何时使用哪个?

场景 推荐方案
浏览器实时接收服务器推送(通知、日志) SseEmitter
向非浏览器客户端流式返回数据(如服务间调用) ResponseBodyEmitter
返回分块 JSON(NDJSON) ResponseBodyEmitter + 手动加 \n
文件下载、视频流 ResponseBodyEmitter(发送 byte[])
需要双向通信 ❌ 改用 WebSocket

六、总结

类型 协议 数据格式 客户端 适用场景
SseEmitter HTTP + SSE data: ...\n\n 浏览器 EventSource 实时单向推送
ResponseBodyEmitter 普通 HTTP 任意(JSON/文本/二进制) 任意 HTTP 客户端 通用异步流

💡 最佳实践

  • 优先考虑是否真的需要流式响应。
  • 若面向浏览器且只需服务器推,选 SseEmitter
  • 若需灵活控制响应格式或对接非浏览器客户端,选 ResponseBodyEmitter
  • 高并发场景下,评估是否应迁移到 Spring WebFlux 以获得真正的非阻塞能力。
posted @ 2026-02-03 17:01  蓝迷梦  阅读(8)  评论(0)    收藏  举报