长轮询 和 SSE
SSE(Server-Sent Events) 和长轮询(long polling)都是用于实现服务器向客户端主动推送数据的技术,解决了传统 HTTP “请求 - 响应” 模式中客户端需频繁轮询的低效问题。它们的核心都是实现服务端向客户端持续发送数据。
长轮询
客户端向服务器发起一个长时间保持的 HTTP 请求,服务器不立即返回响应(即使没有数据),而是 “挂起” 请求等待直到有新数据产生或请求超时,才返回响应。客户端收到响应后,立即发起下一个长轮询请求,形成 “请求 - 等待 - 响应并发起下一次请求” 的循环。
特点: 多请求多响应。即客户端需要多次发起请求,服务端多次响应。本质上还是“客户端主动请求 - 服务器被动响应”。
客户端 --> 服务端 --> 客户端 --> (循环直到客户端停止请求)...SSE(Server-Sent Events)
基于 HTTP 的单向通信协议(专属的请求头 Content-Type: text/event-stream),客户端与服务器建立一次持久化的 HTTP 连接后,服务器可通过该连接持续向客户端推送数据(无需客户端重复发起请求),且数据仅从服务器流向客户端(单向)。
特点: 单请求多响应。即在一个连接中,服务端单向向客户端持续发送数据。
客户端 --(建立连接)--> 服务端 --(持续发送数据,直到断开连接)--> 客户端使用场景
适合用长轮询的场景
- 需要双向通信:如客户端需向服务器发送指令,同时接收服务器的响应(如 “客户端发起任务,服务器推送任务进度”);
- 需支持旧浏览器:如需兼容 IE 等不支持 SSE 的浏览器;
- 需传输二进制数据:如推送图片、文件片段等(无需编码,直接传输二进制流);
- 简单场景,不想处理 SSE 的格式规范:如小型项目,自定义 JSON 响应更灵活。
适合用 SSE 的场景
- 单向推送需求:如 AI 聊天、股票行情推送、日志实时打印(仅需服务器推数据,客户端无需反馈);
- 追求低开销:避免长轮询的 “重连开销”,一次连接减少网络损耗;
- 希望简化开发:依赖EventSource的自动重连、状态监听,减少客户端手动逻辑(如无需写重连定时器);
- 仅传输文本数据:如 JSON 格式的消息、文本日志等(无需二进制)。
示例代码
以下示例代码使用 express 实现。
长轮询示例
js
const express = require('express');
const router = express.Router();
// 存放请求的数组
let requestList = [];
// 模拟一个耗时任务
function task(num){
// 每 3 秒计算一次
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(num + Math.floor(Math.random()*10));
},3000);
})
}
// 对于一个请求来说,不使用 end 就不会主动断开连接
// 建立长轮询接口(不主动断开链接,不使用res.send响应)
router.get('/', (req, res) => {
// 每次请求将请求存入数组,并重置30s等待
const timeout = 30000;// 30s超时
if(requestList.length >= 10){
res.json({
code: 0,
message: '请求频繁,请稍后再试',
})
return;
}
requestList.push(res);
const timer = setTimeout(() => {
// 30s超时后(30s没有响应本次请求),将本次请求从数组中移除并响应超时消息
requestList = requestList.filter(item => item !== res);
res.json({
code: 0,
message: '连接超时,没有响应新数据',
})
}, timeout);
// 监听客户端主动断开链接
res.on("close",()=>{
clearTimeout(timer);
// 移除本次请求
requestList = requestList.filter(item => item !== res);
})
});
// 处理轮询的请求,真正处理请求的接口。此处触发了数据更新
router.post("/update",(req,res)=>{
// 遍历数组,响应所有请求(也可以根据具体需求,指定响应某个请求),广播或者单播
requestList.forEach(async item => {
const result = await task(1);
item.json({
code: 1,
message: '连接成功,数据已更新',
data: result,
})
})
res.json({
code: 1,
message: '数据更新',
})
})
module.exports = router;SSE 示例
js
const express = require('express');
const router = express.Router();
// 存放客户端的数组
let clients = [];
// 模拟一个连续的耗时任务
function task(num){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
if(num >= 10){
resolve("done");
return;
}
num += Math.floor(Math.random()*10);
resolve(num);
},1000);
})
}
// SSE端点
router.get('/', (req, res) => {
// 设置响应头
res.setHeader('Content-Type', 'text/event-stream');// 设置 sse 响应头
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive'); // 保持连接
res.setHeader('Access-Control-Allow-Origin', '*');
// 生成唯一客户端ID
const clientId = Date.now();
const newClient = {
id: clientId,
res: res
};
// 添加到客户端列表
clients.push(newClient);
console.log(`客户端 ${clientId} 已连接,当前连接数: ${clients.length}`);
// 发送连接成功事件
res.write(`event: connected\n`);
res.write(`data: ${JSON.stringify({ message: '连接成功', clientId: clientId })}\n\n`);
setInterval(async()=>{
const result = await task(0);
if(result === "done"){ // 如果任务完成,则结束连接
res.end();
}
res.write(`data: ${JSON.stringify({clientId: clientId, result: result })}\n\n`);
},1500)
// 客户端断开连接时清理
req.on('close', () => {
console.log(`客户端 ${clientId} 已断开连接`);
clients = clients.filter(client => client.id !== clientId);
});
});
module.exports = router;客户端请求示例
index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Express</title>
<link rel="stylesheet" href="style/style.css">
</head>
<body>
<h1>Express</h1>
<p>Welcome to Express</p>
<button id="btn">开始获取sse数据</button>
<div id="result"></div>
<script src="./script/index.js"></script>
</body>
</html>js
const computeButton = document.getElementById('btn');
const result = document.getElementById('result');
// 长轮询检测是否有数据更新
function longPolling() {
fetch('/long-polling', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
.then(res => {
if (res.ok) {
return res.json();
}
throw new Error('请求失败');
})
.then(data => {
console.log(data);
longPolling();
})
.catch(err => {
// 请求失败
console.log(err.message);
setTimeout(() => {
longPolling();
}, 3000);
})
}
// 获取流式数据
async function sse() {
const res = await fetch('/sse', {
method: 'GET',
headers: {
'Content-Type': 'text/event-stream',
},
})
if (!res.ok) {
console.log('请求失败');
return;
}
const decode = new TextDecoder('utf-8');
const reader = res.body.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
const data = decode.decode(value,{stream: true});
result.innerHTML +=`<p>${data}</p>`;
}
}
longPolling();
setInterval(() => {
// 5 秒更新一次数据
fetch('/long-polling/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
.then(res => {
if (res.ok) {
return res.json();
}
throw new Error('请求失败');
})
.then(data => {
console.log(data);
})
.catch(err => {
console.log(err.message);
})
}, 5000);
computeButton.addEventListener('click', () => {
sse();
});