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

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


在现代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库:

npm install event-source-polyfill

前端实现步骤

1. 创建SSE连接

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

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连接:

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连接:

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连接:

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向客户端实时推送上传进度:

@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推送任务进度:

@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("任务执行成功");
}
<think>嗯,用户想了解Spring Boot中如何通过HTTP长连接后端发送数据到前端。首先,我得确认用户的需求是什么。长连接通常指的是在客户端服务器之间保持一个持久的连接,允许服务器主动推送数据,而不是客户端频繁轮询。常见的实现方式可能有几种,比如SSE(Server-Sent Events)、WebSocket或者长轮询。不过用户提到的是HTTP长连接,可能更偏向于SSE,因为SSE是基于HTTP的,而WebSocket是独立的协议。 接下来,我需要考虑用户的技术背景。他们可能已经熟悉Spring Boot的基础,但不太清楚如何实现实时数据传输。需要分步骤解释,结构清晰。首先应该介绍SSE,因为这是HTTP长连接的典型应用,然后可能提到WebSocket作为补充,但重点在SSE上。 步骤方面,需要先说明依赖问题,不过Spring Boot可能已经内置了相关的支持,不需要额外依赖。然后创建一个控制器,使用@GetMapping并指定produces为MediaType.TEXT_EVENT_STREAM_VALUE,这样客户端就知道这是SSE流。接着,方法返回一个SseEmitter对象,然后通过另一个线程定期发送数据,模拟实时推送。 要注意的是,代码示例需要正确,比如正确使用SseEmitter的send方法,处理可能的IOException,以及最后complete的调用。同时,前端部分需要简单展示如何使用EventSource来接收数据,这样用户能有一个完整的实现图景。 另外,可能用户会混淆SSEWebSocket,需要简要说明两者的区别。SSE是单向的,服务器到客户端,而WebSocket是双向的。但用户的问题是从后端发送数据到前端,SSE已经足够,而且更简单,不需要复杂的握手。 还需要考虑异常处理,比如连接中断时的处理,比如在emitter.onCompletionemitter.onTimeout中注册回调,释放资源,避免内存泄漏。 可能用户在实际应用中会遇到的问题,比如如何发送更复杂的数据结构,这时候可以提到使用JSON格式,或者在事件中指定类型。此外,前端可能需要处理不同的事件类型,但基础示例应该足够用户入门。 总结下来,回答的结构应该是先介绍概念,然后分步骤实现后端前端代码,最后补充注意事项扩展建议。确保用户能够一步步跟随,理解每个部分的作用,同时了解不同技术的适用场景。</think>在Spring Boot实现HTTP长连接后端主动推送数据到前端,主要有两种常见方式:Server-Sent Events (SSE) WebSocket。以下是分步说明实现示例: 1. **技术选型对比** - **SSE(推荐)**:基于HTTP协议,单向通信(服务端→客户端),简单易用 - **WebSocket**:需要协议升级,支持双向通信,适合高频交互场景 - **长轮询**:传统方式,通过反复请求实现,效率较低 2. **SSE实现步骤(以定时推送为例)** **后端实现** ```java @RestController public class SseController { private final ExecutorService executor = Executors.newCachedThreadPool(); @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter streamData() { SseEmitter emitter = new SseEmitter(60_000L); // 超时时间60秒 executor.execute(() -> { try { for (int i = 0; i < 10; i++) { SseEmitter.event() .data("实时数据 " + i + " - " + LocalTime.now()) .id(String.valueOf(i)) .comment("推送注释") .reconnectTime(5000) .send(emitter); Thread.sleep(1000); // 每秒推送一次 } emitter.complete(); } catch (IOException | InterruptedException e) { emitter.completeWithError(e); } }); // 连接关闭处理 emitter.onCompletion(() -> System.out.println("连接完成")); emitter.onTimeout(() -> System.out.println("连接超时")); return emitter; } } ``` **前端实现** ```javascript const eventSource = new EventSource('/stream'); eventSource.onmessage = (event) => { console.log('收到数据:', event.data); // 更新DOM }; eventSource.onerror = (err) => { console.error('连接异常:', err); eventSource.close(); }; ``` 3. **关键配置说明** ```properties # application.properties spring.mvc.async.request-timeout=60000 # 超时时间设置 ``` 4. **注意事项** - 保持连接:浏览器默认会自动重连(可通过`event.reconnectTime()`配置) - 数据传输格式:支持传输JSON等结构化数据 ```java .data(new ObjectMapper().writeValueAsString(dataObj)) ``` - 跨域问题:需要配置CORS - 性能优化:合理设置线程池,避免资源泄漏 5. **扩展应用场景** - 实时监控系统 - 股票价格推送 - 即时通知提醒 - 日志实时展示 实际生产环境中,建议: 1. 使用Spring Boot的`@Async`实现异步处理 2. 添加心跳机制保持连接活跃 3. 结合消息队列(如RabbitMQ/Kafka)处理大规模推送 4. 使用Nginx保持代理层对长连接的支持 是否需要进一步了解WebSocket实现方案或其他具体应用场景的细节?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值