目录

使用Spring Boot、VUE实现SSE长连接:跟踪文件上传和任务进度

使用Spring Boot实现SSE长连接:跟踪文件上传和任务进度

文章目录

  • [使用Spring Boot实现SSE长连接:跟踪文件上传和任务进度](#使用Spring Boot实现SSE长连接:跟踪文件上传和任务进度)
    • 什么是SSE?
    • 使用场景
    • 前端库选择
      • 安装`event-source-polyfill`
        • [1. 创建SSE连接](#1. 创建SSE连接)
        • [2. 关闭SSE连接](#2. 关闭SSE连接)
        • [3. 结合Vue.js使用](#3. 结合Vue.js使用)
    • [使用Spring Boot实现SSE](#使用Spring Boot实现SSE)
      • [1. 创建SSE工具类](#1. 创建SSE工具类)
      • [2. 实现文件上传进度通知](#2. 实现文件上传进度通知)
      • [3. 实现任务执行进度跟踪](#3. 实现任务执行进度跟踪)

在现代Web应用中,服务器实时向客户端推送数据是一项非常常见的需求。在多种实现技术中,Server-Sent Events(SSE)是一个轻量级的解决方案,适用于对实时性要求不高、数据量不大的场景。本文将介绍如何在Spring Boot中使用SSE,结合实际案例展示在文件上传和任务执行中的应用。

什么是SSE?

Server-Sent Events(SSE)是HTML5标准的一部分,允许服务器单向推送消息到客户端。它与WebSocket不同,SSE只支持服务器向客户端推送数据,而不支持客户端向服务器发送数据。SSE的优点在于其实现简单、兼容性好,非常适合不需要双向通讯的场景。

使用场景

  • 文件上传进度通知:当用户上传文件时,服务器可以通过SSE实时告知客户端上传进度。
  • 任务执行进度跟踪:对于耗时的任务(如数据处理、批量导入等),可以通过SSE向客户端实时推送任务进度。

前端库选择

由于原生的EventSource在某些浏览器中可能不支持自定义请求头,因此选择event-source-polyfill库来建立SSE连接。这一库允许在初始化时设置请求头,如身份验证所需的token等。

安装event-source-polyfill

首先安装event-source-polyfill库:

bash 复制代码
npm install event-source-polyfill

前端实现步骤

1. 创建SSE连接

通过封装一个createSseConnection函数,建立与服务端的SSE连接,并定义如何处理不同消息事件:

javascript 复制代码
import { EventSourcePolyfill } from "event-source-polyfill";
import config from "../../../../config";

export function createSseConnection(context, topic, callbacks, showMessage, onError) {
  const url = config.sse_host[process.env.NODE_ENV] + "/techik/sse/subscribe?topic=" + topic;
  const headers = { "token": localStorage.getItem("token") };
  
  const source = new EventSourcePolyfill(url, {
    headers,
    heartbeatTimeout: 30 * 60 * 1000,
  });

  source.onopen = () => {
    console.log("SSE connection established.");
  };

  source.onmessage = (e) => {
    const message = e.data;
    if (callbacks.onMessage) {
      callbacks.onMessage(message, context);
    }
    if (showMessage && message.includes('success')) {
      context.$message({
        type: "success",
        duration: 3000,
        message: "提交成功!",
      });
    }
  };

  source.onerror = (e) => {
    console.error("SSE error:", e);
    if (callbacks.onError) {
      callbacks.onError(e, context);
    }
    if (e.readyState === EventSource.CLOSED) {
      console.log("SSE connection closed.");
      if (callbacks.onClose) {
        callbacks.onClose(context);
      }
    } else if (onError) {
      onError(e);
    }
  };

  return source;
}
2. 关闭SSE连接

提供一个closeSseConnection函数,当不再需要接收消息时,手动关闭SSE连接:

javascript 复制代码
export function closeSseConnection(source, context, afterClose) {
  if (source) {
    source.close();
    console.log("SSE connection closed.");
    if (afterClose) {
      afterClose(context);
    }
  }
}
3. 结合Vue.js使用

在Vue组件中使用createSseConnection和closeSseConnection管理SSE连接:

javascript 复制代码
export default {
  data() {
    return {
      sseSource: null,
    };
  },
  methods: {
    startListening() {
      const topic = "uploadProgress"; // 根据需求选择不同的topic
      this.sseSource = createSseConnection(this, topic, {
        onMessage: this.handleMessage,
        onError: this.handleError,
        onClose: this.handleClose,
      }, true);
    },
    handleMessage(message, context) {
      console.log("Received message:", message);
      // 处理接收到的消息
    },
    handleError(error, context) {
      console.error("Error received:", error);
    },
    handleClose(context) {
      console.log("Connection closed.");
    },
    stopListening() {
      closeSseConnection(this.sseSource, this, () => {
        this.sseSource = null;
      });
    }
  },
  beforeDestroy() {
    this.stopListening();
  }
};

使用Spring Boot实现SSE

1. 创建SSE工具类

首先,创建一个工具类SseUtils来管理SSE连接:

java 复制代码
package com.techik.Util;

import lombok.extern.slf4j.Slf4j;
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.atomic.AtomicInteger;
import java.util.function.Consumer;

@Slf4j
public class SseUtils {

    // 原子计数器,用于跟踪活跃的连接数
    private static final AtomicInteger COUNT = new AtomicInteger(0);

    // 存储主题和对应的SseEmitter映射关系,确保线程安全
    private static final Map<String, SseEmitter> SSE_EMITTER_MAP = new ConcurrentHashMap<>();

    /**
     * 建立新的SSE连接,并设置相关的回调函数
     * 
     * @param topic 要连接的主题
     * @return 新创建的SseEmitter对象
     */
    public static SseEmitter connect(String topic) {
        // 设置超时时间为30分钟
        SseEmitter sseemitter = new SseEmitter(30 * 60 * 1000L);

        // 设置连接完成后的回调
        sseemitter.onCompletion(completionCallBack(topic));

        // 设置连接出错时的回调
        sseemitter.onError(errorCallBack(topic));

        // 设置连接超时时的回调
        sseemitter.onTimeout(timeoutCallBack(topic));

        // 将新的SseEmitter存储到Map中
        SSE_EMITTER_MAP.put(topic, sseemitter);

        // 增加活跃连接数
        COUNT.incrementAndGet();

        log.info("创建新的sse连接,当前的主题:{}", topic);

        return sseemitter;
    }

    /**
     * 发送消息到指定的主题
     * 
     * @param topic   目标主题
     * @param message 要发送的消息内容
     */
    public static void sendMessage(String topic, String message) {
        if (SSE_EMITTER_MAP.containsKey(topic)) {
            try {
                // 发送消息
                SSE_EMITTER_MAP.get(topic).send(message);
            } catch (IOException e) {
                log.error("当前的主题:{},发送消息-错误:{}", topic, e.getMessage());
            }
        }
    }

    /**
     * 移除指定主题的连接
     * 
     * @param topic 要移除的主题
     */
    public static void removeTopic(String topic) {
        // 从Map中移除SseEmitter
        SSE_EMITTER_MAP.remove(topic);

        // 减少活跃连接数
        COUNT.decrementAndGet();

        log.info("删除主题:{}", topic);
    }

    // 创建连接完成的回调函数
    private static Runnable completionCallBack(String topic) {
        return () -> {
            log.info("结束连接,{}", topic);
            removeTopic(topic);
        };
    }

    // 创建连接超时的回调函数
    private static Runnable timeoutCallBack(String topic) {
        return () -> {
            log.info("连接超时,{}", topic);
            removeTopic(topic);
        };
    }

    // 创建连接出错的回调函数
    private static Consumer<Throwable> errorCallBack(String topic) {
        return throwable -> {
            log.error("连接异常,{}", topic);
            removeTopic(topic);
        };
    }
}

2. 实现文件上传进度通知

在上传文件的过程中,可以使用SseEmitter向客户端实时推送上传进度:

java 复制代码
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(MultipartFile file) {
    String topic = "uploadProgress";
    SseEmitter emitter = SseUtils.connect(topic);
    // 模拟上传文件并推送进度
    for (int i = 0; i <= 100; i += 10) {
        SseUtils.sendMessage(topic, "上传进度: " + i + "%");
        Thread.sleep(500); // 模拟耗时操作
    }
    SseUtils.sendMessage(topic, "上传完成!");
    return ResponseEntity.ok("文件上传成功");
}

3. 实现任务执行进度跟踪

类似文件上传,当需要执行耗时任务时,可以使用SSE推送任务进度:

java 复制代码
@GetMapping("/executeTask")
public ResponseEntity<String> executeTask() {
    String topic = "taskProgress";
    SseEmitter emitter = SseUtils.connect(topic);
    // 模拟任务执行并推送进度
    for (int i = 0; i <= 100; i += 20) {
        SseUtils.sendMessage(topic, "任务进度: " + i + "%");
        Thread.sleep(1000); // 模拟耗时操作
    }
    SseUtils.sendMessage(topic, "任务完成!");
    return ResponseEntity.ok("任务执行成功");
}
本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
山有木兮丶丶13 分钟前
spring boot大文件与多文件下载
spring boot·后端
余瑾瑜31 分钟前
如何在CentOS部署青龙面板并实现无公网IP远程访问本地面板
开发语言·后端·golang
爱的叹息33 分钟前
Spring Boot 测试详解,包含maven引入依赖、测试业务层类、REST风格测试和Mock测试
spring boot·后端·maven
小白的一叶扁舟42 分钟前
Java设计模式全解析(共 23 种)
java·开发语言·设计模式·springboot
啊是特嗷桃1 小时前
复刻系列-星穹铁道 3.2 版本先行展示页
游戏·vue·米哈游·星穹铁道
kkkkatoq1 小时前
设计模式 四、行为设计模式(2)
java·开发语言·设计模式
peiwang2451 小时前
网页制作中的MVC和MVT
后端·mvc
酱酱们的每日掘金1 小时前
一键连接 6000 + 应用dify MCP 插件指南、谷歌 AI 编程产品一网打尽、MCP玩出花了丨AI Coding 周刊第 4 期
前端·后端·ai编程·mcp
隔壁小王攻城狮1 小时前
完整源码停车场管理系统,含新能源充电系统,实现了停车+充电一体化
java·开源·iot·停车场系统·新能源汽车充电·停车场管理系统源码
橘子青衫2 小时前
多线程编程探索:阻塞队列与生产者-消费者模型的应用
java·后端·架构