现状

自从 HTTP/1.1 发明以来,网站已经发生了很大的变化:

  • 一个页面请求的消息大小从几 KB到几 MB
  • 每个页面从小于10个资源,到多于100个资源
  • 网站内容从文本为主到富媒体为主
  • 对页面内容实时性高要求的应用越来越多

高延迟

虽然 HTTP/1.1 默认使用了长连接,但是随着带宽的提高以及网站资源数量的提升,延迟并没有显著下降

HTTP/1.1 的缺点慢慢暴露出来:

  • 同一连接同时只能在完成一个 HTTP 事务(请求/响应)才能处理下一个事务
  • 即使使用管道网络,但由于服务器的响应是顺序的,存在队头阻塞的问题
  • 浏览器对一个域名下的并发连接有限(TCP 连接),Chrome 中同域名下资源加载的最大并发连接数为 6
  • 需要重复的传输一些体积巨大的 HTTP 头部,例如用于维护用户状态的 cookie
  • 不支持服务器的消息推送

HTTP/1.1 主要的问题就是并发度不够,在单个连接上串行的请求,没有充分的利用带宽,使得随着带宽的增加,延迟并没有显著下降

性能优化

为了提高网站的性能,在 HTTP/1.1 下所做的一些努力:

  • 雪碧图,将许多张小图片放到一张大图片中,较少请求
  • 将一些文件进行 base64 编码内嵌到 HTML 文件中,减少请求
  • 将体积较小的 JavaScript 文件打包成一个大 JavaScript 文件,减少请求
  • Sharding 分片,将同一页面的资源分散到不同域名下,提升连接上限

HTTP/2

HTTP/2 的前身是 SPDY 协议,该协议由Google开发,最早在Chromium中提出,HTTP/2 的关键功能主要来自 SPDY 技术。

HTTP/ 2 的提出就是为了解决 HTTP/1.1 没有充分利用带宽的问题。

特点

  • HTTP/2 没有改变 HTTP 的基本模型,仅在应用层上修改并充分挖掘 TCP 协议性能
  • 支持多路复用,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流
  • 支持消息优先级
  • 传输的数据量大幅减少,全部采用二进制传输,头部而且会进行压缩
  • 服务器可以推送消息

核心概念

  • Stream:流是连接中的一个虚拟信道,可以承载双向的消息;
  • Message:HTTP/1 中的请求/响应,在 HTTP/2 中是一个逻辑上的概念,对应的就是一系列的帧
  • Frame:帧是 HTTP/2 通信的最小单位,承载着特定类型的数据,如 HEAD 帧、DATA 帧

Frame、Stream、Message三者关系

Frame

HTTP/2 将所有传输的信息分割为更小的,并采用二进制格式对它们编码。

HTTP/1.1 是明文传输,报文的内容就是文本,而 HTTP/2 是一个彻彻底底的二进制协议。

HTTP/2 所有性能增强的核心在于分帧,网络传输的数据就是一个一个的帧,在抓包的层面,抓取到的每一个 HTTP/2 报文都是一个

帧就是 HTTP/2 的报文,这一点需要和 HTTP/1 严格区分。

帧具有多种类型,就比如:头部帧、数据帧,对应的就是HTTP/1 中的头部和消息体。

HTTP/2 具有很多其他的特性,这些特性往往依托于特定类型的帧实现。

Stream

一个流对应的是 HTTP/1 中的一个事务(一个请求和一个响应),一个流中会有很多的,这些帧都属于这次的请求和响应的消息。

在一个 TCP 连接中,会同时存在很多不同的流,流和流之间是乱序到达的,但是属于同一个流的帧之间是有序的。

每个都有一个 StreamID 头部,用于标识这个帧属于哪一个流,在到达之后属于同一个流的帧会被组装成消息。

StreamID 是 HTTP/2 实现多路复用的关键,接收端据此实现并发组装消息

  • 同一 Stream 内的 frame 必须是有序的
  • SETTINGS_MAX_CONCURRENT_STREAMS 控制着并发 Stream 数
  • 由客户端主动发送的流的 ID 为奇数
  • 在服务器推送的情况下,流的 ID 为偶数
  • Stream ID 为 0 的流仅用于传输控制帧

Message

Message 本质上对应的是一个响应 / 请求,在 HTTP/2 中这其实是一个逻辑上的概念。

因为 HTTP/2 传输的数据都是帧,所以本质上一个 Message 是由多个帧(HEAD 帧 + DATA帧)组成。

Message 这个概念在 HTTP/2 中没什么意义,也没有实体存在,只对上层的应用有意义。

Frame 和 Message 的关系

多路复用

所谓多路复用,通常表示在一个信道上传输多路信号或数据流的过程和技术。

HTTP/2 实现多路复用的核心就是二进制分帧和流:

有了二进制分帧之后,HTTP/2 不再依赖 TCP去实现多流并行了,发送端可以将 HTTP 消息分解为互不依赖的帧交错发送,最后接收端依据 StreamID 将它们重新组装起来。

多路复用很好的解决了浏览器限制同一个域名下的请求数量的问题,同时也更容易实现全速传输(因为TCP具有慢启动的特性)。

多路复用的特点

  • 并行交错地发送多个请求,请求之间互不影响
  • 并行交错地发送多个响应,响应之间互不干扰
  • 消除不必要的延迟和提高带宽的利用率,减少页面加载时间
  • 解决了 HTTP/1.1 中存在的队首阻塞问题

帧格式

在 HTTP/2 中,帧本质上就是该层的报文,Type 字段指明了帧的类型,31位的 Stream ID 字段指明了该帧对应的 Stream,Frame Payload 存放该帧附带的数据。

HTTP/2 的很多特性都是通过帧来实现的,所以 HTTP/2 具有多种类型的帧,每个帧都有一个编码类型,对应帧的 Type 字段值。

安全性

IETF 标准不要求 HTTP/2 必须基于 TLS/SSL 协议,但是浏览器要求使用 HTTP/2 必须基于 TLS/SSL 协议。

所以大部分使用 HTTP/2 的网站都是基于 TLS的。

  • h2: 基于 TLS 协议运行的 HTTP/2 被称为 h2

  • h2c: 直接在 TCP 协议之上运行的 HTTP/2 被称为 h2c

协议升级

在不使用 TLS 协议进行协议升级的情况,就像升级 websocket 协议一样。

在 TCP 三次握手之后,客户端发送 Connect: Upgrade 报文,然后服务器返回 101 状态码完成升级。

对于 h2c 协议升级发生在 TLS 握手过程中,在 TLS 层 ALPN 扩展做协商,只认 HTTP/1 的代理服务器不会干扰 HTTP/2。

Magic帧

协议升级完成之后还需要发送 Preface,也就是一个 Magic 帧。

  • 接收到服务器发送来的 101 Switching Protocols 之后
  • TLS 握手成功后

Magic 一般而言都是一个固定的内容,HTTP/2 Preface 内容:0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a

解码之后的内容就是 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

SETTING帧

建立起 HTTP/2 连接之后就会发送 SETTING 帧,其对应的 Stream ID 一定为 0

SETTING 帧并不是“协商”,而是发送方向接收方通知其特性、能力

  • SETTINGS_HEADER_TABLE_SIZE:通知对端索引表的最大尺寸(单位字节,初始 4096 字节)
  • SETTINGS_ENABLE_PUSH:设置为 0 时可禁用服务器推送功能,1 表示启用推送功能
  • SETTINGS_MAX_CONCURRENT_STREAMS:告诉接收端允许的最大并发流数量
  • SETTINGS_INITIAL_WINDOW_SIZE:声明发送端的窗口大小,用于Stream级别流控,初始值21612^16-1字节
  • SETTINGS_MAX_FRAME_SIZE:设置帧的最大大小,初始值 $2^{14} $(16,384)字节
  • SETTINGS_MAX_HEADER_LIST_SIZE:知会对端头部索引表的最大尺寸,单位字节,基于未压缩前的头部

当 Magic 帧发送完毕之后,客户端紧接着会发一些 SETTING 帧:

头部压缩

HTTP/1 使用文本的形式传输 header,每次都可能重复传输几百到几千的字节,尤其在携带 cookie 的情况下

HTTP/2 使用 HPACK 算法来压缩 HTTP 头部,三种压缩方式:

  • 静态字典
  • 动态字典
  • Huffman 压缩算法

静态字典就是预先定义了一些头部,这些头部 + 取值对一个一个数字,发送时直接使用数字就可以。

动态字典是在请求过程中维护的一张表,将存储的 header 字段与索引值相关联,类似于静态表。

编码过程

服务器推送

HTTP/2 的服务器推送是指服务器提前将资源推送至浏览器缓存,所以推送是基于已发送的请求。

例如:浏览器在请求 HTML 页面时,服务器在响应 HTML 文件的同时主动推送页面所需的 CSS 文件。

实现服务器推送的方式:

  • 推送资源必须对应一个请求(未来的请求)
  • 请求信息由服务器端 PUSH_PROMISE 帧发送
  • 推送在偶数 ID 的 STREAM 中发送

PUSH_PROMISE 帧的作用是通知客户端服务器即将推送一个资源,这个资源对应未来的一个请求,请先缓存下来

PUSH_PROMISE 帧中包含该资源对应的请求信息,未来的这个请求请求时直接使用缓存即可

当然了资源会在另一个流中推送过来,并且这个推送资源的流和前一个流可以是并发的,而且 PUSH_PROMISE 帧只有服务器才能发送

HTTP/3

HTTP/2 存在的问题

  1. TCP 以及 TCP+TLS 建立连接握手过多
    1. 基于 TCP 的 h2c 协议需要先进行三次握手
    2. 基于 TLS 的 h2 协议需要进行六次握手
  2. TCP 队头阻塞问题
    1. HTTP/2 使用单个 TCP 连接
    2. 流虽然是并发发送的,一旦前面的一个流在网络中丢包了,TCP 的按序到达就会阻塞后面所有的流

可以看到 HTTP/2 存在的问题都是由于 TCP 所引起的,所以 HTTP/3 所解决的问题就是 TCP 对 HTTP/2 的影响

QUIC

QUIC 由 Google 公开,基于 UDP 实现了可靠的报文传输,提高了目前使用 TCP 的面向连接的网络应用的性能

HTTP/3 指的是运行在 QUIC 协议之上的 HTTP,HTTP/ 3 新特性:

  • TLS3 升级成了最新的 1.3 版本,握手次数减少
  • 允许客户端更换 IP 地址、端口后,仍然可以复用前连接
  • 基于 UDP,不存在队头阻塞问题