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

相关推荐
鱼樱前端11 分钟前
前端程序员集体破防!AI工具same.dev像素级抄袭你的代码,你还能高傲多久?
前端·javascript·后端
羊思茗52029 分钟前
Spring Boot中@Valid 与 @Validated 注解的详解
java·spring boot·后端
尤宸翎1 小时前
Julia语言的饼图
开发语言·后端·golang
穆韵澜1 小时前
SQL语言的云计算
开发语言·后端·golang
uhakadotcom2 小时前
提升PyODPS性能的实用技巧
后端·面试·github
字节源流2 小时前
【SpringMVC】入门版
java·后端
MrWho不迷糊2 小时前
Spring Boot 的优雅启停:确保停机不影响交易
spring boot·后端
xjz18422 小时前
Netty底层原理深度解析:高并发网络编程的核心设计
后端
陈随易2 小时前
PM2 突然更新,从v5.4.2跳到v6.0.5,正式支持Node.js最强竞品Bun
前端·后端·程序员
风象南2 小时前
告别“我觉得”!用 JMH 搞懂你的 Java 代码性能
java·后端·性能优化