sse实现进度条功能
理论上, SSE 和 WebSocket 做的是同一件事情。当你需要用新数据局部更新网络应用时,SSE 可以做到不需要用户执行任何操作,便可以完成。
举例我们要做一个统计系统的管理后台,我们想知道统计数据的实时情况。类似这种更新频繁、 低延迟的场景,SSE 可以完全满足。
其他一些应用场景:例如邮箱服务的新邮件提醒,微博的新消息推送、管理后台的一些操作实时同步等,SSE 都是不错的选择。
后端代码
执行逻辑
1.前端代码调用/connect方法,与后端建立连接,后端代码创建SseEmitter对象保存在map中并交给spring管理,通知前端保持连接
2.前端代码调用/start方法,开启进度条获取代码,获取到通过第一步建立的SseEmitter对象通道向前端发送消息
java
package com.cloud.app.system.controller;
import com.cloud.app.system.domain.vo.ProcessVo;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
@RequestMapping("/sse")
public class SseController2 {
// 【解析 1】存储活跃的 SSE 连接
// Key: taskId (字符串), Value: SseEmitter (发射器)
// 使用 ConcurrentHashMap 保证多线程环境下的线程安全
private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
// 【解析 2】创建一个线程池
// 用于在后台异步执行耗时任务,避免阻塞 Tomcat 的主线程
private final ExecutorService executor = Executors.newFixedThreadPool(10);
/**
* 【解析 3】建立 SSE 连接的接口
* 前端访问:GET /sse/connect?taskId=123
* produces = TEXT_EVENT_STREAM_VALUE: 告诉浏览器这是一个流式响应,不要关闭连接
*/
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@RequestParam String taskId) {
// 创建发射器,超时时间设为 0 (表示永不超时,直到手动关闭)
SseEmitter emitter = new SseEmitter(0L);
// 将 emitter 存入 Map,方便后续通过 taskId 找到它并发送消息
emitterMap.put(taskId, emitter);
// 【解析 4】注册回调函数 (非常重要,防止内存泄漏)
// 当连接正常完成时(前端主动关闭或后端发送完成信号)
emitter.onCompletion(() -> {
System.out.println("连接完成: " + taskId);
emitterMap.remove(taskId); // 从 Map 中移除,释放内存
});
// 当连接超时时
emitter.onTimeout(() -> {
System.out.println("连接超时: " + taskId);
emitterMap.remove(taskId);
emitter.complete(); // 确保彻底关闭
});
// 当发生错误时(如网络断开)
emitter.onError((e) -> {
System.out.println("连接错误: " + taskId + ", " + e.getMessage());
emitterMap.remove(taskId);
});
// 【解析 5】发送第一条欢迎消息
try {
// event().name("init") 设置事件类型为 "init",前端可以针对性监听
emitter.send("连接成功!任务 ID: " + taskId);
} catch (IOException e) {
emitter.completeWithError(e);
}
return emitter; // 返回给 Spring,Spring 会保持这个 HTTP 连接打开
}
/**
* 【解析 6】触发任务的接口
* 前端访问:POST /sse/start?taskId=123
* 这个接口立即返回,实际工作在后台线程运行
*/
@PostMapping("/start")
public String startTask(@RequestParam String taskId) {
// 提交任务到线程池异步执行
executor.submit(() -> runLongTask(taskId));
return "任务已启动,请查看 SSE 进度";
}
/**
* 【解析 7】模拟耗时任务逻辑
* 这里演示如何循环发送进度
*/
private void runLongTask(String taskId) {
System.out.println("任务开始: " + taskId);
while (true){
try {
Thread.sleep(1000);
// 【解析 8】获取对应的 emitter 并发送消息
SseEmitter emitter = emitterMap.get(taskId);
if (emitter != null) {
// 构造消息内容
ProcessVo processVo = new ProcessVo();
processVo.setProcess("100");
processVo.setStatus("done");
emitter.send(processVo);
if("done".equals(processVo.getStatus())){
emitter.send("任务完成!");
emitter.complete(); // 主动关闭连接,触发 onCompletion 回调
break;
}
} else {
// 如果 emitter 不存在(可能用户刷新了页面),则停止任务
System.out.println("客户端已断开,任务终止: " + taskId);
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (IOException e) {
// 发送失败(通常意味着客户端断开),停止任务
System.out.println("发送失败,任务终止: " + taskId);
break;
}
}
}
}
前端代码
这段代码放置于src/main/resource/static 目录中,启动项目即可访问到该页面进行测试
java
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>SSE 简单案例</title>
<style>
#log { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: scroll; font-family: monospace; }
.msg { margin: 5px 0; }
.init { color: blue; }
.progress { color: orange; font-weight: bold; }
.success { color: green; font-weight: bold; }
.error { color: red; }
</style>
</head>
<body>
<h2>SSE 进度演示</h2>
<button onclick="startTask()">1. 启动任务</button>
<div id="log"></div>
<script>
let eventSource = null;
function log(message, type) {
const div = document.createElement('div');
div.className = 'msg ' + type;
div.innerText = new Date().toLocaleTimeString() + ' - ' + message;
document.getElementById('log').appendChild(div);
document.getElementById('log').scrollTop = document.getElementById('log').scrollHeight;
}
function startTask() {
const taskId = 'task_' + Date.now(); // 生成唯一 ID
log('正在连接 SSE...', 'init');
// 【解析 A】建立 SSE 连接
// 指向后端的 /sse/connect 接口,带上 taskId 参数
eventSource = new EventSource(`/sse/connect?taskId=${taskId}`);
// 【解析 B】监听不同类型的消息
// 1. 监听初始化消息 (对应后端 name="init")
eventSource.addEventListener('init', (event) => {
log('服务器说: ' + event.data, 'init');
});
// 2. 监听进度消息 (对应后端 name="progress")
eventSource.addEventListener('progress', (event) => {
log('进度更新: ' + event.data, 'progress');
});
// 3. 监听成功消息 (对应后端 name="success")
eventSource.addEventListener('success', (event) => {
log('最终结果: ' + event.data, 'success');
eventSource.close(); // 收到成功后,主动关闭连接
});
// 4. 监听通用错误
eventSource.onerror = (err) => {
log('SSE 连接错误', 'error');
eventSource.close();
};
// 【解析 C】调用后端启动任务接口
// 注意:这里只是触发后端开始跑循环,真正的进度是通过上面的 SSE 连接推过来的
fetch(`/sse/start?taskId=${taskId}`, { method: 'POST' })
.then(res => res.text())
.then(data => log('后端响应: ' + data, 'init'));
}
</script>
</body>
</html>