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 那样处理复杂的协议和重连逻辑。