**摘要:** WEB 开发总有一天会用到这些技术。
- 作者:[浪里行舟](https://segmentfault.com/u/langlixingzhou)
**[Fundebug](https://www.fundebug.com/)经授权转载,版权归原作者所有。**
### 前言
随着 Web 的发展,用户对于 Web 的实时推送要求也越来越高 ,比如,工业运行监控、Web 在线通讯、即时报价系统、在线游戏等,都需要将后台发生的变化主动地、实时地传送到浏览器端,而不需要用户手动地刷新页面。本文对过去和现在流行的 Web 实时推送技术进行了比较与总结。
**本文完整的源代码请猛戳[Github 博客](https://github.com/ljianshu/Blog),纸上得来终觉浅,建议大家动手敲敲代码。**
### 一、双向通信
HTTP 协议有一个缺陷:通信只能由客户端发起。举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。**在 WebSocket 协议之前,有三种实现双向通信的方式:轮询(polling)、长轮询(long-polling)和 iframe 流(streaming)**。
#### 1. 轮询(polling)

轮询是客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。其缺点也很明显:连接数会很多,一个接受,一个发送。而且**每次发送请求都会有 Http 的 Header,会很耗流量,也会消耗 CPU 的利用率**。
- 优点:实现简单,无需做过多的更改
- 缺点:轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担
```html
// 1.html
<div id="clock"></div>
<script>
let clockDiv = document.getElementById("clock");
setInterval(function() {
let xhr = new XMLHttpRequest();
xhr.open("GET", "/clock", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
clockDiv.innerHTML = xhr.responseText;
}
};
xhr.send();
}, 1000);
</script>
```
```javascript
//轮询 服务端
let express = require("express");
let app = express();
app.use(express.static(__dirname));
app.get("/clock", function(req, res) {
res.end(new Date().toLocaleString());
});
app.listen(8080);
```
启动本地服务,打开`http://localhost:8080/1.html`,得到如下结果:

#### 2. 长轮询(long-polling)

长轮询是对轮询的改进版,客户端发送 HTTP 给服务器之后,看有没有新消息,如果没有新消息,就一直等待。当有新消息的时候,才会返回给客户端。在某种程度上减小了网络带宽和 CPU 利用率等问题。由于 http 数据包的头部数据量往往很大(通常有 400 多个字节),但是真正被服务器需要的数据却很少(有时只有 10 个字节左右),这样的数据包在网络上周期性的传输,难免**对网络带宽是一种浪费**。
- 优点:比 Polling 做了优化,有较好的时效性
- 缺点:保持连接会消耗资源; 服务器没有返回有效数据,程序超时。
```html
// 2.html 服务端代码同上
<div id="clock"></div>
<script>
let clockDiv = document.getElementById("clock");
function send() {
let xhr = new XMLHttpRequest();
xhr.open("GET", "/clock", true);
xhr.timeout = 2000; // 超时时间,单位是毫秒
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
//如果返回成功了,则显示结果
clockDiv.innerHTML = xhr.responseText;
}
send(); //不管成功还是失败都会发下一次请求
}
};
xhr.ontimeout = function() {
send();
};
xhr.send();
}
send();
</script>
```
#### 3. iframe 流(streaming)

iframe 流方式是在页面中插入一个隐藏的 iframe,利用其 src 属性在服务器和客户端之间创建一条长连接,服务器向 iframe 传输数据(通常是 HTML,内有负责插入信息的 javascript),来实时更新页面。
- 优点:消息能够实时到达;浏览器兼容好
- 缺点:服务器维护一个长连接会增加开销;IE、chrome、Firefox 会显示加载没有完成,图标会不停旋转。
```html
// 3.html
<body>
<div id="clock"></div>
<iframe src="/clock" style="display:none"></iframe>
</body>
```
```javascript
//iframe流
let express = require("express");
let app = express();
app.use(express.static(__dirname));
app.get("/clock", function(req, res) {
setInterval(function() {
let date = new Date().toLocaleString();
res.write(`
<script type="text/javascript">
parent.document.getElementById('clock').innerHTML = "${date}";//改变父窗口dom元素
</script>
`);
}, 1000);
});
app.listen(8080);
```
启动本地服务,打开`http://localhost:8080/3.html`,得到如下结果:

上述代码中,客户端只请求一次,然而服务端却是源源不断向客户端发送数据,这样服务器维护一个长连接会增加开销。
以上我们介绍了三种实时推送技术,然而各自的缺点很明显,使用起来并不理想,接下来我们着重介绍另一种技术--websocket,它是比较理想的双向通信技术。
### 二、WebSocket
#### 1. 什么是 websocket
WebSocket 是一种全新的协议,随着 HTML5 草案的不断完善,越来越多的现代浏览器开始全面支持 WebSocket 技术了,它将 TCP 的 Socket(套接字)应用在了 webpage 上,从而使通信双方建立起一个保持在活动状态连接通道。
一旦 Web 服务器与客户端之间建立起 WebSocket 协议的通信连接,之后所有的通信都依靠这个专用协议进行。通信过程中可互相发送 JSON、XML、HTML 或图片等任意格式的数据。**由于是建立在 HTTP 基础上的协议,因此连接的发起方仍是客户端,而一旦确立 WebSocket 通信连接,不论服务器还是客户端,任意一方都可直接向对方发送报文**。
初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?
#### 2. HTTP 的局限性
- HTTP 是半双工协议,也就是说,在同一时刻数据只能单向流动,客户端向服务器发送请求(单向的),然后服务器响应请求(单向的)。
- 服务器不能主动推送数据给浏览器。这就会导致一些高级功能难以实现,诸如聊天室场景就没法实现。
#### 3.WebSocket 的特点
- 支持双向通信,实时性更强
- 可以发送文本,也可以发送二进制数据
- 减少通信量:只要建立起 WebSocket 连接,就希望一直保持连接状态。和 HTTP 相比,不但每次连接时的总开销减少,而且由于 WebSocket 的首部信息很小,通信量也相应减少了

相对于传统的 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。**在海量并发和客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显**。
接下来我看下 websocket 如何实现客户端与服务端双向通信:
```html
// websocket.html
<div id="clock"></div>
<script>
let clockDiv = document.getElementById("clock");
let socket = new WebSocket("ws://localhost:9999");
//当连接成功之后就会执行回调函数
socket.onopen = function() {
console.log("客户端连接成功");
//再向服务 器发送一个消息
socket.send("hello"); //客户端发的消息内容 为hello
};
//绑定事件是用加属性的方式
socket.onmessage = function(event) {
clockDiv.innerHTML = event.data;
console.log("收到服务器端的响应", event.data);
};
</script>
```
```javascript
// websocket.js
let express = require("express");
let app = express();
app.use(express.static(__dirname));
//http服务器
app.listen(3000);
let WebSocketServer = require("ws").Server;
//用ws模块启动一个websocket服务器,监听了9999端口
let wsServer = new WebSocketServer({ port: 9999 });
//监听客户端的连接请求 当客户端连接服务器的时候,就会触发connection事件
//socket代表一个客户端,不是所有客户端共享的,而是每个客户端都有一个socket
wsServer.on("connection", function(socket) {
//每一个socket都有一个唯一的ID属性
console.log(socket);
console.log("客户端连接成功");
//监听对方发过来的消息
socket.on("message", function(message) {
console.log("接收到客户端的消息", message);
socket.send("服务器回应:" + message);
});
});
```
启动本地服务,打开`http://localhost:3000/websocket.html`,得到如下结果:

### 三、Web 实时推送技术的比较

综上所述:Websocket 协议不仅解决了 HTTP 协议中服务端的被动性,即通信只能由客户端发起,也解决了数据同步有延迟的问题,同时还带来了明显的性能优势,所以 websocket
是 Web 实时推送技术的比较理想的方案,但如果要兼容低版本浏览器,可以考虑用轮询来实现。
### 参考文章
- [WebSocket 教程](http://www.ruanyifeng.com/blog/2017/05/websocket.html)
- [珠峰前端架构课](http://video.zhufengpeixun.cn/)
- [Web 实时推送技术的总结](https://waylau.com/web-real-time-push-technology/)
- [WebSocket(1): 服务端“实时推送”的演变](https://github.com/kaola-fed/blog/issues/277)
- [长连接/websocket/SSE 等主流服务器推送技术比较](https://zhuanlan.zhihu.com/p/31297574)
### 关于Fundebug
[Fundebug](https://www.fundebug.com/)专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家[免费试用](https://www.fundebug.com/team/create)!

- 如何高效地遍历超大MongoDB集合
- 深入理解 JavaScript 执行上下文和执行栈
- JavaScript 为什么要有 Symbol 类型?
- Web 性能优化:21 种优化 CSS 和加快网站速度的方法
- 深入理解 JavaScript 作用域和作用域链
- WEB 实时推送技术的总结
- 前端面试:谈谈 JS 垃圾回收机制
- TypeScript,初次见面,请多指教 ?
- 使用这些 HTTP 头保护 Web 应用
- JavaScript 的 4 种数组遍历方法: for VS forEach() VS for/in VS for/of
- 装上这几个 VSCode 插件后,上班划水摸鱼不是梦
- 一文搞懂TCP与UDP的区别
- 如何优雅地查看 JS 错误堆栈?
- 一文读懂 HTTP/2 及 HTTP/3 特性
- Web 性能优化: 图片优化让网站大小减少 62%
- 我们应该如何给需求排序?
- Web 性能优化: 使用 Webpack 分离数据的正确方法
- 2019 前端面试题汇总(主要为 Vue)
- 高效使用VSCode的9点建议
- 小程序多端框架全面测评:chameleon、Taro、uni-app、mpvue、WePY
- 灵活使用 console 让 js 调试更简单
- 深入了解浏览器存储:对比Cookie、LocalStorage、sessionStorage与IndexedDB
- JavaScript是如何工作的:引擎,运行时和调用堆栈的概述!
- Web 性能优化:Preload与Prefetch的使用及在 Chrome 中的优先级
- 如何来一次说干就干的重构 (流程篇)
- JavaScript 新语法详解:Class 的私有属性与私有方法
- JavaScript 原型的深入指南
- 一个程序员的自我修养
- 为什么HTTPS比HTTP更安全?