目录

从实践出发理解HTTP2

HTTP 协议是整个WEB世界的基石,而我们现在站绝对统治地位的HTTP/1.1从诞生之初到现在已经过去19年了。当时的互联网规模和现在相比无论是从量级还是复杂程度上来说,都是天壤之别。自然HTTP/1.1的许多问题,阻碍了web技术的发展,急需新的方案来解决web发展中出现的问题,HTTP2就是这个背景下诞生的。我们从实践角度来学习一下 HTTTP/2是如何解决当前的一些问题。

HTTP发展历史

  • HTTP/0.9 (1989)
    它只有一个 GET 方法, 没有首部, 设计目标是获取HTML(没有图片, 只有文本)

  • HTTP/1.0 (1996)
    在 HTTP/0.9 版本的基础上新增了大量内容,加入了许多重要概念:

    • 首部
    • 响应码
    • 重定向
    • 错误
    • 条件请求
    • 内容编码(压缩)
    • 更多的请求方法 …
  • HTTP/1.1 (1999)
    在 HTTP/1.0 版本的基础上添加:

    强制要求客户端提供 Host 首部, 这使虚拟主机托管成为可能(在一个IP上提供多个web服务)。 当使用新的连接指令时, Web 服务器不需要在每个响应之后关闭连接。添加的变更如下:

    • 缓存相关首部的扩展
    • OPTIONS 方法
    • Upgrade 首部
    • Range 请求
    • 压缩和传输编码(Transfer-encoding)
    • 管道化(pipelining)
  • SPDY (2009)
    Google 工程师提出了一种替代 HTTP 的方案: SPDY 不是第一个希望替代 HTTP 的方案, 但它是其中最重要的一个, 因为它带来了显而易见的性能提升. 它是 HTTP/2 的基础.

  • HTTP/2 (2015)
    RFC 7540 在 2015 年 5 月 14 日发布了HTTP/2的正式协议. 期望:

    • 相比于使用 TCP 的 HTTP/1.1, 最终用户可感知的多数延迟都有能够量化的显著改善
    • 解决 HTTP 中的队头阻塞问题
    • 并行的实现机制不依赖与服务器建立多个连接, 从而提升 TCP 连接的利用率, 特别是在拥塞控制方面
    • 保留 HTTP/1.1 的语义, 可以利用已有的文档资源, 包括(但不限于) HTTP 方法, 状态码, URI 和首部字段
    • 明确定义 HTTP/2.0 和 HTTP/1.x 交互的方法, 特别是通过中介时的方法(双向)
    • 明确指出它们可以被合理使用的新的扩展点和策略

解决的问题

  • 队头阻塞
    服务端收到多个管道请求后,需要按接收顺序逐个响应。如果恰好第一个请求特别慢,后续所有响应都会跟着被阻
  • TCP利用低效
    h1 并不支持多路复用, 所以浏览器一般会针对指定域名开启6个并发连接。这意味着 多个TCP连接(同域名6个)dns开销;TCP拥塞控制经过多个RTT才能达到理想的吞吐量
  • 消息头冗余
    HTTP/1 协议头部使用纯文本格式,没有任何压缩,且包含很多冗余信息(例如 Cookie、UserAgent 每次都会携带),所以一个页面的请求数越多,头部带来的额外开销就越大(使用短小和多个域名)
  • 优先级设置受限
    浏览器为了先请求优先级高的资源, 会推迟请求其他资源. 但优先级高的资源获取后, 在处理的过程中, 浏览器并不会发起新的资源请求, 所以服务器无法利用这段时间发送优先级低的资源, 总的页面下载时间因此延长了; 一个高优先级资源被浏览器发现后, 但受制于浏览器处理的方式, 它被排在了一个正在获取的低优先级资源之后

HTTP2核心

我们先介绍一下HTTP2的核心概念便于更好的理解h2的特性。 HTTP/2 将 HTTP 协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息。所有这些都在一个 TCP 连接内复用。这是 HTTP/2 协议所有其他功能和性能优化的基础。

  • 数据流stream:已建立的连接内的双向字节流,可以承载一条或多条消息。
  • 消息message:与逻辑请求或响应消息对应的完整的一系列帧。
  • 帧frame:HTTP/2 通信的最小单位,每个帧都包含帧头,至少也会标识出当前帧所属的数据流。 这些概念的关系总结如下:

所有通信都在一个 TCP 连接上完成,此连接可以承载任意数量的双向数据流。 每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息。 每条消息都是一条逻辑 HTTP 消息(例如请求或响应),包含一个或多个帧。 帧是最小的通信单位,承载着特定类型的数据,例如 HTTP 标头、消息负载,等等。 来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

下图是我们用Wireshark抓到的h2数据包, 图中箭头指出了帧的组成部分

h2-frame

用表格的形式说明h2的帧结构 h2-new-frame

HTTP2 重要特性

多路复用

什么是连接,抽象的说是HTTP依赖的可靠的传输层的连接,一般来说指的是一个TCP连接。 HTTP/2 中引入了多路复用的概念,对于同一个域名的多个请求,会复用同一个连接。
不同请求的的帧就是用 Stream Identifier 来区分。
下图展示了h2中的数据交互过程

h2-stream

流量控制

h2使得客户端和服务端都具备调整传输速度的能力,用 WINDOW_UPDATE帧来指示流量控制信息。每个帧告诉对方,发送方想要接受多少字节。当一端接收并处理被发送的数据时,它也会发出一个*WINDOW_UPDATE帧来更新数据接收窗口。

下图展示了流量控制帧调整数据收发窗口

h2-window

优先级设置

将 HTTP 消息分解为很多独立的帧之后,我们就可以复用多个数据流中的帧,客户端和服务器交错发送和传输这些帧的顺序就成为关键的性能决定因素。为了做到这一点,HTTP/2 标准允许每个数据流都有一个关联的权重和依赖关系:

  • 可以向每个数据流分配一个介于 1 至 256 之间的整数。
  • 每个数据流与其他数据流之间可以存在显式依赖关系。

用下面这个示意图来描述优先级设置的原理 h2-priority
HTTP/2 内的数据流依赖关系通过将另一个数据流的唯一标识符作为父项引用进行声明;如果忽略标识符,相应数据流将依赖于“根数据流”。声明数据流依赖关系指出,应尽可能先向父数据流分配资源,然后再向其依赖项分配资源。换句话说,“请先处理和传输响应 D,然后再处理和传输响应 C”。
共享相同父项的数据流(即,同级数据流)应按其权重比例分配资源。 例如,如果数据流 A 的权重为 12,其同级数据流 B 的权重为 4,那么要确定每个数据流应接收的资源比例,请执行以下操作:
将所有权重求和:4 + 12 = 16
将每个数据流权重除以总权重:A = 12/16, B = 4/16因此,数据流 A 应获得四分之三的可用资源,数据流 B 应获得四分之一的可用资源;数据流 B 获得的资源是数据流 A 所获资源的三分之一。
抓包观察PRIORITY

h2-pri-exam

服务端推送

在h1中我们在网页中内联 CSS、JavaScript,或者通过数据 URI 内联过其他资源,就是为了减少请求次数来降低页面响应时间。对于将资源手动内联到文档中的过程,我们实际上是在将资源推送给客户端,而不是等待客户端请求。使用 HTTP/2,我们不仅可以实现相同结果,还会获得其他性能优势。 服务器可以对一个客户端请求发送多个响应。 换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外响应,而无需客户端明确地请求。 所有服务器推送数据流都由 PUSH_PROMISE 帧发起,表明了服务器向客户端推送所述资源的意图,并且需要先于请求推送资源的响应数据传输。这种传输顺序非常重要:客户端需要了解服务器打算推送哪些资源,以免为这些资源创建重复请求。满足此要求的最简单策略是先于父响应(即,DATA 帧)发送所有 PUSH_PROMISE 帧,其中包含所承诺资源的 HTTP header。下图展示了服务端推送的流程
h2-push

抓包观察服务端推送

push-exam

头部压缩

每个 HTTP 传输都承载一组header,这些header说明了传输的资源及其属性。 在 HTTP/1.x 中, headwer始终以纯文本形式,通常会给每个传输增加 500–800 字节的开销。如果使用 HTTP Cookie,增加的开销有时会达到上千字节。为了减少此开销和提升性能,HTTP/2 使用 HPACK 压缩格式压缩请求和响应标头元数据,这种格式采用两种简单但是强大的技术:

  • 通过静态 Huffman 代码对传输的标头字段进行编码,从而减小了各个传输的大小。
  • 客户端和服务器同时维护和更新一个包含之前见过的header的索引列表(换句话说,它可以建立一个共享的压缩上下文),此列表随后会用作参考,对之前传输的值进行有效编码。
    利用 Huffman 编码,可以在传输时对各个值进行压缩,而利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的header键值对。HPACK 压缩上下文包含一个静态表和一个动态表:静态表在规范中定义,并提供了一个包含所有连接都可能使用的常用 HTTP 标头字段(例如,有效标头名称)的列表;动态表最初为空,将根据在特定连接内交换的值进行更新。每个动态表只针对一个连接,每个连接的压缩解压缩的上下文有且仅有一个动态表。 HPACK示意
    hpack

我们抓包观察压缩效果

h2-header

存在的问题

  • 与H1的兼容
    • 域名拆分:
      多个TCP连接浪费资源(cpu和内存) 破坏HTTP2 stream 优先级;头部压缩效果变差
    • 资源内联:
      无法利用缓存;多页面公用的资源内联浪费
    • 资源合并:
      缓存内用率低,消耗更多的CPU和内存
  • TCP队头阻塞
    构建于TCP上层协议的H2多路复用,一旦发生出现TCP队头阻塞,需要小心对待多路的业务数据发送
  • 服务端推送,优先级设置之类的新特性还处于发展阶段,不够完善