VUE+Spring Flux实现SSE长连接

VUE代码

javascript 复制代码
        // 初始化EventSource
        initEventSource(url) {
          const token = getAccessToken();
          const eventSource = new EventSourcePolyfill(url, {
            headers: {
              'Authorization': `Bearer ${token}`,
              'tenant-id': getTenantId(),
            }
          });

          eventSource.onerror = (e) => {
            console.log("SSE连接错误", e);
            if (e.readyState === EventSource.CLOSED || eventSource.readyState === EventSource.CONNECTING) {
              console.log("SSE连接已关闭或正在重连");
            } else {
              // 当发生错误时,尝试重新连接
              if (reconnectAttempts < maxReconnectAttempts) {
                console.log(`尝试第${reconnectAttempts + 1}次重连`);
                reconnectAttempts++;
                setTimeout(() => {
                  eventSource.close(); // 关闭当前连接
                  this.initEventSource(url); // 重新初始化EventSource
                }, reconnectDelay * reconnectAttempts);
              } else {
                console.error("达到最大重连次数,不再尝试重连");
              }
            }
          };

          eventSource.addEventListener("message", res => {
            const data = JSON.parse(res.data)
            if (data.type == 2) {
              this.unreadCount = data.content;
            }
            if (data.type == 1) {
              this.createNotify(data)
            }
          })
        },

后端采用redis做管道,能够兼容分布式服务

JAVA 监听接口

java 复制代码
 @GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamEvents() {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        USER_IDS.add(loginUserId);
        SseMessageVO heartbeat = new SseMessageVO()
                .setType(SseNotifyTypeEnum.HEARTBEAT.getType())
                .setUserId(loginUserId)
                .setContent("Heartbeat");
        return  reactiveRedisOperations.listenToChannel(SseService.getDestination(loginUserId))
                .map(data -> sendMsg(loginUserId,data.getMessage(),heartbeat))
                .publishOn(Schedulers.boundedElastic())
                .doOnSubscribe(subscription -> {
                    // 订阅时发送一次心跳,确认连接
                    heartbeat(heartbeat);
                });
    }

    private String sendMsg(Long loginUserId,String sseMessage,SseMessageVO heartbeat){
        SseMessageVO sseMessageVO = JSONUtil.toBean(sseMessage, SseMessageVO.class);
        if (null != sseMessageVO && Objects.equals(sseMessageVO.getUserId(), loginUserId)){
            return JSONUtil.toJsonStr(sseMessageVO);
        }
        return JSONUtil.toJsonStr(heartbeat);
    }

    /**
     * 登录时心跳
     */
    private void heartbeat(SseMessageVO heartbeat) {
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        executorService.schedule(() -> {
            sseService.publishEventToChannel(heartbeat).subscribe();
        }, 1, TimeUnit.SECONDS);
        executorService.shutdown();
    }

    /**
     * 保活
     */
    @PostConstruct
    public void heartbeatTimer() {
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleAtFixedRate(() -> {
            if (CollectionUtil.isNotEmpty(USER_IDS)){
                for (Long userId : USER_IDS) {
                    String message = "Heartbeat at " + LocalDateTime.now();
                    SseMessageVO heartbeat = new SseMessageVO()
                            .setType(SseNotifyTypeEnum.HEARTBEAT.getType())
                            .setUserId(userId)
                            .setContent(message);
                    sseService.publishEventToChannel(heartbeat).subscribe();
                }
            }
        }, 0, 10, TimeUnit.SECONDS);
    }

JAVA 提交数据服务

java 复制代码
@Component
public class SseService {

    @Resource
    private ReactiveRedisOperations<String, String> reactiveRedisOperations;

    private static final String DESTINATION = "event-channel-user:";

    /**
     * 获取指定通道
     * @param userId
     * @return
     */
    public static String getDestination(Long userId){
        return DESTINATION+userId;
    }

    /**
     * 推送事件到通道
     * @param sseMessageVO
     * @return
     */
    public Mono<Long> publishEventToChannel(SseMessageVO sseMessageVO) {
        return reactiveRedisOperations.convertAndSend(getDestination(sseMessageVO.getUserId()), JSONUtil.toJsonStr(sseMessageVO));
    }

}
相关推荐
weedsfly2 小时前
迭代器、生成器与异步迭代——让数据“按需流动”的艺术
前端·javascript
假如让我当三天老蒯2 小时前
前端跨域解决方案(学习用)
前端·javascript·面试
铁皮饭盒4 小时前
Bun 哪比 Node.js 快?
javascript·后端
JieE21211 小时前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
candyTong14 小时前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构
_柳青杨18 小时前
深入理解 JavaScript 事件循环
前端·javascript
大家的林语冰1 天前
ES5 凉凉,Babel 8 正式发布,默认不再编译为 ES5 和 CJS......
前端·javascript·前端工程化
weedsfly1 天前
异步编程全景与事件循环——彻底搞懂 JS 执行机制
前端·javascript
用户1733598075371 天前
纯前端 PDF 数字签名实战:Vue 3 + pdf-lib 在浏览器里完成签名嵌入
前端·javascript
JieE2122 天前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法