使用多实例EventSources进行SSE数据流时应用冻结问题
我正在使用ExpressJS和HTML开发一款仪表盘,采用Server-Sent Events(SSE)实现数据流。每次打开页面都会创建一个新的EventSource对象,但发现当EventSource实例数量达到6个时,应用会出现冻结情况。
简化客户端代码
const eventSource = new EventSource("/api/data/stream/last"); eventSource.onmessage = function (event) { const type = event.type; const data = JSON.parse(event.data); console.log("Received ", type, ": ", data); }; eventSource.onerror = function (error) { console.error("EventSource failed:", error); };
简化服务端代码
router.get("/all", function (req, res) { /* Set response headers for Server-Sent Events */ res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); /* Send all data */ sendData(err, res, data); /* Handle client connection close event */ req.on("close", () => { res.end(); }); });
问题现象
当我在不同浏览器标签或窗口中打开多个仪表盘实例时,一切正常,直到打开第6个实例,此时应用会冻结。
推测原因
我怀疑该问题与EventSource对象发起新HTTP会话并打开新TCP/IP套接字的方式有关。由于SSE会保持连接持续从服务器推送数据,每个套接字都会处于活跃状态。根据RFC 2616,大多数浏览器对同一服务器的同时活跃HTTP/1连接有上限,通常在4到6个之间,这可能是达到第6个连接时应用冻结的原因。
咨询问题
- 如何更高效地管理多个SSE连接以防止应用冻结?
- 是否可以增加现代浏览器的同时连接数上限?
- 在该场景下,是否有替代方案实现实时数据流而不触及这些限制?
1. 高效管理SSE连接的方案
(1)复用浏览器内的连接
同一浏览器的不同标签页可共享单个SSE连接,借助localStorage或Broadcast Channel API实现跨标签通信:
- 首个打开的标签页创建SSE连接,后续标签页监听跨标签通信事件接收数据
- 所有标签页关闭时再销毁SSE连接
- 客户端示例思路:
const channel = new BroadcastChannel('dashboard-stream'); if (!localStorage.getItem('sse-connected')) { const eventSource = new EventSource("/api/data/stream/last"); eventSource.onmessage = function (event) { const data = JSON.parse(event.data); // 推送数据到其他标签页 channel.postMessage(data); // 当前标签页处理数据 handleData(data); }; localStorage.setItem('sse-connected', 'true'); // 监听页面关闭,清理资源 window.addEventListener('beforeunload', () => { localStorage.removeItem('sse-connected'); eventSource.close(); }); } else { // 接收主连接标签页的数据 channel.addEventListener('message', (e) => { handleData(e.data); }); } function handleData(data) { // 业务逻辑处理 console.log("Received data:", data); }
(2)服务端实现连接分组与复用
- 让客户端订阅特定数据主题,服务端仅推送对应主题的更新,避免全量数据推送
- 合并相同数据流的连接,多个客户端订阅同一主题时,服务端维护一份推送逻辑,降低资源占用
(3)及时清理无效连接
除了监听close事件,还要监听abort事件,确保客户端断开时及时释放服务端资源:
req.on("close", () => { res.end(); // 移除该连接的订阅或清理相关资源 }); req.on("abort", () => { res.end(); // 同步清理资源 });
2. 关于浏览器同时连接数上限
现代浏览器的HTTP/1.1并发连接上限确实为4-6个,无法通过前端代码或常规配置修改——这是浏览器内置的安全与性能限制,防止单个服务器被过度请求压垮。
如果服务端支持HTTP/2,浏览器对同一域名的并发连接限制会大幅提升(通常数十到上百个),因为HTTP/2支持多路复用,多个请求可通过同一TCP连接传输。升级到HTTP/2是解决该限制的有效方式,Express可配合spdy模块或Node.js 8+原生HTTP/2 API实现。
3. 实时数据流的替代方案
(1)WebSocket
WebSocket是双向通信协议,同一连接可双向传输数据,浏览器对其并发连接限制远宽松于HTTP/1.1:
- 客户端示例:
const socket = new WebSocket('ws://your-server/api/data/stream'); socket.onmessage = function(event) { const data = JSON.parse(event.data); handleData(data); };
- 服务端示例(使用
ws模块):
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', (ws) => { // 定时推送数据 const interval = setInterval(() => { ws.send(JSON.stringify(getLatestData())); }, 1000); // 客户端断开时清理定时器 ws.on('close', () => { clearInterval(interval); }); });
(2)轮询(短轮询/长轮询)
短轮询是客户端定期请求服务器获取数据,长轮询是服务器保持连接直至有数据更新再返回。虽然实时性不如SSE或WebSocket,但可通过控制请求频率降低并发连接数,适合实时性要求不高的场景。
(3)消息队列中间件
若应用规模较大,可引入Redis Pub/Sub或MQTT等中间件,客户端通过MQTT协议连接(支持多路复用),服务端将数据发布到队列,客户端订阅对应主题,大幅减少服务端直接连接数。
内容的提问来源于stack exchange,提问作者VictoRPiles

