跳到主要内容

HTTP2

HTTP2 相比较于 HTTP1.X 大幅度的提升了 web 性能, 在于 HTTP1.1 完全兼容的基础上, 进一步减少了网络延迟, 而对于前端开发人员来说, 无疑减少了在前端方面的优化工作.

具体的文档在这里: HTTP/2: the Future of the Internet

技术方案在这里: RFC 7541

多路复用

众所周知,在 HTTP/1.1 协议中, 浏览器客户端在同一时间. 针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞.

HTTP2 的多路复用则允许通过单一的 HTTP2 链接发起多重的请求-响应的消息.

image

因此 HTTP2 可以很容易的实现多流并行而不用依赖建立多个 TCP 链接, HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧, 这些帧对应逻辑流中的消息, 并行的在同一个 TCP 连接上双向交换消息.

二进制分帧

HTTP2 在兼容 HTTP1.X 的情况下, 在应用层(HTTP/2)和传输层(TCP/UDP)之间增加了一个二进制分帧层.

image

在这个二进制分帧层中, HTTP2 会将所有传输的信息分割为更小的消息和帧(frame), 并对他们采用二进制格式的编码, 其中 HTTP1.x 的首部信息会被封装到HEADER frame, 而相应的 Request Body 则封装到 DATA frame 里面.

HTTP/2 通信都在一个连接上完成, 这个连接可以承载任意数量的双向数据流.

在 1.X 版本中, HTTP 性能优化的关键并不在于高带宽, 而是低延迟. TCP 连接会随着时间进行自我调节, 期初会限制连接的最大速度, 如果数据传输成功, 会随着时间的推移提高传输的速度. 这种调谐被称为 TCP 慢启动.

HTTP2 通过让所有数据流共用一个连接, 可以更有效的使用 TCP 连接, 让高带宽服务于 HTTP 的性能提升.

总之:

  1. 单连接多资源的方式, 减少了服务端的压力, 内存占用更少, 连接吞吐量更大
  2. 由于 TCP 的连接的减少而使用网络拥塞状况得以改善, 同时慢启动时间的减少, 使得拥塞和丢包恢复速度更快.

请求优先级

把HTTP消息分为很多独立帧之后, 就可以通过优化这些帧的交错和传输顺序进一步优化性能. 每一个流都可以带一个31比特的优先值: 0表示最高优先级, 2^31 - 1表示最低优先级.

权重: 服务器可以根据流的优先级, 控制资源分配(CPU, 内存, 带宽), 而在响应数据准备好之后, 优先级将最高优先级的帧发送给客户端. 高优先级的流会优先发送. 但也不是绝对的, 因为会引入首队阻塞的问题: 高优先级的请求导致阻塞其他资源的交付.

分配处理资源和客户端与服务器之间的带宽, 不同优先级的混合也是必须的. 客户端可以指定哪个流是重要的.

流依赖关系: 每个流都可以明确的依赖另一个流, 客户端会使用权重和流依赖关系的组合信息, 向服务端构造和传递"优先级树". 这个树表明了其希望如何接受响应. 即我们期望优先级越高的请求越快得到响应.

浏览器中有一个默认的优先级。浏览器基于自身对资源重要性的判读:

  • 优先级最高: html
  • 优先级高: css
  • 优先级中: js
  • 优先级低: 图片等其他资源

首部压缩

HTTP1.X 不支持首部压缩, 因此 SPDY 和 HTTP2 应运而生, SPDY 使用的是 DEFLATE 算法, HTTP2 则使用了专门的 HPACK 算法. 减少了 header 的大小。并在两端维护了索引表,用于记录出现过的 header ,后面在传输过程中就可以传输已经记录过的 header 的键名,对端收到数据后就可以通过键名找到对应的值。

为什么要压缩

在HTTP/1 中, HTTP请求和响应都是由状态行, 请求/响应头部, 消息主题 三部分组成的. 一般而言, 消息主体都会经过gzip压缩, 或者本身传输的就是压缩过后的二进制文件(比如图片, 视频), 但状态行和头部却没有经过任何压缩, 而直接以纯文本传输

随着web功能越来越复杂, 每个页面产生的请求数量也越来越多. 根据HTTP Archive的统计, 当前平均每个页面都会产生上百个请求. 越来越多的请求导致消耗的头部的流量越来越多.

在HTTP/1时代, 为了减少头部消耗的流量, 有很多优化方案可以尝试, 例如合并请求, 启用Cookie-Free域名等等, 但是这些方案多少有点问题.

技术原理

image

通俗的来说, 头部压缩需要在支持HTTP/2的浏览器和服务端之间:

  • 维护一份相同的静态字典(Static Table), 包含常见的头部名称, 以及特别常见的头部名称和值的组合
  • 维护一份相同的动态字典(Dynamic Table), 可以动态的添加内容
  • 支持基于静态哈夫曼码表的哈夫曼编码(Huffman Coding)

静态字典的作用有两个:

  1. 对于完全匹配的头部键值对, 例如: method: GET, 可以直接使用一个字符表示
  2. 对于头部名称可以匹配的键值对, 例如cookie: xxxx, 可以将名称使用一个字符表示.

HTTP/2中的静态字典大致如下(https://httpwg.org/specs/rfc7541.html#static.table.definition):

IndexHeader NameHeader Value
1:authority
2:methodGET
3:methodPOST
4:path/
5:path/index.html
6:schemehttp
7:schemehttps
8:status200
32cookie
60via
61www-authenticate

同时, 浏览器可以告知服务端, 将cookie: xxx添加到动态字典中, 这样后续整个键值对就可以使用一个字段表示了. 类似的, 服务端也可以更新对方的动态字典. 需要注意的是, 动态字典是上下文有关的, 需要为每个HTTP/2连接维护不同的字典.

使用字典可以极大的提升压缩效果, 其中静态字典在首次请求中就可以使用. 对于静态, 动态字典不存在的内容, 还可以使用哈夫曼编码来减小体积. HTTP/2使用了一份静态哈夫曼码表, 也需要内置在客户端和服务端之中.

此外,HTTP/1 的状态行信息(Method、Path、Status 等),在 HTTP/2 中被拆成键值对放入头部(冒号开头的那些),同样可以享受到字典和哈夫曼压缩。另外,HTTP/2 中所有头部名称必须小写

服务端推送

在 HTTP 2.0 中,服务端可以在客户端某个请求后,主动推送其他资源。

image

可以想象以下情况,某些资源客户端是一定会请求的,这时就可以采取服务端 push 的技术,提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间。当然在浏览器兼容的情况下你也可以使用prefetch

服务端推送还有一个比较大的优势: 可以缓存. 也就是说可以在遵循同源策略的情况下, 不同页面之间共享缓存资源.

注意, 服务端推送有两点:

  1. 推送是遵循同源策略的
  2. 这种服务端的推送是基于客户端的请求响应来决定的.

当服务端需要主动推送某个资源的时候, 就会发送一个Frame TypePUSH_PROMISEFrame, 里面携带了PUSH需要新建的Stream ID. 意思是告诉客户端: 接下来我要用这个ID给你发送信息. 客户端解析Frame的时候, 发现它是一个PUSH_PROMISE类型, 就会接受服务端要推送的流.

性能瓶颈

启用HTTP/2会带来很大的性能提升, 但是也会带来新的性能瓶颈. 因为现在所有的压力都集中在底层一个TCP的连接上, TCP很可能就是下一个性能瓶颈, 比如TCP分组的队首阻塞问题, 单个TCP packet丢失导致的整个连接阻塞, 无法避免, 此时所有消息都会受到印象.

nginx 升级 HTTP2

  1. nginx版本高于1,9,5
  2. 安装了--with-http_ssl_module--with-http_v2_module
  3. 配置了HTTPS(启用HTTP2的前提条件)
  4. 配置listen 443 ssl http2
  5. nginx restart(注意不要直接nginx -s reload)

参考链接