短轮询&长轮询&WebSocket

451 阅读7分钟

短轮询 Short Polling

定义

短轮询是一种‌客户端与‌服务器之间的通讯方式,客户端定期向服务器发送 HTTP 请求,检查是否有新消息。没有新消息,服务器会返回一个空响应。

短轮询的基本原理是客户端每隔一段时间(如每隔5秒)向服务器发起一次普通 HTTP 请求。服务端查询当前接口是否有数据更新,若有数据更新则向客户端返回最新数据,若无则提示客户端无数据更新。‌

image.png

优点

  • 简单直接:实现简单,容易部署
  • 兼容性好:所有浏览器都支持,使用‌ 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 住请求,直到服务端的数据发生了变更,或者达到了一定的时间就会返回。

相比于常规轮询,长轮询的无效请求可以大大减少,另外也具有类似实时推送的特性,消息具有实时性。

image.png

流程

  1. 请求发送到服务器
  2. 服务器在有新数据之前不会响应请求
  3. 当新数据出现、或超过预设等待时间时,服务器将对其请求作出响应
  4. 浏览器立即发出一个新的请求

代码实现

客户端:

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 优缺点

  1. 优点
  • 全双工通信(客户端 & 服务端都能主动发)
  • 延迟极低,适合实时业务
  • 连接复用,节省请求头开销
  • 支持二进制数据(音视频、protobuf)
  1. 缺点
  • 长连接管理复杂(心跳、重连、清理)
  • 服务端扩展成本高(连接有状态)
  • 负载均衡需要特殊处理(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 生命周期托管
  1. 心跳保活
  • 定时发 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)
  }, [])

  1. 发送限流(前端自保)
  • 每秒最多 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
  }, [])