作为一只前端开发 🐶,HTTP 是我们知识地图里面必不可少的一部分,也是面试必问知识点。HTTP2 号称可以让我们的应用更快、更简单、更稳定,它完美解决了 1.1 版本的诸多问题,本文和大家一起聊聊 HTTP2 的改进点。
HTTP 发展史
正式讲 HTTP2 之前我们先讲一下 HTTP 的发展史。
- HTTP/0.9 - 单行协议 HTTP 于 1990 年问世,那时候 HTTP 非常简单:只支持 GET 方法;没有首部;只能获取纯文本。
- HTTP/1.0 - 搭建协议的框架 1996 年,HTTP 正式被作为标准公布,版本为 HTTP/1.0。1.0 版本增加了首部、状态码、权限、缓存、长连接(默认短连接)等规范,可以说搭建了协议的基本框架。
- HTTP/1.1 - 进一步完善 1997 年,1.1 版本接踵而至。1.1 版本的重大改进在于默认长连接;强制客户端提供 Host 首部;管线化;Cache-Control、ETag 等缓存的相关扩展。
目前存在的问题
现在我们先不聊 HTTP2, 看一下 HTTP 发展到 1.1 存在有哪些问题:
- 线头阻塞:TCP 连接上只能发送一个请求,前面的请求未完成前,后续的请求都在排队等待。
- 多个 TCP 连接 虽然 HTTP/1.1 管线化可以支持请求并发,但是浏览器很难实现,chrome、firefox 等都禁用了管线化。所以 1.1 版本请求并发依赖于多个 TCP 连接,建立 TCP 连接成本很高,还会存在慢启动的问题。
- 头部冗余,采用文本格式 HTTP/1.X 版本是采用文本格式,首部未压缩,而且每一个请求都会带上 cookie、user-agent 等完全相同的首部。
- 客户端需要主动请求
HTTP/2.0 的时代来了
先来一个 demo 感受一下吊炸天的 HTTP/2.0,这个 demo 是加载 379 张图片,来对比 HTTP/1.1 和 HTTP/2.0 的性能。 HTTP/1.1 与 2.0 性能比较
理论上 HTTP/2.0 会比 HTTP/1.1 有一倍多的性能提升,弱网环境下,性能提升会更加明显。 下面两张图是我在设置网络在 fast 3G 和 slow 3G 的性能对比。
是不是被 HTTP/2.0 的速度亮瞎了双眼?2333,接下来我们正式开始聊聊 2.0。看看 2.0 相比与 1.1 的一些重大改进。
二进制分帧层
HTTP2 性能提升的核心就在于二进制分帧层。HTTP2 是二进制协议,他采用二进制格式传输数据而不是 1.x 的文本格式。
看图吧!很清晰的表达了 HTTP/1.1 的响应和 2.0 的区别。1.1 响应是文本格式,而 2.0 把响应划分成了两个帧,图中的 HEADERS(首部)和 DATA(消息负载) 是帧的类型。了解更多帧的类型也就是说一条 HTTP 响应,划分成了两个帧来传输,并且采用二进制来编码。
这里我们来提三个概念。
- 流(Stream):已建立的 TCP 连接上的双向字节流,可以承载一个或多个消息。
- 消息(Message):一个完整的 HTTP 请求或响应,由一个或多个帧组成。特定消息的帧在同一个流上发送,这意味着一个 HTTP 请求或响应只能在一个流上发送。
- 帧(Frame):通信的基本单位。 一个 TCP 连接上可以有任意数量的流。
多路复用
上面提到 HTTP/1.1 的线头阻塞和多个 TCP 连接的问题,HTTP2 的多路复用完美解决。HTTP2 让所有的通信都在一个 TCP 连接上完成,真正实现了请求的并发。我们来看一下 HTTP2 具体是怎么实现的:
HTTP2 建立一个 TCP 连接,一个连接上面可以有任意多个流(stream),消息分割成一个或多个帧在流里面传输。帧传输过去以后,再进行重组,形成一个完整的请求或响应。这使得所有的请求或响应都无法阻塞。 我们再来回看上面的那个 demo:
打开控制台可以看到,HTTP/1.1 的方式,后面的图片的加载时间主要耗时在 stalled,stalled 的意思是从 TCP 连接建立完成,到真正可以传输数据之间的时间差。这就是队头阻塞,前面的请求没有处理,后面的请求都在排队等待。
这里例子我们能很直观的看到就是多路复用起到的优化作用。因为 HTTP2 实现了请求并发,后面的请求不用再等待,加载时长当然少了很多。截一张 HTTP2 的图片加载耗时详情来看看(要看比较靠后的请求):
咦??什么情况?我们发现后面的很多请求依旧有在排队哎,只是排队的时间相对 1.1 少了很多。一个 TCP 连接可以有任意数量的流,也就是同时可以并发任意数量的请求啊,为啥还会排队呢?原因就是请求太多时,浏览器或服务器会受不了,这超出了它的处理能力。流控制帮我们解决了这个问题,流控制会管理数据的传输,允许接收者停止或减少发送的数据量,免得接收方不堪重负。所以请求太多时,还是会存在排队等待的问题,因为不管是客户端或服务器端,能同时处理请求或响应都是有限的。
头部压缩
头部压缩也是 HTTP2 的一大亮点。在 1.X 版本中,首部用文本格式传输,通常会给每个传输增加 500-800 字节的开销。现在打开一个网页上百个请求已是常态,而每个请求带的一些首部字段都是相同的,例如 cookie、user-agent 等。HTTP2 为此采用 HPACK 压缩格式来压缩首部。头部压缩需要在浏览器和服务器端之间:
- 维护一份相同的静态字典,包含常见的头部名称,以及常见的头部名称和值的组合
- 维护一份相同的动态字典,可以动态的添加内容
- 通过静态 Huffman 编码对传输的首部字段进行编码
HTTP2 的静态字典是长这个样子的(只截取了部分,完整表格在这里):
所以我们在传输首部字段的时候,例如要传输 method:GET,那我们只需要传输静态字典里面 method:GET 对应的索引值就可以了,一个字节搞定。像 user-agent、cookie 这种静态字典里面只有首部名称而没有值的首部,第一次传输需要 user-agent 在静态字典中的索引以及他的值,值会采用静态 Huffman 编码来减小体积。
第一次传输过 user-agent 之后呢,浏览器和服务器端就会把它添加到自己的动态字典中。后续传输就可以传输索引了,一个字节搞定。
我们用 WireShark 来抓包验证一下: HTTP2 目前都是 HTTPS 的请求,WireShark 对 HTTPS 网站抓包解密请参考这里。
首次传输 user-agent 和第二次传输 user-agent
由于第一次传输的时候,字典里面并没有 user-agent 的值,这时候 user-agent 是 63 字节,第二次传输时,他已经在动态字典里面了,只传索引,一个字节搞定。
- HPACK 的首部压缩力度
Header 解码后的长度有 471 个字节,而 HEADERS 流只有 246 个字节。这只是第一个请求,后续的请求压缩力度会更大,因为前面请求用到的首部(静态字典中没有的)会添加到动态字典中,使得后续请求只需要传输字典里面的索引。
服务器端推送
服务器端推送使得服务器可以预测客户端需要的资源,主动推送到客户端。 例如:客户端请求 index.html,服务器端能够额外推送 script.js 和 style.css。 实现原理就是客户端发出页面请求时,服务器端能够分析这个页面所依赖的其他资源,主动推送到客户端的缓存,当客户端收到原始网页的请求时,它需要的资源已经位于缓存。
针对每一个希望发送的资源,服务器会发送一个 PUSH_PROMISE 帧,客户端可以通过发送 RST_STREAM 帧来拒绝推送(当资源已经位于缓存)。这一步的操作先于父响应(index.html),客户端了解到服务器端打算推送哪些资源,就不会再为这些资源创建重复请求。当客户端收到 index.html 的响应时,script.js 和 style.css 已经位于缓存。
想要搭一个 HTTP2 服务器的话推荐 node,很简单。链接
参考文章
结语
简单讲了 HTTP2 相比 1.1 版本的重要改进点,感受了一下 h2 的强大。还有一些流优先化等特性文中未涉及,感兴趣的可以在参考文章中看看。如有错误,恳请指正!