服务器推(Server Push)是一类特定技术的总称。通常状况,客户端与服务器的交互方式是:客户端发起请求,服务器收到请求返回响应结果,客户端接收响应结果进行处理。从上述的交互过程当中能够看出,客户端想要获取数据,须要自主地向服务端发起请求,获取相关数据。javascript
在大多数场景下,客户端的“主动式”行为已经能够知足需求了。然而,在一些场景下,须要服务器“主动”向客户端推送数据。例如:html
这类应用有几个重要特色:要求较高的实时性,同时客户端没法预期数据更新周期,在服务端获取最新数据时,须要将信息同步给客户端。这类应用场景被称为“服务器推”(Server Push)。前端
“服务器推”技术由来已久,从最初的简单轮询,到后来基于长轮询的COMET,到HTML5规范的SSE,以及实现全双工的WebSocket协议,“服务器推”的技术不断发展。本文会介绍这些技术的基本原理以及实现方式,来帮助你们迅速了解与掌握“服务器推”各种技术的基本原理。文末会附上完整的demo地址。java
简易轮询是“解决”该问题最简陋的一个技术方式。node
简易轮询本质上就是在前端建立一个定时器,每隔必定的时间去查询后端服务,若是有数据则进行相应的处理。git
function polling() {
fetch(url).then(data => {
process(data);
return;
}).catch(err => {
return;
}).then(() => {
setTimeout(polling, 5000);
});
}
polling();
复制代码
轮询开始时,向后端发送请求,待响应结束后,间隔必定时间再去请求数据,如此循环往复。效果以下:github
这种作法的优势就是很是简单,几乎不须要进行任何额外的配置或开发。web
而与此同时,缺点也十分明显。首先,这种至关于定时轮询的方式在获取数据上存在显而易见的延迟,要想下降延迟,只能缩短轮询间隔;而另外一方面,每次轮询都会进行一次完整的HTTP请求,若是没有数据更新,至关因而一次“浪费”的请求,对服务端资源也是一种浪费。ajax
所以,轮询的时间间隔须要进行仔细考虑。轮询的间隔过长,会致使用户不能及时接收到更新的数据;轮询的间隔太短,会致使查询请求过多,增长服务器端的负担。算法
随着web应用的发展,尤为是基于ajax的web2.0时代中web应用需求与技术的发展,基于纯浏览器的“服务器推”技术开始受到较多关注,Alex Russell(Dojo Toolkit 的项目 Lead)称这种基于HTTP长链接、无须在浏览器端安装插件的“服务器推”技术为“Comet”。
经常使用的COMET分为两种:基于HTTP的长轮询(long-polling)技术,以及基于iframe的长链接流(stream)模式。
在简单轮询中,咱们会每隔必定的时间向后端请求。这种方式最大的问题之一就是,数据的获取延迟受限于轮询间隔,没法第一时间获取服务想要推送数据。
长轮询是再此基础上的一种改进。客户端发起请求后,服务端会保持住该链接,直到后端有数据更新后,才会将数据返回给客户端;客户端在收到响应结果后再次发送请求,如此循环往复。关于简单轮询与长轮询的区别,一图胜千言:
这样,服务端一旦有数据想要推送,能够及时送达到客户端。
function query() {
fetchMsg('/longpolling')
.then(function(data) {
// 请求结束,触发事件通知eventbus
eventbus.trigger('fetch-end', {data, status: 0});
});
}
eventbus.on('fetch-end', function (result) {
// 处理服务端返回的数据
process(result);
// 再次发起请求
query();
});
复制代码
以上是一段简略版的前端代码,经过eventbus来通知请求结束,收到结束消息后,process(result)
处理所需数据,同时再次调用query()
发起请求。
而在服务端,以node为例,服务端只须要在监听到有消息/数据更新时,再进行返回便可。
const app = http.createServer((req, res) => {
// 返回数据的方法
const longPollingSend = data => {
res.end(data);
};
// 当有数据更新时,服务端“推送”数据给客户端
EVENT.addListener(MSG_POST, longPollingSend);
req.socket.on('close', () => {
console.log('long polling socket close');
// 注意在链接关闭时移除监听,避免内存泄露
EVENT.removeListener(MSG_POST, longPollingSend);
});
});
复制代码
效果以下:
当咱们在页面中嵌入一个iframe并设置其src时,服务端就能够经过长链接“源源不断”地向客户端输出内容。
例如,咱们能够向客户端返回一段script标签包裹的javascript代码,该代码就会在iframe中执行。所以,若是咱们预先在iframe的父页面中定义一个处理函数process()
,而在每次有新数据须要推送时,在该链接响应中写入<script>parent.process(${your_data})</script>
。那么iframe中的这段代码就会调用父页面中预先定义的process()
函数。(是否是有点像JSONP传输数据的方式?)
// 在父页面中定义的数据处理方法
function process(data) {
// do something
}
// 建立不可见的iframe
var iframe = document.createElement('iframe');
iframe.style = 'display: none';
// src指向后端接口
iframe.src = '/long_iframe';
document.body.appendChild(iframe);
复制代码
后端仍是以node为例
const app = http.createServer((req, res) => {
// 返回数据的方法,将数据拼装成script脚本返回给iframe
const iframeSend = data => {
let script = `<script type="text/javascript"> parent.process(${JSON.stringify(data)}) </script>`;
res.write(script);
};
res.setHeader('connection', 'keep-alive');
// 注意设置相应头的content-type
res.setHeader('content-type', 'text/html; charset=utf-8');
// 当有数据更新时,服务端“推送”数据给客户端
EVENT.addListener(MSG_POST, iframeSend);
req.socket.on('close', () => {
console.log('iframe socket close');
// 注意在链接关闭时移除监听,避免内存泄露
EVENT.removeListener(MSG_POST, iframeSend);
});
});
复制代码
效果以下:
不过使用iframe有个小瑕疵,所以这个iframe至关于永远也不会加载完成,因此浏览器上会一直有一个loading标志。
总得来讲,长轮询和iframe流这两种COMET技术,具备了不错的实用价值,其特色在于兼容性很是强,不须要客户端或服务端支持某些新的特性。不过,为了便于处理COMET使用时的一些问题,仍是推荐在生产环境中考虑一些成熟的第三方库。值得一提的是,Socket.io在不兼容WebSocket(咱们后面会提到)的浏览器中也会回退到长轮询模式。
然而,COMET技术并非HTML5标准的一部分,从兼容标准的角度出发的话,并不推荐使用。(尤为在咱们有了一些其余技术以后)
SSE (Server-Sent Events) 是HTML5标准中的一部分。其实现原理相似于咱们在上一节中提到的基于iframe的长链接模式。
HTTP响应内容有一种特殊的content-type —— text/event-stream,该响应头标识了响应内容为事件流,客户端不会关闭链接,而是等待服务端不断得发送响应结果。
SSE规范比较简单,主要分为两个部分:浏览器中的EventSource
对象,以及服务器端与浏览器端之间的通信协议。
在浏览器中能够经过EventSource
构造函数来建立该对象
var source = new EventSource('/sse');
复制代码
而SSE的响应内容能够当作是一个事件流,由不一样的事件所组成。这些事件会触发前端EventSource
对象上的方法。
// 默认的事件
source.addEventListener('message', function (e) {
console.log(e.data);
}, false);
// 用户自定义的事件名
source.addEventListener('my_msg', function (e) {
process(e.data);
}, false);
// 监听链接打开
source.addEventListener('open', function (e) {
console.log('open sse');
}, false);
// 监听错误
source.addEventListener('error', function (e) {
console.log('error');
});
复制代码
EventSource
经过事件监听的方式来工做。注意上面的代码监听了my_msg
事件,SSE支持自定义事件,默认事件经过监听message
来获取数据。
SSE中,每一个事件由类型和数据两部分组成,同时每一个事件能够有一个可选的标识符。不一样事件的内容之间经过仅包含回车符和换行符的空行("\r\n")来分隔。每一个事件的数据可能由多行组成。
my_msg
事件。能够看到,SSE确实是一个比较简单的协议规范,服务端实现也比较简单:
const app = http.createServer((req, res) => {
const sseSend = data => {
res.write('retry:10000\n');
res.write('event:my_msg\n');
// 注意文本数据传输
res.write(`data:${JSON.stringify(data)}\n\n`);
};
// 注意设置响应头的content-type
res.setHeader('content-type', 'text/event-stream');
// 通常不会缓存SSE数据
res.setHeader('cache-control', 'no-cache');
res.setHeader('connection', 'keep-alive');
res.statusCode = 200;
res.write('retry:10000\n');
res.write('event:my_msg\n\n');
EVENT.addListener(MSG_POST, sseSend);
req.socket.on('close', () => {
console.log('sse socket close');
EVENT.removeListener(MSG_POST, sseSend);
});
});
复制代码
效果以下:
此外,咱们还能够考虑结合HTTP/2的优点来使用SSE。然而,一个可能不太好的消息是,IE/Edge并不兼容。
固然,你能够经过一些手段来写一个兼容IE的polyfill。不过,因为IE上的XMLHttpRequest对象并不支持获取部分的响应内容,所以只能使用XDomainRequest来替代,固然,这也致使了一些小问题。若是你们对具体的实现细节感兴趣,能够看一下这个polyfill库Yaffle/EventSource。
WebSocket与http协议同样都是基于TCP的。WebSocket其实不只仅限于“服务器推”了,它是一个全双工的协议,适用于须要进行复杂双向数据通信的场景。所以也有着更复杂的规范。
当客户端要和服务端创建WebSocket链接时,在客户端和服务器的握手过程当中,客户端首先会向服务端发送一个HTTP请求,包含一个Upgrade
请求头来告知服务端客户端想要创建一个WebSocket链接。
在客户端创建一个WebSocket链接很是简单:
var ws = new WebSocket('ws://127.0.0.1:8080');
复制代码
固然,相似于HTTP
和HTTPS
,ws
相对应的也有wss
用以创建安全链接。
这时的请求头以下:(注意其中的Upgrade字段)
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cache-Control: no-cache
Connection: Upgrade
Cookie: Hm_lvt_4e63388c959125038aabaceb227cea91=1527001174
Host: 127.0.0.1:8080
Origin: http://127.0.0.1:8080
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: 0lUPSzKT2YoUlxtmXvdp+w==
Sec-WebSocket-Version: 13
Upgrade: websocket
复制代码
而服务器在收到请求后进行处理,响应头以下
Connection: Upgrade
Origin: http://127.0.0.1:8080
Sec-WebSocket-Accept: 3NOOJEzyscVfEf0q14gkMrpV20Q=
Upgrade: websocket
复制代码
表示升级到了WebSocket协议。
注意,上面的请求头中有一个Sec-WebSocket-Key
,这其实和加密、安全性关系不大,最主要的做用是来验证服务器是否真的正确“理解”了WebSocket、该WebSocket链接是否有效。服务器会使用Sec-WebSocket-Key
,并根据一个固定的算法
mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 一个规定的字符串
accept = base64(sha1(key + mask));
复制代码
生成Sec-WebSocket-Accept
响应头字段,交由浏览器验证。
接下来,浏览器与服务器之间就能够愉快地进行双向通讯了。
鉴于篇幅,关于WebSocket协议的具体规范与细节(例如数据帧格式、心跳检查等)就不在这里深刻了,网络上也有不少这类的不错的文章能够阅读,感兴趣的读者也能够阅读本文最后的参考资料。
下面简单介绍一下WebSocket的使用。
在浏览器端,创建WebSocket链接后,能够经过onmessage
来监听数据信息。
var ws = new WebSocket('ws://127.0.0.1:8080');
ws.onopen = function () {
console.log('open websocket');
};
ws.onmessage = function (e) {
var data = JSON.parse(e.data);
process(data);
};
复制代码
在服务器端,因为WebSocket协议具备较多的规范与细节须要处理,所以建议使用一些封装较完备的第三方库。例如node中的websocket-node和著名的socket.io。固然,其余语言也有许多开源实现。node部分代码以下:
const http = require('http');
const WebSocketServer = require('websocket').server;
const app = http.createServer((req, res) => {
// ...
});
app.listen(process.env.PORT || 8080);
const ws = new WebSocketServer({
httpServer: app
});
ws.on('request', req => {
let connection = req.accept(null, req.origin);
let wsSend = data => {
connection.send(JSON.stringify(data));
};
// 接收客户端发送的数据
connection.on('message', msg => {
console.log(msg);
});
connection.on('close', con => {
console.log('websocket close');
EVENT.removeListener(MSG_POST, wsSend);
});
// 当有数据更新时,使用WebSocket链接来向客户端发送数据
EVENT.addListener(MSG_POST, wsSend);
});
复制代码
效果以下:
服务器推(Server Push)做为一类特定的技术,在一些业务场景中起到了重要的做用,了解各种技术实现的原理与特色,有利于在实际的业务场景中帮助咱们作出必定的选择与判断。
为了便于理解文中的内容,我把全部代码整理在了一个demo里,感兴趣的朋友能够在这里下载,并在本地运行查看。