在实际项目开发中,前端页面经常需要展示实时更新的数据。比如股票行情、聊天消息、进度状态等场景,传统的一次性返回所有数据的API设计会导致两个明显问题:一是用户需要长时间等待所有数据处理完成,二是服务器内存压力大。这时候流式API就派上用场了。
我最近就遇到一个典型场景:前端同事需要展示一个包含2000多条记录的表格,每条记录都需要经过复杂计算。如果等所有数据计算完再返回,用户点击刷新后要等待近30秒。这体验实在太糟糕了。我们尝试了以下几种方案:
流式API的核心思想是"边计算边返回",就像打开水龙头一样,数据源源不断地流向客户端。而SSE(Server-Sent Events)则是专门为这种单向实时通信设计的标准协议,相比WebSocket更轻量,实现起来也简单得多。
让我们从一个最简单的Flask流式API开始。这个例子会每秒返回一个数字:
python复制from flask import Flask, Response
import time
app = Flask(__name__)
@app.route('/stream')
def stream_numbers():
def generate():
for i in range(1, 6):
yield f"数据块 {i}\n"
time.sleep(1)
return Response(generate())
if __name__ == '__main__':
app.run(port=5000)
这段代码的关键点在于:
测试时可以直接用curl命令:
bash复制curl http://localhost:5000/stream
前端使用EventSource API接收流式数据:
javascript复制const eventSource = new EventSource('http://localhost:5000/stream');
eventSource.onmessage = (event) => {
console.log('收到数据:', event.data);
};
eventSource.onerror = () => {
console.error('连接出错');
eventSource.close();
};
现代前端框架(Vue/React)通常运行在独立端口,这就涉及跨域问题。Flask中解决跨域最简单的办法是使用flask-cors扩展:
python复制from flask_cors import CORS
# 允许所有来源访问所有路由
CORS(app)
# 更安全的做法是指定具体来源
CORS(app, resources={
r"/stream": {"origins": ["http://localhost:8080"]}
})
在开发环境中,我习惯这样配置:
python复制CORS(app, supports_credentials=True)
同时前端请求需要带上credentials:
javascript复制new EventSource(url, { withCredentials: true })
要让EventSource正常工作,必须设置正确的响应头。以下是生产环境推荐的配置:
python复制headers = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' # 禁用Nginx缓冲
}
return Response(generate(), headers=headers)
这些头部的含义:
Content-Type: text/event-stream:声明SSE协议Cache-Control: no-cache:禁用缓存Connection: keep-alive:保持长连接X-Accel-Buffering: no:防止代理服务器缓冲标准的SSE消息格式要求每条消息包含:
示例格式:
code复制id: 123
event: update
data: {"value":42}
完整的生产级实现:
python复制import json
import time
@app.route('/stream')
def stream_data():
def generate():
message_id = 0
while True:
message_id += 1
data = {
"time": time.time(),
"value": random.random()
}
yield f"id: {message_id}\n"
yield "event: metrics\n"
yield f"data: {json.dumps(data)}\n\n"
time.sleep(1)
headers = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
return Response(generate(), headers=headers)
前端可以这样处理不同类型的事件:
javascript复制eventSource.addEventListener('metrics', (event) => {
const data = JSON.parse(event.data);
console.log('收到指标数据:', data);
});
实际项目中必须处理好连接生命周期:
改进后的生成器函数:
python复制def generate():
try:
while True:
if not connected(): # 自定义的连接检查
break
yield heartbeat_message()
time.sleep(15)
except GeneratorExit:
print('客户端断开连接')
finally:
cleanup_resources()
在大流量场景下,我总结了这些优化经验:
code复制proxy_buffering off;
proxy_read_timeout 24h;
虽然现代浏览器都支持EventSource,但需要注意:
解决方案是添加特性检测:
javascript复制if (typeof EventSource !== 'undefined') {
// 使用原生SSE
} else {
// 降级方案:轮询或WebSocket
}
实际项目中遇到的典型问题:
我的解决方案是:
前端重连示例:
javascript复制let reconnectDelay = 1000;
function connect() {
const es = new EventSource('/stream');
es.onopen = () => {
reconnectDelay = 1000; // 重置重连延迟
};
es.onerror = () => {
es.close();
setTimeout(connect, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 60000);
};
}
实现类似ChatGPT的流式输出效果:
python复制@app.route('/chat', methods=['POST'])
def chat_stream():
def generate():
prompt = request.json.get('prompt')
for chunk in llm.generate_stream(prompt):
yield format_as_sse(chunk)
return Response(generate(), mimetype='text/event-stream')
前端处理:
javascript复制const es = new EventSource('/chat?prompt=你好');
let fullResponse = '';
es.onmessage = (event) => {
if (event.data === '[DONE]') {
es.close();
} else {
fullResponse += event.data;
updateUI(fullResponse);
}
};
金融数据实时推送示例:
python复制@app.route('/market-data')
def market_data():
def generate():
while True:
data = get_latest_market_data()
yield f"data: {json.dumps(data)}\n\n"
time.sleep(1)
return Response(generate(), mimetype='text/event-stream')
前端可视化更新:
javascript复制const es = new EventSource('/market-data');
es.onmessage = (event) => {
const data = JSON.parse(event.data);
updateChart(data);
};
生产环境部署时需要特别注意:
Gunicorn示例配置:
bash复制gunicorn -k gevent -w 4 --timeout 300 app:app
在Kubernetes等环境中:
Nginx配置示例:
code复制location /stream {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
}
保护SSE端点的几种方案:
python复制@app.route('/stream')
def stream():
token = request.args.get('token')
if not validate_token(token):
abort(401)
# ...其余代码
防止滥用很重要:
python复制from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)
@app.route('/stream')
@limiter.limit("10 per minute")
def stream():
# ...原有代码
关键指标需要监控:
Prometheus监控示例:
python复制from prometheus_client import Counter
SSE_CONNECTIONS = Counter('sse_connections', 'Active SSE connections')
@app.route('/stream')
def stream():
SSE_CONNECTIONS.inc()
try:
# ...原有代码
finally:
SSE_CONNECTIONS.dec()
前端开发时实用的调试方法:
javascript复制eventSource.onmessage = (event) => {
console.debug('SSE Event:', {
type: event.type,
data: event.data,
origin: event.origin,
lastEventId: event.lastEventId
});
// ...业务处理
};
在我的MacBook Pro上测试不同方案的性能表现:
| 方案 | 内存占用 | CPU负载 | 延迟 | 最大连接数 |
|---|---|---|---|---|
| 普通HTTP | 低 | 低 | 高 | 高 |
| SSE | 中 | 中 | 低 | 中 |
| WebSocket | 高 | 高 | 最低 | 低 |
测试环境:Flask 2.3, Python 3.9, 100个并发客户端
当SSE不适用时可以考虑:
长轮询:
WebSocket:
HTTP/2 Server Push:
在电商价格监控项目中,我们使用SSE实现了以下功能:
踩过的坑:
解决方案:
javascript复制document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
reconnect();
}
});
javascript复制function useSSE(url, callback) {
useEffect(() => {
const es = new EventSource(url);
es.onmessage = callback;
return () => es.close();
}, [url, callback]);
}
// 使用示例
function StockTicker() {
const [price, setPrice] = useState(0);
useSSE('/stock-price', (event) => {
setPrice(JSON.parse(event.data).price);
});
return <div>当前价格: {price}</div>;
}
javascript复制import { ref, onMounted, onUnmounted } from 'vue';
export function useEventSource(url) {
const data = ref(null);
const error = ref(null);
let eventSource;
onMounted(() => {
eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
data.value = JSON.parse(event.data);
};
eventSource.onerror = () => {
error.value = '连接错误';
};
});
onUnmounted(() => {
eventSource?.close();
});
return { data, error };
}
良好的SSE消息协议应该包含:
标准字段:
json复制{
"event": "message_type",
"data": {},
"timestamp": 1620000000,
"sequence": 123
}
错误消息格式:
json复制{
"event": "error",
"code": "invalid_request",
"message": "Missing required field"
}
心跳消息:
json复制{
"event": "heartbeat",
"timestamp": 1620000000
}
对于大量文本数据,可以考虑启用压缩:
python复制@app.after_request
def compress_response(response):
response.headers['Content-Encoding'] = 'gzip'
response.direct_passthrough = False
return gzip_compress(response)
高频小消息可以适当合并:
python复制def generate():
buffer = []
while True:
data = get_data()
buffer.append(data)
if len(buffer) >= 5 or time.time() - last_send > 0.5:
yield format_message(buffer)
buffer = []
last_send = time.time()
python复制def generate():
try:
while True:
try:
data = fetch_data()
yield format_message(data)
except DataError as e:
yield format_error(e)
break
except Exception as e:
logging.exception("SSE生成器异常")
yield format_error(e)
javascript复制function connectSSE() {
const es = new EventSource(url);
es.onerror = () => {
es.close();
setTimeout(connectSSE, 1000);
};
return es;
}
python复制def test_stream_endpoint(client):
response = client.get('/stream')
assert response.status_code == 200
assert response.content_type == 'text/event-stream'
# 测试流式输出
data = b''.join(response.response)
assert b'data:' in data
使用pytest-asyncio测试完整流程:
python复制async def test_full_flow():
# 启动测试服务器
async with AsyncClient(app) as client:
async with client.stream("GET", "/stream") as response:
async for chunk in response.aiter_lines():
assert json.loads(chunk[6:]) # 跳过"data: "前缀
break
使用上下文管理器确保资源释放:
python复制def generate():
with DatabaseConnection() as db:
while True:
data = db.fetch()
if not data:
break
yield format_message(data)
Flask提供了request.environ检测连接状态:
python复制def generate():
while not request.environ.get('werkzeug.server.shutdown'):
yield data
time.sleep(1)
iOS/Android上的特殊处理:
javascript复制// 请求后台运行权限
navigator.serviceWorker.register('/sw.js');
// Service Worker中保持连接
self.addEventListener('message', (event) => {
if (event.data === 'keepAlive') {
const es = new EventSource('/stream');
}
});
移动设备上需要特别注意:
javascript复制navigator.connection.addEventListener('change', () => {
adjustStreamingQuality();
});
虽然SSE是GET请求,但仍需防护:
python复制@app.route('/stream')
def stream():
if not validate_csrf(request.args.get('token')):
abort(403)
# ...原有代码
更精细的限流策略:
python复制limiter.limit("100/hour;10/minute", key_func=user_id)
扩展SSE协议支持更多事件:
python复制yield "event: user-notification\n"
yield f"data: {json.dumps(notification)}\n\n"
前端处理:
javascript复制eventSource.addEventListener('user-notification', (e) => {
showNotification(JSON.parse(e.data));
});
虽然SSE主要设计用于文本,但可以通过Base64传输二进制:
python复制import base64
yield f"data: {base64.b64encode(binary_data).decode()}\n\n"
推荐工具:
bash复制curl -N http://localhost:5000/stream
Chrome开发者工具技巧:
虽然SSE已经非常成熟,但在以下方面还有改进空间:
在实际项目中,我建议持续关注这些新兴标准:
最后分享一个生产环境中使用的完整示例:
python复制from flask import Flask, Response, request
import json
import time
from functools import wraps
app = Flask(__name__)
def sse_stream(f):
@wraps(f)
def wrapper(*args, **kwargs):
def generate():
try:
for data in f(*args, **kwargs):
event = data.get('event', 'message')
yield f"event: {event}\n"
yield f"data: {json.dumps(data)}\n\n"
except Exception as e:
yield f"event: error\n"
yield f"data: {json.dumps({'message': str(e)})}\n\n"
headers = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
return Response(generate(), headers=headers)
return wrapper
@app.route('/api/stream')
@sse_stream
def realtime_stream():
user_id = authenticate(request)
for i in range(100):
if request.environ.get('werkzeug.server.shutdown'):
break
yield {
"event": "update",
"data": get_user_data(user_id),
"sequence": i
}
time.sleep(1)
yield {"event": "complete"}
def authenticate(request):
token = request.args.get('token')
# 实现实际的认证逻辑
return "user123"
def get_user_data(user_id):
# 实现实际的数据获取逻辑
return {"user": user_id, "value": time.time()}
前端对应实现:
javascript复制class SSEClient {
constructor(url) {
this.url = url;
this.callbacks = {};
this.reconnectDelay = 1000;
this.connect();
}
connect() {
this.es = new EventSource(this.url);
this.es.onopen = () => {
this.reconnectDelay = 1000;
};
this.es.onerror = () => {
this.es.close();
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 60000);
};
this.es.addEventListener('update', (e) => {
this.emit('update', JSON.parse(e.data));
});
this.es.addEventListener('complete', () => {
this.es.close();
this.emit('complete');
});
}
on(event, callback) {
this.callbacks[event] = this.callbacks[event] || [];
this.callbacks[event].push(callback);
}
emit(event, data) {
(this.callbacks[event] || []).forEach(fn => fn(data));
}
}
// 使用示例
const client = new SSEClient('/api/stream?token=abc123');
client.on('update', (data) => {
console.log('更新:', data);
});
client.on('complete', () => {
console.log('流结束');
});