前言
-
大部分的网页请求服务器的场景如下:
-
即用户在浏览器中主动发起一次 http 请求,服务器响应一次 http 请求,但是这种方式,服务器从来都不会主动发送消息给客户端
-
那么用户如何在浏览器上不作任何操作的情况下接收到服务器的消息?
-
一般来说,Web 端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的 Web 端即时通讯方案大致有 3 种:Comet 技术、WebSocket 技术、SSE(Server-sent Events)。
-
本章节主要讲解 Comet 技术。
-
Comet 是一种用于 web 的推送技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有三种实现方式:
-
- 定时短轮询(polling)
-
- 长轮询(long-polling)
-
- iframe 流(streaming)
-
以上的方式称为服务器推送技术, 但本质上还是客户端主动请求数据,只不过在用户无感知的情况下,也叫 comet 技术。
定时短轮询(polling)
-
轮询是客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。
-
这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有 Http 的 Header,会很耗流量,也会消耗 CPU 的利用率
-
比如在前端定时 3 秒请求服务器接口查询状态
-
场景:扫码登录、扫码支付等等
-
缺点:
-
- 会定时频繁地请求服务器接口,会消耗带宽,增加服务器的负担;
-
- 虽然是用户无感知得从服务器获取消息,但是控制台上会有一堆的网络请求
-
- 用户进行微信扫码后需要等待几秒时间(因为是前端定时短轮询),页面才能跳转,会有明显的卡顿;
-
-
那为什么微信扫码还用这种方式呢?因为相对来说数据量较小...
根目录\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>
- 执行:nodemon server.js
- 访问:http://localhost:8080
长轮询(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
-
会发现每 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>
- 执行:nodemon server.js
- 访问:http://localhost:8080