SSE服务

SSE(Server-Sent Events) ------ 它是一种服务器向客户端单向实时推送的通信协议,基于 HTTP 协议实现,无需像 WebSocket 那样建立独立连接,适合需服务器主动推送、客户端仅接收的场景(如实时通知、行情更新)

核心概念

  • 单向通信:仅支持服务器向客户端主动推送数据,客户端无法通过 SSE 向服务器发送数据(需搭配 HTTP 接口补充双向通信)。
  • 基于 HTTP 协议:复用 HTTP 连接,无需额外创建新协议(如 WebSocket 的 ws://),兼容性更好,服务器部署更简单。
  • 协议标识:客户端通过 EventSource API 建立连接,请求头指定 Accept: text/event-stream,服务器以特定格式(文本流)响应。
  • 核心特性
    • 自动重连:客户端连接断开后,默认会自动重试(可配置重连时间);
    • 断线重连时可恢复历史数据(通过 Last-Event-ID 实现);
    • 支持自定义事件类型,不止于默认的 message 事件;
    • 仅支持文本数据传输(二进制数据需编码为 Base64)。
  • 生命周期:客户端建立连接 → 服务器持续推送文本流 → 客户端断开连接(主动 / 超时)。

基本用法

1. 前端(浏览器)核心 API:EventSource

浏览器原生支持 EventSource 对象,无需依赖第三方库,核心方法与事件如下:

js 复制代码
// 前端:SSE 客户端实现
let eventSource = null;

// 1. 建立 SSE 连接
function initSSE() {
  // 检测浏览器支持(所有现代浏览器均支持,IE 不支持)
  if (!window.EventSource) {
    console.warn('当前浏览器不支持 SSE(建议使用 Chrome/Firefox/Edge)');
    return;
  }

  // 建立连接(URL 为后端 SSE 接口,支持跨域,需服务器配置 CORS)
  eventSource = new EventSource('http://localhost:8080/sse', {
    withCredentials: true // 跨域时携带 Cookie(如需)
  });

  // 2. 连接建立成功事件
  eventSource.onopen = function() {
    console.log('SSE 连接已建立');
    // 客户端如需向服务器发数据,需通过 HTTP 接口(如 POST)
    // 示例:发送用户登录状态到服务器(SSE 本身不支持客户端→服务器)
    fetch('http://localhost:8080/login', {
      method: 'POST',
      body: JSON.stringify({ username: '前端用户' }),
      headers: { 'Content-Type': 'application/json' }
    });
  };

  // 3. 接收服务器默认事件(事件类型为 message)
  eventSource.onmessage = function(event) {
    // event.data 为服务器推送的文本数据
    const data = JSON.parse(event.data);
    console.log('收到默认消息:', data);
    handleServerData('message', data);
  };

  // 4. 接收服务器自定义事件(如 "notice" 事件)
  eventSource.addEventListener('notice', function(event) {
    const data = JSON.parse(event.data);
    console.log('收到自定义 notice 事件:', data);
    handleServerData('notice', data);
  });

  // 5. 接收服务器行情事件(如 "stock" 事件)
  eventSource.addEventListener('stock', function(event) {
    const data = JSON.parse(event.data);
    console.log('收到股票行情:', data);
    handleServerData('stock', data);
  });

  // 6. 连接错误事件(如网络中断、服务器异常)
  eventSource.onerror = function(error) {
    console.error('SSE 连接错误:', error);
    // 错误时会自动重连,可手动处理(如提示用户)
    if (eventSource.readyState === EventSource.CLOSED) {
      console.log('SSE 连接已关闭,等待自动重连...');
    }
  };

  // 7. 连接关闭事件(仅主动关闭时触发,自动重连时不触发)
  eventSource.onclose = function() {
    console.log('SSE 连接已主动关闭');
  };
}

// 处理服务器推送的数据
function handleServerData(eventType, data) {
  switch (eventType) {
    case 'message':
      // 渲染普通消息
      const msgDom = document.createElement('div');
      msgDom.textContent = `系统消息:${data.content}`;
      document.getElementById('sse-container').appendChild(msgDom);
      break;
    case 'notice':
      // 渲染通知(弹窗)
      alert(`【实时通知】${data.title}:${data.content}`);
      break;
    case 'stock':
      // 更新行情数据
      document.getElementById('stock-price').textContent = data.price;
      document.getElementById('stock-time').textContent = data.timestamp;
      break;
  }
}

// 主动关闭 SSE 连接
function closeSSE() {
  if (eventSource) {
    eventSource.close(); // 关闭后不会自动重连
    eventSource = null;
  }
}

// 页面加载时建立连接
window.addEventListener('load', initSSE);

// 页面卸载时关闭连接
window.addEventListener('beforeunload', closeSSE);

2. 后端(Node.js)SSE 服务实现(基于 Express)​

SSE 服务器需持续向客户端发送文本流,需设置特定响应头和数据格式,这里以 Node.js + Express 为例(需安装 express:npm install express):

js 复制代码
// 后端:Node.js SSE 服务(server.js)
const express = require('express');
const app = express();
const port = 8080;

// 跨域配置(SSE 支持跨域,需允许特定域名)
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000'); // 允许前端域名
  res.setHeader('Access-Control-Allow-Credentials', 'true'); // 允许携带 Cookie
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  next();
});

// 解析 JSON 请求体(用于接收客户端 HTTP 接口数据)
app.use(express.json());

// 存储所有 SSE 连接(用于广播消息)
let sseClients = new Set();

// 1. SSE 核心接口(客户端通过 EventSource 连接此接口)
app.get('/sse', (req, res) => {
  // 设置 SSE 响应头(关键!)
  res.setHeader('Content-Type', 'text/event-stream'); // 声明为事件流
  res.setHeader('Cache-Control', 'no-cache'); // 禁用缓存
  res.setHeader('Connection', 'keep-alive'); // 保持连接
  res.flushHeaders(); // 立即发送响应头(避免缓存)

  // 存储当前连接(用于后续推送)
  sseClients.add(res);
  console.log('新 SSE 客户端连接,当前连接数:', sseClients.size);

  // 客户端断开连接时移除
  req.on('close', () => {
    sseClients.delete(res);
    console.log('SSE 客户端断开连接,当前连接数:', sseClients.size);
  });

  // 初始推送(连接成功后发送欢迎消息)
  sendSSEMessage(res, 'message', { content: '欢迎连接 SSE 实时推送服务' });
});

// 2. 接收客户端数据的 HTTP 接口(SSE 单向,需补充此接口实现双向通信)
app.post('/login', (req, res) => {
  const { username } = req.body;
  console.log('用户登录:', username);
  // 广播登录通知给所有 SSE 客户端
  broadcastSSEMessage('notice', {
    title: '登录通知',
    content: `${username} 已上线`,
    timestamp: new Date().toLocaleTimeString()
  });
  res.json({ code: 200, msg: '登录成功' });
});

// 3. 模拟服务器定时推送数据(如股票行情)
setInterval(() => {
  if (sseClients.size === 0) return;
  // 模拟股票价格波动
  const stockData = {
    code: '600000',
    name: '浦发银行',
    price: (10 + Math.random() * 2).toFixed(2),
    timestamp: new Date().toLocaleTimeString()
  };
  // 向所有客户端推送行情
  broadcastSSEMessage('stock', stockData);
}, 3000); // 每 3 秒推送一次

// 工具函数:向单个客户端发送 SSE 消息
function sendSSEMessage(res, eventType = 'message', data) {
  // SSE 数据格式规范(必须严格遵守):
  // 1. 每行以 "\n" 结尾;
  // 2. 事件类型:event: 事件名\n;
  // 3. 数据:data: 字符串数据\n;
  // 4. 可选 ID:id: 消息ID\n;
  // 5. 结束符:"\n\n"(必须两个换行)
  const message = `event: ${eventType}\n` +
                  `data: ${JSON.stringify(data)}\n` +
                  `id: ${Date.now()}\n` + // 用于断线重连时恢复数据
                  `\n`; // 消息结束符
  res.write(message); // 发送消息(持续写入流)
}

// 工具函数:向所有客户端广播 SSE 消息
function broadcastSSEMessage(eventType, data) {
  sseClients.forEach(res => {
    // 确保连接未关闭
    if (!res.finished) {
      sendSSEMessage(res, eventType, data);
    }
  });
}

// 启动服务器
app.listen(port, () => {
  console.log(`SSE 服务启动成功,地址:http://localhost:${port}`);
});

3. 前端 HTML 结构(实时行情 + 通知示例)

html 复制代码
<!DOCTYPE html>
<html>
<body>
  <h3>SSE 实时推送示例(股票行情 + 通知)</h3>
  <div>
    <h4>浦发银行(600000)</h4>
    <p>当前价格:<span id="stock-price">--</span></p>
    <p>更新时间:<span id="stock-time">--</span></p>
  </div>
  <div id="sse-container" style="margin-top: 20px; padding: 10px; border: 1px solid #ccc;"></div>
  <button onclick="closeSSE()" style="margin-top: 10px;">关闭 SSE 连接</button>
  <script src="前端 SSE 核心逻辑.js"></script>
</body>
</html>

核心 API

前端 EventSource 核心 API

属性 / 方法 说明
new EventSource(url[, options]) 创建 SSE 连接,url 为后端接口,options 可选(如 withCredentials: true 跨域带 Cookie)
onopen 连接建立成功时触发
onmessage 接收服务器默认 message 事件时触发(event.data 为消息内容)
onerror 连接错误或断开时触发(自动重连)
onclose 主动调用 close() 后触发(不会自动重连)
addEventListener(eventType, callback) 监听服务器自定义事件(如 notice、stock)
close() 主动关闭 SSE 连接(关闭后需重新创建 EventSource 实例才能重连)
readyState 连接状态:0(CONNECTING,连接中)、1(OPEN,已连接)、2(CLOSED,已关闭)

数据格式详解

服务器 SSE 响应格式规范(关键!)​

服务器必须以特定文本格式响应,否则客户端无法解析,核心格式如下:

js 复制代码
# 完整消息格式(每行以 \n 结尾,消息以 \n\n 结束)
event: 自定义事件名\n       # 可选,默认 message
data: 文本数据(可多行)\n  # 必选,支持 JSON 字符串
id: 消息ID\n               # 可选,用于断线重连时通过 Last-Event-ID 恢复
retry: 重连时间(毫秒)\n  # 可选,指定客户端自动重连间隔(默认 3000ms)
\n\n  # 消息结束符(必须两个换行)

# 示例 1:默认 message 事件
data: {"content": "这是默认事件消息"}\n
id: 1699999999999\n
\n\n

# 示例 2:自定义 notice 事件
event: notice\n
data: {"title": "通知", "content": "实时通知内容"}\n
retry: 5000\n
\n\n

# 示例 3:多行数据(data 可分多行)
data: {\n
data: "content": "多行消息",\n
data: "time": "2024-01-01"\n
data: }\n
\n\n

典型应用场景

  • 实时通知:如系统公告、订单状态更新、消息提醒(无需客户端向服务器反馈);
  • 实时数据推送:如股票 / 加密货币行情、体育赛事比分、实时监控数据(服务器持续更新,客户端仅展示);
  • 日志流展示:如后台系统操作日志、构建日志实时输出(服务器推送日志,客户端实时渲染);
  • 新闻 / 资讯推送:如实时新闻、活动更新(服务器有新内容时主动推送给客户端)。

进阶用法与优化

1. 断线重连与历史数据恢复

SSE 原生支持通过 Last-Event-ID 恢复历史数据,客户端重连时会在请求头携带上次接收的 id,服务器可据此推送未接收的消息:

js 复制代码
// 后端:获取客户端 Last-Event-ID,恢复历史数据
app.get('/sse', (req, res) => {
  const lastEventId = req.headers['last-event-id']; // 客户端重连时携带
  if (lastEventId) {
    console.log('客户端重连,上次消息 ID:', lastEventId);
    // 模拟恢复历史数据(实际场景从数据库/缓存查询)
    const historyData = { content: '恢复断线前未接收的消息' };
    sendSSEMessage(res, 'message', historyData);
  }
  // ... 其余逻辑不变
});

2. 自定义重连时间

服务器可通过 retry 字段指定客户端自动重连间隔,客户端也可通过 eventSource.retry 读取(但无法修改,需服务器返回):

js 复制代码
// 服务器响应时添加 retry 字段(客户端重连间隔改为 5 秒)
const message = `event: notice\n` +
                `data: {"content": "自定义重连间隔"}\n` +
                `retry: 500</doubaocanvas>
相关推荐
Mintopia1 小时前
🛰️ 低带宽环境下的 AIGC 内容传输优化技术
前端·人工智能·trae
h***34631 小时前
Nginx 缓存清理
android·前端·后端
Mintopia2 小时前
⚡Trae Solo Coding 的效率法则
前端·人工智能·trae
wa的一声哭了2 小时前
WeBASE管理平台部署-WeBASE-Web
linux·前端·网络·arm开发·spring boot·架构·区块链
孟陬2 小时前
我的 starship 终端配置
前端
Moment2 小时前
专为 LLM 设计的数据格式 TOON,可节省 60% Token
前端·javascript·后端
JarvanMo2 小时前
Apple更新App审核条款,严打擅自与第三方 AI 共享个人数据的应用
前端
掘金安东尼2 小时前
🧭 前端周刊第440期(2025年11月10日–11月16日)
前端
青梅主码2 小时前
麦肯锡联合QuantumBlack最新发布《2025年人工智能的现状:智能体、创新和转型》报告:32% 的企业预计会继续裁员
前端·人工智能·后端