使用 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);
                };
相关推荐
没有故事、有酒2 分钟前
Ajax介绍
前端·ajax·okhttp
朝新_6 分钟前
【SpringMVC】详解用户登录前后端交互流程:AJAX 异步通信与 Session 机制实战
前端·笔记·spring·ajax·交互·javaee
裴嘉靖8 分钟前
Vue 生成 PDF 完整教程
前端·vue.js·pdf
毕设小屋vx ylw28242610 分钟前
Java开发、Java Web应用、前端技术及Vue项目
java·前端·vue.js
间彧30 分钟前
Kubernetes的Pod与Docker Compose中的服务在概念上有何异同?
后端
间彧34 分钟前
从开发到生产,如何将Docker Compose项目平滑迁移到Kubernetes?
后端
间彧40 分钟前
如何结合CI/CD流水线自动选择正确的Docker Compose配置?
后端
间彧41 分钟前
在多环境(开发、测试、生产)下,如何管理不同的Docker Compose配置?
后端
间彧42 分钟前
如何为Docker Compose中的服务配置健康检查,确保服务真正可用?
后端
间彧1 小时前
Docker Compose和Kubernetes在编排服务时有哪些核心区别?
后端