HTTP SSE Server-Sent Events 推送

FreeGuideOnline 最新 2026-07-10

什么是 Server-Sent Events?

Server-Sent Events(简称 SSE)是一项基于 HTTP 协议的服务器推送技术。它允许服务器通过一个持久的、单向的 HTTP 连接向客户端不断发送数据更新。与请求-响应模式不同,SSE 的连接由客户端发起,之后服务器可随时主动推送数据,客户端无需反复轮询。

  • 协议基础:基于标准 HTTP/HTTPS,无需额外协议或握手。
  • 数据方向:单向——从服务器流向客户端。
  • 内容格式:UTF-8 编码的纯文本,遵循 text/event-stream MIME 类型。
  • 浏览器支持:所有现代浏览器原生支持 EventSource API,除 IE 外兼容性极佳。

SSE 是如何工作的?

建立连接

客户端通过 EventSource 对象向服务器发起一个 HTTP GET 请求,并在请求头中声明可接受的 MIME 类型:Accept: text/event-stream。服务器一旦接受该连接,必须返回状态码 200 以及响应头 Content-Type: text/event-stream,并保持连接不关闭。

消息格式

SSE 的数据流由一系列以两个换行符 \n\n 分隔的字段组成。每个字段以关键字开头,后跟冒号和值。最常见的字段有:

  • data:携带消息的实际内容。可以多行连续出现,合并成一个完整消息。
  • event:自定义事件类型。不指定时,默认触发 message 事件。
  • id:消息的唯一标识,用于断线重连时的 Last-Event-ID 头。
  • retry:指定客户端在连接断开后重新尝试连接的毫秒数。

一个完整的 SSE 消息示例:

event: user-update
id: 42
data: {"name": "Alice", "status": "online"}

data: 这条消息没有指定事件类型,会触发默认 message 事件

注意:每条数据末尾由两个换行符 \n\n 终结,表示一条消息的结束。

连接保持与重连

若连接意外断开,浏览器内置的 EventSource 会自动尝试重连。服务器可以在任意时点发送 retry 字段来调整重连间隔。重连时,浏览器会在新请求头中携带 Last-Event-ID,服务器可利用该 ID 从断点恢复数据发送。


SSE 与 WebSocket 的对比

特性 SSE WebSocket
数据传输方向 单向(服务器到客户端) 双向
协议 HTTP/1.1 或 HTTP/2 独立协议(ws:// / wss://)
复杂程度 非常低,只需普通 HTTP 服务器即可 较高,需处理升级握手和帧协议
浏览器 API EventSource 简洁易用 WebSocket API
自动重连 内置支持 需手动实现
二进制数据 仅支持文本(UTF-8) 支持文本与二进制
防火墙/代理友好 极友好,在标准 HTTP 端口上运行 可能被某些代理阻挡
HTTP/2 多路复用 天然兼容,可共享连接 独立连接,无法复用 HTTP/2 流

选型建议:当你只需要服务器主动推送更新(例如新闻推送、股价更新、赛事比分),且从客户端到服务器的通讯可通过常规 AJAX 完成时,SSE 是更简单、更易于扩展的选择。需要双向实时交互(如聊天、协作编辑)才考虑 WebSocket。


适用场景

  • 实时仪表盘与监控:系统指标、日志流、用户行为分析。
  • 社交动态与通知:新消息提醒、点赞、评论更新。
  • 金融行情推送:股票、加密货币、汇率变动。
  • 物联网设备状态:传感器读数、设备告警。
  • AI 生成内容流:如流式输出的聊天机器人或长篇文本生成。
  • 游戏中的状态同步:适合单向状态广播(如排行榜、全局倒计时)。

服务器端实现

以下以 Node.js + Express 为例,展示一个基础 SSE 服务端。

1. 基本推送服务

const express = require('express');
const app = express();

app.get('/events', (req, res) => {
  // 设置 SSE 必要的响应头
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    // 允许跨域(按需配置)
    'Access-Control-Allow-Origin': '*'
  });

  // 发送初始注释或 retry
  res.write(': connected\n\n');

  // 定时发送当前时间
  const intervalId = setInterval(() => {
    const data = {
      time: new Date().toISOString(),
      value: Math.floor(Math.random() * 100)
    };
    // 发送带事件类型的消息
    res.write(`event: update\ndata: ${JSON.stringify(data)}\n\n`);
  }, 2000);

  // 客户端断开连接时清理资源
  req.on('close', () => {
    clearInterval(intervalId);
    res.end();
  });
});

app.listen(3000, () => console.log('SSE server running on port 3000'));

2. 利用消息 ID 实现断点续传

为每条消息添加 id 字段,客户端重连时会通过 Last-Event-ID 请求头发送最后收到的 ID。服务器可据此从历史记录中继续推送。

let lastId = 0;

app.get('/events-with-id', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  // 检查客户端断线时最后的 id
  const lastEventId = req.headers['last-event-id'];
  if (lastEventId) {
    // 实际场景中可从消息队列或日志中获取该 ID 之后的事件
    console.log(`Client reconnected with id: ${lastEventId}`);
  }

  const intervalId = setInterval(() => {
    lastId++;
    const message = {
      id: lastId,
      text: `Event number ${lastId}`
    };
    res.write(`id: ${lastId}\ndata: ${JSON.stringify(message)}\n\n`);
  }, 1000);

  req.on('close', () => {
    clearInterval(intervalId);
  });
});

3. 生产环境考虑

  • 连接管理:每个客户端占用一个连接,需注意服务器最大连接数限制。使用 HTTP/2 可复用连接,大幅提升并发能力。
  • 负载均衡:需确保同一客户端的请求始终落到同一后端实例(粘性会话),否则重连 ID 机制会失效。或采用发布/订阅模式将事件广播至所有实例。
  • 消息格式:纯文本传输,数据量大时可考虑启用 Gzip 压缩(但注意缓冲可能导致延迟)。
  • 超时:一些代理或 CDN 会断开长时间空闲的 HTTP 连接。可定期发送心跳注释(以冒号开头的行,如 : heartbeat\n\n)保持连接。

客户端使用

1. 建立连接与接收消息

const eventSource = new EventSource('http://localhost:3000/events');

// 默认消息事件(未指定 event 字段)
eventSource.onmessage = (event) => {
  console.log('Default message:', event.data);
};

// 自定义事件监听
eventSource.addEventListener('update', (event) => {
  const data = JSON.parse(event.data);
  console.log('Update received:', data);
  // 更新 UI...
});

// 连接打开时触发
eventSource.onopen = () => {
  console.log('SSE connection opened');
};

// 错误处理(包含连接关闭等情况)
eventSource.onerror = (err) => {
  console.error('SSE error:', err);
  // 浏览器会自动重连,除非手动关闭
};

2. 主动关闭连接

eventSource.close();

3. 重连与 readyState

  • EventSource.CONNECTING (0):正在连接或重连。
  • EventSource.OPEN (1):连接已建立。
  • EventSource.CLOSED (2):连接已关闭(手动调用 close() 或连接失败且浏览器放弃重连)。

高级特性

多路复用与 HTTP/2

当使用 HTTP/2 时,多个 SSE 连接可复用在同一个 TCP 连接上(流并发),大幅减少资源消耗。所有现代浏览器和主流服务器(Nginx、Caddy、Node.js)均支持。只需确保服务器启用 HTTP/2,无需改动 SSE 代码。

仅发送注释保持连接

注释行可作心跳使用,对客户端不可见:

: heartbeat

客户端 EventSource 会忽略它,但能刷新底层 TCP 连接,防止代理切断。

动态重试间隔

通过 retry 字段控制浏览器重连频率:

retry: 3000

表示断开后 3 秒重新连接。每条新 retry 值都会覆盖前一个设定。

自定义事件与多通道

通过 event 字段可将数据分类推送,客户端按需监听。例如同一个连接可同时推送 chatalertsystem 事件,客户端只处理自己关心的类型。

CORS 支持

跨域请求时,服务端必须返回 Access-Control-Allow-Origin 头。浏览器不会在 EventSource 中发送非简单请求(如包含自定义头),但会发送 Origin 头,服务端需允许。


安全与性能最佳实践

  • 始终使用 HTTPS:防止数据篡改和窃听。
  • 鉴权:SSE 连接无法自定义请求头,通常将令牌放在查询参数或 Cookie 中。如 ?token=xxx,服务端验证后维持连接。
  • 防止资源耗尽:限制单个 IP 的连接数,设置合理的心跳间隔,及时释放关闭的连接。
  • 消息大小控制:SSE 是纯文本流,避免单条消息过大导致内存压力。
  • 避免缓存:在响应头中设置 Cache-Control: no-cache(或 no-store),防止中间节点缓存事件流。
  • 内容安全策略 (CSP):如使用 connect-src 指令,确保 EventSource 的 URL 被允许。

常见问题排查

现象 可能原因 解决措施
客户端连接后不触发事件 服务器未返回正确的 Content-Type 或消息格式缺少两个换行 检查响应头必须为 text/event-stream,每条消息以 \n\n 结尾
连接频繁断开重连 网络代理或 CDN 超时切断 添加定期注释心跳,调整 retry 间隔
重连后数据丢失 服务器未实现 Last-Event-ID 逻辑 在服务端读取该请求头,补发缺失事件
跨域请求失败 缺少 CORS 响应头 添加 Access-Control-Allow-Origin
HTTP/1.1 并发瓶颈 每个客户端独占一个 TCP 连接 升级到 HTTP/2 或使用负载均衡分散连接

总结

Server-Sent Events 是一种轻量、易维护的实时推送方案。它完美契合了“服务器主动通知客户端”的多数场景,且得益于原生 HTTP 协议和浏览器的自动重连、事件管理等支持,极大降低了开发复杂性。当你面临实时单向数据推送需求时,不妨优先考虑 SSE——你可能会惊喜于它的简洁与高效。