Skip to content

Server-Sent Events 基础

Server-Sent Events(SSE)是一种 HTML5 技术,允许服务器通过 HTTP 连接向浏览器单向推送数据。与 WebSocket 的双向通信不同,SSE 专门用于服务器到客户端的单向数据流,特别适合实时通知、股票行情、日志监控等场景。SSE 基于 HTTP 协议,使用简单,自动重连,且对服务器要求较低。

前置知识

阅读本节前,建议先了解:WebSocket 实战

基础概念

什么是 SSE

Server-Sent Events 让服务器可以通过持久的 HTTP 连接持续向客户端推送消息。客户端使用 EventSource API 接收这些消息,无需复杂的连接管理。

SSE vs 传统轮询

特性短轮询长轮询SSE
通信方式客户端定时请求服务器保持请求持久连接推送
实时性差(有延迟)较好优秀
服务器开销高(频繁请求)低(单连接)
实现复杂度简单中等简单
协议HTTPHTTPHTTP
自动重连需手动实现需手动实现内置

SSE 的特点

特性说明
单向通信服务器 → 客户端
自动重连浏览器自动处理断线重连
文本协议text/event-stream Content-Type
事件类型支持命名事件和自定义事件
HTTP 基础不需要特殊协议或端口

语法与 API

服务器端数据格式

SSE 的数据格式是纯文本,每条消息由若干字段组成:

data: 消息内容\n
\n
// 简单消息
data: Hello World\n
\n

// 多行消息
data: 第一行\n
data: 第二行\n
data: 第三行\n
\n

// 带事件类型的消息
event: notification\n
data: 你有新消息\n
\n

// 带 ID 的消息
id: 12345\n
data: 带ID的消息\n
\n

// 带重试间隔的消息
retry: 3000\n
data: 3秒后重连\n
\n

SSE 服务端示例(Node.js)

javascript
// Node.js SSE 服务端示例
const http = require('http');

const server = http.createServer((req, res) => {
  // 设置 SSE 响应头
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*'
  });

  // 每秒发送一次数据
  let counter = 0;
  const interval = setInterval(() => {
    counter++;

    // 发送数据
    res.write(`data: ${JSON.stringify({ time: new Date().toISOString(), count: counter })}\n\n`);

    // 发送自定义事件
    if (counter % 5 === 0) {
      res.write(`event: milestone\ndata: 已发送 ${counter} 条消息\n\n`);
    }
  }, 1000);

  // 客户端断开时清理
  req.on('close', () => {
    clearInterval(interval);
  });
});

server.listen(3000, () => {
  console.log('SSE 服务器运行在 http://localhost:3000');
});

客户端 EventSource

javascript
// 创建 EventSource 连接
const eventSource = new EventSource('http://localhost:3000/sse');

// 监听默认消息(无事件类型的消息)
eventSource.onmessage = (event) => {
  console.log('收到消息:', event.data);
  const data = JSON.parse(event.data);
  console.log('时间:', data.time, '计数:', data.count);
};

// 监听自定义事件
eventSource.addEventListener('milestone', (event) => {
  console.log('里程碑:', event.data);
});

// 监听连接打开
eventSource.onopen = () => {
  console.log('SSE 连接已建立');
};

// 监听错误
eventSource.onerror = (event) => {
  console.error('SSE 错误:', event);
  // EventSource 会自动重连
};

// 关闭连接
eventSource.close();

详细说明

EventSource 属性

属性类型说明
readyStatenumber连接状态(0=连接中, 1=已连接, 2=已关闭)
urlstring连接 URL
withCredentialsboolean是否发送跨域凭证
javascript
const es = new EventSource('http://localhost:3000/sse');

// readyState 常量
EventSource.CONNECTING; // 0
EventSource.OPEN;       // 1
EventSource.CLOSED;     // 2

// 跨域发送凭证
es.withCredentials = true;

MessageEvent 对象

javascript
eventSource.onmessage = (event) => {
  console.log(event.data);    // 消息数据
  console.log(event.origin);  // 来源域名
  console.log(event.lastEventId); // 事件 ID
  console.log(event.type);    // 事件类型('message')
};
属性说明
data消息内容字符串
origin服务器域名
lastEventId服务器发送的最后事件 ID
type事件类型名称
bubbles是否冒泡
cancelable是否可取消

自动重连机制

EventSource 内置自动重连机制:

  1. 连接断开时自动尝试重新连接
  2. 使用服务器设置的 retry 间隔(默认 3 秒)
  3. 重连时发送 Last-Event-ID 请求头,服务器可从断点续传
javascript
// 服务器端发送重试间隔
// retry: 5000\n\n  // 5 秒后重连

// 客户端自动重连流程
// 1. 连接断开
// 2. 等待 retry 间隔(默认 3 秒)
// 3. 自动发送新请求
// 4. 请求头包含 Last-Event-ID(如果有)

实战示例

SSE 实时数据面板

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>SSE 实时数据面板</title>
  <style>
    body { font-family: -apple-system, sans-serif; padding: 20px; background: #f5f7fa; }

    .dashboard {
      max-width: 600px; margin: 0 auto;
    }

    h2 { text-align: center; color: #1a1a2e; }

    .status-bar {
      display: flex; justify-content: space-between; padding: 12px 16px;
      background: white; border-radius: 8px; margin-bottom: 16px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    }

    .status-dot {
      width: 10px; height: 10px; border-radius: 50%;
      display: inline-block; margin-right: 6px;
    }

    .status-dot.connected { background: #4CAF50; }
    .status-dot.disconnected { background: #F44336; }

    .metrics {
      display: grid; grid-template-columns: repeat(3, 1fr);
      gap: 12px; margin-bottom: 16px;
    }

    .metric-card {
      text-align: center; padding: 20px;
      background: white; border-radius: 12px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
    }

    .metric-value { font-size: 32px; font-weight: 700; color: #1a73e8; }
    .metric-label { font-size: 13px; color: #888; margin-top: 4px; }

    .event-log {
      height: 300px; overflow-y: auto; padding: 12px;
      background: white; border-radius: 12px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.08);
      font-family: monospace; font-size: 13px;
    }

    .log-entry { padding: 4px 0; border-bottom: 1px solid #f0f0f0; }
    .log-time { color: #aaa; }
    .log-data { color: #333; }
    .log-event { color: #1a73e8; font-weight: 600; }

    .controls {
      display: flex; gap: 10px; margin-top: 12px;
    }

    .btn {
      flex: 1; padding: 10px; border: none; border-radius: 8px;
      cursor: pointer; font-size: 14px; font-weight: 600; color: white;
    }
  </style>
</head>
<body>
  <div class="dashboard">
    <h2>SSE 实时数据面板</h2>

    <div class="status-bar">
      <span><span class="status-dot disconnected" id="statusDot"></span><span id="statusText">未连接</span></span>
      <span id="eventCount">事件数: 0</span>
    </div>

    <div class="metrics">
      <div class="metric-card">
        <div class="metric-value" id="cpuValue">--</div>
        <div class="metric-label">CPU 使用率</div>
      </div>
      <div class="metric-card">
        <div class="metric-value" id="memValue">--</div>
        <div class="metric-label">内存使用率</div>
      </div>
      <div class="metric-card">
        <div class="metric-value" id="connValue">--</div>
        <div class="metric-label">活跃连接</div>
      </div>
    </div>

    <div class="event-log" id="eventLog">等待连接...</div>

    <div class="controls">
      <button class="btn" style="background:#4CAF50;" id="connectBtn" onclick="connectSSE()">连接</button>
      <button class="btn" style="background:#F44336;" id="disconnectBtn" onclick="disconnectSSE()" disabled>断开</button>
    </div>
  </div>

  <script>
    let eventSource = null;
    let count = 0;

    function connectSSE() {
      // 使用模拟 SSE(实际使用 EventSource 连接真实服务器)
      // const eventSource = new EventSource('http://localhost:3000/sse');
      count = 0;

      // 模拟 SSE 数据流
      const interval = setInterval(() => {
        count++;
        const cpu = (30 + Math.random() * 40).toFixed(1);
        const mem = (50 + Math.random() * 30).toFixed(1);
        const conn = Math.floor(Math.random() * 200 + 50);

        document.getElementById('cpuValue').textContent = cpu + '%';
        document.getElementById('memValue').textContent = mem + '%';
        document.getElementById('connValue').textContent = conn;

        addLog('data', `CPU: ${cpu}%, 内存: ${mem}%, 连接: ${conn}`);
        document.getElementById('eventCount').textContent = `事件数: ${count}`;

        if (count % 5 === 0) {
          addLog('milestone', `已推送 ${count} 条数据`);
        }
      }, 2000);

      eventSource = { interval, active: true };

      document.getElementById('statusDot').className = 'status-dot connected';
      document.getElementById('statusText').textContent = '已连接';
      document.getElementById('connectBtn').disabled = true;
      document.getElementById('disconnectBtn').disabled = false;
    }

    function disconnectSSE() {
      if (eventSource) {
        clearInterval(eventSource.interval);
        eventSource = null;
      }
      document.getElementById('statusDot').className = 'status-dot disconnected';
      document.getElementById('statusText').textContent = '已断开';
      document.getElementById('connectBtn').disabled = false;
      document.getElementById('disconnectBtn').disabled = true;
    }

    function addLog(type, data) {
      const log = document.getElementById('eventLog');
      const time = new Date().toLocaleTimeString();
      log.innerHTML += `<div class="log-entry">
        <span class="log-time">[${time}]</span>
        <span class="log-event">[${type}]</span>
        <span class="log-data">${data}</span>
      </div>`;
      log.scrollTop = log.scrollHeight;
    }
  </script>
</body>
</html>

注意事项

  1. 单向通信:SSE 只支持服务器到客户端,客户端不能发送数据
  2. 文本协议:SSE 只能传输文本数据,不能传输二进制
  3. 连接限制:每个域名最多 6 个 SSE 连接
  4. IE 不支持:IE 不支持 EventSource(Edge 支持)
  5. 自动重连:浏览器会在连接断开后自动重连

最佳实践

  1. 使用 event 字段区分不同类型的消息
  2. 使用 id 字段支持断点续传
  3. 使用 retry 字段控制重连间隔
  4. 在服务器端正确设置 CORS 头
  5. 客户端监听 error 事件处理异常

下一节

继续学习:EventSource API

参考链接