前端项目集成SSE与后端进行实时消息推送

389 阅读5分钟

一、技术选型

1.项目背景

该系统为后台业务运营平台,主要用于操作人员查看业务数据及处理异常工单。由于当前系统缺乏实时消息提醒机制,导致工单响应延迟,影响业务处理效率。因此,需要实现后端主动推送消息的功能,确保操作人员能够及时接收并处理工单。

SSE 简介

SSE(Server-Sent Events)是一种基于 HTTP 的服务器推送技术,允许服务端向客户端单向发送实时事件流。与 WebSocket 不同,SSE 仅支持服务端主动推送数据,适用于无需双向通信的场景(如消息通知、实时日志、状态更新等)。

SSE 核心特点

✅ 单向通信:服务端主动推送,客户端仅监听,无需双向交互,符合工单提醒场景需求。
✅ 基于 HTTP:无需额外协议(如 WebSocket 的 ws://),兼容标准 HTTP/HTTPS,部署更简单。
✅ 自动重连:内置断线重连机制,确保消息可靠送达。
✅ 轻量高效:协议开销小,适合低频但需即时触达的消息推送。
✅ 标准兼容:原生支持 EventSource API,现代浏览器(Chrome/Firefox/Edge/Safari)均兼容。

2. 技术方案对比

后端向前端推送数据,常见方案有两种:

  1. WebSocket:全双工通信,适用于需要前后端频繁交互的场景(如聊天应用、实时协作)。
  2. SSE(Server-Sent Events):基于 HTTP 的单向通信,仅支持服务端向客户端推送数据。

3. 技术方案敲定

  • 功能匹配度高:当前需求仅需后端推送消息,无需前端主动发送数据,SSE 的单向通信特性完全满足需求。
  • 实现成本低:SSE 基于标准 HTTP 协议,无需额外协议升级(如 WebSocket 的 ws://),兼容性更好,开发及维护更简单。
  • 轻量高效:相比 WebSocket,SSE 在仅需服务端推送的场景下,资源消耗更低,适合工单提醒等低频但需即时触达的场景。

4.预期收益

  • 提升工单处理时效:通过实时消息推送,减少人工刷新页面的依赖,确保异常工单及时处理。
  • 技术方案可持续:未来如需扩展其他通知类功能(如系统告警、状态变更),可基于 SSE 快速实现,降低后续开发成本。

二、技术方案实现

  1. 为了确保用户在全站任何页面都能实时接收系统推送消息,我们需要在应用根组件中建立全局SSE(Server-Sent Events)连接。这种实现方式具有以下优势:

    • 全局可用:不受路由切换影响,始终保持连接
    • 即时推送:任何页面操作都能实时接收消息
    • 资源优化:全站维护单一连接,避免重复创建
  2. 代码模块

    import { notification } from 'antd';
    import React, { ReactNode, useEffect, useState } from 'react';
    
    type NotificationType = 'success' | 'info' | 'warning' | 'error';
    
    /**
     * SSE (Server-Sent Events) 连接组件
     * 用于建立和维护与服务器的SSE连接,接收服务器推送的消息
     */
    export default function SSEConnection() {
      const [api, contextHolder] = notification.useNotification();
      const [hasShownError, setHasShownError] = useState(false); // 是否已显示断开连接提示
    
      /**
       * 断开SSE连接并通知后端
       * @param userName 当前登录用户名
       * 
       * 问题背景:
       * - 当组件卸载时,我们调用source.close()关闭前端连接
       * - 但仅这样处理会导致后端不知道连接已断开,会继续尝试推送消息
       * - 这会造成后端资源浪费和错误日志污染
       * 
       * 解决方案:
       * - 在关闭前端连接的同时,调用专门的断开连接API通知后端
       * - 确保前后端连接状态同步,避免资源泄漏
       */
      const disConnectSSE = async (userName: string) => {
        try {
          await fetch(`http://xxx/xxx/disconnect?clientId=${userName}`, {
            method: 'POST',
          });
          console.log('SSE连接已正常断开');
        } catch (error) {
          console.error('断开SSE连接时出错:', error);
        }
      };
    
      /**
       * 获取当前登录用户名
       */
      const getLoginUserName = (): string => {
        // 实际项目中这里应该从store或localStorage获取
        return 'current_user';
      };
    
      /**
       * 显示通知消息
       * @param type 通知类型
       * @param description 通知内容
       */
      const openNotificationWithIcon = (type: NotificationType, description: ReactNode) => {
        api[type]({
          message: '系统消息',
          description,
          duration: 10,
        });
      };
    
      useEffect(() => {
        if (window.EventSource) {
          const userName = getLoginUserName();
    
          // 创建SSE连接
          const source = new EventSource(`http://xxx/xxx/createConnect?clientId=${userName}`);
    
          // 连接成功建立
          source.addEventListener('open', () => {
            console.log('SSE连接已建立');
            setHasShownError(false); // 重置错误状态
          });
    
          // 接收服务器消息
          source.addEventListener('message', (e) => {
            console.log('收到服务器消息:', e.data);
            openNotificationWithIcon('info', e.data);
          });
    
          // 连接出错
          source.addEventListener('error', (e) => {
            console.error('SSE连接错误:', e);
            if (!hasShownError) {
              setHasShownError(true);
              openNotificationWithIcon('error', '服务器连接已断开');
            }
          });
    
          // 清理函数:组件卸载时执行
          return () => {
            console.log('清理SSE连接');
            source.close(); // 关闭前端连接
            disConnectSSE(userName); // 通知后端关闭连接
          };
        } else {
          openNotificationWithIcon('warning', '该浏览器不支持SSE');
        }
      }, [hasShownError]);
    
      return (
        <>
          {contextHolder}
          {/* 其他组件内容 */}
        </>
      );
    }
    

三、遇到的问题及解决方案

问题

  1. 前端使用source.close()主动断开连接,但后端还是会持续请求导致后端报错日志不断输出。

解决方案

  1. 在页面离开之后调用后端断开连接接口,通知后端服务断开连接。

四、后续优化

问题

  1. 在项目中会遇到后端服务器断开连接的问题,这时候前端会一直请求后端接口,有一些无效请求堆积,带宽消耗,造成资源浪费。而且会频繁的弹出错误提示,影响用户体验。

解决方案

  1. 采用指数退避,实现带退避算法的有限次重试。

    async function fetchWithRetry(url, retries = 3, delay = 1000) {
      try {
        const res = await fetch(url);
        if (!res.data) throw new Error(res.message);
        return res;
      } catch (err) {
        if (retries <= 0) throw err;
        await new Promise(r => setTimeout(r, delay));
        return fetchWithRetry(url, retries - 1, delay * 2); // 延迟时间翻倍
      }
    }
    

指数退避算法

  1. 指数退避算法(Exponential Backoff)是一种在网络请求失败后,逐步增加重试间隔时的策略。

  2. 特点:

    1. 等待时间呈指数增长:每次重试的等待时间 = 基础间隔 × 2^(重试次数-1)
    2. 随机抖动(Jitter):为避免多个客户端同步重试造成"惊群效应",通常会添加随机因子