山地人

002.前端问题-即时通讯的演进历史

山地人
山地人
2021-05-13

前端进化岛的小伙伴想了解即时通讯方面的内容,这一期我就来和大家一起探讨下即时通讯方面的话题。

一、Web上的即时通讯选型为何难?

为何基于B/S(浏览器/服务器)架构的即时通讯,在做技术选型时这么困难?这和浏览器本身的对不同技术方案的兼容性有很大关系。有些技术方案的兼容性好,但是其他方面又会有问题,比如对服务造成的通讯压力。另外一些看似先进的技术方案却存在有些浏览器无法兼容实现的问题。因此,在Web上实现即时通讯就变成一个难事。下面我们先来看看,当前我们能够用来做即时通讯的技术方案都有哪些。并对他们进行一些横向对比,以了解各种方案的优劣。

二、不同通讯方案的演进

我们平时用的最多的和服务器交互的通讯方式就属于这一种。每次客户端(通常是我们的浏览器)要想获取一点服务器上的最新内容,我们会有两种常用的做法,刷新URL和Ajax局部刷新。

2-1.传统的Web通讯方式——刷页面

刷新整个页面——通过触发浏览器的URL刷新,我们可以拿到当前服务器上的最新数据。当然这种做法在用户体验上非常不友好,用户在使用的过程中会看到页面会有一次白屏闪烁。很多时候页面要更新的内容只是一小块区域,整页更新浪费了很多资源。

2-2.对传统的Web通讯的改进——Ajax局部刷新

更新局部页面——既然整页刷新体验差费资源,当然就需要改进。这也就是大家平时所说的Ajax技术。采用Ajax局部刷新技术后,浏览器不用整个页面刷新了。在当前页面上通过一小段JavaScript代码,发起一个或几个XMLHttpRequest请求到我们的服务器端。获取到服务器上的数据后,根据返回的数据把页面上要调整的部分小范围调整更新掉。这种方式也就是大家平时用的最多的局部更新技术。

当然上面这两种方式适合做页面更新频率不高的场景,对于那些需要实时监控,页面数据频繁变动的场景就不合适了。

2-3.基于Ajax局部刷新的升级——短轮询 (短轮询=定时器+Ajax)

什么是短轮询——就是在客户端设定一个定时器,定时去查询服务器的某些API来达到模拟实时更新的效果的一种实现。

v2-8a490c5fa757a2291c2f30962a993cd6_hd

那你可能会问为啥不适用?因为一旦页面数据变动频繁,整页刷新的方式自然不用考虑。而频繁的局部刷新(polling短轮询)的方式虽然在用户观感上没什么问题,但是由于每次Ajax请求,都是一次完整的Http请求。每一次的请求和响应数据中都会带有Http头。可能通讯过程中传递的有效数据信息还没有那个Http头来的多。这样就造成了很大的资源浪费。这么多的额外Http头并没有带来实际的价值。

上面讲到的频繁发起Ajax的技术其实就是所谓的短轮询。短轮询技术除了上面讲到的浪费带宽资源,频繁和服务器建立连接,获取数据,然后断开连接,这个也会对服务器带来很大的性能压力。你想如果每隔5秒轮询一次,一分钟一个客户端要和服务器通讯12次。1000个客服端访问,一分钟就要和服务器产生12000次通讯。

2-3.在短轮询基础上的改进——长轮询v1

既然短轮询有频繁建立链接,增加额外请求头的问题。我们聪明的工程师们自然会想到改进的思路。短轮询不行,那咱就改长轮询。

何为长轮询——先回顾下短轮询的过程,短轮询一次通讯过程是先发送请求给服务器,建立连接,服务器返回数据,断开连接,通讯结束。下次要请求数据再来一次上述过程。好,现在长轮询稍微做一些调整。其他步骤都不变,只改变一个环节。服务器建立连接,并不立即断开连接。而是把这个连接保持一段时间,直到服务器数据有数据改变才返回数据断开或者会话设定时间到期才断开连接。

v2-b263a6bc5dc3df049aa048a76cbcc1b6_hd

这样做的好处是,客户端不需要频繁和服务端建立连接,通讯的次数可以减少。也就没那么多无用的Htpp头产生。但如果服务器的数据变动频率很高,依然会出现和短轮询时一样的问题。

2-4.长轮询上的继续演进——Steaming流(可以看成 长轮询v2)

服务器如果数据变动频繁,上面的长轮询v1版本的效果就会和之前的短轮询表现一样。那么工程师们需要继续寻找改进的思路。那能否让一次通讯带更多的货回来?基于这样的思考,聪明的工程师又找到了新思路。这次在长轮询v1上又做了一点点改动。原来是发现服务器变动后,返回数据然后断开。好,这次我不让你断开,把服务器变动的数据传回来后,连接依然保持住,继续等待服务器新的数据变化,这样经过若干次“带货”操作后才断开链接。于是长轮询v2也就悄然诞生。

v2-a5750cdb4bd66d8d4a3fd6b2d27f2d4f_hd

要实现这种Steaming流,有两种技术手段:

  1. 隐藏的IFrame
  2. XMLHttpRequest开启multipart的方式

上面的这些实现手段都是比较细节的问题,想要了解实现细节的小伙伴,可以查看这篇博客 Steaming流技术的实现细节

在查阅资料的过程,可能会看到Comet,你看到的这个Comet其实就是我们上面说的基于HTTP长连接的服务器推送技术。

Comet概念的出处可以参考Comet: Low Latency Data for the Browser

2-5.WebSocket的推出

在WebSocket推出之前,工程师们经历了上面的各种探索(换句话说是折腾)。为了获得实时性真是挖空心思。其实这也暴露出来了HTTP协议的一个缺陷:HTTP协议只能由客户端向服务器发起(这里指的是HTTP1.0 和 HTTP1.1)请求,服务器不能主动推送消息给客户端。

因此在2008年诞生了WebSocket协议,它的最大特点就是大家熟知的可以服务器主动推送消息给客户端,客户端也能主动推动消息给服务器。也就是所谓的全双工通讯(Full Duplex),不了解全双工,半双工概念的小伙伴可以看下百度百科的这个词条参考

v2-dd03b92f7ad12778776ffdd4300b941c_hd

一、WebSocket建立通讯的过程

服务端客户端(浏览器端)
1.启动WebSocket服务端监听,等待客户端连接
2.创建WebSocket连接
3.收到客户端连接,建立好连接。此时已经可以向客户端发送消息
4.收到服务器连接成功的消息,可以像服务器发送消息
5.发送消息给服务器
6.服务器收到客户端发来的消息,服务器也可以回复消息给客户端
7.客户端收到服务器发来的消息
8.服务端也可以选择断开和某个客户端的连接8.客户端可以选择主动关闭连接
9.收到连接断开的消息9.收到连接断开的消息

二、WebSocket通讯的客户端代码实现

var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("连接【已打开】");
//向服务器发送消息
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log("接收到服务器消息: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("连接【关闭】");
};

lALPDeC2t__xW_bNAwzNBK4_1198_780.png_620x10000q90g

WebSocket客户端代码参考

三、WebSocket通讯的客户端和服务器代码实现

这里给出服务端的代码,完整代码参考底部的连接

var app = require("express")();
var http = require("http").Server(app);
var io = require("socket.io")(http);
//浏览器发来请求,返回浏览器 index.html页面
app.get("/", function(req, res) {
res.sendFile(__dirname + "/index.html");
});
//服务器如果收到浏览器的连接请求,进来处理
io.on("connection", function(socket) {
console.log("an user connected");
//连接建立好后,给浏览器推送一条chat message类型的消息
//消息内容为:欢迎光临
io.emit("chat message", "欢迎光临");
//如果收到客户端发来的chat message类型的消息,进来处理
socket.on("chat message", function(msg) {
console.log("message: " + msg);
io.emit("chat message", "服务器返回:" + msg);
});
});
http.listen(3000, function() {
console.log("listening on *:3000");
});

lALPDeC2uAAbmu3NAxDNAps_667_784.png_620x10000q90g

WebSocket客户端+服务端 参考代码

延时阅读:

Comet:基于 HTTP 长连接的“服务器推”技术](https://www.ibm.com/developerworks/cn/web/wa-lo-comet/index.html)