HTTP/2.0 协议基本上已经开始大规模使用,本文主要介绍 HTTP/2.0 协议的一些特性,以及前端应该对应做哪些优化的调整。
最早期的 HTTP/1.0 时代,一个资源 == 一个 TCP 链接 == 一个 HTTP 链接,每个 HTTP 请求结束都会断开 TCP 连接,新的 HTTP 请求会另外新建一个 TCP 连接,并且只有当一个完整的请求结束(TCP closed)才会开始下一个请求(阻塞)。
这种使用 TCP 的方式最严重的问题是阻塞。后来为了解决阻塞问题,请求允许并发,即同一个域名允许建立多个 TCP 链接,这样算是解决了部分阻塞问题,但是还是有多次建立 TCP 连接的延迟(Hand shaking)以及 TCP 特有的慢启动(Slow start)问题。
TCP 连接会随著时间自我调整,起初会限制连接的最大速度,如果数据传输成功,会随著时间的推移提高传输速度
TCP 协议本身太复杂了,细节以后再看吧。简单来说,TCP 协议主要解决的是两个问题
同时,TCP 还有两套控制机制
这些机制使得 TCP 协议不仅仅知道自己信道的情况,还能知道整个网路的情况
慢启动是堵塞控制的一个基本演算法,简单说就是,先设置小一点的窗口,发送少一点的数据,然后接受 ACK,然后慢慢增加窗口大小,通过丢包率,Round Trip Time 等等来判断网路环境,设定一个合适的窗口大小。
为了解决 TCP 连接利用率低的问题,所以又提出了长链接(1.0 开启,1.1 默认开启)。即一个请求完成后,不会立刻断开连接,而是在一定的时间内保持连接,以便快速处理即将到来的 HTTP 请求,复用同一个 TCP 通道,直到客户端心跳检测失败或伺服器连接超时。
Connection: keep-alive
Connection: close
长链接解决了多次创建 TCP 连接的延迟,但是还是有线头阻塞(Head of line blocking)的问题,即一个 TCP 连接一次只能发出一个请求,所以客户端必须等待收到响应后才能发出另外一个请求,这样耗时的请求如果在前面会 block 后面耗时的请求。
HTTP 管道曾被提出来用以解决线头阻塞问题,即把多个 HTTP 请求通过一个 TCP 连接传送,在发送过程中不需要等待伺服器对前一个请求的响应,但是客户端还是要按照发送请求的顺序接收响应。这种方式并没有根本解决线头阻塞问题,因为响应按顺序接收还是会有阻塞,所以这个功能都被浏览器默认关闭(或者压根没有)
HTTP/2.0 通过分帧将数据通过帧的方式传送,彻底解决了这个问题,并且做了一些别的优化
协议抽象描述(下面这段话很牛逼,需要仔细研读)
在客户端与伺服器之间仅建立一个 TCP 连接,而且该连接在交互持续期间一直处于打开状态。在此连接上,消息是通过逻辑流进行传递的。一条消息包含一个完整的帧序列。在经过整理后,这些帧表示一个响应或请求。
这里有一些概念很重要,下面的内容都围绕这些概念展开
其中 FLAG :
分帧层处理的栗子
HTTP/2 保留了原始 HTTP 协议的语义,但更改了在系统之间传输数据的方式
HTTP/2.0 文本格式跟二进位格式的主要差别在于解析
HTTP/2.0 允许保留原来的文本格式,但会经过一个"二进位分帧"的过程,将文本彻底转化为二进位传输。
旧的 HTTP 协议有个问题叫线头阻塞(Head of line blocking),之前的解决方案是同时开启多个 TCP 链接(Chrome 有6个),但是这样多个 TCP 连接就很耗费网路资源了。实际上"单线程"就够了,方式就是将消息分成更小的单位(帧),这样就实现了完全双向?的请求和响应消息复用。
比如下图有三个逻辑流,一个请求(深蓝),两个响应(浅蓝,绿),每一块代表一个帧
将帧分解成 HEADER 和 DATA 帧
优点:
每个伺服器(域)只是用一个链接,而不是每个文件一个链接
允许伺服器预测客户端的需要,在请求处理完成之前就可以先发一个 PUSH_PROMISE frame,然后在 push 资源。为防止发送不必要的资源,伺服器会给每一个要推送的资源发送一个 PUSH_PROMISE frame,如果资源已经有缓存,浏览器可以 respond 一个 RST_STREAM frame,来拒绝 PUSH
(伺服器推送这个选项怎么说呢,理论上有用,但是实际上还需要 tune,有待观察吧)
防止 receiver overwhelmed by the sender,允许 receiver 停止/减少发送的数据。
比如视屏流媒体服务,用户点击暂停,client will informs the server to stop sending video data。
连接一旦 open,server 和 client 便会交换 SETTINGS frame,从而构建 flow-control window 的大小。默认为 65KB,但是可以通过 WINDOW_UPDATE frame 来改变。
头部压缩的原理是 --- 缓存,与其叫头部压缩还不如叫头部缓存。使用 HPACK 协议,要求客户端和伺服器各自维护一个 HEADER 栏位的列表,在多次发送的时候,只发送差异的部分,其余的从缓存表里面取。
// 用 JavaScript 描述的话大概是下面的意思 const cachedHeaders = { method: GET, host: example.com } const newHeaders = { method: POST } const receivedHeaders = { ...cachedHeaders ...newHeaders }
举个栗子,第一次请求后,第二次请求只发送与之前请求头不同的部分
Messgae 通过流传输,每个流都会被指定一个优先顺序,优先顺序决定处理的顺序和分配的资源,优先顺序的大小会在 HEADER frame 或者是 PRIORITY frame,为 0 - 256 的数字。
优先顺序还可以为树形结构,来标记依赖关系
A 先发送,B / C 同时发送,分别拿到 40% / 60% 的资源,D / E 则拿到 C 的各一半的资源。
优先顺序仅仅是一个参考(only a suggestion),让客户端告诉服务如何处理请求是不对的,伺服器应该根据自己的能力(capabilities)来决定如何处理。
sharding 指的是将服务分散到多个主机上,因为 2.0 以前是通过多个 TCP 连接来达到并发的目的,而浏览器对每个域名可以建立的 TCP 连接是有限制的,所以以前的优化手段是将多个资源分散到多个伺服器。
但是 2.0 协议使用多个 TCP 连接反而会造成性能的下降,因为建立连接很耗费网路资源。同理原来的一些优化方式也都没什么用了比如
这些原来的优化方式反而会损害性能,因为各种合成和拼接都会导致缓存没那么容易。
推荐阅读: