协议包含太多细节和可选部分。
HTTP Pipelining是这样一种技术:在等待上一个请求响应的同时,发送下一个请求。(译者注:作者这个解释并不完全正确,HTTP Pipelining其实是把多个HTTP请求放到一个TCP连接中一一发送,而在发送过程中不需要等待服务器对前一个请求的响应;只不过,客户端还是要按照发送请求的顺序来接收响应。)
HTTP/1.x 虽然通过 pipeline 也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。
雪碧图,合并小图片,减少请求,在HTTP 1.1里,下载一张大图比下载100张小图快得多。缺点是: 不能单独保存常用的图片在缓存中,不利于缓存。
用dataURI等方式将图片存在css文件里。缺点和Spriting类似,不利于缓存。
合并文件,利用webpack等工具打包多个文件为一个大文件。
顾名思义,Sharding就是把你的服务分散在尽可能多的主机上。用这种技术来提升连接的数量。而随着资源个数的提升,网站会需要更多的连接来保证HTTP协议的效率,从而提升载入速度。
另外一个将图片或者其他资源分发到不同主机的理由是可以不使用cookies,毕竟现今cookies的大小已经非常可观了。无cookies的图片服务器往往意味着更小的HTTP请求以及更好的性能!
http2 的前身是由 google 领导开发的 SPDY,后来 google 把整个成果交给 IETF,IETF 把 SPDY 标准化之后变成 http2。google 也很大方的废弃掉 SPDY,转向支持 http2。http2 是完全兼容 http/1.x 的,在此基础上添加了 4 个主要新特性:
二进制分帧
多路复用
头部压缩
服务端推送
优化手段
http/1.x 是一个文本协议,而 http2 是一个彻彻底底的二进制协议。
基于二进制的http2可以使成帧的使用变得更为便捷。在HTTP1.1和其他基于文本的协议中,对帧的起始和结束识别起来相当复杂。而通过移除掉可选的空白符以及其他冗余后,再来实现这些会变得更容易。
而另一方面,这项决议同样使得我们可以更加便捷的从帧结构中分离出那部分协议本身的内容。而在HTTP1中,各个部分相互交织,犹如一团乱麻。
事实上,由于协议提供了压缩这一特性,而其经常运行在TLS之上的事实又再次降低了基于纯文本实现的价值,反正也没办法直接从数据流上看到文本。因此通常情况下,我们必须习惯使用类似Wireshark这样的工具对http2的协议层一探究竟。
帧由 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 的第二个特性『多路复用』。
在 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 来重新组合得到完整的请求。
每个流都包含一个优先级(也就是“权重”),它被用来告诉对端哪个流更重要。当资源有限的时候,服务器会根据优先级来选择应该先发送哪些流。
借助于PRIORITY帧,客户端同样可以告知服务器当前的流依赖于其他哪个流。该功能让客户端能建立一个优先级“树”,所有“子流”会依赖于“父流”的传输完成情况。
优先级和依赖关系可以在传输过程中被动态的改变。这样当用户滚动一个全是图片的页面的时候,浏览器就能够指定哪个图片拥有更高的优先级。或者是在你切换标签页的时候,浏览器可以提升新切换到页面所包含流的优先级。
HTTP是一种无状态的协议。简而言之,这意味着每个请求必须要携带服务器需要的所有细节,而不是让服务器保存住之前请求的元数据。因为http2并没有改变这个范式,所以它也以同样原理工作。
这也保证了HTTP可重复性。当一个客户端从同一服务器请求了大量资源(例如页面的图片)的时候,所有这些请求看起来几乎都是一致的,而这些大量一致的东西则正好值得被压缩。
每个页面请求的资源数量在增多(如前所述),同时 cookies 的使用和请求的大小也在日渐增长。cookies需要被包含在所有请求中,且他们在多个请求中经常是一模一样的。
HTTP 1.1请求的大小正变得越来越大,有时甚至会大于TCP窗口的初始大小,这会严重拖累发送请求的速度。因为它们需要等待带着ACK的响应回来以后,才能继续被发送。这也是另一个需要压缩的理由。
在 http/1.x 协议中,每次请求都会携带 header 数据,而类似 User-Agent, Accept-Language 等信息在每次请求过程中几乎是不变的,那么这些信息在每次请求过程中就变成了浪费。所以, http2 中提出了一个 HPACK 的压缩方式,用于减少 http header 在每次请求中消耗的流量。
HPACK 压缩的原理如下 :
客户端和服务端共同维护一个『静态字典』,字典中每行 3 列,类似下表
当请求的 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 和哈夫曼编码值追加到静态字典后面,这些追加的行称之为『动态字典』。
客户端在发送请求的时候,也会把该行添加到自己维护的静态字典表后面,这样子客户端和服务端维护的字典表就会保持一致。之后的请求客户端如果需要携带 User-Agent 字段,只要发送 62 即可。
http2 中情况就完全不一样了,所有的请求都是在一个 TCP 连接中完成的。
这个功能通常被称作“缓存推送”。主要的思想是:当一个客户端请求资源X,而服务器知道它很可能也需要资源Z的情况下,服务器可以在客户端发送请求前,主动将资源Z推送给客户端。这个功能帮助客户端将Z放进缓存以备将来之需。
服务器推送需要客户端显式的允许服务器提供该功能。但即使如此,客户端依然能自主选择是否需要中断该推送的流。如果不需要的话,客户端可以通过发送一个RST_STREAM帧来中止。
服务端推送指的是服务端主动向客户端推送数据。
举个例子,index.html 有如下代码
<html>
<head>
<link rel="stylesheet" href="style.css">
head>
<body>
<h1>hello worldh1>
<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 优化实践(例如内联)所带来性能提升的优雅方式,同时也避免了原先实践的一些缺点。减少了请求,也利于缓存。
既然HTTP2是基于流的,多路复用,多个HTTP共用一个TCP请求,是否可以不在去打包文件从而减少请求数量了呢。
问题没那么简单。
- 对于多个请求来说依然会有比单个请求多的协议开销(protocol overhead )
- 对于单个大文件的压缩要比多个小文件要更高效。
- 对于服务端,处理一个大文件要比处理多个小文件要快。
所以我们需要找一个折中的方式,来集中两种方式的长处。我们要将modules打包成n个bundles,这个n是个最优解,要比打包成1个bundle要好也比打包成小于n的值要好。
webpack 2 提供了这个工具。
AggressiveSplittingPlugin 可以将 bundle 拆分成更小的 chunk,直到各个 chunk 的大小达到 option 设置的 maxSize。它通过目录结构将模块组织在一起。
它记录了在 webpack Records里的分离点,并尝试按照它开始的方式还原分离。这确保了在更改应用程序后,旧的分离点(和 chunk)是可再使用的,因为它们可能早已在客户端的缓存中。因此强烈推荐使用Records。
仅有在 chunk 超过规定的 minSize 时才会保存在Records里。可以确保 chunk 随着应用程序的增加而增加,而不是每次改变的时候创建很多的 chunk。
如果模块更改,chunk 可能会无效。无效 chunk 中的模块会回到模块池(module pool)中,会同时创建一个新的模块。
当应用更新时,我们需要尽最大努力去复用之前创建过的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。最好是先推送新的文件,如果文件已存在,客户端可以取消文件推送。
我们知道,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 流量