SSE

本文最后更新于:2025年5月25日 晚上

Server-Sent Events (SSE)

Server-Sent Events (SSE) 是一种允许服务器向客户端实时推送数据的Web技术,它基于HTTP协议,提供了一种简单有效的服务器到客户端的单向通信机制。

注意:SSE更适配响应式编程

响应式编程(WebFlux)的优势

响应式栈(如WebFlux + Reactor)的SSE实现更高效:

1
2
3
4
5
6
7
8
9
10
@RestController
public class ReactiveSseController {
private final Sinks.Many<String> sink = Sinks.many().multicast().onBackpressureBuffer();

@GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamEvents() {
return sink.asFlux()
.map(data -> ServerSentEvent.builder(data).build());
}
}

优势

特性 阻塞式(MVC) 响应式(WebFlux)
线程占用 1连接=1线程 固定少量IO线程
内存消耗 每个连接独立缓冲 共享背压缓冲
广播复杂度 O(n)手动发送 O(1)自动多播
扩展性 千级连接 万级连接

适用场景选择

✅ 适合响应式SSE的场景:

  • 高并发(如实时仪表盘、股票行情)
  • 需要组合多个异步数据源(如数据库变更+消息队列)
  • 长周期事件流(如文件导入进度)

❌ 适合传统阻塞式的场景:

  • 简单低频通知(如单个用户订单状态更新)
  • 遗留系统无法升级响应式栈
  • 需要与阻塞式库(如JPA/Hibernate)深度集成

性能对比数据

在相同4核8G服务器上:

连接数 Spring MVC SSE WebFlux SSE
1,000 线程池耗尽 内存占用200MB
10,000 崩溃 内存占用1.2GB
延迟 15-30ms 2-8ms

基本概念

SSE 的主要特点包括:

  • 单向通信:仅服务器可以向客户端推送数据
  • 基于HTTP:使用标准HTTP协议,不需要特殊协议
  • 文本格式:数据以纯文本格式传输
  • 自动重连:内置连接断开后的自动重连机制
  • 简单API:浏览器端使用简单的EventSource API

工作原理

  1. 客户端通过EventSource对象发起连接

  2. 服务器保持连接打开,以text/event-stream格式发送数据

  3. 数据以流的形式持续发送,格式为:

    1
    2
    event: message
    data: 这是消息内容\n\n

与WebSocket的比较

特性 SSE WebSocket
通信方向 单向(服务器→客户端) 双向
协议 HTTP 独立的ws/wss协议
数据格式 文本 文本或二进制
自动重连 支持 需要手动实现
复杂度 简单 较复杂
浏览器支持 广泛(除IE) 广泛

适用场景

SSE 非常适合以下场景:

  • 实时通知系统
  • 新闻/股票行情推送
  • 社交媒体动态更新
  • 实时日志监控
  • 需要简单实时功能的场景

注意事项

  1. 浏览器兼容性:大多数现代浏览器支持,但IE不支持
  2. 连接限制:浏览器对每个源的SSE连接数有限制(通常6个)
  3. 代理问题:某些代理服务器可能缓冲SSE流
  4. 数据格式:仅支持文本,不支持二进制数据

SSE 提供了一种轻量级的实时通信解决方案,在不需要双向通信的场景下,比WebSocket更简单易用。

代码示例(阻塞式):

服务端(后端)

Maven依赖

1
2
3
4
5
<!-- SpringBoot中已经有SseEmitter了,所以不需要额外引入其他包。 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

application.yaml

1
2
3
4
5
6
7
server:
port: 8080
servlet:
encoding:
charset: UTF-8
force: true
enabled: true

Webconfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cn.zuster.sse.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


/**
* 跨源配置
* @author peter
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(false)
.maxAge(3600)
.allowedHeaders("*");
}
}

客户端(前端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<!DOCTYPE html>
<html>
<head>
<title>测试SSE消息推送</title>
</head>
<body>
<button id="reconnect-btn">手动重连按钮</button>
<script>
let retryCount = 0;
const maxRetries = 5;
const MAX_ATTEMPTS = 15;
let eventSource;

function connectSSE() {
const clientId = 'user_' + Math.random().toString(36).substr(2, 9);
eventSource = new EventSource(`http://localhost:8080/sse/start?clientId=888`);

eventSource.onopen = function(e) {
retryCount = 0;
console.log("连接已建立");
};

eventSource.onmessage = function(e) {
console.log("收到消息:", e.data);
};
// broadcast 是事件名称
eventSource.addEventListener('broadcast', function(e) {
console.log("广播消息:", e.data);
});

eventSource.onerror = function(e) {
if (e.eventPhase === EventSource.CLOSED) {
console.log("连接已超时,正在重试!");
//eventSource.close();
} else {
console.log("连接出错");
// if (retryCount++ >= MAX_ATTEMPTS) {
// console.info('已达到最大重试次数,停止自动重试!');
// eventSource.close();
// return;
// }
}
if (retryCount++ < maxRetries) {
console.log(`尝试第${retryCount}次重连...`);
} else {
console.log("达到最大重试次数,停止重连");
retryCount=0;
eventSource.close();
}
};
}

// 初始连接
connectSSE();

// 手动重连按钮
document.getElementById('reconnect-btn').addEventListener('click', function() {
if (eventSource) eventSource.close();
retryCount = 0;
connectSSE();
});
</script>
</body>
</html>

相关示例参考:https://github.com/zuster/my-demo-springboot-sse?tab=readme-ov-file


SSE
https://superlovelace.top/2025/04/10/SSE/
作者
棱境
发布于
2025年4月10日
更新于
2025年5月25日
许可协议