为了下降加载时间,相信大多数人都作过以下尝试javascript
不能否认,这些优化在必定程度上下降了网站加载时间,但对于一个web应用庞大的请求量来讲,这些只是冰上一角、隔靴搔痒。css
以上问题归根结底是HTTP1.1协议自己的问题,若要从根本上解决HTTP1.1的低效,只能从协议自己入手。为此Google开发了SPDY协议,主要是为了下降传输时间;基于SPDY协议,IETF和SPDY组全体成员共同开发了HTTP/2,并在2015年5月以RFC 7504正式发表。SPDY或者HTTP/2并非一个全新的协议,它只是修改了HTTP的请求与应答在网络上的传输方式,增长了一个spdy传输层,用于处理、标记、简化和压缩HTTP请求,因此它们并不会破坏现有程序的工做,对于支持的场景,使用新特性能够得到更快的速度,对于不支持的场景,也能够实现平稳退化。html
HTTP/2继承了spdy的多路复用、优先级排序等诸多优秀特性,也额外作了很多改进。其中较为显著的改进是HTTP/2使用了一份通过定制的压缩算法,以此替代了SPDY的动态流压缩算法,用于避免对协议的Oracle攻击。java
多数主流浏览器已在2015年末支持了该标准(划重点)。具体支持度以下:node
数据来源git
能够看到国内有58.55%的浏览器已经彻底支持HTTP/2,而全球的支持度更是高达85.66%。这么高的支持度,so,你心动了吗github
咱们知道HTTP/1.1的头信息确定是文本(ASCII编码),数据体能够是文本,也能够是二进制(须要作本身作额外的转换,协议自己并不会转换)。而在HTTP/2中,新增了二进制分帧层,将数据转换成二进制,也就是说HTTP/2中全部的内容都是采用二进制传输。web
使用二进制有什么好处吗?固然!效率会更高,并且最主要的是能够定义额外的帧,若是用文本实现帧传输,解析起来将会十分麻烦。HTTP/2共定义了十种帧,较为常见的有数据帧、头部帧、PING帧、SETTING帧、优先级帧和PUSH_PROMISE帧等,为未来的高级应用打好了基础。算法
如上图,Binary Framing就是新增的二进制分帧层。express
二进制分帧层把数据转换为二进制的同时,也把数据分红了一个一个的帧。帧是HTTP/2中数据传输的最小单位;每一个帧都有stream_ID
字段,表示这个帧属于哪一个流,接收方把stream_ID
相同的全部帧组合到一块儿就是被传输的内容了。而流是HTTP/2中的一个逻辑上的概念,它表明着HTTP/1.1中的一个请求或者一个响应,协议规定client发给server的流的stream_ID
为奇数,server发给client的流ID是偶数。须要注意的是,流只是一个逻辑概念,便于理解和记忆的,实际并不存在。
理解了帧和流的概念,完整的HTTP/2的通讯就能够被形象地表示为这样:
能够发现,在一个TCP连接中,能够同时双向地发送帧,并且不一样流中的帧能够交错发送,不须要等某个流发送完,才发送下一个。也就是说在一个TCP链接中,能够同时传输多个流,便可以同时传输多个HTTP请求和响应,这种同时传输不须要遵循先入先出等规定,所以也不会产生阻塞,效率极高。
在这种传输模式下,HTTP请求变得十分廉价,咱们不须要再时刻顾虑网站的http请求数是否太多、TCP链接数是否太多、是否会产生阻塞等问题了。
为何须要压缩?
在 HTTP/1 中,HTTP 请求和响应都是由「状态行、请求 / 响应头部、消息主体」三部分组成。通常而言,消息主体都会通过 gzip 压缩,或者自己传输的就是压缩事后的二进制文件(例如图片、音频),但状态行和头部却没有通过任何压缩,直接以纯文本传输。
随着 Web 功能愈来愈复杂,每一个页面产生的请求数也愈来愈多,根据 HTTP Archive 的统计,当前平均每一个页面都会产生上百个请求。愈来愈多的请求致使消耗在头部的流量愈来愈多,尤为是每次都要传输 UserAgent、Cookie 这类不会频繁变更的内容,彻底是一种浪费。
为了减小冗余的头部信息带来的消耗,HTTP/2采用HPACK 算法压缩请求和响应的header。下面这张图很是直观地表达了HPACK头部压缩的原理:
具体规则能够描述为:
当要发送一个请求时,会先将其头部和静态表对照,对于彻底匹配的键值对,能够直接使用一个数字表示,如上图中的2:method: GET
,对于头部名称匹配的键值对,能够将名称使用一个数字传输,如上图中的19:path: /resource
,同时告诉服务端将它添加到动态表中,之后的相同键值对就用一个数字表示了。这样,像cookie这些不常常变更的值,只用发送一次就行了。
在开始HTTP/2 server push 前,咱们先来看看一个HTTP/1.1的页面是如何加载的。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<script src="user.js"></script>
</head>
<body>
<h1>hello http2</h1>
</body>
</html>
复制代码
/user.html
/user.html
发给浏览器/user.html
,发现还须要请求/user.js
和style.css
静态资源/user.js
和style.css
至此,这个页面才加载完毕,能够被用户看到。能够发如今步骤3和4中,服务器一直处于空闲等待状态,而浏览器到第6步才能获得资源渲染页面,这使页面的首次加载变得缓慢。
而HTTP/2的server push容许服务器在未收到请求时就向浏览器推送资源。即服务器发送/user.html
时,就能够主动把/user.js
和style.css
push给浏览器,使资源提早达到浏览器;除了静态文件,还能够推送比较耗时的API,只是须要提早将参数和cookie等信息经过某个方式告知服务端(如和路由关联)。Apache、GO的net/http、node-spdy都实现了server push(但ngnix没有=_=),本文后面的实践部分用node-spdy写了一个极为简陋的例子,有兴趣的小伙伴能够动手尝试一下。
Server push是HTTP/2协议里面惟一一个须要开发者本身配置的功能。其余功能都是服务器和浏览器自动实现,无需开发者介入。
在HTTP1.1时代,也有提早获取资源的方法,如preload和prefetch,前者是在页面解析初期就告诉浏览器,这个资源是浏览器立刻要用到的,能够马上发送对资源的请求,当须要用到该资源时就能够直接用而不用等待请求和响应的返回了;后者是当前页面用不到但下一页面可能会用到的资源,优先级较低,只有当浏览器空闲时才会请求prefetch标记的资源。从应用层面上看,preload和server push并无什么区别,可是server push减小浏览器请求的时间,略优于preload,在一些场景中,能够将二者结合使用。
纸上谈兵终觉浅,来实践一下吧!亲手搭建本身的 HTTP/2 demo,并抓包验证。
spdy这个库实现了 HTTP/2,同时也提供了对express的支持,因此这里我选用spdy + express搭建demo。demo源码
路径说明:
- ca/ 证书、秘钥等文件
- src/
- img/
- js/
- page1.html
- server.js
复制代码
虽然HTTP/2有加密(h2)和非加密(h2c)两种形式,但大多主流浏览器只支持h2-基于TLS/1.2或以上版本的加密链接,因此在搭建demo前,咱们首先要自颁发一个证书,这样就能够在浏览器访问中使用https了,你能够自行搜索证书颁发方法,也能够按照下述步骤去生成
首先要安装open-ssl,而后执行如下命令
$ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
....
$ openssl rsa -passin pass:x -in server.pass.key -out server.key
writing RSA key
$ rm server.pass.key
$ openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
....
$ openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
复制代码
而后你就会获得三个文件server.crt
, server.csr
, server.key
,将它们拷贝到ca文件夹中,稍后会用到。
express是一个Node.js框架,这里咱们用它声明了路由/
,返回的html文件page1.html
中引用了js和图片等静态资源。
// server.js
const http2 = require('spdy')
const express = require('express')
const app = express()
const publicPath = 'src'
app.use(express.static(publicPath))
app.get('/', function (req, res) {
res.setHeader('Content-Type', 'text/html')
res.sendFile(__dirname + '/src/page1.html')
})
var options = {
key: fs.readFileSync('./ca/server.key'),
cert: fs.readFileSync('./ca/server.crt')
}
http2.createServer(options, app).listen(8080, () => {
console.log(`Server is listening on https://127.0.0.1:8080 .`)
})
复制代码
用浏览器访问https://127.0.0.1:8080/
,打开控制台能够看全部的请求和它们的瀑布图:
能够清楚地看到,当第一个请求,也就是对document的请求彻底返回并解析后,浏览器才开始发起对js和图片等静态资源的的请求。前面说过,server push容许服务器主动向浏览器推送资源,那么是否能够在第一个请求未完成时,就把接下来所需的js和img推送给浏览器呢?这样不只充分利用了HTTP/2的多路复用,还减小了服务器的空闲等待时间。
对路由的处理函数进行改造:
app.get('/', function (req, res) {
+ push('/img/yunxin1.png', res, 'image/png')
+ push('/img/yunxin2.png', res, 'image/png')
+ push('/js/log3.js', res, 'application/javascript')
res.setHeader('Content-Type', 'text/html')
res.sendFile(__dirname + '/src/page1.html')
})
function push (reqPath, target, type) {
let content = fs.readFileSync(path.join(__dirname, publicPath, reqPath))
let stream = target.push(reqPath, {
status: 200,
method: 'GET',
request: { accept: '*/*' },
response: {
'content-type': type
}
})
stream.on('error', function() {})
stream.end(content)
}
复制代码
来看下应用了server push的瀑布图:
很明显,被push的静态资源能够很快地被使用,而没有被push的资源,如log1.js
和log2.js
则须要通过较长的时间才能被使用。
浏览器控制台看到的东西毕竟颇有限,咱们来玩点更有意思的~
wireshark是一款能够识别HTTP/2的抓包工具,它的原理是直接读取并分析网卡数据,咱们用它来验证是否真正实现了HTTP/2以及其底层通讯原理。
首先去官网下载安装包并安装wireshark,这一步没啥好说的。
咱们知道,http/2里的请求和响应都被拆分红了帧,若是咱们直接去抓取HTTP/2通讯包,那抓到的只能是一帧一帧地数据,像这样:
能够看到,抓到的都是TCP类型的包(红色方框);观察前三个包的内容(绿色方框),分别是SYN、[SYN, ACK]和ACK,这就咱们所熟知的TCP三次握手;右下角的黄色小方框是请求当前页面后抓到的TCP包的总数,其实这个页面只有七八个请求,但抓到的包的数量却有334个,这也验证了HTTP/2的请求和响应的确是被分红了一帧一帧的。
抓HTTP1.1的包,咱们能够清楚地看到都有哪些请求和响应,它们的协议、大小等,而HTTP/2的数据包倒是一帧一帧地,那么怎么看HTTP/2都有哪些请求和响应呢?其实wireshark会自动帮咱们重组拥有相同stream_ID的帧,重组后就可看到实际有哪些请求和响应了,可是由于咱们用的是https,全部的数据都被加密了,wireshark就不知道该怎么去重组了。
有两个办法能够在wireshark中解密 HTTPS 流量:第一若是你拥有 HTTPS 网站的加密私钥,能够用加密私钥来解密这个网站的加密流量;2)某些浏览器支持将 TLS 会话中使用的对称密钥保存在外部文件中,可供 Wireshark 解密使用。
可是HTTP/2为了前向安全性,不容许使用RAS秘钥交换,全部咱们没法使用第一个方法来解密HTTP/2流量。介绍第二种方法:当系统环境变量中存在SSLKEYFILELOG时,Chrome和firefox会将对称秘钥保存在该环境变量指向的文件中,而后把这个文件导入wireshark,就能够解密HTTP/2流量了,具体作法以下:
SSLKEYFILELOG
,指向第一步建立的文件这时用Chrome或Firefox访问任何一个https页面,ssl.log中应该就有写入的秘钥数据了。
解密完成后,咱们就能够看到HTTP/2的包了
下图是在demo的主页面抓取的包,能够清楚地看到有哪些HTTP/2请求。
HTTP/2协议中的流和能够在一个TCP链接中交错传输,只需创建一个TCP链接就能够完成和服务器的全部通讯,咱们来看下在demo中的HTTP/2是否是这样的:
wireshark下方还有一个面板,里面有当前包的具体信息,如大小、源IP、目的IP、端口、数据、协议等,在Transmission Control Protocol下有一个[Stream index],以下图,它是TCP链接的编号,表明当前包是从哪一个TCP链接中传输的。观察demo页面请求产生的包,能够发现它们的stream index 都相同,说明这些HTTP/2请求和响应是在一个TCP链接中被传输的,这么多流的确复用了一个TCP链接。
除了多路复用外,咱们还能够经过抓包来观察HTTP/2的头部压缩。下图是当前路由下的第一个请求,实际被传输的头部数据有253bytes,解压后的头部信息有482bytes。压缩后的大小减小了几乎一半
但这只是第一个请求,咱们看看后来的请求,如第三个,实际传输的头部大小只有30bytes,而解压后的大小有441byte,压缩后的体积仅为原来的1/14!现在web应用单是一个页面就动辄几百的请求数,HPACK能节约的流量可想而知。
在文章开篇,咱们列举了HTTP1.x时代的困境,引入并简要说明了HTTP/2的起源;而后对比着HTTP1.x,介绍了HTTP/2的诸多优秀特性,来讲明为何选择HTTP/2;在文章的最后一部分,介绍了如何一步一步搭建一个HTTP/2实例,并抓包观察,验证了HTTP/2的多路复用,头部压缩等特性。最后,您是否也被这些高效特性吸引了呢?动手试试吧~
参考: