HTTP 2.0 服务器推送技术的创新
HTTP 2.0 允许服务器为一次客户端请求发送多个响应(并行的) - 即, 服务器推送。等等,我们为什么想要这样? 是这样的,一个一般的网页需要一堆的额外资源,比如JavaScript,CSS,和图片, 对这些资源的引用就内置在服务器产生的HTML页面里。那么,与其等待客户端去发现对这些资源的引用,为什么不让服务器立即把这些资源都发送过去呢?服务器推送可以消除这种不必要的网络等待所带来的整个往返耗时。
事实上,如果你曾经内联的引用过一个资源(CSS, JS,或者一个图片),那么你已经“模拟”过服务器推送:一个内联资源被作为父文档的一部分“推送”过来。唯一的不同是HTTP 2.0使得这一模式更有效而且更强大!
着手进行HTTP 2.0服务器推送
一个内联的资源,从定义上说,是父文档的一部分。那么,它就不能被独立的缓存,并且它需要在许多不同的页面之间被复制 - 这是很低效的。相反的,推送的资源可以被浏览器独立的缓存起来,从而在许多页面中复用。这里准备了一个例子:
spdy.createServer(options, function(req, res) { // push JavaScript asset (/main.js) to the client res.push('/main.js', {'content-type': 'application/javascript'}, function(err, stream) { stream.end('alert("hello from push stream!")'); }); // write main response body and terminate stream res.end('Hello World! <script src="/main.js"></script>'); }).listen(443);
上例中,我们有一个借助于node-spdy module实现的最简SPDY服务,它对所有的入站请求的响应是,输出一个字符串"Hello World!",跟着是一个脚本标记。除此之外,我们还做了些聪明的事情: 我们推送“main.js”文件到客户端,该文件触发了一个JavaScript弹出框。
结果是,当浏览器发现HTML响应中的脚本标记时,“main.js”文件已经在缓存中了,不会发生额外的网络往返!HTTP 2.0 服务器推送 废弃了内联引用。最重要的是,服务器推送已经被所有的支持SPDY的浏览器所支持(Firefox,Opera,和Chrome)。
我们还能推送什么?
用服务器推送替换内嵌资源是一个典型例子。但是,为什么止步于此,我们还能推送什么? 其实, 任何HTTP响应都是平等的游戏。我们能推送一次重定向吗? 是的, 那很简单:
spdy.createServer(options, function(req, res) { //push JavaScript asset (/newasset.js) to the client res.push('/newasset.js', {'content-type': 'application/javascript'}, function(err, stream) { stream.end('alert("hello from (redirected) push stream!")'); }); // push 301 redirect: /asset.js -> /newasset.js res.push('/asset.js', {':status': 301, 'Location': '/newasset.js'}, function(err, stream) { stream.end('301 Redirect'); }); // write main response body and terminate stream res.end('<script src="/asset.js"></script>'); }).listen(443);
跟上面的例子一样,只不过我们已经把“asset.js”资源替换为一次对“newasset.js”文件的301重定向。浏览器对重定向和“asset.js”都进行了缓存,并且在无任何额外网络往返的情况下执行脚本。怎么应用?我把这个问题留给读者当练习吧。
我们还能做的更多吗?推送缓存失效信息和再验证信息到客户端,怎么样?我们可以把已经存在于客户端缓存中的任何资源标记为失效(即推送一个失效时间戳),或者相反,推送一个携带未来时间戳的304来更新资源的生存期。简言之,服务器能够主动的管理客户端缓存!
如果服务器过于激进或者不守规则,那么客户端可以限制推送流的数量,或者只要它愿意它可以取消某一个流。找出合适的策略并在两端之间作出平衡,是找出服务器推送最佳实践的关键 - 这不是一项轻松的挑战,但却能带来很高回报。
注: 目前的浏览器不支持推送缓存再验证。应该支持吗?
服务器推送的客户端通知
HTTP 2.0服务器推送不是诸如Server-Sent Events (SSE)或WebSocket的技术替代品。通过HTTP 2.0服务器推送传递过来的资源由浏览器来处理,但却不能上升到程序代码的层面 - 因为没有JavaScript API来获取这些事件的通知。不过,解决这个难题的方法却很简单,因为我们可以把一个SSE通道和服务器推送结合起来,使两者都达到最佳效果:
spdy.createServer(options, function(req, res) { // set content type for SSE stream res.setHeader('Content-Type', 'text/event-stream'); messageId = 1; setInterval(function(){ // push a simple JSON message into client's cache var msg = JSON.stringify({'msg': messageId}); var resourcePath = '/resource/'+messageId; res.push(resourcePath, {}, function(err, stream) { stream.end(msg) }); // notify client that resource is available in cache res.write('data:'+resourcePath+'\n\n'); messageId+=1; }, 2000); }).listen(443);诚然,下面这个例子有点儿傻,但却很能说明问题: 服务器每隔两秒钟产生一条消息,并把它推送到客户端的缓存里,然后发送一个SSE通知到客户端。另一方面,客户端在应用程序代码中订阅这些事件,并执行自己的逻辑来处理这些事件:
<script> var source = new EventSource('/'); source.onmessage = function(e) { document.body.innerHTML += "SSE notification: " + e.data + '<br />'; // fetch resource via XHR... from cache! var xhr = new XMLHttpRequest(); xhr.open('GET', e.data); xhr.onload = function() { document.body.innerHTML += "Message: " + this.response + '<br />'; }; xhr.send(); }; </script>
长效的SSE流,被推送的资源,和其它所有HTTP 2.0流都有效的复用了同一个TCP链接 - 没有额外的链接开销和不必要的网络往返。通过把SSE和服务器推送结合起来,我们可以传送任意资源(二进制或文本)到客户端,利用浏览器本地缓存带来的好处,并执行恰当的应用逻辑! 最重要的是,今天这已经成为可能。
服务器推送创新
服务器推送开启了一个充满了优化机会的全新世界。我们上面所举的例子只是展示了冰山一角,仍有许多其他问题需要考虑:
- 什么资源应当被推送? 何时被推送?它们是否已经在缓存中?
- 服务器能够自动推断应当推送哪些资源吗?
- 是否应当支持诸如主动缓存管理之类的高级应用?
- 我们应当怎样设计我们的应用,才能从服务器推送中获益最多?
- ...
通过HTTP 2.0,服务器有机会变得非常非常智能,包括如何优化传送一些具体的资源,更重要的是,也包括优化对整个应用的传送。类似的,浏览器可能增加额外的API和能力来使这个过程更加高效。事实上,长期来看,服务器推送很可能会成为HTTP 2.0的杀手级特性!