HTTP/3

HTTP/3

既然有 HTTP/3 的诞生,那么就说明在 HTTP/1.1HTTP/2 当中是存在一些问题的,下面我们就先从这个根本上的问题开始看起

本文主要参考的是 HTTP/3 原理与实践,内容有所调整,主要是方便自己理解,更为详细的内容可以参考原文

RTT

RTTRound Trip Time 的缩写,通俗地说就是通信一来一回的时间,下面我们就先来看看在连接过程当中需要哪些 RTT

TCP 建立连接时间

最早大家使用 TCP 来运输 HTTPTCP 需要三次握手,建立了 TCP 虚拟通道,那么这三次握手需要几个 RTT 时间呢?

  • 一去(SYN
  • 二回(SYN + ACK
  • 三去(ACK

相当于一个半来回,故 TCP 连接的时间 = 1.5 RTT

HTTP 通信时间

这意味着,用户在浏览器里输入的网址 URL,直到时间流逝了 1.5 RTT 之后,TCP 才开始运输 HTTP Request,浏览器收到服务器的 HTTP Response,又要等待的时间为

  • 一去(HTTP Request
  • 二回(HTTP Responses

HTTP 的通信时间 = 1 RTT,那么我们可以大致得出,基于 TCP 传输的 HTTP 通信,一共花费的时间总和

1
HTTP 通信时间总和 = TCP 连接时间 + HTTP 通信时间 = 1.5 RTT + 1 RTT = 2.5 RTT

HTTPS

随着互联网的爆发式增长,人类发现完全明文传输的 HTTP 通信很不安全,做为 OSI 七层参考模型的现实实现的 TCP/IP 协议,在设计之初没有考虑安全加密的环节,互联网先驱 Netscape 公司,创造性发明了 SSLSecure Socket Layer),SSL 位于 TCPHTTP 之间,做为 HTTP 的安全供应商,全权负责 HTTP 的安全加密工作,也就像下面这样

1
IP / TCP / SSL / [HTTP]

随着 SSL 的名气攀升,互联网标准化组织 IETFSSL 3.0 版本的基础上,重新设计并命名了这个协议,其全新的名字为 TLSTransport Layer Security),最初的版本为 1.0 版本,从其名字就可以看出,其核心使命就是保证传输层的安全,各个通信部门成员的占位与 SSL 占位一致

1
IP / TCP / TLS / [HTTP]

我们通常将 TLS 安全保护的 HTTP 通信,称之为 HTTPS,以区别于没有 TLS 安全防护的 HTTP 明文通信,那么下面我们就以 1.2 版本为例,看看 HTTPS 通信一共要消耗几个 RTT 时间?

  • 浏览器给服务器发送的 Client Hello 消息(一去)
  • 服务器给浏览器发送的 Server Hello 消息(二回)
  • 浏览器给服务器发送的 Key Exchange 消息(三去)

所以我们可以得出

1
HTTPS 通信时间总和 = TCP 连接时间 + TLS 连接时间 + HTTP 通信时间 = 1.5 RTT + 1.5 RTT + 1 RTT = 4 RTT

如果浏览器与服务器物理距离很近,RTT < 10 ms,即使 4 RTT 最大也不过 40 ms 的时间,用户压根感觉不到慢,但是如果浏览器与服务器相隔上万公里,一个 RTT 时间通常在 200ms 以上,4RTT 时间通常在 1 秒以上,用户会明显感觉到网速慢了

HTTP/1.x

浏览器从服务器获取的一个页面,通常由很多资源链接所组成,服务器给浏览器推送的第一个页面,页面里通常嵌入了图片资源文本链接、以及动态页面资源链接、或第三方网站的链接资源,还需要浏览器根据这些文本链接内容,去链接所对应的服务器,继续下载链接所对应的内容

浏览器通常采用的流程是,重新建立一个 TCP 连接、TLS 连接、HTTP 通信,这又是一个漫长的 4RTT 等待过程,用户看到浏览器完整页面的时间为

1
完整页面加载时间 = 4 RTT * 2 = 8 RTT

HTTP/2

既然第一次页面与第二次页面都是同一个网站服务器,为何第二次页面要重新建立一个 TCP 连接,一个 TLS 连接?如果重用第一个 TCP 连接,那么就少了 1.5 RTT + 1.5 RTT = 3 RTT 的时间

的确,用户的多个 HTTP Request 请求,使用同一个逻辑通道进行运输,这样会大大减少重新建立连接所花费的时间,但是,这样会带来一个副作用,多个 HTTP 流使用同一个 TCP 连接,遵守同一个流量状态控制,只要第一个 HTTP 流遭遇到拥塞,剩下的 HTTP 流压根没法发出去,这就是头部阻塞(Head of line Blocking

至此,我们可以发现问题的根源所在,那就是 TCP 连接,那么有人就会问了,我们不使用 TCP 不就可以了吗?所以这也就引出了我们今天的主角,HTTP/3QUIC

HTTP/3

在介绍 HTTP/3 之前,我们先简单看下 HTTP 的历史,了解下 HTTP/3 出现的背景

随着网络技术的发展,1999 年设计的 HTTP/1.1 已经不能满足需求,所以 Google2009 年设计了基于 TCPSPDY,后来 SPDY 的开发组推动 SPDY 成为正式标准,不过最终没能通过,不过 SPDY 的开发组全程参与了 HTTP/2 的制定过程,参考了 SPDY 的很多设计,所以我们一般认为 SPDY 就是 HTTP/2 的前身

无论 SPDY 还是 HTTP/2,都是基于 TCP 的,TCPUDP 相比效率上存在天然的劣势,所以 2013Google 开发了基于 UDP 的名为 QUIC 的传输层协议,QUIC 全称 Quick UDP Internet Connections,希望它能替代 TCP,使得网页传输更加高效,后经提议,互联网工程任务组正式将基于 QUIC 协议的 HTTPHTTP over QUIC)重命名为 HTTP/3

关于 UDPTCP 的区别可见之前整理过的 TCP 和 UDP 的比较

QUIC 协议

TCP 一直是传输层中举足轻重的协议,而 UDP 则默默无闻,长期以来 UDP 给人的印象就是一个很快但不可靠的传输层协议,正是看中了 UDP 的速度与效率,同时 QUIC 也整合了 TCPTLSHTTP/2 的优点,并加以优化,用一张图可以清晰地表示他们之间的关系

QUICHTTP/3 什么关系呢?QUIC 是用来替代 TCPSSL/TLS 的传输层协议,在传输层之上还有应用层,我们熟知的应用层协议有 HTTPFTPIMAP 等,这些协议理论上都可以运行在 QUIC 之上,其中运行在 QUIC 之上的 HTTP 协议被称为 HTTP/3,这就是 HTTP over QUICHTTP/3 的含义

因此想要了解 HTTP/3QUIC 是绕不过去的,下面我们就通过几个比较重要的特性来简单的了解一下 QUIC 相关内容

零 RTT 建立连接

用一张图可以形象地看出 HTTP/2HTTP/3 建立连接的差别

HTTP/2 的连接需要 3 RTT,如果考虑会话复用,即把第一次握手算出来的对称密钥缓存起来,那么也需要 2 RTT,更进一步的,如果 TLS 升级到 1.3,那么 HTTP/2 连接需要 2 RTT,考虑会话复用则需要 1 RTT,有人会说 HTTP/2 不一定需要 HTTPS,握手过程还可以简化,这没毛病,HTTP/2 的标准的确不需要基于 HTTPS,但实际上所有浏览器的实现都要求 HTTP/2 必须基于 HTTPS,所以 HTTP/2 的加密连接必不可少,而 HTTP/3 首次连接只需要 1 RTT,后面的连接更是只需 0 RTT,意味着客户端发给服务端的第一个包就带有请求数据,这一点在 HTTP/2 当中是难以实现的,那这背后又是什么原理呢?我们可以具体看下 QUIC 的连接过程

  1. 首次连接时,客户端发送 Inchoate Client Hello 给服务端,用于请求连接
  2. 服务端生成 g、p、a,根据 g、pa 算出 A,然后将 g、p、A 放到 Server Config 中再发送 Rejection 消息给客户端
  3. 客户端接收到 g、p、A 后,自己再生成 b,根据 g、p、b 算出 B,根据 A、p、b 算出初始密钥 KBK 算好后,客户端会用 K 加密 HTTP 数据,连同 B 一起发送给服务端
  4. 服务端接收到 B 后,根据 a、p、B 生成与客户端同样的密钥,再用这密钥解密收到的 HTTP 数据,为了进一步的安全(前向安全性),服务端会更新自己的随机数 a 和公钥,再生成新的密钥 S,然后把公钥通过 Server Hello 发送给客户端,连同 Server Hello 消息,还有 HTTP 返回数据
  5. 客户端收到 Server Hello 后,生成与服务端一致的新密钥 S,后面的传输都使用 S 加密

这样,QUIC 从请求连接到正式接发 HTTP 数据一共花了 1 RTT,这 1RTT 主要是为了获取 Server Config,后面的连接如果客户端缓存了 Server Config,那么就可以直接发送 HTTP 数据,实现 0 RTT 建立连接

这里使用的是 DH 密钥交换算法,DH 算法的核心就是服务端生成 a、g、p 三个随机数,a 自己持有,gp 要传输给客户端,而客户端会生成 b1 个随机数,通过 DH 算法客户端和服务端可以算出同样的密钥,在这过程中 ab 并不参与网络传输,安全性大大提高,因为 pg 是大数,所以即使在网络中传输的 p、g、A、B 都被劫持,那么靠现在的计算机算力也没法破解密钥

连接迁移

TCP 连接基于四元组(源 IP、源端口、目的 IP、目的端口),切换网络时至少会有一个因素发生变化,导致连接发生变化,当连接发生变化时,如果还使用原来的 TCP 连接,则会导致连接失败,就得等原来的连接超时后重新建立连接,所以我们有时候发现切换到一个新网络时,即使新网络状况良好,但内容还是需要加载很久,如果实现得好,当检测到网络变化时立刻建立新的 TCP 连接,即使这样,建立新的连接还是需要几百毫秒的时间

QUIC 的连接不受四元组的影响,当这四个元素发生变化时,原连接依然维持,那这是怎么做到的呢?道理很简单,QUIC 连接不以四元组作为标识,而是使用一个 64 位的随机数,这个随机数被称为 Connection ID,即使 IP 或者端口发生变化,只要 Connection ID 没有变化,那么连接依然可以维持

队头阻塞/多路复用

HTTP/1.1HTTP/2 都存在队头阻塞问题(Head of line blocking),那什么是队头阻塞呢?

TCP 是个面向连接的协议,即发送请求后需要收到 ACK 消息,以确认对方已接收到数据,如果每次请求都要在收到上次请求的 ACK 消息后再请求,那么效率无疑很低,后来 HTTP/1.1 提出了 Pipelining 技术,允许一个 TCP 连接同时发送多个请求,这样就大大提升了传输效率

在这个背景下,下面就来谈 HTTP/1.1 的队头阻塞,下图中,一个 TCP 连接同时传输 10 个请求,其中第 1、2、3 个请求已被客户端接收,但第 4 个请求丢失,那么后面第 5 - 10 个请求都被阻塞,需要等第 4 个请求处理完毕才能被处理,这样就浪费了带宽资源

因此 HTTP 一般又允许每个主机建立 6TCP 连接,这样可以更加充分地利用带宽资源,但每个连接中队头阻塞的问题还是存在

HTTP/2 的多路复用解决了上述的队头阻塞问题,不像 HTTP/1.1 中只有上一个请求的所有数据包被传输完毕下一个请求的数据包才可以被传输,HTTP/2 中每个请求都被拆分成多个 Frame 通过一条 TCP 连接同时被传输,这样即使一个请求被阻塞,也不会影响其他的请求,如下图所示,不同颜色代表不同的请求,相同颜色的色块代表请求被切分的 Frame

HTTP/2 虽然可以解决请求这个粒度的阻塞,但 HTTP/2 的基础 TCP 协议本身却也存在着队头阻塞的问题,HTTP/2 的每个请求都会被拆分成多个 Frame,不同请求的 Frame 组合成 StreamStreamTCP 上的逻辑传输单元,这样 HTTP/2 就达到了一条连接同时发送多条请求的目标,这就是多路复用的原理

我们看一个例子,在一条 TCP 连接上同时发送 4Stream,其中 Stream1 已正确送达,Stream2 中的第 3Frame 丢失,TCP 处理数据时有严格的前后顺序,先发送的 Frame 要先被处理,这样就会要求发送方重新发送第 3FrameStream3Stream4 虽然已到达但却不能被处理,那么这时整条连接都被阻塞

不仅如此,由于 HTTP/2 必须使用 HTTPS,而 HTTPS 使用的 TLS 协议也存在队头阻塞问题,TLS 基于 Record 组织数据,将一堆数据放在一起(即一个 Record)加密,加密完后又拆分成多个 TCP 包传输,一般每个 Record16K 左右,包含 12TCP 包,这样如果 12TCP 包中有任何一个包丢失,那么整个 Record 都无法解密

队头阻塞会导致 HTTP/2 在更容易丢包的弱网络环境下比 HTTP/1.1 更慢,那 QUIC 是如何解决队头阻塞问题的呢?主要有两点

  • QUIC 的传输单元是 Packet,加密单元也是 Packet,整个加密、传输、解密都基于 Packet,这样就能避免 TLS 的队头阻塞问题
  • QUIC 基于 UDPUDP 的数据包在接收端没有处理顺序,即使中间丢失一个包,也不会阻塞整条连接,其他的资源会被正常处理

拥塞控制

拥塞控制的目的是避免过多的数据一下子涌入网络,导致网络超出最大负荷,QUIC 的拥塞控制与 TCP 类似,并在此基础上做了改进,所以我们先简单介绍下 TCP 的拥塞控制,关于 TCP 当中的拥塞控制更为详细的内容可以参考 TCP 的流量控制和拥塞控制 这篇博文,这里我们只简单介绍一二

TCP 拥塞控制由 4 个核心算法组成,即 『慢启动』、『拥塞避免』、『快速重传』和『快速恢复』,理解了这 4 个算法,对 TCP 的拥塞控制也就有了大概了解

  • 慢启动,发送方向接收方发送 1 个单位的数据,收到对方确认后会发送 2 个单位的数据,然后依次是 4 个、8 个呈指数级增长,这个过程就是在不断试探网络的拥塞程度,超出阈值则会导致网络拥塞
  • 拥塞避免,指数增』长变为线性增长
  • 快速重传,发送方每一次发送时都会设置一个超时计时器,超时后即认为丢失,需要重发
  • 快速恢复,在上面快速重传的基础上,发送方重新发送数据时,也会启动一个超时定时器,如果收到确认消息则进入拥塞避免阶段,如果仍然超时,则回到慢启动阶段

QUIC 重新实现了 TCP 协议的 Cubic 算法进行拥塞控制,并在此基础上做了不少改进,下面介绍一些 QUIC 改进的拥塞控制的特性

热插拔

TCP 中如果要修改拥塞控制策略,需要在系统层面进行操作,QUIC 修改拥塞控制策略只需要在应用层操作,并且 QUIC 会根据不同的网络环境、用户来动态选择拥塞控制算法

前向纠错 FEC

QUIC 使用前向纠错(FECForward Error Correction)技术增加协议的容错性,一段数据被切分为 10 个包后,依次对每个包进行异或运算,运算结果会作为 FEC 包与数据包一起被传输,如果不幸在传输过程中有一个数据包丢失,那么就可以根据剩余 9 个包以及 FEC 包推算出丢失的那个包的数据,这样就大大增加了协议的容错性

这是符合现阶段网络技术的一种方案,现阶段带宽已经不是网络传输的瓶颈,往返时间才是,所以新的网络传输协议可以适当增加数据冗余,减少重传操作

单调递增的 Packet Number

TCP 为了保证可靠性,使用 Sequence NumberACK 来确认消息是否有序到达,但这样的设计存在缺陷

超时发生后客户端发起重传,后来接收到了 ACK 确认消息,但因为原始请求和重传请求接收到的 ACK 消息一样,所以客户端就郁闷了,不知道这个 ACK 对应的是原始请求还是重传请求,如果客户端认为是原始请求的 ACK,但实际上是左图的情形,则计算的采样 RTT 偏大,如果客户端认为是重传请求的 ACK,但实际上是右图的情形,又会导致采样 RTT 偏小,图中有几个术语,RTO 是指超时重传时间(Retransmission TimeOut),跟我们熟悉的 RTT 长得很像,采样 RTT 会影响 RTO 计算,超时时间的准确把握很重要,长了短了都不合适

QUIC 解决了上面的歧义问题,与 Sequence Number 不同的是,Packet Number 严格单调递增,如果 Packet N 丢失了,那么重传时 Packet 的标识不会是 N,而是比 N 大的数字,比如 N + M,这样发送方接收到确认消息时就能方便地知道 ACK 对应的是原始请求还是重传请求

ACK Delay

TCP 计算 RTT 时没有考虑接收方接收到数据到发送确认消息之间的延迟,如下图所示,这段延迟即 ACK DelayQUIC 考虑了这段延迟,使得 RTT 的计算更加准确

更多的 ACK 块

一般来说,接收方收到发送方的消息后都应该发送一个 ACK 回复,表示收到了数据,但每收到一个数据就返回一个 ACK 回复太麻烦,所以一般不会立即回复,而是接收到多个数据后再回复,TCP SACK 最多提供 3ACK block,但有些场景下,比如下载,只需要服务器返回数据就好,但按照 TCP 的设计,每收到 3 个数据包就要返回一个 ACK,而 QUIC 最多可以捎带 256ACK block,在丢包率比较严重的网络下,更多的 ACK block 可以减少重传量,提升网络效率

流量控制

TCP 会对每个 TCP 连接进行流量控制,流量控制的意思是让发送方不要发送太快,要让接收方来得及接收,不然会导致数据溢出而丢失,TCP 的流量控制主要通过滑动窗口来实现的,可以看出,拥塞控制主要是控制发送方的发送策略,但没有考虑到接收方的接收能力,流量控制是对这部分能力的补齐

QUIC 只需要建立一条连接,在这条连接上同时传输多条 Stream,好比有一条道路,两头分别有一个仓库,道路中有很多车辆运送物资,QUIC 的流量控制有两个级别

  • 连接级别(Connection Level
  • Stream 级别(Stream Level

好比既要控制这条路的总流量,不要一下子很多车辆涌进来,货物来不及处理,也不能一个车辆一下子运送很多货物,这样货物也来不及处理

QUIC 是怎么实现流量控制的呢?我们先看单条 Stream 的流量控制,Stream 还没传输数据时,接收窗口(flow control receive window)就是最大接收窗口(flow control receive window),随着接收方接收到数据后,接收窗口不断缩小,在接收到的数据中,有的数据已被处理,而有的数据还没来得及被处理,如下图所示,蓝色块表示已处理数据,黄色块表示未处理数据,这部分数据的到来,使得 Stream 的接收窗口缩小

随着数据不断被处理,接收方就有能力处理更多数据,当满足下列条件时

1
(flow control receive offset - consumed bytes) < (max receive window / 2)

接收方会发送 WINDOW_UPDATE frame 告诉发送方你可以再多发送些数据过来,这时 flow control receive offset 就会偏移,接收窗口增大,发送方可以发送更多数据到接收方

Stream 级别对防止接收端接收过多数据作用有限,更需要借助 Connection 级别的流量控制,理解了 Stream 流量那么也很好理解 Connection 流控,Stream 中,接收窗口为

1
(flow control receive window) = 最大接收窗口(max receive window) - 已接收数据(highest received byte offset)

而对 Connection 来说

1
接收窗口 = Stream1 接收窗口 + Stream2 接收窗口 + ... + StreamN 接收窗口

参考

# HTTP

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×