web

HTTP/2

改革开放, 走进新时代

Posted by Lorry on November 26, 2018

文章字数:4424, 阅读全文大约需要:12 分钟

http2是很早就想写的内容了, 这是 http/1.1以来跨版本的升级, 解决了以前很多问题, 可以大幅度的提高 http 的响应速度, 充分利用每一个 tcp 连接. 头图就是来自一个非常出名的 http/2的 demo, 感兴趣的可以点进去看一下, 会有更直观的了解.

网络请求最主要的影响因素有

  • 1 带宽
  • 2 延迟

带宽

现在动辄百兆光纤入户, 带宽已经不再是很大的因素, 但是在网络刚起步的时候, 还是拨号上网, 带宽十分的珍贵, 所以那时候的网页不会像现在这样花里胡哨, 朴素很多.

这是2018年的雅虎主页:

这是2000年的雅虎主页:

BTW, 推荐一个查看历史网页快照的网站, 需科学上网: https://web.archive.org, 上述图片均来自此.

延迟

延迟是 http 的天然缺陷, 从出生之初就有, 主要原因有以下几点:

  • 查找 ip 需要 DNS 解析, 耗时.
  • 找到 ip 后, 建立 TCP 的时间长, 需要三次握手以确保可靠性, 俗称慢启动
  • 一条 TCP 连接无法复用, 每次请求都需要重新去建立 TCP
  • 浏览器并发请求的数量有限, 大多数对同域下的请求数不超过6个[1], 所以一旦有请求被阻塞, 就非常耽误加载时间.HOL(head-of-line) blocking–线头阻塞

线头阻塞: 源自维基百科

在 Http/2以前有以下的解决方案:

  • 设置请求头 Connection: Keep-Alive, 可定义是否一定时间内复用连接, 这个时间是由服务器决定, 且在现在移动 App 的使用场景下都是分散请求, 间隔时间不固定, 使用该方法有一定的局限性.
  • 建立基于 tcp 的长连接, 比如socket, 但实现难度比较高
  • long-polling, 极端情况下导致多个连接被挂起, 服务器压力增大
  • streaming, 在 response 中增加Transfer Encoding:chunckedContent-Length: xxx 来告诉客户端后续还有数据. 问题在于有些业务数据不能按照请求进行分割, 或者分割成本高
  • websocket, 和传统的 tcp socket 类似, 提供双向的数据流, 比传统的更简单, 但技术较新, 不是所有浏览器都支持. 具体的分析可以看前文Websockt
  • pipelining 解决请求阻塞.(图片源自维基百科), 请求不需要等待服务器响应, 可以全部发过去, 但是对请求方式限制(GET, HEAD), 且请求间相互不能依赖, 在2018年, 这个功能因为代理服务器和线头阻塞被默认关闭了.

  • SPDY, 支持对请求进行优先级排序和多路复用, 只需求一个 TCP 即可传输多个请求, 广泛使用了 TLS 加密, 传输内容也以 gzip 或 DEFLATE 格式压缩, 还可以主动推送内容.它相当于在 TCP 的 SSL 层上又新增了一层自有的 SPDY层, SPDY 之上为 HTTP层.可以很好的兼容老版本的 HTTP 协议. 在2015年谷歌宣布移除 SPDY 支持, 采用 HTTP/2. 这里只关注多路复用的特性.

重点介绍 HTTP/2.0: SPDY 的升级版

与 SPDY 也有一些区别:

  • 支持 HTTP 传输, SPDY 强制 HTTPS
  • 消息头压缩算法采用 HPACK, SPDY 采用 DEFLATE

HTTP/2 与 HTTP/1.x 的

特性 HTTP/2 HTTP/1.x 优点
解析格式 二进制 文本 更健壮
多路复用 支持 http/1.0一个响应一个请求一个连接, http/1.1支持 pipeling, 但有HOLB 一个连接对应多个 request, 接收方根据 request_id 归属各自服务器
header 压缩 支持 不支持 使用 encoder 减少 header 大小, 通讯双方均缓存一份 header fields 字段表, 避免重复传输, 也减小传输大小
服务端推送 支持 不支持 消息应用, 提前把需要的 js,css跟着 index 请求响应, 之后直接从缓存拿

重点介绍一下对速度影响很大的几个因素:

  1. 多路复用
  2. header 压缩

多路复用可以很好的利用 TCP 连接特性

在连接初期限制最大速度, 数据成功传输以后随着时间推移提高速度, 俗称的 TCP慢启动, 但是 HTTP 是突发性和短时性的, 就像刚从国道开进高速收费站, 正准备上高速, 撒开丫子跑, 然后说mission complete, 不用开了. 如果可以在高速上持续的开上一段时间, 就可以极大的提示响应速度. SPDY和 HTTP/2 就是在里面家里一层二进制层, 就相当于多了一个维度, 不用把车开回去运请求, 而是可以把请求都放进这个正在跑的车里, 车只管开.自然速度提升很快 再次祭出该图片, 直观说明了为什么连接会很快.

给我最大的感受就是再也不用雪碧图了, 可以直接一个一个请求, 因为每次请求不会有额外的请求头消耗.它还有其他的特性:
  • 插入多个平行的请求而不用阻塞其他任何请求
  • 插入多个平行的响应而不用阻塞其他任何请求
  • 使用单个连接传输多个平行的请求或响应
  • 移除不必要的 HTTP/1.X 的优化方法, (pipeling, domain sharding–将资源分配6个 domain, bundle resources–sprite, inline small resources–base64)
  • 通过移除不必要的延迟降低页面载入次数并且提高网络利用率

header 压缩

在每次的传输中都会携带请求头去描述被传输的资源和它的特性. 在 HTTP/1.X 中, 这些元数据总是发送一个 plain text, 并且在每个传输的500-800比特的某个地方插入, 并且又是 会多上 kb 如果 HTTP cookies 被使用的话. HTTP/2 使用 HPACK 压缩格式压缩了请求和响应的请求头元数据, 仅仅使用了两个简单但是非常强大的技术:
  • 将传输头字段通过 Huffman 算法编码, 这将减少这些传输头字段的传输体积
  • 需要客户端和服务器端都维护并且更新一个可索引的列表, 里面存之前传过来的头字段,比如建立一个共享的压缩上下文, 这将被用来引用高效编码之前的传输值
Huffman 算法允许单个值在传输时被压缩, 并且保存了之前传输值的可索引的列表允许通过传输索引值去编码多个值, 因此可以高效的查找和重构整个请求头的 keys 和 values.

举例看下面这张图:

在Stream 3中请求就只剩下一个:path 了, 就请求头的体积来说节省了5倍的空间.

更进一步优化, HPACK 压缩上下文可以由动态的和静态的表组成, 这样请求可以进一步被减小, haffman 只编码没有见过的值, 而其他已有的直接通过 index 索引来替代(在 static 或 dynamic 表中), 要注意这个 static 和 dynamic 表需要在客户端和服务器端同时存在同时更新.
  • 静态的表是标准范围内的定义并且提供一个通用的所有的连接都可能使用到的 HTTP 请求头字段, 比如一个可用的请求头名称列表.
  • 动态的表初始化为空, 并且在只要有特殊字段的交换传输实时更新
服务端推送

我把你要的给你, 我还给你你可能要的,还记得曾经看过的一个鸡汤: 老板让两个销售员去市场上看土豆的价格, 一个小时后, 其中一个销售员A说土豆价格为1.5元每斤, 另一个销售员B拿了好几个土豆回来, 土豆 A 是1.1, 土豆 B 是1.3 土豆 C 是1.5, 土豆 D 是1.7, 这是他们的品质, 如果要的量大的话每一家还分别另有优惠, A 打9折, B 打8.8折, C 打9.3折 D 不打折. 所以B受到了老板的重用.

服务端推送也是这个道理, 比如客户端请求一个 html, 我把这个 html 中包含的 js 和 css 都发给客户端, 客户端就不需要再进行这两者的请求, 直接可以从缓存中获取到了.

HTTP/2打破了严格的request-response 的一一对应关系, 可以实现一对多, 并且服务端推送的工作流打开了一个新的交互世界, 不管是在浏览器里还是浏览器外.

服务端推送的性能优势有:

  • 推送资源可以被客户端缓存
  • 推送的资源可以复用于不同页面
  • 推送的资源可以复用其他的资源
  • 推送的资源可以被 server 排列优先级
  • 推送的资源可以被服务端拒绝

唯一一个严格的安全限制是在浏览器端, 推送的资源必须是遵从同源策略的, 也就是说, 服务器必须为提供内容去获取授权.

PUSH_PROMISE101

所有的服务器推送都是由PUSH_PROMISE 帧去初始化的, 这是服务器希望推送描述的资源给客户端的信号, 并且需要在请求推送的资源被响应数据前传输. 传输的顺序十分的重要, 客户端需要知道服务端想要推送什么资源以避免重新创建一个新的或者重复的请求. 最简单的策略是发送所有的 PUSH_PROMISE 帧, 仅仅包含这些 promised 资源的 HTTP 头, 在所有的父响应之前.(比如 DATA 帧)

一旦客户端接收到 PUSH_PROMISE帧,客户端如果想的话(比如已经储存在 cache 里了)可以拒绝这个流(通过 RST_STREAM 帧), 这是相比于 HTTP/1.x 来说很重要的改进, 相反的, 使用行内资源(inline resources), 对 HTTP/1.x 来说是一种优化, 因为等于说是强制推送, 客户端无法选择拒绝或者对这些行内资源单独进行处理

自从有了HTTP/2, 客户端保留了对服务端如何进行推送的控制权

  • 客户端可以限制同时推送stream 数量
  • 调节初始流控制窗口去控制 steam 刚打开时的推送的数据量大小
  • 完全禁止服务端推送

这些偏好都可以通过在 HTTP/2连接开始时的 SETTINGS 帧来进行沟通, 并且也可以在任何时候去更新.

引用

[1] Ilya Grigorik , High Performance Browser Networking(2013), 
    chapter11 HTTP/1.X, https://hpbn.co/http1x/#using-multiple-tcp-connections