【SSE】

什么是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;
    }
}
相关推荐
014-code2 小时前
kafka + springboot快速入门
java·spring boot·分布式·kafka
盐水冰2 小时前
【烘焙坊项目】后端搭建(11)- 用户&商家订单板块
java·后端·学习
吧啦蹦吧2 小时前
idea---------------
java·ide·intellij-idea
李白的粉2 小时前
基于springboot的教师工作量管理系统
java·spring boot·毕业设计·课程设计·教师工作量管理系统·源代码
武超杰2 小时前
SpringMVC入门指南:从零开始掌握核心用法
java·spring·mvc
小王不爱笑1322 小时前
深入浅出 Docker 核心知识点,解锁容器化技术精髓
java·spring boot·docker
一只大袋鼠2 小时前
并发编程(二十四):单例模式(三):构造方法私有:单例模式的 “第一道防线”
java·单例模式·并发编程
myloveasuka2 小时前
[Java]包装类
java·开发语言
myloveasuka2 小时前
时间相关类
java·开发语言