SSE消息推送前后端代码

复制代码
现在很多的场景需要实现服务端向客户端发送消息的功能,比如AI对话,实时数据推送等等,这是我之前封装的SSE前后代码,支持断开重连,心跳机制,消息重发,前端订阅回传等功能。代码复制粘贴可用。

一、后端部分

SSEMessage 类SSE消息实体

kotlin 复制代码
@Data
public class SSEMessage {
    private SseEnum event; // 事件类型(如"update")
    private String data;  // 消息内容
    private String id;       // 消息ID
}

SseEnum消息类型枚举类

kotlin 复制代码
public enum SseEnum {
    HEARTBEAT("heartbeat"),
    NOTIFICATION("notification"),
    DATA("data"),
    ERROR("error"),
    SYSTEM("system"),
    CUSTOM("custom"),
    CHAT("chat")

    private String val;
    SseEnum(String val) {
        this.val = val;
    }
    public String value(){
        return val;
    }

}

这里引入线程池,用来执行定时任务。

java 复制代码
@Component
public class ThreadPoolManager {

    private static final Logger log = LoggerFactory.getLogger(ThreadPoolManager.class);

    private static final int CORE_POOL_SIZE_MIN = 2;
    private static volatile ScheduledExecutorService scheduledExecutorService;


    public static ScheduledExecutorService getScheduled() {
        if (ThreadPoolManager.scheduledExecutorService == null) {
            synchronized (ThreadPoolManager.class) {
                if (ThreadPoolManager.scheduledExecutorService == null) {
                    instant();
                }
            }
        }
        return ThreadPoolManager.scheduledExecutorService;
    }

    public static void instant() {
        ThreadPoolManager.scheduledExecutorService = new ScheduledThreadPoolExecutor(CORE_POOL_SIZE_MIN,
                new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
                new ThreadPoolExecutor.CallerRunsPolicy()) {
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);
                log.error("线程池异常",t);
            }
        };
    }

}

SSEmitterManager管理类,用来管理和存储SSE对象

java 复制代码
@Service
public class SSEmitterManager {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private final ConcurrentHashMap<String, SSEmitterWrapper> emitters = new ConcurrentHashMap<>();

    public SSEmitterManager() {
    }

    public SseEmitter createSseEmitter(String userId) {
        if(StringUtils.isEmpty(userId)){
            throw new RuntimeException("userId is  null");
        }

        SSEmitterWrapper wrapper=  emitters.computeIfAbsent(userId,
                        (k) -> {
                            SSEmitterWrapper emitter = new SSEmitterWrapper(loginUser, () -> emitters.remove(k));
                            emitter.start();
                            return emitter;
                        });

        return  wrapper.getEmitter();
    }

    /**
     * 群发消息
     */
    public void SendMessage(List<String> usrId, SSEMessage sseMessage) {
        for (String id : usrId) {
            SSEmitterWrapper wrapper = emitters.get(id);
                if (wrapper != null && wrapper.isActive()) {
                    wrapper.sendMessage(sseMessage);
                } else {
                    logger.warn("用户 {} 的SSE连接不存在或已断开", usrId);
                }
        }
    }


    /**
     * 单发消息
     */
    public void SendMessage(String usrId, SSEMessage sseMessage) {
        SSEmitterWrapper wrapper = emitters.get(usrId);
            if (wrapper != null && wrapper.isActive()) {
                wrapper.sendMessage(sseMessage);
            } else {
                logger.warn("用户 {} 的SSE连接不存在或已断开", usrId);
            }
    }

    /**
     * 定时去轮询重发消息
     */
    public void reSendMessagesTask() {
        if (emitters.isEmpty()) return;
        ThreadPoolManager.getScheduled().schedule( () ->
                emitters.values().forEach(SSEmitterWrapper::retryFailedMessages),
                5,TimeUnit.SECONDS
        );
    }

    /**
     * 定时检查SSE链接是否活跃
     */
    public void checkActive() {
        if (emitters.isEmpty()) return;
        ThreadPoolManager.getScheduled().schedule(() -> {
            List<String> inactiveKeys = new ArrayList<>();
            emitters.forEach((key, emitter) -> {
                if (!emitter.isActive()) {
                    inactiveKeys.add(key);
                }
            });
            inactiveKeys.forEach(emitters::remove); // 批量删除
        }, 1, TimeUnit.HOURS);
    }


    /**
     * 判断这些用户是否处在链接状态
     */
    public List<OnlineVo> getOnlineUser(List<String> usrId) {
        if (usrId.isEmpty()) return new ArrayList<>();
        List<OnlineVo> onlineUsers = new ArrayList<>();
        for (String usr : usrId) {
            if (usr == null || Objects.equals("null", usr)) continue;
            OnlineVo onlineVo = new OnlineVo();
            onlineVo.setId(Long.parseLong(usr));
            if (emitters.containsKey(usr)) {
                onlineVo.setOnline(1);
                onlineUsers.add(onlineVo);
                continue;
            }
            onlineVo.setOnline(0);
            onlineUsers.add(onlineVo);
        }
        return onlineUsers;
    }


}

SSEmitterWrapper消息包装类

java 复制代码
public class SSEmitterWrapper {

    private static final Logger log = LoggerFactory.getLogger(SSEmitterWrapper.class);

    private static final long TIMEOUT = 21600000L;
    private static final long HEARTBEAT_INTERVAL = 5L;
    private static final int MAX_RETRY = 2;


    private final LoginUser user;
    private final Runnable closeCallback;

    private final AtomicBoolean active = new AtomicBoolean(false);
    private final AtomicBoolean closed = new AtomicBoolean(false);

    @Getter
    private SseEmitter emitter;
    private ScheduledFuture<?> heartbeatTask;

    /**
     * 失败消息重试队列
     */
    private final BlockingQueue<RetryMessage> retryQueue = new LinkedBlockingQueue<>(10);

    public SSEmitterWrapper(LoginUser user, Runnable closeCallback) {
        this.user = user;
        this.closeCallback = closeCallback;
    }


    public SseEmitter start() {
        this.emitter = new SseEmitter(TIMEOUT);
        this.active.set(true);

        registerListener();
        startHeartbeat();

        return emitter;
    }

    private void registerListener() {
        emitter.onCompletion(this::close);
        emitter.onTimeout(this::close);
        emitter.onError(e -> {
            log.warn("SSE error userId={}, msg={}", user.getUsrId(), e.getMessage());
            close();
        });
    }

    private void startHeartbeat() {
        heartbeatTask = ThreadPoolManager.getScheduled().scheduleAtFixedRate(() -> {
            try {
                sendInternal(SseEmitter.event()
                        .name(SseEnum.HEARTBEAT.value())
                        .data("heart"));
            } catch (Exception e) {
                close();
                log.warn("SSE thread error userId={}, msg={}", user.getUsrId(), e.getMessage());
            }
        }, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
    }

    public boolean isActive() {
        return active.get();
    }

    public void sendMessage(SSEMessage message) {
        if (!isValid(message) || closed.get()) return;
        if (!isActive() || closed.get()) {
            log.info("SSE connection closed, skipping message send userId={}", user.getUsrId());
            return;
        }
            try {
                sendInternal(SseEmitter.event()
                        .id(message.getId())
                        .name(message.getEvent().value())
                        .data(message.getData()));
            } catch (IOException e) {
                enqueueRetry(message);
            } 
    }

    private void sendInternal(SseEmitter.SseEventBuilder event) throws IOException {
        emitter.send(event);

    }

    private void enqueueRetry(SSEMessage message) {
        retryQueue.offer(new RetryMessage(message));
    }

    /**
     * 定时由 Manager 调用
     */
    public void retryFailedMessages() {
        int size = retryQueue.size();
        for (int i = 0; i < size; i++) {
            RetryMessage retry = retryQueue.poll();
            if (retry == null) continue;

            if (retry.retryCount >= MAX_RETRY) {
                log.warn("SSE message dropped userId={}, msgId={}",
                        user.getUsrId(), retry.message.getId());
                //todo可以放到日志里面去
                continue;
            }

            try {
                sendInternal(SseEmitter.event()
                        .id(retry.message.getId())
                        .name(retry.message.getEvent().value())
                        .data(retry.message.getData()));
            } catch (IOException e) {
                retry.retryCount++;
                retryQueue.offer(retry);
            }
        }
    }

    private boolean isValid(SSEMessage message) {
        return message != null
                && Objects.nonNull(message.getId())
                && !StringUtils.isEmpty(message.getData());
    }

    public void close() {
        if (!closed.compareAndSet(false, true)) return;

        active.set(false);
        if (heartbeatTask != null) {
            heartbeatTask.cancel(true);
            heartbeatTask = null;
        }
        ;

        try {
            emitter.complete();
        } catch (Exception ignored) {
        }

        closeCallback.run();
        log.info("SSE closed userId={}", user.getUsrId());
    }

    private static class RetryMessage {
        private final SSEMessage message;
        private int retryCount = 0;

        RetryMessage(SSEMessage message) {
            this.message = message;
        }
    }
}

至此后端封装完毕了只需要调用

SSEmitterManager的createSseEmitter方法就可以创建一个SSE对象

二、前端部分

这里是使用的Vite+Vue3的项目

SSEServer.ts

typescript 复制代码
import { getToken } from "@/utils/auth.js";
import useUserStore from "@/store/modules/user";

/**
 * SSE 服务器连接管理类
 * 支持消息接收处理、事件回调、自动重连和心跳机制
 */

// 消息类型枚举
export enum SSEMessageType {
    HEARTBEAT = 'heartbeat',
    NOTIFICATION = 'notification',
    DATA = 'data',
    ERROR = 'error',
    SYSTEM = 'system',
    CUSTOM = 'custom',
    CHAT="chat"
}

// SSE 消息接口
export interface SSEMessage {
    type: SSEMessageType | string;
    data: any;
    id?: string;
    timestamp?: number;
}

// 事件回调类型
export type SSECallback = (message: SSEMessage) => void;

// 连接状态枚举
export enum SSEConnectionStatus {
    DISCONNECTED = 'disconnected',
    CONNECTING = 'connecting',
    CONNECTED = 'connected',
    RECONNECTING = 'reconnecting',
    ERROR = 'error'
}

// 重连配置接口
export interface ReconnectConfig {
    enabled: boolean;
    maxRetries: number;
    retryInterval: number; // 毫秒
    backoffMultiplier: number; // 退避倍数
}

// 心跳配置接口
export interface HeartbeatConfig {
    enabled: boolean;
    interval: number; // 心跳间隔(毫秒)
    timeout: number; // 心跳超时(毫秒)
}

// SSE 服务器配置接口
export interface SSEServerConfig {
    url: string;
    reconnect?: Partial<ReconnectConfig>;
    heartbeat?: Partial<HeartbeatConfig>;
    headers?: Record<string, string>;
    withCredentials?: boolean;
}





class SSEServer {
    private eventSource: EventSource | null = null;
    private status: SSEConnectionStatus = SSEConnectionStatus.DISCONNECTED;
    // private config: Required<SSEServerConfig>;
    private reconnectConfig: ReconnectConfig;
    private heartbeatConfig: HeartbeatConfig;

    // 事件回调映射
    private callbacks: Map<string, Set<SSECallback>> = new Map();
    
    // 通用回调(所有消息都会触发)
    private generalCallbacks: Set<SSECallback> = new Set();

    // 重连相关
    private reconnectAttempts: number = 0;
    private reconnectTimer: NodeJS.Timeout | null = null;

    // 心跳相关
    private heartbeatTimer: NodeJS.Timeout | null = null;
    private lastHeartbeatTime: number = 0;
    private heartbeatTimeoutTimer: NodeJS.Timeout | null = null;

    // 消息队列(连接断开时暂存消息)
    private messageQueue: SSEMessage[] = [];
    private maxQueueSize: number = 100;

    //当前链接的URL
    private currentUrl: string | null = null;

    constructor() {
        // 默认配置
        const defaultReconnectConfig: ReconnectConfig = {
            enabled: true,
            maxRetries: 10,
            retryInterval: 3000,
            backoffMultiplier: 1.5
        };

        const defaultHeartbeatConfig: HeartbeatConfig = {
            enabled: true,
            interval: 30000, // 30秒
            timeout: 60000 // 60秒超时
        };

        this.reconnectConfig = defaultReconnectConfig;
        this.heartbeatConfig = defaultHeartbeatConfig;
    }

    /**
     * 连接到 SSE 服务器
     */
    public connect(): void {


        if (this.status === SSEConnectionStatus.CONNECTED ||
            this.status === SSEConnectionStatus.CONNECTING) {
            console.warn('SSE 连接已存在或正在连接中');
            return;
        }

        this.status = SSEConnectionStatus.CONNECTING;
        this.emitStatusChange();
        const url: string = this.assembleUrl();
        try {
            // 创建 EventSource 连接
            this.eventSource = new EventSource(url, {
                withCredentials: true,
            });

            // 监听连接打开
            this.eventSource.onopen = (event) => {
                this.status = SSEConnectionStatus.CONNECTED;
                this.reconnectAttempts = 0;
                this.emitStatusChange();
                this.startHeartbeat();
                this.processMessageQueue();
            };


            for (const message of Object.values(SSEMessageType)) {
                this.eventSource.addEventListener(message, (event: any) => {
                    this.handleMessage(message, event);
                })
            }


            // 监听错误
            this.eventSource.onerror = (error) => {
                console.error('SSE 连接错误', error);
                this.handleError(error);
            };


        } catch (error) {
            console.error('创建 SSE 连接失败', error);
            this.status = SSEConnectionStatus.ERROR;
            this.emitStatusChange();
            this.attemptReconnect();
        }
    }


    /**
     * 处理接收到的消息
     */
    private handleMessage(type: string, event: MessageEvent): void {
        try {
            const message: SSEMessage = {
                type: type as string,
                data: event.data,
                id: event.lastEventId,
                timestamp: Date.now()
            };
            this.processMessage(message);
        } catch (error) {
            console.error('处理消息失败', error);
        }
    }



    /**
     * 处理消息
     */
    private processMessage(message: SSEMessage): void {
        // 更新心跳时间
        if (message.type === SSEMessageType.HEARTBEAT) {
            this.lastHeartbeatTime = Date.now();
            return;
        }

        // 如果连接断开,将消息加入队列
        if (this.status !== SSEConnectionStatus.CONNECTED) {
            this.addToQueue(message);
            return;
        }

        // 触发对应类型的回调
        const typeCallbacks = this.callbacks.get(message.type);
        if (typeCallbacks) {
            typeCallbacks.forEach(callback => {
                try {
                    callback(message);
                } catch (error) {
                    console.error(`执行回调失败 [${message.type}]`, error);
                }
            });
        }

        // 触发通用回调
        if (this.generalCallbacks.size > 0) {
            this.generalCallbacks.forEach(callback => {
                try {
                    callback(message);
                } catch (error) {
                    console.error('执行通用回调失败', error);
                }
            });
        }

    }

    /**
     * 处理错误
     */
    private handleError(error: Event): void {
        this.status = SSEConnectionStatus.ERROR;
        this.emitStatusChange();

        // 发送错误消息
        const errorMessage: SSEMessage = {
            type: SSEMessageType.ERROR,
            data: { error: '连接错误', originalEvent: error },
            timestamp: Date.now()
        };

        this.processMessage(errorMessage);

        // 如果连接关闭,尝试重连
        if (this.eventSource?.readyState === EventSource.CLOSED) {
            this.stopHeartbeat();
            this.attemptReconnect();
        }
    }

    /**
     * 尝试重连
     */
    private attemptReconnect(): void {
        const userStore = useUserStore()
        if (!userStore.checkIsLogin()) {
            console.log("用户未登录")
            return
        }
        if (!this.reconnectConfig.enabled) {
            console.log('重连机制已禁用');
            return;
        }

        if (this.reconnectAttempts >= this.reconnectConfig.maxRetries) {
            console.error('达到最大重连次数,停止重连');
            this.status = SSEConnectionStatus.ERROR;
            this.emitStatusChange();
            return;
        }

        this.status = SSEConnectionStatus.RECONNECTING;
        this.emitStatusChange();

        // 计算重连延迟(指数退避)
        const delay = this.reconnectConfig.retryInterval *
            Math.pow(this.reconnectConfig.backoffMultiplier, this.reconnectAttempts);
        console.log(`将在 ${delay}ms 后尝试第 ${this.reconnectAttempts + 1} 次重连`);
        this.reconnectTimer = setTimeout(() => {
            this.reconnectAttempts++;
            this.disconnect();
            this.connect();
        }, delay);
    }

    /**
     * 启动心跳检测
     */
    private startHeartbeat(): void {
        if (!this.heartbeatConfig.enabled) return;

        this.lastHeartbeatTime = Date.now();

        // 定期检查心跳
        this.heartbeatTimer = setInterval(() => {
            const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeatTime;
            if (timeSinceLastHeartbeat > this.heartbeatConfig.timeout) {
                console.warn('心跳超时,连接可能已断开');
                this.handleHeartbeatTimeout();
            }
        }, this.heartbeatConfig.interval);
    }

    /**
     * 处理心跳超时
     */
    private handleHeartbeatTimeout(): void {
        this.stopHeartbeat();
        this.disconnect();
        this.attemptReconnect();
    }

    /**
     * 停止心跳检测
     */
    private stopHeartbeat(): void {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
        if (this.heartbeatTimeoutTimer) {
            clearTimeout(this.heartbeatTimeoutTimer);
            this.heartbeatTimeoutTimer = null;
        }
    }

    /**
     * 断开连接
     */
    public disconnect(): void {
        this.stopHeartbeat();

        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }


       // this.callbacks.clear()
       //  this.generalCallbacks.clear()
        this.messageQueue = []
        this.currentUrl = null

        if (this.eventSource) {
            this.eventSource.close();
            this.eventSource = null;
        }

        this.status = SSEConnectionStatus.DISCONNECTED;
        this.emitStatusChange();


    }

    /**
     * 注册消息类型回调
     * @param type 消息类型
     * @param callback 回调函数
     */
    public on(type: string | SSEMessageType, callback: SSECallback): void {
        if (!this.callbacks.has(type)) {
            this.callbacks.set(type, new Set());
        }

        this.callbacks.get(type)!.add(callback);
    }

    /**
     * 移除消息类型回调
     */
    public off(type: string | SSEMessageType, callback?: SSECallback): void {
        const callbacks = this.callbacks.get(type);
        if (!callbacks) return;

        if (callback) {
            callbacks.delete(callback);
            if (callbacks.size === 0) {
                this.callbacks.delete(type);
            }
        } else {
            this.callbacks.delete(type);
        }
    }



    /**
     * 注册通用回调(所有消息都会触发)
     */
    public onMessage(callback: SSECallback): void {
        this.generalCallbacks.add(callback);
    }

    /**
     * 移除通用回调
     */
    public offMessage(callback: SSECallback): void {
        this.generalCallbacks.delete(callback);
    }

    /**
     * 触发状态变化事件
     */
    private emitStatusChange(): void {
        const statusMessage: SSEMessage = {
            type: SSEMessageType.SYSTEM,
            data: {
                event: 'statusChange',
                status: this.status,
                reconnectAttempts: this.reconnectAttempts
            },
            timestamp: Date.now()
        };

        this.processMessage(statusMessage);
    }

    /**
     * 将消息添加到队列
     */
    private addToQueue(message: SSEMessage): void {
        this.messageQueue.push(message);
        if (this.messageQueue.length > this.maxQueueSize) {
            this.messageQueue.shift(); // 移除最旧的消息
        }
    }

    /**
     * 处理消息队列
     */
    private processMessageQueue(): void {
        while (this.messageQueue.length > 0) {
            const message = this.messageQueue.shift();
            if (message) {
                this.processMessage(message);
            }
        }
    }

    /**
     * 获取当前连接状态
     */
    public getStatus(): SSEConnectionStatus {
        return this.status;
    }

    /**
     * 检查是否已连接
     */
    public isConnected(): boolean {
        return this.status === SSEConnectionStatus.CONNECTED;
    }

    /**
     * 获取重连次数
     */
    public getReconnectAttempts(): number {
        return this.reconnectAttempts;
    }

    /**
     * 清空消息队列
     */
    public clearQueue(): void {
        this.messageQueue = [];
    }

    private static SSEServer: SSEServer | null = null;

    static getInstance(): SSEServer {
        if (!this.SSEServer) {
            this.SSEServer = new SSEServer();
        }
        return this.SSEServer;
    }

    private assembleUrl() {
        return `${import.meta.env.VITE_APP_BASE_API}/sse?token=${getToken()}`;
    }


}

export default SSEServer;
相关推荐
搬搬砖得了2 小时前
当 GraphQL 变成“全家桶”,Stream 写成“天书”,老板变身“谜语人”:我在代码屎山里的渡劫日常
后端
像我这样帅的人丶你还2 小时前
JavaScript 迭代器详解
前端·javascript
默海笑2 小时前
Java 基础 12:JavaDoc 生成文档 学习笔记
后端
写Cpp的小黑黑2 小时前
React Native 项目实战指南
后端
逍遥归来2 小时前
《SWIFTER -Swift开发者必备Tips》学习笔记
前端
timi先生2 小时前
语料库全栈项目部署 (Vue + Java + CQPweb)
java·前端·vue.js
Lazy_zheng2 小时前
Map / Set / WeakMap / WeakSet,一次给你讲透
前端·javascript·面试
learyuan2 小时前
Windows原生开发
前端
uzong2 小时前
ClaudeCode 入门详细教程,手把手带你Vibe Coding
前端·人工智能