什么是SSE
SSE是一种基于TCP的应用层协议,让server可以向client主动的单向推送消息。
标准的tcp协议里通信的双方都存着对方的一些信息,比如对方的IP/端口等信息,这样才能在网络层面精准的通过路由转发等节点找到对方。所以基于TCP,SSE中的server可以找到client。
sse的实现要由client和server双向奔赴,client端要去向server端发起建立sse连接的请求,server端要去响应请求,并且推送数据。client端还要去进行一些管理,比如防止重复连接等。
client端:
目前主流的浏览器都支持sse,并且实现了比如防止重复连接、重连接等功能。
server端:
SSE是JAVA EE的一个标准,所以诸如tomcat等主流的web server都对其进行了实现,诸如Spring等主流的框架,也对其进行了实现。
SSE的建立过程

由Client向Server申请建立SSE
GET /events HTTP/1.1
Host: api.example.com
Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)...
Origin: https://myapp.com
Last-Event-ID: 42 (如果有断线续传 / If resuming after disconnect)
报文中的关键点:
- Accept: text/event-stream:告诉服务器客户端期望 SSE 数据流
- Connection: keep-alive:要求保持连接持久化
- Last-Event-ID:可选,用于断线续传
server收到后通过设置正确的响应头来和client建立SSE连接:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Access-Control-Allow-Origin: * (如果需要跨域 / If CORS needed)
数据报文
| field | require | description | example |
|---|---|---|---|
| id | no | 消息 ID,用于断线续传 | id: 1001 |
| event | no | 事件类型,默认为 "message" | event: stock-update |
| data | yes | 消息内容,可以有多行 | data: Hello World |
| retry | no | 重连时间(毫秒) | retry: 5000 |
sse有一套标准事件:
- open:当客户端与服务器的连接成功建立时触发。你可以在此事件中执行一些初始化操作,比如更新UI状态为"已连接"。
- message:这是最核心的通用事件。当服务器推送的消息没有指定 event 字段时,就会触发该事件。你可以在它的回调中处理默认数据。
- error:当连接发生错误(如网络中断、请求超时、服务器返回非200状态码)时触发。需要在此进行异常处理,但需要注意的是,EventSource 默认会自动尝试重连。
现在的浏览器基本都支持标准事件,都自带默认实现。当然,也可以前后端自行约定,去自定义一些事件。
如何断开?何时断开?
| 场景 / Scenario | 触发方 / Triggered By | 说明 / Description |
|---|---|---|
| 客户端主动关闭 | 客户端 / Client | 调用 eventSource.close() |
| 服务器主动关闭 | 服务器 / Server | 服务器调用 emitter.complete() |
| 网络断开 | 网络 / Network | TCP 连接中断 |
| 超时 | 服务器 / Server | 超过设定的超时时间 |
| 页面关闭/刷新 | 浏览器 / Browser | 用户关闭标签页或刷新页面 |
| 错误发生 | 双方 / Both | 发生不可恢复的错误 |
SSE的实现
sse实现的关键点在于sse的连接一旦建立后需要持久化,要支持被反复使用,而不是像单次http请求一样,请求完就拆除连接了,否则这样和直接是用http没有任何区别。
最简单的编码实现如下,但会存在问题:
@WebServlet("/bad-sse")
public class BadSseServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
// 设置SSE头
resp.setContentType("text/event-stream");
resp.setHeader("Connection", "keep-alive");
PrintWriter writer = resp.getWriter();
// 试图保持连接并推送数据
while (true) {
writer.write("data: 心跳\n\n");
writer.flush();
try {
Thread.sleep(5000); // 模拟业务处理
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// ❌ 严重问题:这个线程永远卡在这里,不会释放!
// ❌ 每个连接都会占用一个Tomcat线程池的线程
// ❌ 100个并发连接就会用光Tomcat的默认线程池(200个)
}
}
可以配合异步上下文来实现没有问题的sse,可以说正是由于Servlet 3.0标准中提出了AsyncContext,然后Tomcat给出了自己的实现,才得以支持SSE在tomcat中的编码实现:
@WebServlet("/good-sse")
public class GoodSseServlet extends HttpServlet {
// 存储所有连接的AsyncContext
private final List<AsyncContext> clients = new CopyOnWriteArrayList<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
// 设置SSE头
resp.setContentType("text/event-stream");
resp.setHeader("Connection", "keep-alive");
// ⭐ 关键:启动异步上下文
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(0); // 永不超时
// 存储连接
clients.add(asyncContext);
// 发送初始数据
PrintWriter writer = asyncContext.getResponse().getWriter();
writer.write("data: 连接成功\n\n");
writer.flush();
// ⭐ doGet方法立即返回!线程释放回Tomcat线程池!
// 但TCP连接仍然保持,AsyncContext对象仍然存在
}
// 在其他线程中推送数据
@Scheduled(fixedDelay = 5000)
public void pushHeartbeat() {
for (AsyncContext context : clients) {
try {
PrintWriter writer = context.getResponse().getWriter();
writer.write("data: 心跳\n\n");
writer.flush();
// 使用之前保存的AsyncContext,通过同一个TCP连接推送
} catch (IOException e) {
clients.remove(context); // 连接已断开
}
}
}
}
Spring中对AsyncContext做了二次封装,封装成了一个SseEmitter 类,能更方便的进行SSE相关的操作:
@RestController
@RequestMapping("/sse")
public class SseController {
@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handleEvents() {
// 创建SseEmitter,设置超时时间(0表示永不超时)
SseEmitter emitter = new SseEmitter(0L);
// 使用独立线程推送数据,避免阻塞Tomcat线程
CompletableFuture.runAsync(() -> {
try {
for (int i = 0; i < 10; i++) {
// 构建SSE事件
SseEmitter.SseEventBuilder event = SseEmitter.event()
.id(String.valueOf(i)) // 事件ID
.name("message") // 事件名称
.data("Event " + i + " at " + new Date()) // 事件数据
.reconnectTime(3000); // 重连时间(ms)
emitter.send(event);
Thread.sleep(1000); // 模拟延迟
}
emitter.complete(); // 完成推送
} catch (Exception e) {
emitter.completeWithError(e); // 异常处理
}
});
return emitter;
}
}