【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;
    }
}
相关推荐
Wang's Blog16 分钟前
Java 项目部署之 Docker工具快速入门: Docker 镜像仓库
java·docker
企智汇-项目管理软件24 分钟前
离散制造管理系统怎么选?一套系统管住从研发到交付的全周期
java·人工智能·制造·项目经理·项目管理软件·项目管理系统·项目管理软件工程
IT研究室37 分钟前
最新计算机毕业设计选题推荐-基于spring boot的幼儿园管理系统-网站-文档指导-Java-springboot
java·spring boot·课程设计
会编程的吕洞宾39 分钟前
Spring Cloud Gateway限流实战 Redis令牌桶算法+熔断降级
java·后端·spring
Wang's Blog1 小时前
Java 项目部署之 Docker工具快速入门: DockerCompose 部署微服务集群
java·docker·微服务
BullSmall1 小时前
SonarQube 9.9.8-community 适配 Jenkins 版本说明
java·前端·jenkins
威哥爱编程1 小时前
【AI全栈12-10】Spring Boot 用多模态做商品图审核 / 发票识别
java·spring boot·后端
轮子大叔1 小时前
Java8和Java17常见区别
java
威哥爱编程1 小时前
【AI全栈12-09】Spring Boot 用流式输出做打字机对话:点下去不再干等
java·spring boot·后端
梁云亮1 小时前
在 IDEA 中运行 Tomcat 时,直接在 server.xml 中配置的 <Context> 无效
tomcat·intellij-idea·虚拟路径