转自:
https://xiaolincoding.com/network/2_http/http3.html#_3-7-http-3-%E5%BC%BA%E5%8A%BF%E6%9D%A5%E8%A2%AD
美中不足的 HTTP/2
HTTP/2 通过头部压缩、二进制编码、多路复用、服务器推送等新特性大幅提升了 HTTP/1.1 的性能,但美中不足的是 HTTP/2 协议是基于 TCP 实现的,于是存在以下三个缺陷。
- 队头阻塞
- TCP 与 TLS 的握手时延
- 网络迁移需要重新连接
队头阻塞
HTTP/2 的多个请求是跑在一个 TCP 连接中的,当 TCP 丢包时,整个 TCP 都要等待重传,那么就会阻塞该 TCP 连接中的所有请求。
因为 TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,那么即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 的视角来看,就是请求被阻塞了。
上图中发送方发送了很多个 packet,每个 packet 都有自己的序号(可以认为是 TCP 的序列号),其中 packet 3 在网络中丢失了,那么即使 packet 4 ~ 6 被接收方收到,由于内核中的 TCP 数据不是连续的,所以接收方的应用层就无法从内核中读取到,只有等到 packet 3 重传后,接收方的应用层才能从内核中读取到数据,这就是 HTTP/2 的队头阻塞问题,这是发生在 TCP 层面的。
TCP 与 TLS 的握手时延
发起 HTTP 请求时,需要经过 TCP 三次握手和 TLS 四次握手(TLS/1.2)的过程,因此共需要 3 个 RTT 的时延才能发出请求数据。
另外, TCP 由于具有「拥塞控制」的特性,所以刚建立连接的 TCP 会有个「慢启动」的过程,它会对 TCP 连接产生“减速”效果。
网络迁移需要重新连接
一个 TCP 连接是由四元组(源 IP 地址、源端口、目标 IP 地址、目标端口)确定的,这就意味着如果 IP 地址或者端口变动了,就会导致 TCP 与 TLS 需要重新握手。这不利于移动设备切换网络的场景,比如从 4G 网络环境切换成 WIFI。
这些问题都是 TCP 协议的固有问题,无论应用层的 HTTP/2 再怎么设计都无法解决。所以要想解决这个问题,就必须把传输层协议替换成 UDP。
QUIC 协议的特点
UDP 是一个简单的、不可靠的传输协议,且 UDP 包之间是无序的,也没有什么依赖关系,同时 UDP 是不需要连接的,即不需要握手和挥手的过程,所以天然就比 TCP 快。
当然,HTTP/3 不仅仅只是简单地将传输协议替换成了 UDP,还基于 UDP 协议在「应用层」实现了 QUIC 协议,它具有类似 TCP 的连接管理、拥塞窗口、流量控制等网络特性,相当于将不可靠传输的 UDP 协议变成“可靠”的,所以不用担心数据包丢失的问题。
QUIC 协议的优点有很多,这里列举几个。
- 无队头阻塞
- 更快的连接建立
- 连接迁移
无队头阻塞
QUIC 协议也有类似于 HTTP/2 中 Stream 与多路复用的概念,也可以在同一条连接上并发传输多个 Stream,Stream 可以认为就是一条 HTTP 请求。
由于 QUIC 使用的传输协议是 UDP,而 UDP 不关心数据包的顺序,即使发生数据包丢失,UDP 也不关心。
不过 QUIC 协议会保证数据包的可靠性,每个数据包都有一个唯一标识序号。当某个流中的一个数据包丢失了,即使该流的其他数据包到达了,数据也无法被 HTTP/3 读取,直到 QUIC 重传丢失的报文,数据才会交给 HTTP/3。
而其他流的数据报文只要能够被完整接收,HTTP/3 就可以读取到数据。这点与 HTTP/2 不同,HTTP/2 是只要某个流中的数据包丢失了,其他流也会因此受到影响。
所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,即使某个流发生丢包,也只会影响该流,而其他流不受影响。
更快的连接建立
对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、OpenSSL 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手,再 TLS 握手。
HTTP/3 在传输数据前虽然需要 QUIC 协议握手,但这个握手过程只需要 1 RTT,握手的目的是为了确认双方的「连接 ID」,连接迁移就是基于连接 ID 来实现的。
HTTP/3 的 QUIC 协议并没有与 TLS 分层,而是 QUIC 内部就包含了 TLS,它会在自己的帧里携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS/1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,从而达到 0-RTT 的效果。
如下图右边部分,当 HTTP/3 会话恢复时,有效负载数据与第一个数据包一起发送,就可以做到 0-RTT。
连接迁移
前面提到过,基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目标 IP、目标端口)来确定一条 TCP 连接的,那么当移动设备的网络从 4G 切换到 WIFI 时,就意味着 IP 地址发生了变化,那么就必须要断开连接,然后重新建立连接。而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。
而 QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络发生了变化,导致 IP 地址变化,但只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用旧连接,消除重连的成本,没有丝毫卡顿感,从而达到连接迁移的效果。
HTTP/3 协议
了解完 QUIC 协议的特点后,再来看看 HTTP/3 协议在 HTTP 这一层做了什么变化。
HTTP/3 和 HTTP/2 一样,都采用了二进制帧的结构,不同的地方在于 HTTP/2 的二进制帧里需要定义 Stream,而 HTTP/3 自身不需要再定义 Stream,直接使用 QUIC 里的 Stream,于是 HTTP/3 的帧的结构也变的简单了。
如上图所示,HTTP/3 帧头只有两个字段:类型和长度。
根据帧类型的不同,大体上分为数据帧和控制帧两大类,Headers 帧(HTTP 头部)和 DATA 帧(HTTP 包体)属于数据帧。
HTTP/3 在头部压缩算法方面也做了升级,升级成了 QPACK。与 HTTP/2 中的 HPACK 编码方式类似,HTTP/3 中的 QPACK 也采用了静态表、动态表及 Huffman 编码。
对于静态表的变化,HTTP/2 中的 HPACK 的静态表只有 61 项,而 HTTP/3 中的 QPACK 的静态表扩大到了 91 项。
HTTP/2 和 HTTP/3 的 Huffman 编码并没有多大不同,但是动态表编解码方式不同。
所谓的动态表,就是在首次「请求 - 响应」后,双方会将未包含在静态表中的 Header 项更新到各自的动态表,接着在后续传输时仅需用 1 个数字表示,然后对方就可以根据这 1 个数字从动态表里查到对应的数据,就不必每次都传输长长的数据,从而大大提升了编码效率。
可以看到,动态表是具有时序性的,如果首次出现的请求发生了丢包,那么后续收到的请求,对方就无法解码出 HPACK 头部,因为对方还没建立好动态表,因此后续的请求解码会阻塞到首次请求中丢失的数据包重传过来为止。
HTTP/3 的 QPACK 解决了这一问题,那么它是如何解决的呢?
QUIC 会有两个特殊的单向流,所谓的单向流就是指只有一端可以发送消息,双向流则是指两端都可以发送消息,传输 HTTP 消息时用的是双向流,这两个单向流的用法如下。
- 一个叫 QPACK Encoder Stream, 用于将一个字典(key-value)传递给对方,比如面对不属于静态表的 HTTP 请求头部,客户端可以通过这个 Stream 发送字典
- 一个叫 QPACK Decoder Stream,用于响应对方,告诉它刚才发送的字典已经更新到自己的本地动态表了,后续就可以使用这个字典来编码了
这两个特殊的单向流是用来同步双方的动态表的,编码方在收到解码方更新确认的通知后,才会使用动态表编码 HTTP 头部。
总结
HTTP/2 虽然具有并发传输多个流的能力,但是因为传输层还是 TCP 协议,于是存在以下缺陷。
- 队头阻塞,HTTP/2 的多个请求是跑在同一个 TCP 连接中的,如果序列号较低的 TCP 段在网络传输中丢失了,那么即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 的视角来看,就是多个请求被阻塞了
- TCP 和 TLS 握手时延,TCP 三次握手和 TLS 四次握手,共有 3-RTT 的时延
- 连接迁移需要重新连接,由于 TCP 是基于四元组来确定一条 TCP 连接的,当移动设备从 4G 网络环境切换到 WIFI 时,由于网络环境变化了,导致 IP 地址或者端口发生变化,于是 TCP 就只能断开连接,然后再重新建立连接,这样切换网络环境的成本很高
HTTP/3 将传输层从 TCP 替换成了 UDP,并在 UDP 协议的基础上开发了 QUIC 协议,以此来保证数据的可靠传输。
QUIC 协议的特点如下。
- 无队头阻塞,QUIC 连接上的多个 Stream 之间并没有依赖,它们都是独立的,也不会有底层协议的限制,即使某个流发生丢包了,也只会影响该流,而其他流不受影响
- 建立连接速度快,因为 QUIC 内部包含了 TLS/1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,从而达到 0-RTT 的效果
- 连接迁移,QUIC 协议没有用四元组的方式来“绑定”连接,而是通过「连接 ID 」来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络发生了变化,导致 IP 地址变化,但只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用旧连接,消除重连的成本
另外,HTTP/3 的 QPACK 通过两个特殊的单向流来同步双方的动态表,解决了 HTTP/2 的 HPACK 队头阻塞的问题。