总览
| 传统轮询 | 长轮询 | SSE | websocket | socket.io | |
|---|---|---|---|---|---|
| 模式 | 客户端主动 | 客户端请求,阻塞 | 服务端单向推送 | 全双工 | 全双工+HTTP |
| 实时性 | 低 | 中 | 高 | 极高 | 极高 |
| 资源消耗 | 高 | 中 | 低 | 低 | 低 |
| 兼容性 | all | all | 非IE | 现代 | all |
| 场景 | 通知 | 聊天室 | 股票 | 在线游戏 | 兼容性高 |
传统轮询
- 原理: setinterval不间断请求
// 客户端示例
setInterval(() => fetch('/api/data'), 5000);
- 弊端:
- 高延迟(取决于轮询间隔)
- 浪费带宽和服务器资源(频繁空请求)
长轮询
原理:递归+条件判断
/**
* 创建一个轮询函数
* @param {Function} apiRequest - 异步请求函数(例如 fetch 或 axios 请求)
* @param {Function} conditionCheck - 检查条件是否满足的函数
* @param {number} interval - 轮询时间间隔(毫秒)
* @param {number} maxAttempts - 最大尝试次数(默认为 Infinity,表示无限制)
* @returns {Promise<any>} - 返回最终的结果或错误
*/
interface ApiRequest<T> {
(): Promise<T>;
}
interface ConditionCheck<T> {
(response: T): boolean;
}
export function poll<T>(apiRequest: ApiRequest<T>, conditionCheck: ConditionCheck<T>, interval = 1000, maxAttempts = Infinity): Promise<T> {
let attempts = 0;
const executePoll = async (resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => {
try {
// 执行请求
const response = await apiRequest();
// 检查条件是否满足
if (conditionCheck(response)) {
console.log('条件已满足,停止轮询');
resolve(response); // 停止轮询并返回结果
return;
}
// 如果条件未满足,继续下一次轮询
attempts++;
if (attempts < maxAttempts) {
setTimeout(() => executePoll(resolve, reject), interval); // 下一次轮询
} else {
reject(new Error('达到最大尝试次数,停止轮询'));
}
} catch (error) {
console.error('轮询过程中发生错误:', error);
reject(error);
}
};
// 使用 Promise 包装轮询逻辑
return new Promise((resolve, reject) => {
executePoll(resolve, reject);
});
}
- 弊端:
- 出现超时或重连,实现复杂
- 服务器需维护挂起连接(增加资源占用)
解决方案
超时重连:设置定时,超出时间强制重连
function longPoll() {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort(); // 终止请求
console.log('请求超时,重新连接...');
longPoll(); // 立即重试
}, 30000); // 超时时间设为30秒
fetch('/api/long-poll', { signal: controller.signal })
.then(response => response.json())
.then(data => {
clearTimeout(timeoutId); // 清除超时计时器
handleData(data); // 处理数据
longPoll(); // 继续下一次请求
})
.catch(error => {
if (error.name === 'AbortError') {
// 超时错误已由上面处理
} else {
console.error('请求失败:', error);
retryWithBackoff(); // 使用退避策略重试
}
});
}
// 启动长轮询
longPoll();
//优化方案:
//逐步增加重试时间间隔,避免雪崩
let retryCount = 0;
const MAX_RETRIES = 5;
function retryWithBackoff() {
if (retryCount >= MAX_RETRIES) {
console.error('达到最大重试次数,停止轮询');
return;
}
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s...
retryCount++;
setTimeout(() => {
longPoll();
retryCount = 0; // 成功后重置计数器
}, delay);
}
SSE
- 原理:基于HTTP长连接,服务器主动推送数据到客户端。
严格地说,HTTP 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(streaming)。
也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。
客户端实现
- 基本使用,可支持跨域,通过
withCredentials声明,onopen,onmessage,onerror,close属性定义回调函数
var source = new EventSource(url, { withCredentials: true });
source.onopen = function (event) {
// ...
};
source.onmessage = function (event) {
var data = event.data;
// handle message
};
source.onerror = function (event) {
// handle error event
};
source.close();
服务端实现
- 请求头格式
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
第一行的Content-Type必须指定 MIME 类型为event-steam
- 发送message格式
[field]: value\n
[field]包括:
data:数据内容
event:自定义的事件类型,默认是`message`事件
//例如:event:foo,使用addEventListener监听
source.addEventListener('foo', function (event) {
var data = event.data;
// handle message
}, false);
id:数据标志符,客户端通过lastEventId读取
retry:指定浏览器重新发起连接的时间间隔
- node实例
var http = require("http");
http.createServer(function (req, res) {
var fileName = "." + req.url;
if (fileName === "./stream") {
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");
interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);
req.connection.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(8844, "127.0.0.1");
websocket
- 原理:基于TCP的全双工通信协议,建立持久化连接。
// 客户端
const ws = new WebSocket('wss://api.example.com');
ws.onmessage = (e) => console.log(e.data);
ws.send('Hello Server');
// 服务器(Node.js示例)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.send('Welcome');
});
websocket和SSE区别
- SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
- SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
- SSE 默认支持断线重连,WebSocket 需要自己实现。
- SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
- SSE 支持自定义发送的消息类型。
socket.io
- 原理:基于WebSocket的封装库,自动降级为轮询(兼容旧浏览器)。
// 客户端
const socket = io('https://api.example.com');
socket.on('chat', (msg) => console.log(msg));
socket.emit('chat', 'Hello');
// 服务器(Node.js)
const io = require('socket.io')(server);
io.on('connection', (socket) => {
socket.emit('news', { data: 'New event' });
});