SSE(Server-Sent Events)完全指南

SSE(Server-Sent Events)完全指南

一、什么是 SSE?

SSE(Server-Sent Events) 是一种允许服务器主动向客户端推送数据的技术。客户端建立连接后,服务器可以持续、单向地发送数据流。

复制代码
传统 HTTP:客户端请求 → 服务器响应(一次性的)
SSE:       客户端连接 → 服务器可以推送 N 次数据

SSE vs WebSocket vs 轮询

特性 SSE WebSocket 短轮询 长轮询
通信方向 单向(服务器→客户端) 双向 单向(客户端请求) 单向(客户端请求)
协议 HTTP/HTTPS WS/WSS HTTP HTTP
连接方式 持久连接 持久连接 每次新建 等待响应或超时
自动重连 ✅ 内置 ❌ 需手动实现
二进制数据 ❌(只能文本)
浏览器支持 所有现代浏览器 所有现代浏览器 通用 通用
实现复杂度 极低 中等 中等
适用场景 通知、日志、流式响应 聊天、游戏、协作 低频更新 类推送需求

核心结论:如果只需要服务器单向推送(如 AI 流式输出、股票行情、系统通知),SSE 是最简单、最合适的选择。


二、SSE 协议格式

2.1 响应头格式

服务器返回 SSE 流时,必须设置特定的响应头:

http 复制代码
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Access-Control-Allow-Origin: *  # 跨域需要

2.2 数据格式

SSE 使用纯文本格式,每个事件以 data: 开头,以两个换行符 \n\n 结束:

kotlin 复制代码
data: hello\n\n
data: {"message": "world"}\n\n

完整格式(包含可选字段):

makefile 复制代码
event: message    ← 事件类型(可选,默认 message)
id: 12345         ← 事件 ID(可选,用于断线重连)
data: 消息内容    ← 数据内容(必填)
data: 第二行      ← 可以多行 data
\n\n              ← 结束标志

2.3 多行数据示例

javascript 复制代码
// 服务器发送
data: 第一行内容\n
data: 第二行内容\n
\n\n

// 客户端接收到的 event.data = "第一行内容\n第二行内容"

三、后端实现示例

3.1 Node.js + Express 实现

javascript 复制代码
const express = require('express');
const app = express();

// 1. 基础 SSE 端点
app.get('/sse/basic', (req, res) => {
    // 设置 SSE 必需的响应头
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*'
    });

    // 发送初始数据
    res.write('data: 连接成功\n\n');

    // 定时推送
    let count = 0;
    const interval = setInterval(() => {
        count++;
        res.write(`data: 第 ${count} 次推送\n\n`);
        
        if (count >= 10) {
            clearInterval(interval);
            res.write('data: 推送结束\n\n');
            res.end();
        }
    }, 1000);

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

// 2. 模拟 AI 流式输出(逐字推送)
app.get('/sse/ai-stream', async (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });

    const responseText = '你好,我是 AI 助手。这是通过 SSE 实现的流式输出效果。';
    
    for (const char of responseText) {
        res.write(`data: ${char}\n\n`);
        await new Promise(r => setTimeout(r, 50)); // 模拟逐字输出
    }
    
    res.write('data: [DONE]\n\n');
    res.end();
});

// 3. 带事件类型的推送(区分不同消息类型)
app.get('/sse/with-events', (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });

    // 发送状态更新
    res.write('event: status\ndata: 正在加载\n\n');
    
    setTimeout(() => {
        res.write('event: progress\ndata: 50%\n\n');
    }, 500);
    
    setTimeout(() => {
        res.write('event: result\ndata: {"success": true}\n\n');
        res.end();
    }, 1000);
});

app.listen(3000, () => {
    console.log('SSE 服务运行在 http://localhost:3000');
});

3.2 Python + FastAPI 实现

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

app = FastAPI()

# 1. 基础 SSE 端点
@app.get("/sse/basic")
async def basic_sse():
    async def event_generator():
        yield "data: 连接成功\n\n"
        
        for i in range(1, 11):
            yield f"data: 第 {i} 次推送\n\n"
            await asyncio.sleep(1)
        
        yield "data: 推送结束\n\n"
    
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        }
    )

# 2. 模拟 AI 流式输出
@app.get("/sse/ai-stream")
async def ai_stream():
    async def generate():
        text = "你好,我是 AI 助手。这是通过 SSE 实现的流式输出效果。"
        for char in text:
            yield f"data: {char}\n\n"
            await asyncio.sleep(0.05)
        yield "data: [DONE]\n\n"
    
    return StreamingResponse(
        generate(),
        media_type="text/event-stream"
    )

# 3. 从 LangChain 直接流式输出
from langchain_openai import ChatOpenAI
from langchain.callbacks import AsyncIteratorCallbackHandler

@app.post("/sse/langchain-stream")
async def langchain_stream():
    async def generate():
        callback = AsyncIteratorCallbackHandler()
        llm = ChatOpenAI(
            streaming=True,
            callbacks=[callback]
        )
        
        # 异步生成
        asyncio.create_task(llm.ainvoke("给我讲一个简短的笑话"))
        
        async for token in callback.aiter():
            yield f"data: {token}\n\n"
        
        yield "data: [DONE]\n\n"
    
    return StreamingResponse(
        generate(),
        media_type="text/event-stream"
    )

# 运行:uvicorn main:app --reload

3.3 Go + Gin 实现

go 复制代码
package main

import (
    "fmt"
    "time"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // SSE 端点
    r.GET("/sse/stream", func(c *gin.Context) {
        // 设置 SSE 响应头
        c.Header("Content-Type", "text/event-stream")
        c.Header("Cache-Control", "no-cache")
        c.Header("Connection", "keep-alive")
        c.Header("Access-Control-Allow-Origin", "*")

        // 获取写入器
        w := c.Writer
        flusher := w.(http.Flusher)

        // 发送初始消息
        fmt.Fprintf(w, "data: 连接成功\n\n")
        flusher.Flush()

        // 定时推送
        for i := 1; i <= 10; i++ {
            fmt.Fprintf(w, "data: 第 %d 次推送\n\n", i)
            flusher.Flush()
            time.Sleep(1 * time.Second)
        }

        fmt.Fprintf(w, "data: 推送结束\n\n")
        flusher.Flush()
    })

    r.Run(":3000")
}

四、前端实现示例

4.1 原生 JavaScript 使用 EventSource

javascript 复制代码
// 1. 基础连接
const eventSource = new EventSource('http://localhost:3000/sse/basic');

// 监听消息(默认事件)
eventSource.onmessage = (event) => {
    console.log('收到消息:', event.data);
    // event.data 就是服务器发送的内容
};

// 监听打开连接
eventSource.onopen = () => {
    console.log('SSE 连接已建立');
};

// 监听错误
eventSource.onerror = (error) => {
    console.error('SSE 错误:', error);
    // EventSource 会自动尝试重连,不需要手动处理
};

// 2. 监听自定义事件类型
const eventSource2 = new EventSource('http://localhost:3000/sse/with-events');

eventSource2.addEventListener('status', (event) => {
    console.log('状态更新:', event.data);
});

eventSource2.addEventListener('progress', (event) => {
    console.log('进度:', event.data);
});

eventSource2.addEventListener('result', (event) => {
    console.log('结果:', JSON.parse(event.data));
});

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

4.2 React Hook 封装

typescript 复制代码
import { useEffect, useState, useRef } from 'react';

// 自定义 Hook:useSSE
function useSSE<T = string>(url: string) {
    const [data, setData] = useState<T | null>(null);
    const [isConnected, setIsConnected] = useState(false);
    const [error, setError] = useState<Event | null>(null);
    const eventSourceRef = useRef<EventSource | null>(null);

    useEffect(() => {
        // 创建连接
        const eventSource = new EventSource(url);
        eventSourceRef.current = eventSource;

        eventSource.onopen = () => {
            setIsConnected(true);
            setError(null);
        };

        eventSource.onmessage = (event) => {
            // 处理 [DONE] 结束标记
            if (event.data === '[DONE]') {
                eventSource.close();
                setIsConnected(false);
                return;
            }
            setData(event.data as T);
        };

        eventSource.onerror = (err) => {
            setError(err);
            setIsConnected(false);
            // EventSource 会自动重连,不需要 close
        };

        // 清理函数
        return () => {
            eventSource.close();
        };
    }, [url]);

    // 手动关闭连接
    const close = () => {
        eventSourceRef.current?.close();
        setIsConnected(false);
    };

    return { data, isConnected, error, close };
}

// 使用示例:AI 流式对话组件
function AIStreamingChat() {
    const [input, setInput] = useState('');
    const [response, setResponse] = useState('');
    const [isStreaming, setIsStreaming] = useState(false);
    
    const { data, isConnected, error } = useSSE(
        isStreaming ? 'http://localhost:3000/sse/ai-stream' : null
    );
    
    useEffect(() => {
        if (data && data !== '[DONE]') {
            setResponse(prev => prev + data);
        }
    }, [data]);
    
    const sendMessage = () => {
        setResponse('');
        setIsStreaming(true);
    };
    
    return (
        <div>
            <input 
                value={input} 
                onChange={(e) => setInput(e.target.value)}
            />
            <button onClick={sendMessage}>发送</button>
            <div className="response">
                {response}
                {isConnected && <span className="cursor">▊</span>}
            </div>
        </div>
    );
}

4.3 带重连和心跳的完整前端实现

javascript 复制代码
class RobustSSE {
    constructor(url, options = {}) {
        this.url = url;
        this.options = {
            reconnectInterval: 3000,  // 重连间隔(毫秒)
            maxReconnectAttempts: 10, // 最大重连次数
            heartbeatTimeout: 30000,   // 心跳超时(毫秒)
            ...options
        };
        
        this.eventSource = null;
        this.reconnectAttempts = 0;
        this.isClosed = false;
        this.lastHeartbeat = Date.now();
        
        // 事件回调
        this.onMessage = null;
        this.onError = null;
        this.onConnect = null;
        
        this.connect();
        this.startHeartbeatCheck();
    }
    
    connect() {
        if (this.isClosed) return;
        
        console.log(`尝试连接 SSE (${this.reconnectAttempts + 1}/${this.options.maxReconnectAttempts})`);
        
        this.eventSource = new EventSource(this.url);
        
        this.eventSource.onopen = () => {
            console.log('SSE 连接成功');
            this.reconnectAttempts = 0;
            this.lastHeartbeat = Date.now();
            this.onConnect?.();
        };
        
        this.eventSource.onmessage = (event) => {
            this.lastHeartbeat = Date.now();  // 收到数据即心跳
            
            // 处理心跳包(服务器发送 :heartbeat\n\n)
            if (event.data === 'heartbeat') return;
            
            this.onMessage?.(event);
        };
        
        this.eventSource.onerror = (error) => {
            console.error('SSE 错误:', error);
            this.onError?.(error);
            this.reconnect();
        };
    }
    
    reconnect() {
        if (this.isClosed) return;
        
        this.eventSource?.close();
        
        if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
            console.error('超过最大重连次数,停止重连');
            return;
        }
        
        this.reconnectAttempts++;
        setTimeout(() => this.connect(), this.options.reconnectInterval);
    }
    
    startHeartbeatCheck() {
        setInterval(() => {
            if (this.isClosed) return;
            
            const timeSinceLastHeartbeat = Date.now() - this.lastHeartbeat;
            if (timeSinceLastHeartbeat > this.options.heartbeatTimeout) {
                console.warn('心跳超时,尝试重连');
                this.reconnect();
            }
        }, 5000);
    }
    
    close() {
        this.isClosed = true;
        this.eventSource?.close();
    }
}

// 使用
const sse = new RobustSSE('http://localhost:3000/sse/ai-stream', {
    reconnectInterval: 5000,
    maxReconnectAttempts: 5
});

sse.onMessage = (event) => {
    document.getElementById('output').innerHTML += event.data;
};

五、AI 流式对话完整示例

场景:前端发送消息,后端流式返回 AI 回复

sequenceDiagram participant 前端 participant 后端 participant LLM 前端->>后端: POST /chat (用户消息) 后端->>LLM: 调用 API (stream=true) loop 流式生成 LLM-->>后端: token 1 后端-->>前端: data: token1\n\n LLM-->>后端: token 2 后端-->>前端: data: token2\n\n end 后端-->>前端: data: [DONE]\n\n

前端代码(React + TypeScript)

tsx 复制代码
import { useState, useRef, FormEvent } from 'react';

interface Message {
    role: 'user' | 'assistant';
    content: string;
}

function AIChat() {
    const [messages, setMessages] = useState<Message[]>([]);
    const [input, setInput] = useState('');
    const [isLoading, setIsLoading] = useState(false);
    const eventSourceRef = useRef<EventSource | null>(null);
    const currentResponseRef = useRef<string>('');
    
    const sendMessage = async (e: FormEvent) => {
        e.preventDefault();
        if (!input.trim() || isLoading) return;
        
        const userMessage = input.trim();
        setInput('');
        
        // 添加用户消息
        setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
        
        // 添加空的 AI 消息占位
        const assistantMessageIndex = messages.length + 1;
        setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
        
        setIsLoading(true);
        currentResponseRef.current = '';
        
        try {
            // 发送消息并建立 SSE 连接
            const response = await fetch('/api/chat', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ message: userMessage })
            });
            
            const reader = response.body?.getReader();
            const decoder = new TextDecoder();
            
            while (reader) {
                const { done, value } = await reader.read();
                if (done) break;
                
                const chunk = decoder.decode(value);
                const lines = chunk.split('\n');
                
                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        const data = line.slice(6);
                        if (data === '[DONE]') continue;
                        
                        currentResponseRef.current += data;
                        
                        // 更新 UI
                        setMessages(prev => {
                            const newMessages = [...prev];
                            const lastIndex = newMessages.length - 1;
                            if (newMessages[lastIndex]?.role === 'assistant') {
                                newMessages[lastIndex] = {
                                    role: 'assistant',
                                    content: currentResponseRef.current
                                };
                            }
                            return newMessages;
                        });
                    }
                }
            }
        } catch (error) {
            console.error('SSE 错误:', error);
        } finally {
            setIsLoading(false);
        }
    };
    
    return (
        <div className="chat-container">
            <div className="messages">
                {messages.map((msg, idx) => (
                    <div key={idx} className={`message ${msg.role}`}>
                        {msg.content}
                        {msg.role === 'assistant' && isLoading && 
                         idx === messages.length - 1 && <span className="cursor">▊</span>}
                    </div>
                ))}
            </div>
            
            <form onSubmit={sendMessage}>
                <input
                    value={input}
                    onChange={(e) => setInput(e.target.value)}
                    placeholder="输入消息..."
                    disabled={isLoading}
                />
                <button type="submit" disabled={isLoading}>
                    {isLoading ? '思考中...' : '发送'}
                </button>
            </form>
        </div>
    );
}

后端代码(Node.js + OpenAI)

javascript 复制代码
const express = require('express');
const OpenAI = require('openai');
const app = express();

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

app.post('/api/chat', async (req, res) => {
    const { message } = req.body;
    const messages = [{ role: 'user', content: message }];
    
    // 设置 SSE 响应头
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });
    
    try {
        // 调用 OpenAI 流式 API
        const stream = await openai.chat.completions.create({
            model: 'gpt-3.5-turbo',
            messages,
            stream: true,
        });
        
        for await (const chunk of stream) {
            const content = chunk.choices[0]?.delta?.content || '';
            if (content) {
                res.write(`data: ${content}\n\n`);
            }
        }
        
        res.write('data: [DONE]\n\n');
        res.end();
        
    } catch (error) {
        res.write(`data: 错误: ${error.message}\n\n`);
        res.end();
    }
});

app.listen(3000);

六、常见问题与解决方案

问题 原因 解决方案
连接自动断开 默认超时(Chrome 约 60 秒) 发送心跳包(如每 30 秒发 : heartbeat\n\n
跨域问题 不同源 后端设置 Access-Control-Allow-Origin: *
数据被截断 一次发送超过 1KB 分多次发送,前端拼接
断线重连失败 EventSource 行为不可控 自己实现重连逻辑(见 RobustSSE 类)
浏览器兼容性 IE 不支持 用 polyfill 或降级到长轮询
连接数限制 HTTP/1.1 最多 6 个 升级到 HTTP/2 或用 WebSocket

七、心跳包实现

javascript 复制代码
// 后端每 30 秒发送一次心跳
setInterval(() => {
    if (res.writable) {
        res.write(': heartbeat\n\n');  // 冒号开头的行是注释,客户端不会触发 onmessage
    }
}, 30000);

八、总结

要点 说明
最适合 SSE AI 流式输出、实时通知、股票行情、日志流
实现复杂度 极低,比 WebSocket 简单 10 倍
核心优势 自动重连、HTTP 协议兼容、浏览器原生支持
主要限制 单向、文本数据、浏览器连接数限制
前端 API new EventSource(url) + onmessage
后端要点 Content-Type: text/event-stream + 格式 data: xxx\n\n

一句话总结:如果你需要服务器持续推送数据(特别是 AI 的流式输出),SSE 是最简单、最可靠的方案,开箱即用,无需像 WebSocket 那样处理复杂的协议和重连逻辑。

相关推荐
码云之上1 小时前
万星入坞:我们如何用三层插件体系干掉巨石应用
前端·架构·前端框架
kyriewen1 小时前
一口气讲清楚 Monorepo、Turborepo、pnpm、Changesets 到底是什么?
前端·架构·前端工程化
IT_陈寒2 小时前
React性能优化踩的坑,这个错你可能也会犯
前端·人工智能·后端
zhangxingchao2 小时前
AI应用开发三:RAG技术与应用
前端·人工智能·后端
摘星小杨2 小时前
如何在前端循环调取接口,实时查询数据
开发语言·前端·javascript
Hilaku3 小时前
从搜索排名到 AI 回答? 先聊一聊 AI 可见度工具 BuildSOM !
前端·javascript·程序员
zzmgc43 小时前
纯静态 + Web Worker + 虚拟滚动:我是怎么让浏览器吃下 10MB JSON 不卡的
前端·架构
辰同学ovo3 小时前
用 Chrome DevTools MCP 给 AI 写的页面做“质检“
前端·人工智能·chrome devtools
乌托邦3 小时前
uni-mini-ci:让 uniapp 小程序构建后自动预览和上传
前端·vue.js·uni-app