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 生态。

相关推荐
往日情怀酿做酒 V17639296384 分钟前
Django项目之订单管理part3
后端·python·django
uhakadotcom43 分钟前
MaxCompute Python UDF开发指南:从入门到精通
后端·面试·github
前端snow1 小时前
爬取数据利用node也行,你知道吗?
前端·javascript·后端
陈随易1 小时前
告别Node.js:2025年,我为何全面拥抱Bun
前端·后端·程序员
uhakadotcom1 小时前
双Token机制:安全与便利的完美结合
后端·面试·github
雷渊2 小时前
java版本管理工具-jenv
后端·架构
kakaZhou7192 小时前
karmada和vcluster联合作战 [1]
后端
平谷一勺2 小时前
golang中的结构体
开发语言·后端·golang·序列化·结构体·结构体嵌套·匿名结构体嵌套
菜菜的后端私房菜2 小时前
RocketMQ(十一):事务消息如何满足分布式一致性?
java·后端·rocketmq
梦尘啊2 小时前
存储引擎详解:LSM-Tree与B+树比较
数据库·后端