探索 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>
相关推荐
哀木3 小时前
一个简单的套壳方案,就能让你的 Agent 少做重复初始化
前端
问心无愧05133 小时前
ctf show web入门27
前端
小村儿3 小时前
给 AI Agent 装上"长期记忆":Karpathy 的 LLM Wiki 思想,我做成了工具
前端·后端·ai编程
竹林8183 小时前
用ethers.js连接MetaMask实现Web3钱包登录:从踩坑到稳定运行的完整记录
前端·javascript
heyCHEEMS3 小时前
如何用 Recast 实现静态配置文件源码级读写
前端·node.js
心连欣3 小时前
从零开始,学习所有指令!
前端·javascript·vue.js
review445433 小时前
大模型和function calling分别是如何工作的
前端
东东同学3 小时前
耗时一个月,我把 Nuxt 首屏性能排障经验做成了一个 AI Skill
前端·agent
冴羽4 小时前
超越 Vibe Coding —— AI 辅助编程指南
前端·ai编程·vibecoding
梦想的颜色5 小时前
一天一个SKILL——前端最佳自动化测试 webapp-testing
前端·web app