Server-Sent Events

深入理解 Server-Sent Events(SSE)的完整机制:与 WebSocket 的对比、EventSource API、服务器端实现、以及实际应用场景。

Server-Sent Events(Server-Sent Events)

一、SSE 概述

1.1 什么是 Server-Sent Events

Server-Sent Events 是一种让服务器通过 HTTP 协议向浏览器推送消息的技术。与 WebSocket 的双向通信不同,SSE 是单向的——数据只能从服务器流向客户端。

// 创建 EventSource 连接
const eventSource = new EventSource('/api/events');

eventSource.onmessage = (event) => {
  console.log('Message:', event.data);
};

eventSource.onerror = (error) => {
  console.error('Error:', error);
};

1.2 SSE vs WebSocket

特性 Server-Sent Events WebSocket
方向 单向(服务器→客户端) 双向
协议 HTTP/2 特殊协议(ws://)
自动重连 支持 需要手动实现
二进制数据 不支持(仅文本) 支持
兼容性 良好(旧浏览器需 polyfill) 良好
简单性 简单 较复杂
防火墙 容易通过(HTTP) 可能被阻止

二、EventSource API

2.1 基本用法

const eventSource = new EventSource('/api/events');

// 监听消息
eventSource.onmessage = (event) => {
  console.log('Message:', event.data);
};

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

2.2 监听特定事件

// 服务器端发送事件类型
// event: update\ndata: {}\n\n

eventSource.addEventListener('update', (event) => {
  console.log('Update event:', event.data);
});

eventSource.addEventListener('notification', (event) => {
  console.log('Notification:', event.data);
});

2.3 连接管理

const eventSource = new EventSource('/api/events');

// 检查连接状态
console.log(eventSource.readyState);
// EventSource.CONNECTING = 0
// EventSource.OPEN = 1
// EventSource.CLOSED = 2

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

// 检查是否关闭
if (eventSource.readyState === EventSource.CLOSED) {
  console.log('Connection closed');
}

三、服务器端实现

3.1 SSE 格式

服务器需要发送特定格式的响应:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Access-Control-Allow-Origin: *

event: message
data: {"message": "Hello"}

event: update
data: {"status": "active"}

3.2 数据字段

字段 说明
event 事件类型
data 事件数据(可以多行)
id 事件 ID(用于断点续传)
retry 重连时间(毫秒)
: 注释

3.3 服务器示例(Node.js)

// Express 示例
app.get('/api/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // 发送初始消息
  res.write('event: connected\n');
  res.write('data: {"status": "connected"}\n\n');

  // 定时发送更新
  const interval = setInterval(() => {
    res.write(`event: update\n`);
    res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
  }, 1000);

  // 清理
  req.on('close', () => {
    clearInterval(interval);
    res.end();
  });
});

四、实际应用

4.1 实时数据更新

class RealTimeUpdater {
  constructor(url) {
    this.url = url;
    this.eventSource = null;
    this.handlers = new Map();
  }

  connect() {
    this.eventSource = new EventSource(this.url);

    this.eventSource.onmessage = (event) => {
      this.handleMessage(event);
    };

    this.eventSource.onerror = (error) => {
      console.error('SSE Error:', error);
      this.handleError(error);
    };
  }

  on(eventType, handler) {
    this.handlers.set(eventType, handler);
  }

  handleMessage(event) {
    const data = JSON.parse(event.data);
    console.log('Received:', data);
  }

  handleError(error) {
    // 可以在此实现自定义重连逻辑
  }

  disconnect() {
    if (this.eventSource) {
      this.eventSource.close();
      this.eventSource = null;
    }
  }
}

// 使用
const updater = new RealTimeUpdater('/api/events');

updater.on('stock-update', (data) => {
  updateStockPrice(data);
});

updater.on('notification', (data) => {
  showNotification(data.message);
});

updater.connect();

4.2 进度跟踪

// 服务器端
app.get('/api/progress', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');

  let progress = 0;
  const interval = setInterval(() => {
    progress += 10;
    res.write(`data: ${JSON.stringify({ progress })}\n\n`);

    if (progress >= 100) {
      res.write(`event: complete\n`);
      res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
      clearInterval(interval);
      res.end();
    }
  }, 500);
});

// 客户端
const eventSource = new EventSource('/api/progress');

eventSource.onmessage = (event) => {
  const { progress } = JSON.parse(event.data);
  updateProgressBar(progress);
};

eventSource.addEventListener('complete', () => {
  console.log('Process completed');
  eventSource.close();
});

五、断点续传与 ID

5.1 使用 Last-Event-ID

// 客户端自动发送 Last-Event-ID
const eventSource = new EventSource('/api/events');

eventSource.addEventListener('message', (event) => {
  // event.lastEventId 包含服务器发送的 ID
  console.log('Last ID:', event.lastEventId);
});

// 服务器端接收
app.get('/api/events', (req, res) => {
  const lastEventId = req.headers['last-event-id'];
  console.log('Last Event ID:', lastEventId);

  // 从 lastEventId 之后开始发送
});

5.2 重连处理

// 服务器可以指定重连时间
res.write(`retry: 5000\n\n`);  // 5秒后重连

// 客户端重连逻辑
eventSource.onerror = (error) => {
  if (eventSource.readyState === EventSource.CLOSED) {
    console.log('Connection closed permanently');
    // 显示重连按钮
  }
};

参考资料

延展阅读