短轮询 Short Polling
定义
短轮询是一种客户端与服务器之间的通讯方式,客户端定期向服务器发送 HTTP 请求,检查是否有新消息。没有新消息,服务器会返回一个空响应。
短轮询的基本原理是客户端每隔一段时间(如每隔5秒)向服务器发起一次普通 HTTP 请求。服务端查询当前接口是否有数据更新,若有数据更新则向客户端返回最新数据,若无则提示客户端无数据更新。
优点
- 简单直接:实现简单,容易部署
- 兼容性好:所有浏览器都支持,使用 XHR 对象就能实现
缺点
- 频繁的 HTTP 请求:可能会增加服务器和网络的负载,实时性有限,因为客户端只能在轮询间隔内获取到更新
- 资源浪费:大量的无用请求会浪费带宽和服务器资源
实用性
短轮询适用于小型应用、传统的 web 通信模式中,后台处理数据需要一定时间,前端想知道后端的处理结果,就要不定时地向后端发出请求以获得最新情况
代码实现
/**
* 异步方法requestFn
* options:配置项,包括以下内容
* 请求参数initialData?: object;
* 轮询间隔pollingInterval?: number | null;
* 请求成功回调onSuccess?: (res: any) => void;
**/
const useMyRequest = (requestFn,options) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const pollingIntervalTimer = useRef(null);
const { initialData,pollingInterval,onSuccess } = options;
useEffect(() => {
if(!loading){
setLoading(true);
request();
}
}, [loading,request]);
const request = async () => {
try {
if (pollingInterval) {
pollingIntervalTimer.current = setTimeout(() => {
request();
}, pollingInterval);
}
const res = await requestFn(initialData);
setData(res);
onSuccess(res);
} catch (err) {
setError(JSON.stringify(err));
} finally {
setLoading(false);
}
};
const cancel = () => {
if (pollingIntervalTimer.current) {
clearTimeout(pollingIntervalTimer.current);
pollingIntervalTimer.current = null;
}
};
return { data, loading, error, request, cancel };
};
export default useMyRequest;
长轮询 Long Polling
定义
长轮询是一种在客户端和服务器之间进行信息交换的技术,它允许服务器等待或保持连接打开以发送更多数据,而不是立即关闭连接。
客户端发起请求,如果服务端的数据没有发生变更,就 hold 住请求,直到服务端的数据发生了变更,或者达到了一定的时间就会返回。
相比于常规轮询,长轮询的无效请求可以大大减少,另外也具有类似实时推送的特性,消息具有实时性。
流程
- 请求发送到服务器
- 服务器在有新数据之前不会响应请求
- 当新数据出现、或超过预设等待时间时,服务器将对其请求作出响应
- 浏览器立即发出一个新的请求
代码实现
客户端:
function longPoll() {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/long-poll-endpoint', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
// 处理服务器返回的数据
console.log(xhr.responseText);
// 再次发起长轮询
longPoll();
} else {
// 发生错误,可以设置合适的重试策略
console.error('Long polling error: ', xhr.statusText);
}
}
};
xhr.send();
}
// 开始长轮询
longPoll();
服务端:
const express = require('express');
const sleep = require('system-sleep');
const app = express();
const port = 3000;
app.get('/long-poll-endpoint', (req, res) => {
// 假设这里是一些数据检查
let data = getData();
if (data) {
res.send(data);
} else {
// 如果没有数据,保持连接打开
sleep(5000); // 休眠5秒,实际生产环境可以使用非阻塞的方式等待数据
// 重新检查数据
data = getData();
if (data) {
res.send(data);
} else {
res.status(500).send('No data and timed out.');
}
}
});
function getData() {
// 模拟数据检查,实际应用中可能是数据库查询或其他操作
return 'Some data';
}
app.listen(port, () => {
console.log(`Long polling server listening at http://localhost:${port}`);
});
WebSocket
WebSocket 是一种基于 TCP 的全双工通信协议,允许浏览器与服务器建立持久连接,双方可随时主动推送数据。
不再是「我问你答」,而是「你说我听,我说你也听」 WebSocket 提升的是实时性,但把复杂度从“请求次数”转移到了“连接管理”。
WebSocket 的工作原理
1️⃣ 握手阶段(HTTP → WebSocket)
WebSocket 并不是一上来就“另起炉灶”,而是通过 HTTP 协议升级(Upgrade) 完成连接建立:
Client ——HTTP 请求(Upgrade: websocket)——> Server
Client <——101 Switching Protocols—— Server
关键点:
- 使用 HTTP/HTTPS 发起请求
- 服务端返回
101 Switching Protocols - 成功后协议升级为 WebSocket(
ws:///wss://)
2️⃣ 通信阶段(WebSocket Frame)
升级成功后:不再使用 HTTP Request / Response,改为 WebSocket 帧(Frame)通信
- 支持:
- 文本帧(UTF-8)
- 二进制帧
- 控制帧(ping / pong / close)
3️⃣ 心跳机制(生产必备)
WebSocket 本身不会自动保活,常见做法:
- 定时发送
ping - 对端返回
pong - 若超时无响应 → 断线 → 重连
4️⃣ 断线重连
关键是 检测断线 + 指数退避 + 状态管理,避免无限刷新的“重连风暴”。
1. 断线检测
- onclose / onerror:WebSocket 提供原生事件检测连接关闭或错误。
const ws = new WebSocket(url);
ws.onopen = () => console.log('连接成功');
ws.onmessage = (e) => console.log('收到消息', e.data);
ws.onclose = () => console.log('连接关闭');
ws.onerror = () => console.log('连接出错');
- 心跳检测(可选):客户端定期发送 ping,服务器返回 pong,确保连接活跃。
2. 自动重连策略
-
简单重连
function connect() { const ws = new WebSocket(url); ws.onopen = () => console.log('连接成功'); ws.onclose = () => setTimeout(connect, 1000); // 1秒后重连 ws.onerror = () => ws.close(); // 出错关闭触发重连 } connect(); -
指数退避
- 避免网络不稳定时不断刷屏重连。
- 每次重连间隔逐步增加,最大限制避免无限拉长。
let retryCount = 0;
function connect() {
const ws = new WebSocket(url);
ws.onopen = () => {
console.log('连接成功');
retryCount = 0; // 重置计数
};
ws.onclose = () => {
retryCount++;
const timeout = Math.min(1000 * 2 ** retryCount, 30000); // 最大30秒
console.log(`连接关闭,${timeout}ms 后重连`);
setTimeout(connect, timeout);
};
ws.onerror = () => ws.close();
}
connect();
3. 状态管理
-
在断线重连期间,消息可能无法发送。
-
常用做法:
- 消息队列:重连成功后重新发送未发送的消息。
- UI 提示:显示“正在重连...”或灰色状态。
- 避免重复连接:在
connect()前先判断已有连接状态。
4. 心跳机制
- 可防止 NAT/路由器断开空闲连接:
let heartbeatInterval: number;
function startHeartbeat(ws: WebSocket) {
heartbeatInterval = setInterval(() => {
if(ws.readyState === WebSocket.OPEN) ws.send('ping');
}, 5000); // 每5秒
}
function stopHeartbeat() {
clearInterval(heartbeatInterval);
}
- 监听 close/error → 触发重连。
- 指数退避 → 避免频繁重连浪费资源。
- 心跳检测 → 保持连接活跃。
- 消息队列 + 状态管理 → 确保业务逻辑不中断。
WebSocket 作用
一句话总结它的核心价值:低延迟 · 双向通信 · 高实时性
典型能力:
- 🚀 服务端主动推送
- 🔄 双向实时交互
- 📉 减少无效请求与带宽浪费
- 📦 支持二进制数据传输
WebSocket 优缺点
- 优点
- 全双工通信(客户端 & 服务端都能主动发)
- 延迟极低,适合实时业务
- 连接复用,节省请求头开销
- 支持二进制数据(音视频、protobuf)
- 缺点
- 长连接管理复杂(心跳、重连、清理)
- 服务端扩展成本高(连接有状态)
- 负载均衡需要特殊处理(sticky session / 消息总线)
- 对代理、防火墙不够友好(需 wss + keepalive)
WebSocket 为什么比轮询效率高
WebSocket 比轮询效率高,核心原因就在于长连接与消息推送机制
轮询(Polling)模式
客户端周期性发送 HTTP 请求(如每秒一次)询问服务器是否有新数据。
- 每次请求都要建立 HTTP 连接(或使用 keep-alive,但仍有请求头开销)。
- 如果没有新数据,服务器仍然返回响应,浪费带宽和 CPU。
- 延迟固定取决于轮询间隔,间隔短 → 频繁请求,浪费资源;间隔长 → 数据延迟。
客户端 → 请求 → 服务器
客户端 → 请求 → 服务器
客户端 → 请求 → 服务器
WebSocket 模式
客户端与服务器只建立一次 TCP 长连接,双方都可以随时发送消息。
- 一次握手后保持连接,减少重复握手开销。
- 服务器主动推送数据,只有有更新才发消息。
- 低延迟,消息几乎实时到达客户端。
- 更适合高频数据或实时交互场景(聊天、游戏、行情、监控)。
客户端 ↔——— 长连接 ——↔ 服务器
数据实时推送
| 方面 | 轮询 | WebSocket |
|---|---|---|
| 连接开销 | 每次请求都可能建立 TCP/HTTP 连接 | 只建立一次连接 |
| 数据延迟 | 受轮询间隔影响,可能延迟 | 几乎实时 |
| 带宽利用 | 空轮询浪费带宽 | 仅有消息时传输数据 |
| CPU 消耗 | 服务器频繁处理 HTTP 请求 | 服务器只处理实际消息 |
| 扩展性 | 多客户端高频轮询压力大 | 高并发下更节省资源 |
适用场景
- 轮询适合:低频或偶尔查询,不追求实时性。
- WebSocket适合:实时性要求高,数据量中等到大,高并发下节省资源。
WebSocket 的高效源于“保持长连接 + 服务器主动推送”,避免了轮询的重复请求和空等待,从而降低延迟、节省带宽和 CPU。
WebSocket vs 轮询 / SSE
| 方案 | 实时性 | 是否双向 | 带宽消耗 | 复杂度 | 适合场景 |
|---|---|---|---|---|---|
| 短轮询 | ❌ 差 | ❌ | ❌ 高 | ⭐ | 非实时 |
| 长轮询 | ⚠️ 中 | ❌ | ⚠️ 中 | ⭐⭐ | 兼容老系统 |
| SSE | ✅ 好 | ❌ | ✅ 低 | ⭐⭐ | 单向推送 |
| WebSocket | 🚀 极好 | ✅ | ✅ 低 | ⭐⭐⭐⭐ | 实时交互 |
如何选呢?🤔
- 只需要服务端推送? 👉 SSE
- 低频更新? 👉 轮询
- 强实时 + 双向? 👉 WebSocket(毫不犹豫)
React 中如何优雅使用 WebSocket(实战)
警惕以下做法⚠️
- 每个组件都 new WebSocket
- 无重连、无心跳
- 组件卸载不清理连接
✅ 推荐方案:自定义 Hook
const { send, readyState } = useWebSocket({
url: 'wss://example.com/ws',
onMessage: (msg) => {
console.log('收到消息', msg)
},
})
hook 核心能力要求:
-
✅ 单连接可复用
-
❤️ 心跳保活(防中间层掐线)
-
🔁 自动重连(指数退避)
-
🚦 发送限流(防刷、防误触)
-
📊 基础监控指标可上报
-
🧹 组件卸载时 100% 清理
WebSocket 应该是「全局资源」,而不是组件私有资源。
useWebSocket
├── WebSocket 实例管理
├── 心跳系统(ping / pong)
├── 重连系统(指数退避)
├── 消息发送队列 + 限流
├── 状态与指标监控
└── React 生命周期托管
- 心跳保活
- 定时发
ping - 服务端需回
pong - 超时视为连接失效 → 主动 close → 触发重连
const startHeartbeat = useCallback(() => {
if (heartbeatInterval <= 0) return
heartbeatTimer.current = window.setInterval(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: '__ping__' }))
onMetric?.({ type: 'message_sent', timestamp: Date.now(), extra: { heartbeat: true } })
}
}, heartbeatInterval)
}, [heartbeatInterval, onMetric])
const stopHeartbeat = () => {
if (heartbeatTimer.current) {
clearInterval(heartbeatTimer.current)
heartbeatTimer.current = null
}
}
2. 重连机制
delay = min(base * 2^attempt, max)
const scheduleReconnect = useCallback(() => {
if (reconnectAttempts.current >= maxReconnectAttempts) return
const delay = Math.min(
reconnectBaseDelay * 2 ** reconnectAttempts.current,
reconnectMaxDelay
)
reconnectAttempts.current += 1
onMetric?.({
type: 'reconnect',
timestamp: Date.now(),
extra: { attempt: reconnectAttempts.current, delay },
})
setTimeout(connect, delay)
}, [])
- 发送限流(前端自保)
- 每秒最多
N条 - 超过直接丢弃或警告
- 防止按钮连点 / bug 洪水
const send = useCallback((data: any) => {
if (wsRef.current?.readyState !== WebSocket.OPEN) return false
if (sendCountRef.current >= sendRateLimit) {
console.warn('[WebSocket] send rate limited')
return false
}
wsRef.current.send(JSON.stringify(data))
sendCountRef.current += 1
onMetric?.({ type: 'message_sent', timestamp: Date.now() })
if (!sendResetTimer.current) {
sendResetTimer.current = window.setTimeout(() => {
sendCountRef.current = 0
sendResetTimer.current = null
}, 1000)
}
return true
}, [])