1、实时通信有哪些实现方式?
| 特性 | 轮询(Polling) | WebSocket | SSE (Server-Sent Events) |
|---|---|---|---|
| 通信方向 | 单向(客户端 → 服务端) | 双向(客户端 ↔ 服务端) | 单向(服务端 → 客户端) |
| 连接方式 | 客户端定时发起HTTP请求 | 持久连接(基于TCP协议) | 持久连接(基于HTTP协议) |
| 实现复杂度 | 简单 | 较复杂(需处理握手、协议转换等) | 简单 |
| 实时性 | 低(依赖轮询间隔) | 高(支持实时双向通信) | 高(服务端实时推送) |
| 性能开销 | 高(频繁建立/关闭连接) | 低(单个持久连接) | 低(单个持久连接) |
| 示例场景 | 定时获取天气、股票数据 | 在线聊天、多人游戏、实时协作 | 新闻推送、通知、实时股票行情 |
- 轮询:开发简单,但是效率太低了,适合对实时性要求不高的场景。在项目中,基本是不推荐使用,这种做法比较 low。
- WebSocket:开发起来较为复杂。性能好,可以双向实时通信,适合需要交互的场景。推荐使用 Socket.IO 包来开发。
- SSE:开发简单。性能好,但只能单向实时通信,适合服务端向客户端主动推送数据的场景。
另外值得一提的是,现在各个 AI 平台的消息推送,也都是使用SSE来实现的。
2、SSE 的基础实现
// 设置正确的响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 定时发送数据
const intervalId = setInterval(() => {
const data = {
message: `当前时间是 ${
new Date().toLocaleTimeString()}`,
};
res.write(`data: ${
JSON.stringify(data)}\n\n`);
}, 2000);
// 处理客户端断开连接
req.on('close', () => {
clearInterval(intervalId); // 清除定时器
res.end(); // 结束响应
});
- 想要用
SSE来推送数据,顶部要按照这个格式来设置响应头,明确指明为event-stream。这样才能被识别成SSE。 中间部分,简单的写了一个定时执行。每隔两秒钟,计算一下当前的时间。- 然后注意了,这里用的是
res.write,这是实现SSE的关键代码。使用res.write可以不断的,分多次发送数据,而不需要一次性发送完整的响应。 - 还有要注意的是,我们这里
发送数据的格式,按照SSE的规则,必须是以data:开头,以两个\n结束。\n是换行的意思。 - 最下面,如果
连接断开了,就停止定时器,并结束响应。
3、前端部分实现
SSE默认不支持在header中传递数据,那就直接在URL里传token好了
// 定义管理员令牌,用于身份验证,在实际使用时需将 'xxxx' 替换为真实有效的令牌
const token = 'xxxx';
// 创建一个 EventSource 对象,用于建立与服务器的 SSE 连接
// 这里使用模板字符串将 token 拼接到请求的 URL 中,该 URL 指向服务器上处理订单流数据的接口
const eventSource = new EventSource(`http://localhost:3000/admin/charts/stream_order?token=${
token }`);
// 当服务器向客户端发送消息时,会触发 onmessage 事件
eventSource.onmessage = function (event) {
// 使用 try...catch 块来捕获可能出现的异常,例如数据解析失败的情况
try {
// 服务器发送的数据存储在 event.data 中,通常是 JSON 格式的字符串
// 使用 JSON.parse 方法将其解析为 JavaScript 对象
const data = JSON.parse(event.data);
// 将解析后的数据打印到控制台,方便调试和查看
console.log(data);
} catch (error) {
// 如果解析数据过程中出现错误,将错误信息打印到控制台
console.error('解析数据出错:', error);
}
};
// 当 SSE 连接出现错误时,会触发 onerror 事件
eventSource.onerror = function (error) {
// 将连接错误信息打印到控制台,方便调试和排查问题
console.error('SSE 连接出错:', error);
};
4、测试
检查,选择网络。然后刷新一下,就能看到发出的请求了
注意:请求的状态是pending,等待 2 秒钟后,状态变为200的时候我们再点进去。这是因为如果点早了,浏览器还没有将它识别成SSE。



5、 SSE 小结
- 在
Node部分,设置好响应头,并用res.write不断的发数据出去就好。 前端部分,用new EventSource建立连接。并通过onmessage事件,来接收数据。
6、实践
6.1、后端代码实现
6.1.1、新建 SSE 处理工具类
在根目录新建streams文件夹,里面再新建一个sse-handler.js,用于处理服务器发送事件(Server-Sent Events, SSE)的工具类,代码如下:
const {
setKey, getKey} = require('../utils/redis');
// 定义一个名为 SSEHandler 的类,用于处理服务器发送事件(SSE)的连接和数据广播
class SSEHandler {
// 构造函数,在创建 SSEHandler 实例时会自动调用
constructor() {
// 使用 ES6 的 Set 数据结构来存储与浏览器建立 SSE 连接的响应对象(res)
// Set 可以确保存储的元素唯一,避免重复
this.clients = new Set();
}
/**
* 初始化 SSE 数据流,处理新的客户端连接
* @param {Object} res - Express 响应对象,用于向客户端发送数据
* @param {Object} req - Express 请求对象,包含客户端的请求信息
*/
initStream(res, req) {
// 设置响应头,指定响应内容类型为 text/event-stream,这是 SSE 的标准内容类型
res.setHeader('Content-Type', 'text/event-stream');
// 设置缓存控制,禁止浏览器缓存响应内容,确保每次请求都能获取最新数据
res.setHeader('Cache-Control', 'no-cache');
// 设置连接类型为 keep-alive,保持与客户端的长连接
res.setHeader('Connection', 'keep-alive');
// 刷新响应头,将设置的响应头信息发送给客户端
res.flushHeaders();
// 将当前客户端的响应对象添加到 clients 集合中,以便后续广播数据时使用
this.clients.add(res);
// 监听客户端连接关闭事件,当客户端断开连接时触发回调函数
req.on('close', () => {
// 从 clients 集合中删除当前客户端的响应对象
this.clients.delete(res);
// 打印日志,提示客户端已断开连接
console.log('Client disconnected');
});
}
/**
* 向所有连接的客户端广播数据
* @param {Object} data - 要广播的数据对象,会被序列化为 JSON 字符串发送给客户端
*/
async broadcastData(data) {
await setKey('sse_broadcast_data', data);
// 遍历 clients 集合中的每个客户端响应对象
this.clients.forEach((client) => {
// 检查客户端响应是否已经结束,如果未结束则继续发送数据
if (!client.finished) {
// 按照 SSE 的格式,以 "data: " 开头,后面跟上 JSON 序列化后的数据,以两个换行符 "\n\n" 结尾
// 并将其写入客户端响应流,发送给客户端
client.write(`data: ${
JSON.stringify(data)}\n\n`);
}
});
}
}
// 将 SSEHandler 类导出,以便其他模块可以引入和使用
module.exports = SSEHandler;
6.1.2、定义和管理统计查询的 SQL 语句·
创建utils/stats-query.js文件,用于定义和管理统计查询的 SQL 语句:
const statsQueries = {
order: "SELECT DATE_FORMAT(`createdAt`, '%Y-%m') AS `month`, COUNT(*) AS `value` FROM `Orders` GROUP BY `month` ORDER BY `month` ASC",
user: "SELECT DATE_FORMAT(`createdAt`, '%Y-%m') AS `month`, COUNT(*) AS `value` FROM `Users` GROUP BY `month` ORDER BY `month` ASC",
// 可以添加更多类型的统计查询
};
function getStatsQuery(type) {
return statsQueries[type];
}
module.exports = {
getStatsQuery
};
6.1.3、 广播服务的实现
创建utils/broadcast-service.js文件,实现广播服务:
const {

最低0.47元/天 解锁文章
366

被折叠的 条评论
为什么被折叠?



