HTTP/1.1 的缺点

网站的变化

HTTP/1.1 发明以来:

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

出现的问题

HTTP/1.0 一个请求和响应只能跑在一个 TCP 连接上,即每发起一个请求都要新建一次 TCP 连接,而且请求是串行的,通信开销非常的大。

HTTP/1.1 默认长连接(也叫持久连接),只要客户端或服务端没有提出断开连接,则保持 TCP 连接状态,减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。

Connection 头部用于管理持久连接:

  • HTTP/1.1 默认长连接,当服务器想断开连接时只需要指定 Connection: close
  • HTTP/1.1 之前的版本默认短连接,如果想在旧版本也使用就需要指定 Connection: Keep-alive,服务器会返回 Connection: keep-aliveKeep-alive: xxxx 首部

随着带宽的提高以及网站资源数量的提升,延迟并没有显著下降,HTTP/1.1 的缺点慢慢暴露出来:

  • 同一连接同时只能在完成一个 HTTP 事务(请求/响应)才能处理下一个事务

  • 浏览器对一个域名下的并发连接有限(TCP 连接),例如 Chrome 同域名下资源加载的最大并发连接数为 6

  • 需要重复的传输一些体积巨大的 HTTP 头部,例如用于维护用户状态的 cookie

  • 不支持服务器的消息推送

性能优化

为了提高网站的性能,一些优化措施:

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

可以看到 HTTP/1.1 的主要问题就是并发度不够,一个请求需要等到上一个请求处理完毕才能发送。

HTTP/2

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

特点

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

核心概念

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

Frame、Stream、Message三者关系

Frame

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

HTTP/1.1 协议以换行符作为纯文本的分隔符,而 HTTP/2 将所有传输的信息分割为更小的,并采用二进制格式对它们编码。

HTTP/2 所有性能增强的核心在于分帧

在使用 HTTP/2 协议时,网络传输的数据就是一个一个的帧。在抓包的层面,抓取到的每一个 HTTP/2 报文都是一个

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

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

因为 HTTP/2 有很多其他的特性,所以也有很多其他类型的帧用于实现其他功能。

Stream

一个流可以看作是 HTTP/1 中的一个事务,也就是一个请求和一个响应。一个流中会有很多的,这些帧都是属于这次的请求和响应的消息。

在一次 TCP 连接中,会同时存在很多不同的流,因为 HTTP/2 是支持多路复用的,即允许客户端一直请求,服务端一直响应。所以在到达时流和流之间是乱序到达的。

每个都有一个 StreamID 头部,用于标识这个帧属于哪一个流。

由客户端主动发送的流的 ID 为奇数;在服务器推送的情况下,流的 ID 为偶数。

Message

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

因为 HTTP/2 传输的数据都是帧,所以本质上 Message 是由传输许多的帧组成。

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

Frame 和 Message 的关系

多路复用

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

在 HTTP/2 中,也就是指在一个 TCP 连接中可以同时传输多个 Stream,而一个 Stream 对应的是一个事务。

也就是 HTTP/2 中在一个 TCP 连接中可以并发的发送和接收消息,性能上这是一个极大的升级。

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

HTTP/2 多路复用的主要思路是:

有了二进制分帧之后,HTTP/2 不再依赖 TCP 去实现多流并行了,客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端依据 StreamID 把它们重新组装起来。

多路复用的特点

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

帧的细节

在 HTTP/2 中,帧本质上就是该层的报文,而且二进制分帧是 HTTP/2 性能提升的一个核心。

标准格式

帧的 Type 字段指明了帧的类型,31 位的 Stream ID 字段指明了该帧对应的 Stream。

Frame Payload 存在的就是帧附带的数据。

帧类型

由于 HTTP/2 具有很多的特性,这些特性都是通过帧来实现的,所以就具有很多类型的帧。

每个帧都有一个编码类型,对应帧的 Type 字段值。

帧类型

SETTING 帧

SETTING 帧对应的是建立起 HTTP/2 连接之后需要发送的帧,SETTING 帧并不是“协商”,而是发送方向接收方通知其特性、能力。

SETTING 帧对应的 Stream ID 为 0。

例如:

  • 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:知会对端头部索引表的最大尺寸,单位字节,基于未压缩前的头部

安全性

有些地方说 HTTP/2 默认使用 HTTPS,这其实是不严谨的。

IETF 标准不要求必须基于 TLS/SSL 协议;但是浏览器要求必须基于 TLS/SSL 协议。

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

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

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

协议升级

h2c 协议

在不使用 TLS 协议进行协议升级的情况,比较简单类似于升级 websocket 协议一样。

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

1
2
3
GET / HTTP/1.1
Connection: Upgrade
Upgrade: h2c
1
2
3
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c

h2 协议

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

Magic 帧

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

发送的时机:

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

所谓 Magic 就是一个固定的内容,Preface 内容是一个固定的字符串:

0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a

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

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

头部压缩

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

HTTP/2 使用 HPACK 算法来压缩 HTTP 头部。

压缩方式:

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

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

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

编码过程

服务器推送

HTTP/2 的服务器推送是指服务器提前将资源推送至浏览器缓存。

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

实现服务器推送的方式:

  • 推送资源必须对应一个请求(未来的请求)
  • 需要先相应一个 PUSH_PROMISE 帧
  • 推送在偶数 ID 的 STREAM 中发送

PUSH_PROMISE 帧

PUSH_PROMISE 帧,type=0x5,只能由服务器发送。

PUSH_PROMISE帧格式

PUSH_PROMISE 帧的作用就是通知客户端即将推送一个资源,并告知该资源对应的请求,后面符合的请求直接使用推送过来的缓存就行。而且推送的流和前一个流是可以并发的。

HTTP/3

HTTP/2 的问题

  • TCP 以及 TCP+TLS 建立连接握手过多

  • 队头阻塞

握手过多

基于 TCP 的 h2c 协议需要先进行三次握手,基于 TLS 的 h2 协议需要进行六次握手

队头阻塞

HTTP/2 基于 TCP 且每个请求会复用 TCP,流虽然是并发发送的,但是 TCP 是需要保证资源的按序到达,一旦前面的一个流在网络中丢包了,就会阻塞后面所有的流。

QUIC

QUIC 是由 Google 公开,提高了目前使用 TCP 的面向连接的网络应用的性能,基于 UDP。

QUIC 严格来说运行在 HTTP 协议之下,主要就是基于 UDP 实现了可靠的报文传输。

HTTP/3

HTTP/3 指的是运行在 QUIC 协议之上的 HTTP。

HTTP/ 3 新特性:

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