SSE 协议的原理和使用方法

Server-Sent Events (SSE) 是一种基于 HTTP 的 服务器到客户端的单向实时通信协议 ,专门用于实现服务器主动向浏览器推送数据。它比 WebSocket 更轻量级,适合需要 单向实时更新 的场景(如实时通知、股票行情、日志流等)。


一、SSE 协议原理

1. 核心机制

  • 基于 HTTP 长连接:客户端发起一个普通 HTTP 请求,服务器保持连接打开,持续发送数据。
  • 文本协议:数据以纯文本格式传输,默认编码为 UTF-8。
  • 事件驱动 :服务器可以发送不同类型的事件(如 messagecustom-event),客户端通过监听事件处理数据。
  • 自动重连:若连接中断,浏览器默认会尝试重新连接。

2. 数据格式要求

服务器返回的数据必须遵循 SSE 规范:

text 复制代码
event: custom-event\n          // 事件类型(可选,默认是 message)
data: {"key": "value"}\n       // 数据内容(可多行)
id: 12345\n                    // 事件 ID(用于断线重连)
retry: 5000\n                  // 重连间隔(毫秒,可选)
\n                             // 空行表示事件结束
  • 关键点
    • 每个事件以空行(\n\n)分隔。
    • data 字段可多次出现,最终值是多行合并后的结果。
    • 若未指定 event,客户端默认监听 onmessage 事件。

3. 与 WebSocket 对比

特性 SSE WebSocket
通信方向 单向(服务器→客户端) 双向
协议 HTTP 独立的 WS 协议
数据格式 文本 二进制/文本
断线重连 内置支持 需手动实现
浏览器兼容性 现代浏览器(IE 除外) 广泛支持
适用场景 实时通知、数据流 聊天、游戏、双向交互

二、SSE 使用方法

1. 后端实现(FastAPI 示例)

python 复制代码
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json

app = FastAPI()

async def generate_sse_data():
    for i in range(10):
        await asyncio.sleep(1)
        # 发送 SSE 格式数据
        yield f"data: {json.dumps({'count': i, 'status': 'OK'})}\n\n"

@app.get("/sse")
async def sse_endpoint():
    return StreamingResponse(
        generate_sse_data(),
        media_type="text/event-stream"  # 必须指定此 MIME 类型
    )

2. 前端实现

前端接收 FastAPI 流式接口的数据主要依赖于 Fetch API 的流式处理能力,通过逐步读取分块(chunk)数据实现。以下是实现步骤和示例代码:


1. 前端接收流式数据(基础版)

确保你的 FastAPI 接口返回的是 StreamingResponse,例如:

python 复制代码
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

async def generate_data():
    for i in range(5):
        await asyncio.sleep(1)  # 模拟延迟
        yield f"Data chunk {i}\n"  # 发送数据块(注意换行符分隔)

@app.get("/stream")
async def stream_data():
    return StreamingResponse(generate_data(), media_type="text/plain")

使用 fetch + ReadableStream 逐步读取数据:

javascript 复制代码
async function fetchStream() {
  try {
    const response = await fetch('http://your-api/stream');
    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
      const { done, value } = await reader.read();
      if (done) break; // 流结束
      const chunk = decoder.decode(value);
      console.log('Received chunk:', chunk);
      // 更新页面内容(例如:将数据追加到 DOM)
      document.getElementById('output').textContent += chunk;
    }
  } catch (error) {
    console.error('Stream error:', error);
  }
}

// 调用函数
fetchStream();

2. 处理 JSON 流式数据

如果后端返回的是 换行分隔的 JSON 数据(NDJSON)

python 复制代码
# FastAPI 示例:返回 JSON 流
async def generate_json():
    for i in range(3):
        await asyncio.sleep(1)
        yield json.dumps({"id": i, "message": "Hello"}) + "\n"  # 注意换行符

@app.get("/json-stream")
async def json_stream():
    return StreamingResponse(generate_json(), media_type="application/x-ndjson")

前端按行解析 JSON:

javascript 复制代码
let buffer = '';

async function fetchJsonStream() {
  const response = await fetch('http://your-api/json-stream');
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    
    // 按换行符分割处理
    const lines = buffer.split('\n');
    buffer = lines.pop(); // 剩余部分保留到下次处理
    for (const line of lines) {
      if (line.trim() === '') continue;
      try {
        const data = JSON.parse(line);
        console.log('Parsed JSON:', data);
      } catch (e) {
        console.error('JSON parse error:', e);
      }
    }
  }
}

4. 使用 EventSource(SSE 协议)

如果后端支持 Server-Sent Events (SSE),可以更简化:

python 复制代码
# FastAPI 设置 SSE
from fastapi.responses import StreamingResponse

@app.get("/sse-stream")
async def sse_stream():
    async def event_generator():
        for i in range(5):
            await asyncio.sleep(1)
            yield f"data: {i}\n\n"  # SSE 格式要求

    return StreamingResponse(event_generator(), media_type="text/event-stream")

前端使用 EventSource

javascript 复制代码
// 创建 EventSource 对象
const eventSource = new EventSource('http://your-api/sse');

// 监听默认消息事件(未指定 event 字段时触发)
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('Received data:', data);
};

// 监听自定义事件(需服务器发送 event: custom-event)
eventSource.addEventListener('custom-event', (event) => {
  console.log('Custom event:', event.data);
});

// 监听错误事件
eventSource.onerror = (error) => {
  console.error('SSE Error:', error);
  // 可根据错误类型决定是否关闭连接
  // eventSource.close();
};

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

3. 高级功能

  • 断线重连

    • 浏览器自动处理重连,可通过 retry 字段指定重试间隔。
    • 服务器发送 id 字段时,客户端会在重连时通过 Last-Event-ID 请求头告知服务器最后接收的事件 ID。
  • 自定义事件

    python 复制代码
    # FastAPI 后端发送自定义事件
    yield f"event: status\ndata: Server is alive\n\n"
    javascript 复制代码
    // 前端监听
    eventSource.addEventListener('status', (e) => {
      console.log(e.data); // 输出 "Server is alive"
    });

三、关键注意事项

1. 跨域问题

  • 跨域问题 :确保 FastAPI 已配置 CORS 中间件。

    python 复制代码
    from fastapi.middleware.cors import CORSMiddleware
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],  # 根据需求调整
        allow_methods=["*"],
    )

2. 连接保持

  • 防止超时 :某些服务器或代理可能关闭空闲连接,需定期发送心跳包:

    python 复制代码
    async def generate_sse_data():
        while True:
            yield ": keep-alive\n\n"  # 注释行(以冒号开头)可作为心跳
            await asyncio.sleep(15)
  • 流式中断 :如果需要主动终止流,可以使用 AbortController

    javascript 复制代码
    const controller = new AbortController();
    fetch('/stream', { signal: controller.signal });
    // 终止请求
    controller.abort();

3. 数据格式验证

  • 错误处理 :前端需捕获 JSON 解析错误:

    javascript 复制代码
    eventSource.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data);
      } catch (e) {
        console.error("Invalid JSON:", event.data);
      }
    };

四、适用场景

  • ✅ 实时通知(如邮件、站内信)
  • ✅ 数据监控(如服务器状态、实时图表)
  • ✅ 新闻推送、股票行情
  • ❌ 需要客户端频繁发送数据的场景(用 WebSocket)
  • ❌ 需要传输二进制数据的场景(用 WebSocket 或 Fetch API)

通过 SSE 协议,你可以轻松实现服务器到客户端的实时单向通信,无需复杂的协议握手,且天然兼容 HTTP 生态。

相关推荐
南风999几秒前
Claude code安装使用保姆级教程
后端
爱泡脚的鸡腿几秒前
Node.js 拓展
前端·后端
蚂蚁背大象1 小时前
Rust 所有权系统是为了解决什么问题
后端·rust
子玖3 小时前
go实现通过ip解析城市
后端·go
Java不加班3 小时前
Java 后端定时任务实现方案与工程化指南
后端
心在飞扬3 小时前
RAG 进阶检索学习笔记
后端
Moment3 小时前
想要长期陪伴你的助理?先从部署一个 OpenClaw 开始 😍😍😍
前端·后端·github
Das1_3 小时前
【Golang 数据结构】Slice 底层机制
后端·go
得物技术3 小时前
深入剖析Spark UI界面:参数与界面详解|得物技术
大数据·后端·spark