HTTP2简介

1. HTTP1.X的现状

1.1 过于庞大

协议包含太多细节和可选部分。javascript

1.2 未能充分利用的TCP

1.3 HTTP Pipelining

image
HTTP Pipelining是这样一种技术:在等待上一个请求响应的同时,发送下一个请求。(译者注:做者这个解释并不彻底正确,HTTP Pipelining实际上是把多个HTTP请求放到一个TCP链接中一一发送,而在发送过程当中不须要等待服务器对前一个请求的响应;只不过,客户端仍是要按照发送请求的顺序来接收响应。)css

HTTP/1.x 虽然经过 pipeline 也能并发请求,可是多个请求之间的响应会被阻塞的,因此 pipeline 至今也没有被普及应用,而 HTTP/2 作到了真正的并发请求。同时,流还支持优先级和流量控制。html

2. 克服延迟作过的努力

2.1 Spriting

雪碧图,合并小图片,减小请求,在HTTP 1.1里,下载一张大图比下载100张小图快得多。缺点是: 不能单独保存经常使用的图片在缓存中,不利于缓存。java

2.2 内联

用dataURI等方式将图片存在css文件里。缺点和Spriting相似,不利于缓存。webpack

2.3 拼接(Concatenation)

合并文件,利用webpack等工具打包多个文件为一个大文件。web

2.4 分片(Sharding)

顾名思义,Sharding就是把你的服务分散在尽量多的主机上。用这种技术来提高链接的数量。而随着资源个数的提高,网站会须要更多的链接来保证HTTP协议的效率,从而提高载入速度。算法

另一个将图片或者其余资源分发到不一样主机的理由是能够不使用cookies,毕竟现今cookies的大小已经很是可观了。无cookies的图片服务器每每意味着更小的HTTP请求以及更好的性能!
imagejson

3. http2

3.1 起源

http2 的前身是由 google 领导开发的 SPDY,后来 google 把整个成果交给 IETF,IETF 把 SPDY 标准化以后变成 http2。google 也很大方的废弃掉 SPDY,转向支持 http2。http2 是彻底兼容 http/1.x 的,在此基础上添加了 4 个主要新特性:浏览器

二进制分帧
多路复用
头部压缩
服务端推送
优化手段缓存

3.2 http2特性

3.3 二进制分帧

http/1.x 是一个文本协议,而 http2 是一个不折不扣的二进制协议。

基于二进制的http2可使成帧的使用变得更为便捷。在HTTP1.1和其余基于文本的协议中,对帧的起始和结束识别起来至关复杂。而经过移除掉可选的空白符以及其余冗余后,再来实现这些会变得更容易。

而另外一方面,这项决议一样使得咱们能够更加便捷的从帧结构中分离出那部分协议自己的内容。而在HTTP1中,各个部分相互交织,犹如一团乱麻。

事实上,因为协议提供了压缩这一特性,而其常常运行在TLS之上的事实又再次下降了基于纯文本实现的价值,反正也没办法直接从数据流上看到文本。所以一般状况下,咱们必须习惯使用相似Wireshark这样的工具对http2的协议层一探究竟。
image
帧由 Frame Header 和 Frame Payload 组成。以前在 http/1.x 中的 header 和 body 都放在 Frame Payload 中。

Type 字段用来表示该帧中的 Frame Payload 保存的是 header 数据仍是 body 数据。除了用于标识 header/body,还有一些额外的 Frame Type。
Length 字段用来表示 Frame Payload 数据大小。
Frame Payload 用来保存 header 或者 body 的数据。

Stream Identifier 用来标识该 frame 属于哪一个 stream。这句话可能感受略突兀,这里要明白 Stream Identifier 的做用,须要引出 http2 的第二个特性『多路复用』。

3.3 多路复用

在 http/1.x 状况下,每一个 http 请求都会创建一个 TCP 链接,这就意味着每一个请求都须要进行三次握手。这样子就会浪费比较多的时间和资源,这点在 http/1.x 的状况下是没有办法避免的。而且浏览器会限制同一个域名下并发请求的个数。因此,在 http/1.x 的状况下,一个常见的优化手段是把静态资源分布到不一样域名下,以此来突破浏览器并发数的限制。(上节提到的分片sharding)

在 http2 的状况下,全部的请求都会共用一个 TCP 链接,这个能够说是 http2 杀手级的特性了。 :punch: 由于这点,许多在 http/1.x 时代的优化手段均可以退休了。可是这里也出现了一个问题,全部的请求都共用一个 TCP 链接,那么客户端/服务端怎么知道某一帧(别忘记上面说了 http2 是的基本单位是帧)的数据属于哪一个请求呢?

上面的 Stream Identifier 就是用来标识该帧属于哪一个请求的。

当客户端同时向服务端发起多个请求,那么这些请求会被分解成一一个的帧,每一个帧都会在一个 TCP 链路中无序的传输,同一个请求的帧的 Stream Identifier 都是同样的。当帧到达服务端以后,就能够根据 Stream Identifier 来从新组合获得完整的请求。

image

3.4 优先级和依赖性

每一个流都包含一个优先级(也就是“权重”),它被用来告诉对端哪一个流更重要。当资源有限的时候,服务器会根据优先级来选择应该先发送哪些流。

借助于PRIORITY帧,客户端一样能够告知服务器当前的流依赖于其余哪一个流。该功能让客户端能创建一个优先级“树”,全部“子流”会依赖于“父流”的传输完成状况。

优先级和依赖关系能够在传输过程当中被动态的改变。这样当用户滚动一个全是图片的页面的时候,浏览器就可以指定哪一个图片拥有更高的优先级。或者是在你切换标签页的时候,浏览器能够提高新切换到页面所包含流的优先级。

3.5 头部压缩

HTTP是一种无状态的协议。简而言之,这意味着每一个请求必需要携带服务器须要的全部细节,而不是让服务器保存住以前请求的元数据。由于http2并无改变这个范式,因此它也以一样原理工做。

这也保证了HTTP可重复性。当一个客户端从同一服务器请求了大量资源(例如页面的图片)的时候,全部这些请求看起来几乎都是一致的,而这些大量一致的东西则正好值得被压缩。

每一个页面请求的资源数量在增多(如前所述),同时 cookies 的使用和请求的大小也在日渐增加。cookies须要被包含在全部请求中,且他们在多个请求中常常是如出一辙的。

HTTP 1.1请求的大小正变得愈来愈大,有时甚至会大于TCP窗口的初始大小,这会严重拖累发送请求的速度。由于它们须要等待带着ACK的响应回来之后,才能继续被发送。这也是另外一个须要压缩的理由。

3.5.1 HPACK压缩

在 http/1.x 协议中,每次请求都会携带 header 数据,而相似 User-Agent, Accept-Language 等信息在每次请求过程当中几乎是不变的,那么这些信息在每次请求过程当中就变成了浪费。因此, http2 中提出了一个 HPACK 的压缩方式,用于减小 http header 在每次请求中消耗的流量。

HPACK 压缩的原理以下 :
客户端和服务端共同维护一个『静态字典』,字典中每行 3 列,相似下表
image
当请求的 header 头部中包含 :mehtod:GET,客户端在发送请求的时候,会直接发送静态字段中对应的 index 值,在这里也就是 2。服务端在接受到请求的时候,去寻找静态字典中 index = 2 对应的 header name 和 header value,就明白了客户端发起了一个 GET 请求。
客户端和服务端必须维护一套同样的静态字典,这里给出了完整的静态字典,客户端和服务端都会遵照这套静态字典。

你会发现静态字典中有些 header value 没有值。这是由于有些 header 字段的值是不定的,好比 User-Agent 字段,因此标准中没有定下 header value 的值。
那么若是碰到在静态字典中 header value 没有的值,HPEACK 算法会采起下面的方式:
假设 http 请求的 header 中包含了 User-Agent:Mozilla/5.0` (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36,那么 HPACK 会对 User-Agent的值进行哈夫曼编码,而后在静态字典中找到 User-Agent的 index 为 58,那么客户端会把 User-Agent`` 的 index 值和User-Agent“` 值对应的哈夫曼编码值发送给服务端

会被转换陈下面的 kv 值发送给服务端:

58 : Huffman('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36')

服务端收到请求以后,把 User-Agent 和哈夫曼编码值追加到静态字典后面,这些追加的行称之为『动态字典』。

服务端收到请求以后,把 User-Agent 和哈夫曼编码值追加到静态字典后面,这些追加的行称之为『动态字典』。
image
客户端在发送请求的时候,也会把该行添加到本身维护的静态字典表后面,这样子客户端和服务端维护的字典表就会保持一致。以后的请求客户端若是须要携带 User-Agent 字段,只要发送 62 便可。

http2 中状况就彻底不同了,全部的请求都是在一个 TCP 链接中完成的。

3.6 Server Push

这个功能一般被称做“缓存推送”。主要的思想是:当一个客户端请求资源X,而服务器知道它极可能也须要资源Z的状况下,服务器能够在客户端发送请求前,主动将资源Z推送给客户端。这个功能帮助客户端将Z放进缓存以备未来之需。

服务器推送须要客户端显式的容许服务器提供该功能。但即便如此,客户端依然能自主选择是否须要中断该推送的流。若是不须要的话,客户端能够经过发送一个RST_STREAM帧来停止。

服务端推送指的是服务端主动向客户端推送数据。
举个例子,index.html 有以下代码

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>hello world</h1>
  <img src="something.png">
</body>
</html>

那么正常状况下,为了展现页面须要发 3 次请求:

发起 1 次请求 index.html 页面
解析 index.html 页面发现 style.css 和 something.png 资源,发起 2 次请求获取资源。

若是服务端配置了服务端推送以后,那么状况变成下面的样子:

浏览器请求 index.html。
服务器发现浏览器请求的 index.html 中包含 style.css 和 something.png 资源,因而直接 index.html, style.css, something.png 三个资源都返回给浏览器。

这样,服务端和浏览器只须要进行一次通讯,就能够获取到所有资源。

这是一种得到 HTTP/1 优化实践(例如内联)所带来性能提高的优雅方式,同时也避免了原先实践的一些缺点。减小了请求,也利于缓存。
image

3.2 webpack & HTTP2

既然HTTP2是基于流的,多路复用,多个HTTP共用一个TCP请求,是否能够不在去打包文件从而减小请求数量了呢。
问题没那么简单。
- 对于多个请求来讲依然会有比单个请求多的协议开销(protocol overhead )
- 对于单个大文件的压缩要比多个小文件要更高效。
- 对于服务端,处理一个大文件要比处理多个小文件要快。

因此咱们须要找一个折中的方式,来集中两种方式的长处。咱们要将modules打包成n个bundles,这个n是个最优解,要比打包成1个bundle要好也比打包成小于n的值要好。

3.2.1 The AggressiveSplittingPlugin

webpack 2 提供了这个工具。

AggressiveSplittingPlugin 能够将 bundle 拆分红更小的 chunk,直到各个 chunk 的大小达到 option 设置的 maxSize。它经过目录结构将模块组织在一块儿。

它记录了在 webpack Records里的分离点,并尝试按照它开始的方式还原分离。这确保了在更改应用程序后,旧的分离点(和 chunk)是可再使用的,由于它们可能早已在客户端的缓存中。所以强烈推荐使用Records。

仅有在 chunk 超过规定的 minSize 时才会保存在Records里。能够确保 chunk 随着应用程序的增长而增长,而不是每次改变的时候建立不少的 chunk。

若是模块更改,chunk 可能会无效。无效 chunk 中的模块会回到模块池(module pool)中,会同时建立一个新的模块。

3.2.2 当应用更新时

当应用更新时,咱们须要尽最大努力去复用以前建立过的chunks。因此AggressiveSplittingPlugin每次会去找一个合适的chunk(在限制size内的chunk),并把这个chunk的modules和hash存进records。

* Records*是webpack中关于state的一个概念,维护于编译阶段,它从一个json文件中读取。
AggressiveSplittingPlugin再次被调用的时候,它首先会从records中恢复chunks,而后在分割剩下的modules。这保证了这些被缓存的chunks能够被复用。

当一个应用用了这项技术后,最后打包出的再也不是一个单一的HTML文件,它产出的是多个chunks。而且用最优的script-tags 去加载这些chunk。

<script src="1ea296932eacbe248905.js"></script>
<script src="0b3a074667143853404c.js"></script>
<script src="0dd8c061aff2a2791815.js"></script>
<script src="191b812fa5f7504151f7.js"></script>
<script src="08702f45497539ef6ea6.js"></script>
<script src="195c9326275620b0e9c2.js"></script>
<script src="19817b3a0378aedb2143.js"></script>
<script src="0e7a65e649387d773247.js"></script>
<script src="13167c9702de79d2f4fd.js"></script>
<script src="1154be40ff0e8dd16e9f.js"></script>
<script src="129ce3c198a25d9ace74.js"></script>
<script src="032d1fc9a213dfaf2c79.js"></script>
<script src="07df084bbafc95c1df47.js"></script>
<script src="15c45a570bb174ae448e.js"></script>
<script src="02099ada43bbf02a9f73.js"></script>
<script src="17bc99aaed6b9a23da78.js"></script>

webpack将这些chunk按年龄顺序排列。最旧的文件被放在最前面,最新的文件放在最后。这样浏览器能够在等待新文件下载的同时先从缓存中读取旧的文件。由于旧文件有大几率已经被缓存了。

HTTP/2 Server push能够在请求HTML的时候向客户端推送这个chunks。最好是先推送新的文件,若是文件已存在,客户端能够取消文件推送。

3.3 使用 Wireshark 调试 HTTP/2 流量

咱们知道,HTTP/2 引入了二进制分帧层(Binary Framing),将每一个请求和响应分割成为更小的帧,并对它们进行了二进制编码。与此同时,HTTP/2 沿用了以前 HTTP 版本中的绝大部分语义,上层应用基本上感知不到 HTTP/2 的存在,这一点能够经过浏览器的网络调试工具获得验证。

总结

相比 HTTP/1.x,HTTP/2 在底层传输作了很大的改动和优化:
- HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 的文本格式。二进制格式在协议的解析和优化扩展上带来更多的优点和可能。
- HTTP/2 对消息头采用 HPACK 进行压缩传输,可以节省消息头占用的网络的流量。而 HTTP/1.x 每次请求,都会携带大量冗余头信息,浪费了不少带宽资源。头压缩可以很好的解决该问题。
- 多路复用,直白的说就是全部的请求都是经过一个 TCP 链接并发完成。HTTP/1.x 虽然经过 pipeline 也能并发请求,可是多个请求之间的响应会被阻塞的,因此 pipeline 至今也没有被普及应用,而 HTTP/2 作到了真正的并发请求。同时,流还支持优先级和流量控制。
- Server Push:服务端可以更快的把资源推送给客户端。例如服务端能够主动把 JS 和 CSS 文件推送给客户端,而不须要客户端解析 HTML 再发送这些请求。当客户端须要的时候,它已经在客户端了。

参考连接:(以上内容整理来自)
- http简介
- http2讲解
- HTTP/2 Server Push 详解(上)
- HTTP/2 Server Push 详解(下)
- HTTP/2 新特性浅析
- webpack & HTTP2
- 使用 Wireshark 调试 HTTP/2 流量