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

它很容易实现,在很多场景下也很好用。

常规轮询

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

作为响应,服务器首先通知自己,客户端处于在线状态,然后 —— 发送目前为止的消息包。

这可行,但是也有些缺点:

  1. 消息传递的延迟最多为 10 秒(两个请求之间)。
  2. 即使没有消息,服务器也会每隔 10 秒被请求轰炸一次,即使用户切换到其他地方或者处于休眠状态,也是如此。就性能而言,这是一个很大的负担。

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

长轮询

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

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

其流程为:

  1. 请求发送到服务器。
  2. 服务器在有消息之前不会关闭连接。
  3. 当消息出现时 —— 服务器将对其请求作出响应。
  4. 浏览器立即发出一个新的请求。

对于此方法,浏览器发出一个请求并与服务器之间建立起一个挂起的(pending)连接的情况是标准的。仅在有消息被传递时,才会重新建立连接。

长轮询(Long polling) - 图1

如果连接丢失,可能是因为网络错误,浏览器会立即发送一个新请求。

实现长轮询的客户端 subscribe 函数的示例代码:

  1. async function subscribe() {
  2. let response = await fetch("/subscribe");
  3. if (response.status == 502) {
  4. // 状态 502 是连接超时错误,
  5. // 连接挂起时间过长时可能会发生,
  6. // 远程服务器或代理会关闭它
  7. // 让我们重新连接
  8. await subscribe();
  9. } else if (response.status != 200) {
  10. // 一个 error —— 让我们显示它
  11. showMessage(response.statusText);
  12. // 一秒后重新连接
  13. await new Promise(resolve => setTimeout(resolve, 1000));
  14. await subscribe();
  15. } else {
  16. // 获取并显示消息
  17. let message = await response.text();
  18. showMessage(message);
  19. // 再次调用 subscribe() 以获取下一条消息
  20. await subscribe();
  21. }
  22. }
  23. subscribe();

正如你所看到的,subscribe 函数发起了一个 fetch,然后等待响应,处理它,并再次调用自身。

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

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

某些服务器架构是每个连接对应一个进程。对于许多连接的情况,将会有很多进程,并且每个进程占用大量内存。因此,过多的连接会消耗掉全部内存。

使用 PHP,Ruby 语言编写的后端程序会经常遇到这个问题,但是从技术上讲,它不是语言问题,而是实现问题。大多数现代编程语言都允许实现适当的后端,但是其中一些语言比其他语言更容易实现。

使用 Node.js 写的后端通常不会出现这样的问题。

示例:聊天

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

结果

browser.js

server.js

index.html

  1. // Sending messages, a simple POST
  2. function PublishForm(form, url) {
  3. function sendMessage(message) {
  4. fetch(url, {
  5. method: 'POST',
  6. body: message
  7. });
  8. }
  9. form.onsubmit = function() {
  10. let message = form.message.value;
  11. if (message) {
  12. form.message.value = '';
  13. sendMessage(message);
  14. }
  15. return false;
  16. };
  17. }
  18. // Receiving messages with long polling
  19. function SubscribePane(elem, url) {
  20. function showMessage(message) {
  21. let messageElem = document.createElement('div');
  22. messageElem.append(message);
  23. elem.append(messageElem);
  24. }
  25. async function subscribe() {
  26. let response = await fetch(url);
  27. if (response.status == 502) {
  28. // Connection timeout
  29. // happens when the connection was pending for too long
  30. // let's reconnect
  31. await subscribe();
  32. } else if (response.status != 200) {
  33. // Show Error
  34. showMessage(response.statusText);
  35. // Reconnect in one second
  36. await new Promise(resolve => setTimeout(resolve, 1000));
  37. await subscribe();
  38. } else {
  39. // Got message
  40. let message = await response.text();
  41. showMessage(message);
  42. await subscribe();
  43. }
  44. }
  45. subscribe();
  46. }
  1. let http = require('http');
  2. let url = require('url');
  3. let querystring = require('querystring');
  4. let static = require('node-static');
  5. let fileServer = new static.Server('.');
  6. let subscribers = Object.create(null);
  7. function onSubscribe(req, res) {
  8. let id = Math.random();
  9. res.setHeader('Content-Type', 'text/plain;charset=utf-8');
  10. res.setHeader("Cache-Control", "no-cache, must-revalidate");
  11. subscribers[id] = res;
  12. req.on('close', function() {
  13. delete subscribers[id];
  14. });
  15. }
  16. function publish(message) {
  17. for (let id in subscribers) {
  18. let res = subscribers[id];
  19. res.end(message);
  20. }
  21. subscribers = Object.create(null);
  22. }
  23. function accept(req, res) {
  24. let urlParsed = url.parse(req.url, true);
  25. // new client wants messages
  26. if (urlParsed.pathname == '/subscribe') {
  27. onSubscribe(req, res);
  28. return;
  29. }
  30. // sending a message
  31. if (urlParsed.pathname == '/publish' && req.method == 'POST') {
  32. // accept POST
  33. req.setEncoding('utf8');
  34. let message = '';
  35. req.on('data', function(chunk) {
  36. message += chunk;
  37. }).on('end', function() {
  38. publish(message); // publish it to everyone
  39. res.end("ok");
  40. });
  41. return;
  42. }
  43. // the rest is static
  44. fileServer.serve(req, res);
  45. }
  46. function close() {
  47. for (let id in subscribers) {
  48. let res = subscribers[id];
  49. res.end();
  50. }
  51. }
  52. // -----------------------------------
  53. if (!module.parent) {
  54. http.createServer(accept).listen(8080);
  55. console.log('Server running on port 8080');
  56. } else {
  57. exports.accept = accept;
  58. if (process.send) {
  59. process.on('message', (msg) => {
  60. if (msg === 'shutdown') {
  61. close();
  62. }
  63. });
  64. }
  65. process.on('SIGINT', close);
  66. }
  1. <!DOCTYPE html>
  2. <script src="browser.js"></script>
  3. All visitors of this page will see messages of each other.
  4. <form name="publish">
  5. <input type="text" name="message" />
  6. <input type="submit" value="Send" />
  7. </form>
  8. <div id="subscribe">
  9. </div>
  10. <script>
  11. new PublishForm(document.forms.publish, 'publish');
  12. // random url parameter to avoid any caching issues
  13. new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random());
  14. </script>

浏览器代码在 browser.js 中。

使用场景

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

如果消息比较频繁,那么上面描绘的请求-接收(requesting-receiving)消息的图表就会变成锯状状(saw-like)。

每个消息都是一个单独的请求,并带有 header,身份验证开销(authentication overhead)等。

因此,在这种情况下,首选另一种方法,例如:WebsocketServer Sent Events