SSE 流式响应实现方法详解(评测平台为何弃用 WebSocket)

AI 大模型评测平台前端要实时看到多家模型的流式输出。这里用 SSE(Server-Sent Events)而不是 WebSocket,因为评测场景是纯「服务端推、客户端收」的单向流,SSE 基于标准 HTTP、自带重连、实现简单,WebSocket 的双向通信能力在这里用不上,反而增加复杂度。选 SSE 不是因为它功能多,而是因为它刚好够用且省心。

一、SSE 和 WebSocket 的取舍

对比项 SSE WebSocket
通信方向 服务端→客户端(单向) 双向
底层协议 HTTP 独立握手 + TCP 帧
自动重连 浏览器内置 需自己实现
代理/防火墙 普通 HTTP,友好 部分代理不支持
实现复杂度 Spring MVC 一个 SseEmitter 需握手、心跳、状态机
适用场景 推送结果、日志、流 实时聊天、协作
浏览器限制 同域 6 连接(HTTP/1.1) 无此限制

评测平台要的是「后端把模型输出推给前端看」,没有前端反推后端的需求。SSE 的单向模型足够,且省掉 WebSocket 的握手维护和心跳逻辑。WebSocket 的双向能力在这里是多余功能,引入它只会增加复杂度——要写心跳保活、要处理握手失败、要管连接状态机,全是评测场景不需要的负担。

二、Spring 里怎么实现 SSE

  1. Controller 返回 SseEmitter,设置超时;
  2. 把多个模型的并行流订阅到 emitter,每出一段就 send;
  3. 完成后调用 complete,异常时 completeWithError。
@RestController
@RequestMapping("/api/eval")
public class EvalController {

    private final ParallelEvalService evalService;

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter stream(@RequestParam String prompt) {
        SseEmitter emitter = new SseEmitter(120_000L); // 2分钟超时
        evalService.runParallel(prompt, Duration.ofMinutes(2))
            .doOnNext(chunk -> {
                try { emitter.send(SseEmitter.event().name("chunk").data(chunk)); }
                catch (IOException e) { emitter.completeWithError(e); }
            })
            .doOnComplete(emitter::complete)
            .doOnError(emitter::completeWithError)
            .subscribe();
        return emitter;
    }
}

SseEmitter 是 Spring MVC 对 SSE 的封装。Controller 返回它后,Spring 会保持 HTTP 连接打开,后续每次 send 都往这个连接写一个事件。前端用 EventSource 订阅,收到事件就更新对应模型的回答区域。超时设 2 分钟是因为模型流式推理可能要几十秒,设短了会提前断。

三、为什么 SSE 比 WebSocket 更省心

  • 浏览器自动重连:断线后浏览器原生 EventSource 会自动重连,不用前端写重连逻辑;WebSocket 断了得自己检测、自己重连、自己恢复状态;
  • 走标准 HTTP:中间的 WAF、负载均衡、CDN 都按普通请求处理,不用单独配 WebSocket 升级;WebSocket 的握手升级在部分代理和 WAF 下会被拦截,排查成本高;
  • 不用维护心跳:WebSocket 长连接要定期发心跳保活,不然会被中间网络设备断开;SSE 靠 HTTP 连接本身,前端 EventSource 收到断开会自动重连。

四、SSE 的事件格式

SSE 推送的数据格式是 event: 类型\ndata: 内容\n\n。Spring 的 SseEmitter.event().name("chunk").data(obj) 会自动生成这个格式。前端 EventSource 监听对应事件名,拿到 data 后解析。可以发不同 name 的事件区分语义,比如 chunk 是推理片段、done 是完成、error 是失败,前端按 name 分别处理。

五、什么场景该换回 WebSocket

如果评测平台要加「前端实时干预」(比如前端发「停掉某个模型的推理」指令),那 SSE 单向就不够。两种解法:一是换 WebSocket,支持双向;二是 SSE + 一个额外的 POST 接口——前端发 POST 告诉后端停哪个模型,后端收到后取消对应订阅。纯推结果用 SSE 就行,要双向交互再评估是否值得引入 WebSocket 的复杂度。

六、连接数限制要注意

HTTP/1.1 下浏览器对同域 SSE 默认只有 6 个连接,如果评测平台同时开多个评测任务的 SSE 流,可能撞上限。解法是启用 HTTP/2(无此限制),或者把多个评测任务复用到同一个 SSE 流上用事件名区分。HTTP/2 是更干净的做法,现代浏览器和服务器都支持。

常见问题

Q1:SSE 有连接数限制吗?

有。浏览器对同域 SSE 默认 6 个连接限制,用 HTTP/2 可规避。

Q2:SseEmitter 超时怎么定?

按模型推理时间留余量。设短了会提前断,设长了占线程。

Q3:SSE 能传 JSON 吗?

能。SseEmitter.event().data(obj) 会序列化,前端按 JSON 解析即可。

Q4:前端怎么收 SSE?

用 EventSource 对象。监听 message 事件或自定义事件名,收到就更新界面。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部