目录
[一、为什么需要 SSE?](#一、为什么需要 SSE?)
[传统 HTTP 的问题](#传统 HTTP 的问题)
[SSE 的解决方案](#SSE 的解决方案)
[二、SSE 是什么?底层怎么跑?](#二、SSE 是什么?底层怎么跑?)
[2.1 定义](#2.1 定义)
[2.2 协议格式](#2.2 协议格式)
[2.3 浏览器端 API](#2.3 浏览器端 API)
[三、SSE vs WebSocket vs 轮询:怎么选?](#三、SSE vs WebSocket vs 轮询:怎么选?)
[四、Spring Boot SSE 实战:实时消息推送](#四、Spring Boot SSE 实战:实时消息推送)
[4.1 项目结构](#4.1 项目结构)
[4.2 pom.xml(只用 spring-boot-starter-web)](#4.2 pom.xml(只用 spring-boot-starter-web))
[4.3 启动类](#4.3 启动类)
[4.4 SSE 连接管理服务(核心)](#4.4 SSE 连接管理服务(核心))
[4.5 SSE Controller(对外接口)](#4.5 SSE Controller(对外接口))
[4.6 前端页面(static/index.html)](#4.6 前端页面(static/index.html))
[4.7 application.yml](#4.7 application.yml)
[5.1 启动项目](#5.1 启动项目)
[5.2 打开浏览器](#5.2 打开浏览器)
[5.3 用 curl 验证(理解底层协议)](#5.3 用 curl 验证(理解底层协议))
[6.1 定时推送(模拟实时数据)](#6.1 定时推送(模拟实时数据))
[6.2 推送 JSON 对象](#6.2 推送 JSON 对象)
[6.3 带重试间隔的推送](#6.3 带重试间隔的推送)
[6.4 前端拿到断线重连的 Last-Event-ID](#6.4 前端拿到断线重连的 Last-Event-ID)
[Nginx 代理 SSE 的正确配置](#Nginx 代理 SSE 的正确配置)
🧩 一句话读懂 :SSE 是浏览器内置的"服务器单向推送"技术,比 WebSocket 轻量,比轮询高效,Spring Boot 一行代码就能用。 🎯 适合人群 :用过 HTTP 请求-响应模式,想了解实时推送但不想学 WebSocket 全套的 Java 开发者 📊 难度等级 :⭐⭐☆☆☆(入门) ⏱ 阅读时长 :约 15 分钟 💡 前置知识:Spring Boot 基础、HTTP 协议基本概念
一、为什么需要 SSE?
传统 HTTP 的问题
HTTP 是"请求-响应"模式:客户端不问,服务器就不说。
客户端:有新消息吗? → 服务器:没有。
客户端:有新消息吗? → 服务器:没有。
客户端:有新消息吗? → 服务器:有!给你。 ← 第 3 次才问到
客户端:有新消息吗? → 服务器:没有。
...
这叫轮询(Polling),问题很明显:
-
大量请求是无效的("没有"占了 99%)
-
浪费带宽和服务器资源
-
实时性取决于轮询间隔(间隔短→浪费,间隔长→延迟)
SSE 的解决方案
SSE 让服务器主动推送,客户端只需要建立一次连接,之后服务器想发就发:
客户端:我要建立 SSE 连接 → 服务器:好的,连接保持
服务器:给你一条消息
服务器:再给你一条
服务器:又来一条
... (连接一直保持着)
一句话:SSE = 服务器单向推送 + 自动重连 + 纯 HTTP 协议。
二、SSE 是什么?底层怎么跑?
2.1 定义
SSE(Server-Sent Events) 是 W3C 标准,HTML5 的一部分。浏览器通过 EventSource API 建立一个持久的 HTTP 连接,服务器通过这个连接持续推送文本数据。
2.2 协议格式
SSE 的底层就是 HTTP 响应头 Content-Type: text/event-stream,服务器返回的是一种简单的文本格式:
data: 第一条消息\n
\n
data: 第二条消息\n
\n
event: close\ndata: 服务器要关闭了\n
\n
格式规则:
| 字段 | 说明 | 示例 |
|---|---|---|
data: |
消息内容(必须) | data: Hello SSE |
event: |
事件类型(可选) | event: notification |
id: |
消息 ID(可选,用于断线重连) | id: 1001 |
retry: |
重连间隔毫秒(可选) | retry: 5000 |
\n\n |
每条消息之间用两个换行分隔 |
多行数据 用多个 data: 字段:
data: 第一行内容\n
data: 第二行内容\n
\n
浏览器收到后会用换行符拼接成一个字符串。
2.3 浏览器端 API
java
// 创建 SSE 连接
const eventSource = new EventSource('/api/sse/stream');
// 监听默认消息
eventSource.onmessage = function(event) {
console.log('收到消息:', event.data);
};
// 监听自定义事件
eventSource.addEventListener('notification', function(event) {
console.log('通知:', event.data);
});
// 连接建立
eventSource.onopen = function() {
console.log('SSE 连接已建立');
};
// 错误处理(浏览器会自动重连)
eventSource.onerror = function(event) {
console.log('连接断开,浏览器会自动重连...');
};
// 关闭连接
eventSource.close();
关键特性:
-
✅ 自动重连:连接断了,浏览器自动重新连接(默认 3 秒间隔)
-
✅ 基于 HTTP:不需要特殊协议,能走 HTTP 的地方就能用 SSE
-
✅ 纯文本:只能传文本,不能传二进制(图片、文件等)
-
✅ 浏览器内置:原生支持,不需要第三方库
三、SSE vs WebSocket vs 轮询:怎么选?
| 维度 | 轮询 (Polling) | SSE (Server-Sent Events) | WebSocket |
|---|---|---|---|
| 通信方向 | 客户端 → 服务器 | 服务器 → 客户端(单向) | 双向 |
| 协议 | HTTP | HTTP | WS/WSS(独立协议) |
| 连接数 | 每次请求一个 | 一个长连接 | 一个长连接 |
| 实时性 | 差(取决于间隔) | 好(秒级) | 最好(毫秒级) |
| 浏览器支持 | 全部 | 全部(IE 除外) | 全部 |
| 自动重连 | 不需要 | ✅ 内置 | ❌ 需要自己实现 |
| 二进制传输 | ✅ | ❌ 只支持文本 | ✅ |
| 实现复杂度 | ⭐ 最简单 | ⭐⭐ 简单 | ⭐⭐⭐ 较复杂 |
| 适合场景 | 低频查询 | 通知、进度、日志推送 | 聊天、游戏、协同编辑 |
一句话决策:
只需要服务器推送消息给客户端? → SSE ✅
需要客户端和服务器双向实时通信? → WebSocket
数据更新不频繁(分钟级)? → 轮询就够了
四、Spring Boot SSE 实战:实时消息推送
4.1 项目结构
sse-demo/
├── src/main/java/com/example/sse/
│ ├── SseDemoApplication.java // 启动类
│ ├── controller/SseController.java // SSE 接口
│ └── service/SseEmitterService.java // 连接管理
├── src/main/resources/
│ ├── application.yml
│ └── static/index.html // 前端页面
└── pom.xml
4.2 pom.xml(只用 spring-boot-starter-web)
java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>sse-demo</artifactId>
<version>1.0.0</version>
<name>sse-demo</name>
<description>SSE 快速入门示例</description>
<dependencies>
<!-- 就这一个依赖就够了 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4.3 启动类
java
package com.example.sse;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SseDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SseDemoApplication.class, args);
}
}
4.4 SSE 连接管理服务(核心)
java
package com.example.sse.service;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* SSE 连接管理器
* 用 ConcurrentHashMap 管理所有客户端的 SseEmitter 连接
*/
@Service
public class SseEmitterService {
// 存储所有客户端连接,key = 用户ID,value = SseEmitter
private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
/**
* 创建 SSE 连接
* @param userId 用户唯一标识
* @param timeout 超时时间(毫秒),0 表示不超时
*/
public SseEmitter createEmitter(String userId, long timeout) {
// 设置超时时间,0 表示永不超时
SseEmitter emitter = new SseEmitter(timeout);
// 注册回调:连接完成、超时、异常时移除连接
emitter.onCompletion(() -> emitterMap.remove(userId));
emitter.onTimeout(() -> emitterMap.remove(userId));
emitter.onError(e -> emitterMap.remove(userId));
// 存入连接池
emitterMap.put(userId, emitter);
System.out.println("用户 " + userId + " 建立 SSE 连接,当前在线: " + emitterMap.size());
return emitter;
}
/**
* 向指定用户推送消息
*/
public void sendToUser(String userId, String eventName, Object data) {
SseEmitter emitter = emitterMap.get(userId);
if (emitter == null) {
System.out.println("用户 " + userId + " 不在线,消息丢弃");
return;
}
try {
emitter.send(SseEmitter.event()
.name(eventName) // 事件类型
.data(data)); // 消息内容
} catch (IOException e) {
System.out.println("推送给 " + userId + " 失败: " + e.getMessage());
emitterMap.remove(userId);
}
}
/**
* 广播给所有在线用户
*/
public void broadcast(String eventName, Object data) {
emitterMap.forEach((userId, emitter) -> {
try {
emitter.send(SseEmitter.event()
.name(eventName)
.data(data));
} catch (IOException e) {
System.out.println("广播给 " + userId + " 失败: " + e.getMessage());
emitterMap.remove(userId);
}
});
}
/**
* 关闭指定用户的连接
*/
public void closeEmitter(String userId) {
SseEmitter emitter = emitterMap.get(userId);
if (emitter != null) {
emitter.complete(); // 正常关闭
emitterMap.remove(userId);
}
}
/**
* 获取当前在线人数
*/
public int getOnlineCount() {
return emitterMap.size();
}
}
4.5 SSE Controller(对外接口)
java
package com.example.sse.controller;
import com.example.sse.service.SseEmitterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController
@RequestMapping("/api/sse")
public class SseController {
@Autowired
private SseEmitterService sseEmitterService;
/**
* 建立 SSE 连接
* GET /api/sse/connect?userId=user1
*
* produces = MediaType.TEXT_EVENT_STREAM_VALUE 是关键!
* 告诉浏览器这是一个 SSE 流
*/
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@RequestParam String userId) {
// 0 表示永不超时
return sseEmitterService.createEmitter(userId, 0L);
}
/**
* 向指定用户推送消息
* POST /api/sse/send?userId=user1&message=你好
*/
@PostMapping("/send")
public String sendToUser(@RequestParam String userId,
@RequestParam String message) {
sseEmitterService.sendToUser(userId, "message", message);
return "消息已推送给 " + userId;
}
/**
* 广播消息给所有在线用户
* POST /api/sse/broadcast?message=全体通知
*/
@PostMapping("/broadcast")
public String broadcast(@RequestParam String message) {
sseEmitterService.broadcast("notification", message);
return "已广播给 " + sseEmitterService.getOnlineCount() + " 个用户";
}
/**
* 关闭指定用户的连接
* POST /api/sse/close?userId=user1
*/
@PostMapping("/close")
public String close(@RequestParam String userId) {
sseEmitterService.closeEmitter(userId);
return "已关闭 " + userId + " 的 SSE 连接";
}
/**
* 查看当前在线人数
* GET /api/sse/count
*/
@GetMapping("/count")
public int getOnlineCount() {
return sseEmitterService.getOnlineCount();
}
}
4.6 前端页面(static/index.html)
java
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SSE 快速入门演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Microsoft YaHei', sans-serif; padding: 20px; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.card { background: #fff; border-radius: 8px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.card h3 { color: #666; margin-bottom: 12px; font-size: 14px; }
.status { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 13px; }
.status.connected { background: #d4edda; color: #155724; }
.status.disconnected { background: #f8d7da; color: #721c24; }
input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; width: 300px; }
button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; margin-left: 8px; font-size: 14px; }
.btn-primary { background: #007bff; color: #fff; }
.btn-danger { background: #dc3545; color: #fff; }
.btn-success { background: #28a745; color: #fff; }
button:hover { opacity: 0.85; }
#messages { max-height: 400px; overflow-y: auto; }
.msg-item { padding: 8px 12px; border-bottom: 1px solid #eee; font-size: 14px; }
.msg-item .time { color: #999; margin-right: 8px; }
.msg-item .type { color: #007bff; margin-right: 8px; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>🔔 SSE 实时消息推送演示</h1>
<!-- 连接控制 -->
<div class="card">
<h3>连接控制</h3>
<div>
<input type="text" id="userId" placeholder="输入用户ID(如 user1)" value="user1">
<button class="btn-primary" onclick="connectSSE()">建立连接</button>
<button class="btn-danger" onclick="closeSSE()">断开连接</button>
<span id="status" class="status disconnected">未连接</span>
</div>
</div>
<!-- 发送消息 -->
<div class="card">
<h3>推送消息</h3>
<div style="margin-bottom: 8px;">
<input type="text" id="targetUser" placeholder="目标用户ID" value="user1">
<input type="text" id="message" placeholder="消息内容" value="你好,这是测试消息">
<button class="btn-success" onclick="sendToUser()">推送</button>
</div>
<div>
<button class="btn-primary" onclick="broadcast()">📢 广播给所有人</button>
</div>
</div>
<!-- 消息列表 -->
<div class="card">
<h3>消息记录</h3>
<div id="messages">
<div class="msg-item" style="color:#999;">等待连接...</div>
</div>
</div>
</div>
<script>
let eventSource = null;
// 建立 SSE 连接
function connectSSE() {
const userId = document.getElementById('userId').value;
if (!userId) { alert('请输入用户ID'); return; }
if (eventSource) { eventSource.close(); }
// 👇 关键:创建 EventSource 对象,指向后端 SSE 接口
eventSource = new EventSource('/api/sse/connect?userId=' + userId);
// 连接建立
eventSource.onopen = function() {
updateStatus(true);
addMessage('系统', 'SSE 连接已建立');
};
// 监听默认 message 事件
eventSource.onmessage = function(event) {
addMessage('默认', event.data);
};
// 监听自定义 "message" 事件(对应后端的 .name("message"))
eventSource.addEventListener('message', function(event) {
addMessage('消息', event.data);
});
// 监听自定义 "notification" 事件(对应后端的 .name("notification"))
eventSource.addEventListener('notification', function(event) {
addMessage('📢 通知', event.data);
});
// 错误处理(浏览器会自动重连)
eventSource.onerror = function() {
updateStatus(false);
addMessage('系统', '连接断开,正在自动重连...');
};
}
// 断开连接
function closeSSE() {
if (eventSource) {
eventSource.close();
eventSource = null;
updateStatus(false);
addMessage('系统', '已主动断开连接');
}
}
// 推送给指定用户
function sendToUser() {
const targetUser = document.getElementById('targetUser').value;
const message = document.getElementById('message').value;
fetch('/api/sse/send?userId=' + targetUser + '&message=' + encodeURIComponent(message), {
method: 'POST'
}).then(r => r.text()).then(t => addMessage('系统', t));
}
// 广播
function broadcast() {
const message = document.getElementById('message').value;
fetch('/api/sse/broadcast?message=' + encodeURIComponent(message), {
method: 'POST'
}).then(r => r.text()).then(t => addMessage('系统', t));
}
// 更新连接状态
function updateStatus(connected) {
const el = document.getElementById('status');
el.textContent = connected ? '已连接' : '未连接';
el.className = 'status ' + (connected ? 'connected' : 'disconnected');
}
// 添加消息到列表
function addMessage(type, content) {
const container = document.getElementById('messages');
const time = new Date().toLocaleTimeString();
container.innerHTML = '<div class="msg-item"><span class="time">' + time +
'</span><span class="type">[' + type + ']</span>' + content + '</div>' +
container.innerHTML;
}
</script>
</body>
</html>
4.7 application.yml
XML
server:
port: 8080
spring:
application:
name: sse-demo
五、部署与验证
5.1 启动项目
XML
cd sse-demo
mvn spring-boot:run
5.2 打开浏览器
访问 http://localhost:8080/index.html
操作步骤:
-
在页面输入
user1,点击「建立连接」→ 状态变为「已连接」 -
打开第二个浏览器标签页 ,输入
user2,也建立连接 -
在第一个标签页输入消息,点击「推送」→ user1 收到消息
-
点击「广播」→ 两个标签页都收到消息
5.3 用 curl 验证(理解底层协议)
XML
# 建立 SSE 连接
curl -N http://localhost:8080/api/sse/connect?userId=test
# 另开一个终端,推消息
curl -X POST "http://localhost:8080/api/sse/send?userId=test&message=hello"
第一个终端会看到:
event:message data:hello
这就是 SSE 的原始协议格式------event: + data: + 两个换行分隔。
六、进阶用法
6.1 定时推送(模拟实时数据)
在 SseEmitterService 中加一个定时推送方法:
java
/**
* 模拟定时推送(比如每 3 秒推送一次系统状态)
* 配合 @Scheduled 使用
*/
@Scheduled(fixedRate = 3000)
public void pushSystemStatus() {
String status = "在线用户: " + emitterMap.size() + ", 时间: " + LocalDateTime.now();
broadcast("system-status", status);
}
启动类加 @EnableScheduling:
java
@SpringBootApplication
@EnableScheduling // 启用定时任务
public class SseDemoApplication { ... }
6.2 推送 JSON 对象
java
// 推送复杂对象
Map<String, Object> data = new HashMap<>();
data.put("orderId", "ORD-20260616-001");
data.put("status", "已发货");
data.put("expressNo", "SF1234567890");
sseEmitterService.sendToUser("user1", "order-update", data);
// 自动序列化为 JSON:{"orderId":"ORD-20260616-001","status":"已发货",...}
前端接收:
java
eventSource.addEventListener('order-update', function(event) {
const order = JSON.parse(event.data); // 解析 JSON
console.log('订单更新:', order.orderId, order.status);
});
6.3 带重试间隔的推送
java
emitter.send(SseEmitter.event()
.name("message")
.id("msg-1001") // 消息 ID(浏览器断线重连时会带上 Last-Event-ID)
.reconnectTime(5000) // 断线后 5 秒重连
.data("Hello"));
6.4 前端拿到断线重连的 Last-Event-ID
浏览器自动重连时会在请求头里带上 Last-Event-ID,后端可以据此补发漏掉的消息:
java
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@RequestParam String userId,
@RequestHeader(value = "Last-Event-ID", required = false) String lastId) {
System.out.println("用户 " + userId + " 重连,上次收到的消息ID: " + lastId);
// 可以根据 lastId 补发消息
return sseEmitterService.createEmitter(userId, 0L);
}
七、避坑清单
| # | 坑点 | 现象 | 原因 | ✅ 避坑方案 |
|---|---|---|---|---|
| 1 | 连接数打满 | 浏览器同域名最多 6 个 SSE 连接 | HTTP/1.1 浏览器限制同域并发连接数为 6 | 多用户场景用 WebSocket,或用 HTTP/2(无此限制) |
| 2 | Nginx 代理超时 | 推送几分钟后连接断开 | Nginx 默认 proxy_read_timeout 60s |
配置 proxy_read_timeout 3600s; 或按需调整 |
| 3 | Spring 返回 406 | 建立连接报 406 Not Acceptable | 忘了加 produces = MediaType.TEXT_EVENT_STREAM_VALUE |
Controller 方法加 produces 属性 |
| 4 | 中文乱码 | 消息中文变成 ??? |
没指定编码 | data 方法加 charset:.data("中文", MediaType.APPLICATION_JSON) |
| 5 | 连接不释放 | 用户关闭页面后连接还在 | 服务端不知道客户端走了 | 配合心跳检测,或监听 emitter.onCompletion |
| 6 | 生产环境连接数爆满 | 大量用户导致服务器线程耗尽 | SSE 是长连接,每个连接占一个线程 | 用 WebFlux(响应式)替代 Servlet 模式 |
| 7 | 浏览器自动重连导致雪崩 | 服务器挂了,所有客户端同时重连 | 没有设置 retry 间隔 |
服务端设置合理的 reconnectTime,客户端加随机延迟 |
Nginx 代理 SSE 的正确配置
java
location /api/sse/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection ''; # 清除 Connection 头
proxy_buffering off; # 关闭缓冲!否则消息会攒一批才发
proxy_cache off; # 关闭缓存
proxy_read_timeout 3600s; # 超时时间拉长
chunked_transfer_encoding off; # 关闭分块传输
}
八、总结
核心口诀
SSE 是服务器单向推,一条 HTTP 连接永不关。浏览器内置自动重连,Spring 一个注解就能用。
一句话速记
SSE = Content-Type: text/event-stream
+ EventSource API
+ 自动重连
= 最轻量的服务器推送方案
技术选型决策树
需要服务器实时推送数据?
├─ 只需要服务器 → 客户端推送?
│ ├─ 是 → SSE ✅(通知、进度条、日志流、实时数据)
│ └─ 需要双向通信?
│ ├─ 是 → WebSocket(聊天、游戏、协同编辑)
│ └─ 否 → SSE
└─ 数据更新频率?
├─ 分钟级 → 普通轮询就够了
├─ 秒级 → SSE ✅
└─ 毫秒级 → WebSocket
自检清单
- 我能说清楚 SSE 和 WebSocket 的区别
- 我知道 SSE 底层就是
Content-Type: text/event-stream - 我能用 Spring Boot 的
SseEmitter写一个推送接口 - 我知道浏览器同域最多 6 个 SSE 连接(HTTP/1.1)
- 我知道 Nginx 代理 SSE 必须关
proxy_buffering - 我知道生产环境推荐用 WebFlux 避免线程耗尽
下一步学习路线
阶段一(入门):本文 → 跑通 Spring Boot SSE demo
阶段二(实战):在 RuoYi 项目中用 SSE 实现订单状态推送 / 导出进度通知
阶段三(进阶):学习 WebFlux 响应式 SSE(高并发场景)
阶段四(扩展):了解 SSE + 消息队列(Redis Stream / RabbitMQ)的架构