共计 2696 个字符,预计需要花费 7 分钟才能阅读完成。
传统轮询方案的性能瓶颈
在构建长对话系统时,传统轮询方案面临三个核心问题:

- 高延迟:客户端需要不断发起请求检查新数据,平均延迟在轮询间隔的 50% 以上
- 资源浪费:无数据更新时仍然产生完整的 HTTP 请求 - 响应周期
- 上下文丢失:每次独立请求难以维护对话的连贯状态
以每秒轮询一次的聊天系统为例,当 1000 个用户同时在线时,服务端 QPS 将达到惊人的 60,000 次 / 分钟,其中大部分请求返回的是空响应。
流式传输技术选型
SSE (Server-Sent Events)
- 协议特性 :
- 基于 HTTP/1.1 的长连接
- 单工通信(服务端→客户端)
- 自动重连机制
-
默认 UTF- 8 编码
-
优势场景 :
- 单向数据推送(如状态更新、实时日志)
- 兼容普通 HTTP 基础设施
- 浏览器内置 EventSource API
WebSocket
- 协议特性 :
- 基于 TCP 的全双工通道
- 二进制帧支持
- 更低的首字节延迟
-
需要协议升级握手
-
优势场景 :
- 双向交互(如在线协作编辑)
- 需要二进制数据传输
- 高频消息交换(>100msg/s)
兼容性对比
| 特性 | SSE | WebSocket |
|---|---|---|
| IE 支持 | ❌ | 10+ |
| 移动端兼容性 | 优秀 | 良好 |
| 防火墙穿透性 | 优秀 | 中等 |
| 最大并发连接数 / 域 | 6-8 | 不限 |
Node.js 实现示例
服务端 SSE 实现
/**
* 创建 SSE 事件流
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
function createEventStream(req, res) {
// 设置 SSE 必需的头信息
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
// 启用分块传输编码
'Transfer-Encoding': 'chunked'
});
// 发送初始心跳包
const heartbeat = setInterval(() => {res.write(':ping\n\n'); // 注意 SSE 协议要求的双换行
}, 30000);
// 清理逻辑
req.on('close', () => {clearInterval(heartbeat);
res.end();});
}
// 消息分块发送示例
function sendChunkedData(res, data) {
const chunkSize = 1024; // 1KB 分块
for (let i = 0; i < data.length; i += chunkSize) {const chunk = data.slice(i, i + chunkSize);
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
}
}
客户端实现
class ChatStream {constructor(url) {
this.retryCount = 0;
this.MAX_RETRIES = 3;
this.connect(url);
}
connect(url) {this.eventSource = new EventSource(url);
this.eventSource.onmessage = (event) => {if (event.data === ':ping') return;
try {const payload = JSON.parse(event.data);
// 渲染到 UI...
} catch (err) {console.error('SSE 数据解析失败:', err);
}
};
this.eventSource.onerror = () => {if (this.retryCount++ < this.MAX_RETRIES) {setTimeout(() => this.connect(url), 1000 * this.retryCount);
}
};
}
}
性能优化要点
分块大小实验数据
| 分块大小 | TTFB(ms) | 内存占用 (MB) |
|---|---|---|
| 512B | 120 | 15.2 |
| 1KB | 85 | 18.7 |
| 4KB | 62 | 22.4 |
| 16KB | 55 | 41.8 |
建议根据实际网络条件动态调整,WiFi 环境下推荐 4KB 分块,移动网络建议 1KB。
背压处理方案
// 使用 Node.js 可写流背压感知
function createBackpressureAwareSender(res) {let buffer = [];
let isDraining = false;
return (data) => {if (isDraining) {buffer.push(data);
return;
}
isDraining = !res.write(`data: ${data}\n\n`);
if (isDraining) {res.once('drain', () => {
isDraining = false;
while (buffer.length && !isDraining) {const item = buffer.shift();
isDraining = !res.write(`data: ${item}\n\n`);
}
});
}
};
}
生产环境注意事项
Nginx 关键配置
proxy_buffering off; # 必须关闭缓冲
proxy_read_timeout 86400s; # 保持长连接
# 处理 EventSource 请求的特殊配置
location /chat-stream {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection '';
}
上下文管理策略
- 基于会话 ID 的 Redis 存储结构:
// 使用 sorted set 维护对话历史
await redis.zadd(`session:${sessionId}`,
Date.now(),
JSON.stringify({
role: 'user',
content: message
})
);
// 获取最近 10 条上下文
const context = await redis.zrevrange(`session:${sessionId}`, 0, 9);
- 客户端携带最后事件 ID 实现断点续传:
GET /chat-stream HTTP/1.1
Last-Event-ID: 1697045632000
扩展思考
对于万级并发场景,可考虑以下 SSE 优化方向:
- 连接复用:通过会话 ID 将相同用户的多个标签页连接合并
- 边缘计算:在 CDN 边缘节点处理 SSE 连接
- 协议升级:对支持 HTTP/ 2 的服务端,利用多路复用特性
- 连接卸载:使用专门的 SSE 网关服务处理长连接
是否可以通过 QUIC 协议实现更高效的流式传输?如何设计混合 SSE+WebSocket 的降级方案?这些留待读者进一步探索。
正文完
