前端工具——通信方法总结(HTTP协议、websocket)

262 阅读4分钟

总览

传统轮询长轮询SSEwebsocketsocket.io
模式客户端主动客户端请求,阻塞服务端单向推送全双工全双工+HTTP
实时性极高极高
资源消耗
兼容性allall非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' });
});