使用 SseEmitter 实现 Spring Boot 后端的流式传输和前端的数据接收

1.普通文本消息的发送和接收

复制代码
@GetMapping("/stream")
    public SseEmitter streamResponse() {
        SseEmitter emitter = new SseEmitter(0L); // 0L 表示永不超时
        Executors.newSingleThreadExecutor().execute(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    emitter.send("消息 " + i);
                    Thread.sleep(1000); // 模拟延迟
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });
        return emitter;
    }

async function fetchStreamData() {
    const response = await fetch("/api/chat/stream");
    // 确保服务器支持流式数据
    if (!response.ok) {
        throw new Error(`HTTP 错误!状态码: ${response.status}`);
    }
    const reader = response.body.getReader();
    const decoder = new TextDecoder("utf-8");
    // 读取流式数据
    while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        // 解码并输出数据
        const text = decoder.decode(value, { stream: true });
        console.log("收到数据:", text);
    }
    console.log("流式传输完成");
}
// 调用流式请求
fetchStreamData().catch(console.error);

2.使用流式消息发送多个文件流,实现多个文件的传输

复制代码
//这里相当于每个drawCatalogue对象会创建一个文件流,然后发送过去,list中有几个对象就会发送几个文件
//之所以要每个属性都手动write一下,是因为我的每个ajaxResult数据量都特别大,容易内存溢出。如果没有我这种特殊情况的话,直接使用JSONObject.toJSONString(drawCatalogue)就可以,不需要去手动写入每个属性。
public SseEmitter getAllDrawDataThree(String cadCode) {
        SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE); // 设置超时时间为最大值,防止自动结束
        try {
            Long code = Long.parseLong(cadCode);
            DrawExcelList drawExcelList = new DrawExcelList();
            drawExcelList.setCadCode(code);
            List<DrawCatalogue> drawCatalogueList = drawExcelListService.treeTableCatalogue(drawExcelList);

            int splitSize = 20;
            List<DrawCatalogue> newDrawCatalogueList = splitDrawCatalogueList(drawCatalogueList, splitSize);

            for (int i = 0; i < newDrawCatalogueList.size(); i++) {
                String filePath = "drawCatalogue" + i + ".json"; // 文件路径

                DrawCatalogue drawCatalogue = newDrawCatalogueList.get(i);

                try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
                    writer.write("["); // 开始写入最外层JSON数组

                    writer.write("{");

                    writer.write("\"id\": \"" + drawCatalogue.getId() + "\",");
                    writer.write("\"drawName\": \"" + drawCatalogue.getDrawName() + "\",");
                    writer.write("\"drawType\": \"" + drawCatalogue.getDrawType() + "\",");
                    writer.write("\"combineOutType\": \"" + drawCatalogue.getCombineOutType() + "\",");
                    writer.write("\"num\": \"" + drawCatalogue.getNum() + "\",");

                    writer.write("\"children\": ");

                    writer.write("["); // 开始写入childrenJSON数组

                    boolean first = true; // 用于判断是否是第一个元素

                    List<DrawCatalogue> children = drawCatalogue.getChildren();
                    for (DrawCatalogue child : children) {
                        DrawingMain drawingMain = new DrawingMain();
                        drawingMain.setCadCode(code);
                        drawingMain.setDrawName(child.getCombineOutType());
                        drawingMain.setDrawType(child.getDrawType());
                        AjaxResult ajaxResult = drawingMainService.imgListShow(drawingMain);

                        if (!first) {
                            writer.write(","); // 如果不是第一个元素,写入逗号分隔
                        }

                        String tabletJson = JSONObject.toJSONString(ajaxResult);

                        // 逐个属性写入文件流
                        writer.write("{");
                        writer.write("\"id\": \"" + child.getId() + "\",");
                        writer.write("\"drawName\": \"" + child.getDrawName() + "\",");
                        writer.write("\"combineOutType\": \"" + child.getCombineOutType() + "\",");
                        writer.write("\"drawType\": \"" + child.getDrawType() + "\",");
                        writer.write("\"tabletJson\": " + tabletJson);

                        writer.write("}");

                        first = false; // 标记已经写入了一个元素
                    }

                    writer.write("]"); // 结束children数组

                    writer.write("}");

                    writer.write("]"); // 结束最外层JSON数组

                } catch (IOException e) {
                    sseEmitter.completeWithError(e);
                }

                // 读取并发送文件流
                //byte[] fileData = Files.readAllBytes(Paths.get(filePath));

                // 分块读取文件并发送(防止一次性读取的文件过大导致内存溢出)
                Path path = Paths.get(filePath);
                ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                byte[] buffer = new byte[8192]; // 8KB buffer
                try (InputStream in = Files.newInputStream(path)) {
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, bytesRead);
                    }
                }
                
                byte[] fileData = outputStream.toByteArray();

                sseEmitter.send(fileData, MediaType.APPLICATION_OCTET_STREAM);
            }

            sseEmitter.complete();
        } catch (Exception e) {
            sseEmitter.completeWithError(e);
        } finally {
            sseEmitter.complete();
        }
        return sseEmitter;
    }

前端代码,在方法中调用,后端返回几个文件就会弹出几个下载窗口

复制代码
				const eventSource = new EventSource('http://127.0.0.1:1801/tablet/getAllDrawDataThree');

                eventSource.onmessage = function(event) {
                    try {
                        const fileData = event.data;
                        const blob = new Blob([fileData], { type: 'application/octet-stream' });
                        const url = window.URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.style.display = 'none';
                        a.href = url;
                        a.download = 'file.json'; // 设置下载文件名
                        document.body.appendChild(a);
                        a.click();
                        window.URL.revokeObjectURL(url);
                        document.body.removeChild(a);
                    } catch (error) {
                        console.error('Error processing event data:', error);
                    }
                };

                eventSource.onerror = function(event) {
                    console.error('SSE error:', event);
                };
相关推荐
站在风口的猪110819 分钟前
《前端面试题:CSS预处理器(Sass、Less等)》
前端·css·html·less·css3·sass·html5
程序员的世界你不懂44 分钟前
(9)-Fiddler抓包-Fiddler如何设置捕获Https会话
前端·https·fiddler
MoFe11 小时前
【.net core】天地图坐标转换为高德地图坐标(WGS84 坐标转 GCJ02 坐标)
java·前端·.netcore
去旅行、在路上1 小时前
chrome使用手机调试触屏web
前端·chrome
Aphasia3112 小时前
模式验证库——zod
前端·react.js
Mr Aokey2 小时前
Spring MVC参数绑定终极手册:单&多参/对象/集合/JSON/文件上传精讲
java·后端·spring
lexiangqicheng2 小时前
es6+和css3新增的特性有哪些
前端·es6·css3
14L3 小时前
互联网大厂Java面试:从Spring Cloud到Kafka的技术考察
spring boot·redis·spring cloud·kafka·jwt·oauth2·java面试
地藏Kelvin3 小时前
Spring Ai 从Demo到搭建套壳项目(二)实现deepseek+MCP client让高德生成昆明游玩4天攻略
人工智能·spring boot·后端
一个有女朋友的程序员3 小时前
Spring Boot 缓存注解详解:@Cacheable、@CachePut、@CacheEvict(超详细实战版)
spring boot·redis·缓存