1. SSE是什么:先弄明白你在调一个什么东西
SSE全称Server-Sent Events,中文一般叫“服务器推送事件”。它最大的特点是:连接一旦建立,服务器可以持续不断地往客户端推数据,而不是像普通HTTP请求那样“一问一答”就结束。
先说一个很多人上来就会混淆的点:SSE本质还是HTTP协议,不是WebSocket那种独立的协议栈。它复用了HTTP的连接,只是通过特殊的响应头Content-Type: text/event-stream告诉客户端“我这个响应不会马上结束,你要一行一行往上读”。客户端拿到这个响应头之后,连接会被保持住,服务器每次写一段数据,客户端就能实时收到一段。这个过程是单向的——服务器往客户端推,客户端不能通过同一个连接往服务器发数据。如果需要双向通信,那还是得用WebSocket。
和它最像的是我们常见的“轮询”。轮询是客户端每隔几秒发一次请求,问服务器“有没有新数据”。SSE是建立连接之后服务器主动推,客户端不用反复发起请求。从服务端资源占用角度讲,SSE明显更节省,因为它只占用一个连接,而且数据是“有更新才推”,不是每次轮询都带着整包数据跑一遍。
实际工作中我遇到过两类最常见的SSE使用场景:
第一类是任务进度实时推送。比如上传一个很大的文件,后端在做异步处理,处理进度是多少、到哪一步了,通过SSE实时推给前端进度条。这类场景用轮询也能做,但SSE的实时性更好,而且不用频繁创建HTTP连接。
第二类是AI对话流式输出。也就是现在大模型应用里最常见的“打字机”效果。模型的回答是一段一段生成的,后端拿到一部分就通过SSE推给前端,用户看到的就是一个字一个字往外蹦。这种体验用轮询基本做不到,因为每次轮询都会把已经生成的文本重复传一遍,浪费流量不说,体验也跟不上。
回到这个项目标题本身——“javaWeb调用SSE接口”,我要特别强调一下这里的视角。大多数讲SSE的文章,都在讲浏览器端怎么用EventSource监听,或者后端怎么写一个SSE接口。但“javaWeb调用SSE接口”说的不是前端页面在调,而是一个Java后端服务,作为客户端,去调用另一个系统提供的SSE接口。这种场景在微服务架构、服务间数据同步、网关代理层、第二方系统对接时非常常见。
举个例子,你的Java服务要对接一个第三方AI平台,对方提供了一个SSE接口用于流式返回对话结果,那你的Java服务就得自己写一套“SSE客户端”逻辑:建立连接、逐行读取、解析事件、超时处理、断线重连。这一套东西,和浏览器里的EventSource完全是两码事。官方的JDK没给现成的API,需要自己封装或者借助第三方库来实现。
本文后面所有的内容,都围绕这个视角展开:服务端的SSE接口怎么写,Java客户端怎么调,遇到了哪些坑,怎么排查。前端Vue3的部分也会带上,因为很多场景是前台页面先调你的Java服务,你的Java服务再去调外部的SSE接口,整条链路需要打通。
1.1 SSE的报文格式:看懂它才能真正“吃”到数据
刚开始做SSE的时候,很多人容易在解析这一步卡住。因为SSE接口返回的内容不是JSON,而是一个基于文本的流式格式,每一帧由若干行组成,每行一个字段。
一个标准的SSE消息长这样:
code复制id: 1
event: message
data: {"content": "你好"}
data: {"content": "我是第二行"}
这里要注意几个关键点:
- 每个字段一行,格式是
字段名: 空格 + 值。那个冒号后面的空格不是必须的,但建议写上,兼容性更好。 data可以有多行,多行data会被拼接成一个字符串,中间用换行符连接。也就是说上面这个例子,你最终收到的data其实是{"content": "你好"}\n{"content": "我是第二行"}这个整体。- 帧与帧之间,用一个空行隔开。这是整个SSE协议里最容易出问题的地方——服务端如果忘了发这个空行,客户端会一直等,以为一帧数据还没结束。
event表示事件类型,默认是message。如果服务端发的是event: result,客户端只有监听了result这个事件才能收到。id用于断线重连。客户端重连的时候,可以带上Last-Event-ID头告诉服务器“我从哪一条消息之后开始补给我”。retry字段用来告诉客户端重连的间隔时间,单位是毫秒。- 还有一类特殊消息叫注释,以
:开头。服务端经常会用注释行来做“心跳保活”,因为注释行不会触发任何事件,但能让连接保持活跃,防止中间设备超时断开。
所以在写Java客户端解析SSE时,核心逻辑就是:按行读取,遇到空行说明一帧结束了,把这一帧里的data、event、id等字段组装起来交给上层业务处理。如果服务端不按套路出牌,少发了空行或者把JSON挤在一行里,解析就会出问题,我后面会专门讲这些坑。
1.2 轮询、SSE、WebSocket到底怎么选
这里给一个比较直观的选型参考,遇到类似需求别选错了方向:
| 方案 | 通信方向 | 实时性 | 连接开销 | 适用场景 |
|---|---|---|---|---|
| 短轮询 | 客户端拉取 | 取决于频率 | 高,每次都要建连 | 低频数据刷新 |
| 长轮询 | 客户端拉取,服务端挂起 | 中等 | 较高 | 兼容性要求高的老系统 |
| SSE | 服务端推送(单向) | 高 | 低,一条长连接 | 实时通知、进度推送、AI流式输出 |
| WebSocket | 双向通信 | 最高 | 低,一条长连接 | 聊天、协同编辑、游戏 |
我在实际项目里一般这么判断:只需要服务器单方面推数据,就用SSE。别看WebSocket功能更强大,但它带来了协议升级、心跳维护、状态管理等一系列额外复杂度。而SSE是纯HTTP,天然能穿透绝大多数防火墙和代理,服务端实现也简单得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先搭一个能跑的SSE服务端:Spring Boot + SseEmitter
既然要“调用”SSE接口,首先得有一个目标接口。我建议先把服务端搭起来,用浏览器或者命令行验证接口本身没问题,再去写Java客户端调用。不然客户端写完了,连个测试环境都没有,排错都没法排。
Spring Boot 2.x+ 提供了一个非常好用的类SseEmitter,专门用来实现SSE推送。下面是一个最简版本,如果你用的是Spring Boot 3.x,包路径和用法基本一致,只是javax要换成jakarta。
java复制@RestController
@RequestMapping("/api/sse")
public class SseController {
/**
* 建立SSE连接,返回SseEmitter
*/
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() {
// 0L 表示不超时,生产环境建议设置一个合理值,比如60_000L
SseEmitter emitter = new SseEmitter(0L);
// 异步推流:这里用线程池模拟真实场景
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
try {
for (int i = 0; i < 100; i++) {
// 发送一条消息,SseEventBuilder可以自定义事件名、id、data
emitter.send(SseEmitter.event()
.id(String.valueOf(i))
.name("message")
.data("{\"index\": " + i + ", \"msg\": \"hello sse\"}"));
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
});
return emitter;
}
}
看起来很简单是吧?但这个简单版本里藏着几个影响成败的细节,我逐个拆解。
2.1 为什么produces要指定text/event-stream
MediaType.TEXT_EVENT_STREAM_VALUE对应的就是text/event-stream,这是SSE的“身份证”。客户端拿到这个响应头,才知道要按SSE
