2022年12月12日

长轮询

长轮询是与服务器保持持久连接的最简单方法,它不使用任何特定的协议,如 WebSocket 或 Server Sent Events。

它非常容易实现,在很多情况下也足够好。

定期轮询

从服务器获取新信息的简单方法是定期轮询。也就是说,定期向服务器发送请求:“你好,我在,你有我的信息吗?”。例如,每 10 秒一次。

作为响应,服务器首先注意到客户端在线,其次发送它到目前为止收到的消息包。

这有效,但也有缺点

  1. 消息传递延迟高达 10 秒(两次请求之间)。
  2. 即使没有消息,服务器也会每 10 秒被请求轰炸,即使用户切换到其他地方或睡着了。从性能角度来说,这是一个相当大的负担。

因此,如果我们谈论的是一个非常小的服务,这种方法可能是可行的,但总的来说,它需要改进。

长轮询

所谓的“长轮询”是轮询服务器的更好方法。

它也很容易实现,并且可以无延迟地传递消息。

流程

  1. 向服务器发送请求。
  2. 服务器不会关闭连接,直到它有消息要发送。
  3. 当消息出现时 - 服务器用它来响应请求。
  4. 浏览器立即发出新的请求。

在这种情况下,浏览器已发送请求并与服务器保持挂起连接,这是此方法的标准做法。只有在传递消息时,连接才会关闭并重新建立。

如果连接由于网络错误等原因而丢失,浏览器会立即发送新的请求。

客户端 `subscribe` 函数的草图,它发出长请求

async function subscribe() {
  let response = await fetch("/subscribe");

  if (response.status == 502) {
    // Status 502 is a connection timeout error,
    // may happen when the connection was pending for too long,
    // and the remote server or a proxy closed it
    // let's reconnect
    await subscribe();
  } else if (response.status != 200) {
    // An error - let's show it
    showMessage(response.statusText);
    // Reconnect in one second
    await new Promise(resolve => setTimeout(resolve, 1000));
    await subscribe();
  } else {
    // Get and show the message
    let message = await response.text();
    showMessage(message);
    // Call subscribe() again to get the next message
    await subscribe();
  }
}

subscribe();

如您所见,`subscribe` 函数发出一个 fetch,然后等待响应,处理它并再次调用自身。

服务器应该可以处理许多挂起连接

服务器架构必须能够处理许多挂起连接。

某些服务器架构为每个连接运行一个进程,导致进程数量与连接数量相同,而每个进程消耗相当多的内存。因此,太多的连接只会消耗所有内存。

这通常是使用 PHP 和 Ruby 等语言编写的后端的情况。

使用 Node.js 编写的服务器通常不会遇到此类问题。

也就是说,这不是编程语言问题。大多数现代语言,包括 PHP 和 Ruby,都允许实现适当的后端。请确保您的服务器架构能够正常处理许多同时连接。

演示:聊天

这是一个演示聊天,您也可以下载它并在本地运行(如果您熟悉 Node.js 并可以安装模块)

结果
browser.js
server.js
index.html
// Sending messages, a simple POST
function PublishForm(form, url) {

  function sendMessage(message) {
    fetch(url, {
      method: 'POST',
      body: message
    });
  }

  form.onsubmit = function() {
    let message = form.message.value;
    if (message) {
      form.message.value = '';
      sendMessage(message);
    }
    return false;
  };
}

// Receiving messages with long polling
function SubscribePane(elem, url) {

  function showMessage(message) {
    let messageElem = document.createElement('div');
    messageElem.append(message);
    elem.append(messageElem);
  }

  async function subscribe() {
    let response = await fetch(url);

    if (response.status == 502) {
      // Connection timeout
      // happens when the connection was pending for too long
      // let's reconnect
      await subscribe();
    } else if (response.status != 200) {
      // Show Error
      showMessage(response.statusText);
      // Reconnect in one second
      await new Promise(resolve => setTimeout(resolve, 1000));
      await subscribe();
    } else {
      // Got message
      let message = await response.text();
      showMessage(message);
      await subscribe();
    }
  }

  subscribe();

}
let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');

let fileServer = new static.Server('.');

let subscribers = Object.create(null);

function onSubscribe(req, res) {
  let id = Math.random();

  res.setHeader('Content-Type', 'text/plain;charset=utf-8');
  res.setHeader("Cache-Control", "no-cache, must-revalidate");

  subscribers[id] = res;

  req.on('close', function() {
    delete subscribers[id];
  });

}

function publish(message) {

  for (let id in subscribers) {
    let res = subscribers[id];
    res.end(message);
  }

  subscribers = Object.create(null);
}

function accept(req, res) {
  let urlParsed = url.parse(req.url, true);

  // new client wants messages
  if (urlParsed.pathname == '/subscribe') {
    onSubscribe(req, res);
    return;
  }

  // sending a message
  if (urlParsed.pathname == '/publish' && req.method == 'POST') {
    // accept POST
    req.setEncoding('utf8');
    let message = '';
    req.on('data', function(chunk) {
      message += chunk;
    }).on('end', function() {
      publish(message); // publish it to everyone
      res.end("ok");
    });

    return;
  }

  // the rest is static
  fileServer.serve(req, res);

}

function close() {
  for (let id in subscribers) {
    let res = subscribers[id];
    res.end();
  }
}

// -----------------------------------

if (!module.parent) {
  http.createServer(accept).listen(8080);
  console.log('Server running on port 8080');
} else {
  exports.accept = accept;

  if (process.send) {
     process.on('message', (msg) => {
       if (msg === 'shutdown') {
         close();
       }
     });
  }

  process.on('SIGINT', close);
}
<!DOCTYPE html>
<script src="browser.js"></script>

All visitors of this page will see messages of each other.

<form name="publish">
  <input type="text" name="message" />
  <input type="submit" value="Send" />
</form>

<div id="subscribe">
</div>

<script>
  new PublishForm(document.forms.publish, 'publish');
  // random url parameter to avoid any caching issues
  new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
</script>

浏览器代码位于 `browser.js` 中。

使用范围

长轮询在消息很少的情况下非常有效。

如果消息非常频繁,那么上面绘制的请求-接收消息图表将变成锯齿状。

每条消息都是一个单独的请求,附带标题、身份验证开销等。

因此,在这种情况下,更喜欢使用其他方法,例如 WebSocketServer Sent Events

教程地图

评论

在评论之前请阅读…
  • 如果您有改进建议,请提交 GitHub issue或 pull request,而不是评论。
  • 如果您不理解文章中的某些内容,请详细说明。
  • 要插入少量代码,请使用<code>标签,对于多行代码,请用<pre>标签包裹,对于超过 10 行的代码,请使用沙盒(plnkrjsbincodepen…)