# sse实现进度条功能

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>
相关推荐
程序媛徐师姐2 小时前
Java基于微信小程序的线上教育商城,附源码+文档说明
java·微信小程序·线上教育商城小程序·java线上教育商城小程序·线上教育商城微信小程序·线上教育小程序·线上教育微信小程序
有梦想的小何2 小时前
从结算需求出发:基于库存日快照与分区的结算报表的Java实践
java·数据库·mysql
韩立学长2 小时前
基于Springboot的商品库存管理系统369jr3t9(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
java·数据库·spring boot·后端
长安11082 小时前
mysql(C++)----常用的sql命令
java·sql·mysql
醇氧2 小时前
Spring AI Alibaba 学习(一) 集成阿里云百炼大模型应用
java·学习·spring
I_LPL2 小时前
day52 代码随想录算法训练营 图论专题5
java·算法·图论·并查集
y = xⁿ2 小时前
【Java八股锁机制的认识】synchronized和reentrantlock区分,锁升级机制
java·开发语言·后端
Barkamin2 小时前
(有头)链表的实现(Java)
java·数据结构·链表