Server-Sent Events (SSE) 服务器推送事件

基础概念

SSE(服务器推送事件) 是一套基于 HTTP 单向通信的浏览器标准,允许服务端主动向客户端持续推送数据,客户端无需反复轮询。

  • 定位:单向长连接(服务端 → 客户端),客户端不能通过该通道发数据给服务端。
  • 标准:HTML5 规范,原生浏览器支持,无需第三方库。
  • 对比:区别于 WebSocket(双向通信),SSE 更适合纯服务端推送场景,实现更简单。

核心特点

  1. 基于标准 HTTP/HTTPS,兼容现有代理、认证、跨域规则,无需额外端口。
  2. 自动重连:连接中断后浏览器会定时自动重试连接。
  3. 文本格式:仅传输UTF-8 文本,不支持二进制(可 Base64 编码变通)。
  4. 原生 API:浏览器内置 EventSource 对象,开箱即用。
  5. 限流可控:支持事件 ID、重连间隔、自定义事件类型。

适用 & 不适用场景

✅ 适合场景

  • 实时消息播报、公告、系统通知
  • 实时监控、日志流、数据大屏(指标刷新)
  • 直播弹幕、实时行情、进度条(文件导出/任务执行进度)
  • 简单单向推送(首选 SSE,比 WebSocket 轻量)

❌ 不适合场景

  • 双向交互(聊天、游戏、协同编辑)→ 用 WebSocket
  • 高频海量二进制流(视频、大文件)
  • 严格低延迟、高并发双向通信

工作原理

  1. 客户端通过 EventSource 发起 HTTP GET 请求 建立连接。
  2. 服务端响应头标记为 text/event-stream,不关闭 TCP 连接,转为长连接。
  3. 服务端持续按 SSE 格式分段推送数据。
  4. 客户端解析流、触发对应事件回调。
  5. 网络断开时,EventSource 自动等待一段时间后重新发起连接。

SSE 数据格式(服务端输出规范)

响应必须满足两个条件:

  1. 响应头:
    Content-Type: text/event-stream
    Cache-Control: no-cache
    Connection: keep-alive
    
  2. 响应体:按行分割,每行以 字段: 开头,多条消息用空行 \n\n 分隔。

标准字段(共 5 种)

字段 说明 格式示例
data 主体数据,最常用;多行数据用多个 data: data: 消息内容
event 自定义事件名,客户端按事件名监听 event: notice
id 事件 ID,断线重连时客户端会带上 Last-Event-ID 头,用于断点续传 id: 1001
retry 重连等待时间(单位:毫秒),覆盖浏览器默认值 retry: 3000
: 注释行,客户端直接忽略(常用于心跳保活) : 心跳包

格式规则

  • 每条完整事件必须以两个换行 \n\n 结尾。
  • 单个 data 内容含换行,可写多行 data:。
  • 字段名和冒号后建议加空格(规范写法)。

示例 1:基础单条消息

data: 这是一条普通推送消息

示例 2:多行数据

data: 第一行内容
data: 第二行内容

示例 3:带事件名 + ID + 重连时间

event: message
id: 2026052801
retry: 5000
data: 带自定义事件和ID的消息

示例 4:心跳包(防代理断开连接)

: heartbeat

客户端 API:EventSource

1. 基础构造函数

// 语法:new EventSource(url[, options])
const es = new EventSource('/sse/stream');
  • 默认仅支持 GET 请求,无法发 POST。
  • options.withCredentials = true 开启跨域携带 Cookie/认证头。

2. 内置事件

(1)onmessage:接收默认事件(无 event: 字段时触发)

es.onmessage = function (e) {
  console.log("收到数据:", e.data);
  console.log("事件ID:", e.lastEventId);
};

(2)onopen:连接建立成功

es.onopen = function () {
  console.log("SSE 连接已建立");
};

(3)onerror:连接异常/断开

es.onerror = function (err) {
  console.error("SSE 连接出错/断开", err);
  // 可选:手动关闭连接,停止自动重连
  // es.close();
};

(4)监听自定义事件(event: xxx)

// 对应服务端 event: notice
es.addEventListener('notice', function(e) {
  console.log("通知消息:", e.data);
});

// 对应服务端 event: progress
es.addEventListener('progress', function(e) {
  console.log("任务进度:", e.data + "%");
});

3. 关键方法 & 属性

  • es.close():手动关闭连接,关闭后不会自动重连。
  • es.readyState:连接状态枚举
    • 0:CONNECTING 正在连接
    • 1:OPEN 连接正常
    • 2:CLOSED 已关闭

4. 断点续传(结合 Last-Event-ID)

客户端断线重连时,会自动在请求头带上:

Last-Event-ID: 2026052801

服务端读取该头,从该 ID 之后的消息继续推送,实现断点恢复。

5. 限制说明

  • 同源策略:默认跨域需服务端配置 CORS。
  • 部分浏览器单域名 SSE 连接数有限制(通常 6~10 条),大量长连接场景需注意。

服务端实现示例(主流语言)

核心逻辑:设置响应头 + 循环持续输出 SSE 格式文本 + 不结束响应

1. Node.js (Express)

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

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(); // 立即发送响应头

  let count = 0;
  // 定时推送数据
  const timer = setInterval(() => {
    count++;
    // 输出标准 SSE 格式
    res.write(`id: ${count}\n`);
    res.write(`event: msg\n`);
    res.write(`data: 实时计数:${count}\n\n`);
  }, 1000);

  // 客户端断开连接时清理定时器
  req.on('close', () => {
    clearInterval(timer);
    console.log('客户端断开 SSE 连接');
  });
});

app.listen(3000);

2. Python (FastAPI)

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import time

app = FastAPI()

def sse_generator():
    count = 0
    while True:
        count += 1
        # SSE 格式字符串
        yield f"id: {count}\nevent: msg\ndata: 计数 {count}\n\n"
        time.sleep(1)

@app.get("/sse")
async def sse_stream():
    return StreamingResponse(
        sse_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive"
        }
    )

3. Java (Spring Boot)

使用 SseEmitter(Spring 原生封装,极简)

import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@RestController
public class SseController {

    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    @GetMapping("/sse")
    public SseEmitter sse() {
        SseEmitter emitter = new SseEmitter(0L); // 0 = 永不超时
        executor.scheduleAtFixedRate(() -> {
            try {
                long num = System.currentTimeMillis();
                // 发送事件:数据、事件名、ID
                emitter.send(SseEmitter.event()
                        .id(String.valueOf(num))
                        .name("msg")
                        .data("时间戳:" + num));
            } catch (IOException e) {
                emitter.complete(); // 连接断开,结束
            }
        }, 0, 1, TimeUnit.SECONDS);

        return emitter;
    }
}

SSE 与 轮询、WebSocket 对比

特性 短轮询 长轮询 SSE WebSocket
通信方向 单向(客户端拉) 单向(客户端拉) 单向(服务端推) 双向
底层 HTTP 短连接 HTTP 长连接 HTTP 长连接 TCP 自定义协议
自动重连 需手动实现 需手动实现 浏览器原生支持 需手动实现
数据格式 任意 任意 仅文本 文本/二进制
实现复杂度 低 中 极低 较高
适用场景 低频查询 准实时推送 单向实时推送 双向交互、游戏、聊天

常见问题 & 踩坑

  1. 连接被代理/网关断开
    解决方案:定时发送心跳包(: heartbeat\n\n),维持长连接。

  2. 跨域报错
    服务端配置 CORS,允许 Origin、允许 GET、暴露 Last-Event-ID。

  3. 页面刷新/标签页休眠导致重连
    属于浏览器正常行为,配合 id 做断点续传即可。

  4. 无法发送 POST 请求
    SSE 标准仅支持 GET;如需传参可放 URL 或 Cookie。

  5. 多标签页连接数超限
    单域名 SSE 连接有上限,复杂系统可改用 WebSocket 或合并连接。


总结

  • SSE 是轻量、简单的 HTTP 单向长连接方案,优先用于服务端主动推文本数据。
  • 相比轮询:减少请求开销、实时性更高;相比 WebSocket:实现简单、兼容 HTTP 生态。
  • 选型口诀:只推不收用 SSE,双向交互用 WebSocket。

原文出处:/show_1GW3OU9LaDOb.html