一、技术选型
1.项目背景
该系统为后台业务运营平台,主要用于操作人员查看业务数据及处理异常工单。由于当前系统缺乏实时消息提醒机制,导致工单响应延迟,影响业务处理效率。因此,需要实现后端主动推送消息的功能,确保操作人员能够及时接收并处理工单。
SSE 简介
SSE(Server-Sent Events)是一种基于 HTTP 的服务器推送技术,允许服务端向客户端单向发送实时事件流。与 WebSocket 不同,SSE 仅支持服务端主动推送数据,适用于无需双向通信的场景(如消息通知、实时日志、状态更新等)。
SSE 核心特点
✅ 单向通信:服务端主动推送,客户端仅监听,无需双向交互,符合工单提醒场景需求。
✅ 基于 HTTP:无需额外协议(如 WebSocket 的 ws://),兼容标准 HTTP/HTTPS,部署更简单。
✅ 自动重连:内置断线重连机制,确保消息可靠送达。
✅ 轻量高效:协议开销小,适合低频但需即时触达的消息推送。
✅ 标准兼容:原生支持 EventSource API,现代浏览器(Chrome/Firefox/Edge/Safari)均兼容。
2. 技术方案对比
后端向前端推送数据,常见方案有两种:
- WebSocket:全双工通信,适用于需要前后端频繁交互的场景(如聊天应用、实时协作)。
- SSE(Server-Sent Events):基于 HTTP 的单向通信,仅支持服务端向客户端推送数据。
3. 技术方案敲定
- 功能匹配度高:当前需求仅需后端推送消息,无需前端主动发送数据,SSE 的单向通信特性完全满足需求。
- 实现成本低:SSE 基于标准 HTTP 协议,无需额外协议升级(如 WebSocket 的
ws://),兼容性更好,开发及维护更简单。 - 轻量高效:相比 WebSocket,SSE 在仅需服务端推送的场景下,资源消耗更低,适合工单提醒等低频但需即时触达的场景。
4.预期收益
- 提升工单处理时效:通过实时消息推送,减少人工刷新页面的依赖,确保异常工单及时处理。
- 技术方案可持续:未来如需扩展其他通知类功能(如系统告警、状态变更),可基于 SSE 快速实现,降低后续开发成本。
二、技术方案实现
-
为了确保用户在全站任何页面都能实时接收系统推送消息,我们需要在应用根组件中建立全局SSE(Server-Sent Events)连接。这种实现方式具有以下优势:
- 全局可用:不受路由切换影响,始终保持连接
- 即时推送:任何页面操作都能实时接收消息
- 资源优化:全站维护单一连接,避免重复创建
-
代码模块
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} {/* 其他组件内容 */} </> ); }
三、遇到的问题及解决方案
问题
- 前端使用source.close()主动断开连接,但后端还是会持续请求导致后端报错日志不断输出。
解决方案
- 在页面离开之后调用后端断开连接接口,通知后端服务断开连接。
四、后续优化
问题
- 在项目中会遇到后端服务器断开连接的问题,这时候前端会一直请求后端接口,有一些无效请求堆积,带宽消耗,造成资源浪费。而且会频繁的弹出错误提示,影响用户体验。
解决方案
-
采用指数退避,实现带退避算法的有限次重试。
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); // 延迟时间翻倍 } }
指数退避算法
-
指数退避算法(Exponential Backoff)是一种在网络请求失败后,逐步增加重试间隔时间的策略。
-
特点:
- 等待时间呈指数增长:每次重试的等待时间 = 基础间隔 × 2^(重试次数-1)
- 随机抖动(Jitter):为避免多个客户端同步重试造成"惊群效应",通常会添加随机因子