探索 Comet 技术:实现服务端实时数据推送

前言

  • 大部分的网页请求服务器的场景如下:

  • 即用户在浏览器中主动发起一次 http 请求,服务器响应一次 http 请求,但是这种方式,服务器从来都不会主动发送消息给客户端

  • 那么用户如何在浏览器上不作任何操作的情况下接收到服务器的消息?

  • 一般来说,Web 端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的 Web 端即时通讯方案大致有 3 种:Comet 技术、WebSocket 技术、SSE(Server-sent Events)。

  • 本章节主要讲解 Comet 技术。

  • Comet 是一种用于 web 的推送技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有三种实现方式:

      1. 定时短轮询(polling)
      1. 长轮询(long-polling)
      1. iframe 流(streaming)

以上的方式称为服务器推送技术, 但本质上还是客户端主动请求数据,只不过在用户无感知的情况下,也叫 comet 技术。

定时短轮询(polling)

  • 轮询是客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。

  • 这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有 Http 的 Header,会很耗流量,也会消耗 CPU 的利用率

  • 比如在前端定时 3 秒请求服务器接口查询状态

  • 场景:扫码登录、扫码支付等等

  • 缺点:

      1. 会定时频繁地请求服务器接口,会消耗带宽,增加服务器的负担;
      1. 虽然是用户无感知得从服务器获取消息,但是控制台上会有一堆的网络请求
      1. 用户进行微信扫码后需要等待几秒时间(因为是前端定时短轮询),页面才能跳转,会有明显的卡顿;
  • 那为什么微信扫码还用这种方式呢?因为相对来说数据量较小...

根目录\server.js

js 复制代码
let express = require("express");
let app = express();
app.use(express.static(__dirname));

app.get("/clock", function (req, res) {
  res.send(new Date().toLocaleString());
});

app.listen(8080);

根目录\index.html

html 复制代码
<body>
  <div id="clock"></div>
  <script>
    let clock = document.querySelector("#clock");
    setInterval(function () {
      let xhr = new XMLHttpRequest();
      xhr.open("GET", "/clock", true);
      xhr.onreadystatechange = function () {
        if (xhr.readyState == 4 && xhr.status == 200) {
          clock.innerHTML = xhr.responseText;
        }
      };
      xhr.send();
    }, 1000);
  </script>
</body>

长轮询(long-polling)

  • 长轮询是对 定时短轮询 的改进版,客户端发送 HTTP 给服务器之后,看有没有新消息,如果没有新消息,就一直等待当有新消息的时候,才会返回给客户端。

  • 简单地理解:客户端发请求后,得直到等到服务器端返回数据后,客户端才能发送下一次的请求

  • 优点:在某种程度上减小了网络带宽和 CPU 利用率等问题。

  • 缺点: 由于 http 数据包的头部数据量往往很大(通常有 400 多个字节),但是真正被服务器需要的数据却很少(有时只有 10 个字节左右),这样的数据包在网络上周期性的传输,难免对网络带宽是一种浪费

  • 如何在项目中实现:将 http 接口请求的超时时间设置大些(满足用户扫码所需的时间),如果超时就发起下一次请求

  • 场景:百度云网盘浏览器端的扫码登录等

  • 特点:用户扫码之后,在电脑端网页就秒跳转,用户不需要等待

根目录\index.html

html 复制代码
<body>
  <div id="clock"></div>
  <script>
    let clock = document.querySelector("#clock");

    function poll() {
      let xhr = new XMLHttpRequest();
      xhr.open("GET", "/clock", true);
      xhr.onreadystatechange = function () {
        if (xhr.readyState == 4 && xhr.status == 200) {
          clock.innerHTML = xhr.responseText;
          // 直到等到服务器端返回数据后,客户端才能发下一次请求
          poll();
        }
      };
      xhr.send();
    }

    // 第一次先发一个请求
    poll();
  </script>
</body>

根目录\server.js

js 复制代码
let express = require("express");
let app = express();
app.use(express.static(__dirname));

app.get("/clock", function (req, res) {
  let $timer = setInterval(function () {
    let date = new Date();
    let seconds = date.getSeconds();
    if (seconds % 5 === 0) {
      res.send(date.toLocaleString());
      clearInterval($timer);
    }
  }, 1000);
});

app.listen(8080);
  • 执行:nodemon server.js

  • 访问: http://localhost:8080

  • 会发现每 5 秒才向服务器端发一次请求

  • long poll 需要有很高的并发能力

iframe 流(streaming)

  • 通过在 HTML 页面里嵌入一个隐藏的 iframe, 然后将这个 iframe 的 src 属性设为对一个长连接的请求, 服务器端就能源源不断地往客户推送数据。

根目录\server.js

js 复制代码
const express = require("express");
const app = express();
app.use(express.static(__dirname));
app.get("/clock", function (req, res) {
  res.header("Content-Type", "text/html");
  setInterval(function () {
    // 返回一个 js 脚本
    // write 就是只写但是不关闭
    res.write(`
      <script type="text/javascript">
          parent.document.getElementById('clock').innerHTML = "${new Date().toLocaleTimeString()}";
      </script>
    `);
  }, 1000);
});

app.listen(8080);

根目录\index.html

html 复制代码
<body>
  <div id="clock" style="border: 1px solid red; height: 200px"></div>
  <!-- iframe尽管隐藏起来,但是还是会一直向服务器发请求,因为连接没有断开 -->
  <iframe src="/clock" style="display: none"></iframe>

  <script>
    function setTime(ts) {
      document.querySelector("#clock").innerHTML = ts;
    }
  </script>
</body>
相关推荐
SuperEugene13 分钟前
TypeScript+Vue 实战:告别 any 滥用,统一接口 / Props / 表单类型,实现类型安全|编码语法规范篇
开发语言·前端·javascript·vue.js·安全·typescript
我是永恒34 分钟前
上架一个跨境工具导航网站
前端
电子羊40 分钟前
Spec 编程工作流文档
前端
GISer_Jing1 小时前
从CLI到GUI桌面应用——前端工程化进阶之路
前端·人工智能·aigc·交互
还是大剑师兰特1 小时前
Vue3 报错:computed value is readonly 解决方案
前端·vue.js
leaves falling1 小时前
有效的字母异位词
java·服务器·前端
We་ct1 小时前
LeetCode 35. 搜索插入位置:二分查找的经典应用
前端·算法·leetcode·typescript·个人开发
左耳咚1 小时前
Claude Code 中的 SubAgent
前端·ai编程·claude
FPGA小迷弟1 小时前
高频时钟设计:FPGA 多时钟域同步与时序收敛实战方案
前端·学习·fpga开发·verilog·fpga
IT古董1 小时前
【前端】企业级前端调试体系设计(含日志埋点 + Eruda 动态注入 + Sentry)
前端·sentry