当访客通过浏览器访问使用服务器发送事件(Server-Sent Events, SSE)的网页时,可能会遇到数据被重复推送的问题,这种现象通常由连接异常、逻辑错误或配置不当引起,以下从技术原理到解决方案进行系统性分析。
通过SSE建立的实时通信场景中(如股票价格更新、消息通知),客户端可能重复接收相同数据。
浏览器标签页或路由跳转时,若未显式关闭EventSource
连接,可能导致旧的监听器残留,重新建立连接时,新旧监听器同时响应消息,造成数据重复。
// 错误示例:未关闭旧连接直接新建 function initSSE() { const eventSource = new EventSource('/sse'); eventSource.onmessage = (e) => { /* 处理逻辑 */ }; } // 正确做法:先关闭再重建 let eventSource = null; function initSSE() { if (eventSource) eventSource.close(); // 关键:释放旧连接 eventSource = new EventSource('/sse'); eventSource.onmessage = (e) => { /* 处理逻辑 */ }; }
若服务器未跟踪已发送数据,在客户端重连时(如网络中断后),可能重新发送全部数据而非增量更新。
# Flask示例:错误的服务端逻辑 @app.route('/sse') def sse_stream(): def generate(): data = get_all_data() # 每次返回完整数据集 yield f"data: {data}nn" return Response(generate(), mimetype='text/event-stream')
SSE协议约定,客户端重连时会通过Last-Event-ID
头部告知服务器最后接收的消息ID,若服务端未根据该ID定位续推位置,将导致数据重复。
部分浏览器可能缓存SSE响应,特别是在未正确设置Cache-Control
头部时,导致客户端收到陈旧数据。
消息ID去重:在客户端存储已接收消息的ID,新消息到达时校验唯一性。
const receivedIds = new Set(); eventSource.onmessage = (e) => { const msg = JSON.parse(e.data); if (!receivedIds.has(msg.id)) { receivedIds.add(msg.id); // 处理新消息 } };
自动清理机制:为避免内存泄漏,可设定时间窗口,定期清理receivedIds
集合。
# Django示例:使用数据库记录进度 from django.http import StreamingHttpResponse
def sse_view(request):
last_id = request.headers.get(‘Last-Event-ID’, 0)
new_data = Data.objects.filter(id__gt=last_id)
def stream():
for item in new_data:
yield f"id: {item.id}ndata: {item.content}nn"
return StreamingHttpResponse(stream(), content_type='text/event-stream')
- **设置响应头部**:禁用缓存并声明事件流类型。
```nginx
# Nginx配置建议
location /sse {
proxy_cache off;
proxy_buffering off;
proxy_set_header Connection '';
proxy_http_version 1.1;
}
let retryDelay = 1000; // 初始重试间隔1秒 eventSource.onerror = () => { eventSource.close(); setTimeout(() => { retryDelay *= 2; // 每次失败后延长重试时间 initSSE(); }, retryDelay); };
EventStream
标签页的消息时序和ID连续性。Last-Event-ID
及推送的数据范围。Offline
模式测试重连逻辑。SSE数据重复问题需客户端与服务端协同解决:
客户端确保连接唯一性,主动管理消息状态。
服务端实现增量推送,正确处理Last-Event-ID
。
双向关注连接生命周期,加入异常重试机制。