HTTP/3 原理实战

释放双眼,带上耳机,听听看~!
随着网络技术的发展,1999年设计的HTTP/1.1已经不能满足需求,所以Google在2009年设计了基于TCP的SPDY,后来 SPDY的开发组推动SPDY成为正式标准,不过最终没能通过。不过SPDY的开发组全程参与了HTTP/2的制定过程,参考了 SPDY 的很多设计,所以我们一般认为SPDY就是HTTP/2的前身。无论SPDY还是 HTTP/2,都是基于TCP的,TCP与UDP 相比效率上存在天然的劣势,所以2013年Google开发了基于UDP的名为QUIC的传输层协议,QUIC全称Quick UDP Internet Connections,希望它能替代TCP,使得网页传输更加高效。后经提议,互联网工程任务组正式将基于 QUIC协议的HTTP(HTTP over QUIC)重命名为HTTP/3。

1. HTTP/3原理

1.1 HTTP历史

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

1655997443470.png

随着网络技术的发展,1999年设计的HTTP/1.1已经不能满足需求,所以Google在2009年设计了基于TCP的SPDY,后来 SPDY的开发组推动SPDY成为正式标准,不过最终没能通过。不过SPDY的开发组全程参与了HTTP/2的制定过程,参考了 SPDY 的很多设计,所以我们一般认为SPDY就是HTTP/2的前身。无论SPDY还是 HTTP/2,都是基于TCP的,TCP与UDP 相比效率上存在天然的劣势,所以2013年Google开发了基于UDP的名为QUIC的传输层协议,QUIC全称Quick UDP Internet Connections,希望它能替代TCP,使得网页传输更加高效。后经提议,互联网工程任务组正式将基于 QUIC协议的HTTP(HTTP over QUIC)重命名为HTTP/3。

1.2 QUIC协议概览

TCP一直是传输层中举足轻重的协议,而UDP则默默无闻,在面试中问到TCP和UDP的区别时,有关UDP的回答常常寥寥几语,长期以来UDP给人的印象就是一个很快但不可靠的传输层协议。但有时候从另一个角度看,缺点可能也是优点。QUIC(Quick UDP Internet Connections,快速 UDP网络连接) 基于UDP,正是看中了UDP的速度与效率。同时 QUIC也整合了TCP、TLS和HTTP/2的优点,并加以优化。用一张图可以清晰地表示他们之间的关系。
1655997472029.png

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

因此想要了解HTTP/3,QUIC 是绕不过去的,下面主要通过几个重要的特性让大家对 QUIC 有更深的理解。

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

1655997496919.png

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的连接过程。

Step1:首次连接时,客户端发送Inchoate Client Hello给服务端,用于请求连接;

Step2:服务端生成g、p、a,根据g、p和a算出A,然后将g、p、A放到Server Config中再发送Rejection 消息给客户端;

Step3:客户端接收到g、p、A后,自己再生成b,根据 g、p、b算出B,根据A、p、b算出初始密钥K。B和K算好后,客户端会用K加密HTTP数据,连同B一起发送给服务端;

Step4:服务端接收到B后,根据a、p、B生成与客户端同样的密钥,再用这密钥解密收到的HTTP数据。为了进一步的安全(前向安全性),服务端会更新自己的随机数a和公钥,再生成新的密钥S,然后把公钥通过Server Hello发送给客户端。连同Server Hello消息,还有HTTP返回数据;

Step5:客户端收到Server Hello后,生成与服务端一致的新密钥S,后面的传输都使用S加密。

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

1655997517046.png

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

1.4 连接迁移

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

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

1655997532629.png

1.5 队头阻塞/多路复用

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

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

1655997551390.png

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

1655997567671.png

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

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

1655997584246.png

事情还没完,HTTP/2虽然可以解决“请求”这个粒度的阻塞,但HTTP/2的基础TCP协议本身却也存在着队头阻塞的问题。HTTP/2的每个请求都会被拆分成多个 Frame,不同请求的Frame组合成Stream,Strea是TCP上的逻辑传输单元,这样 HTTP/2 就达到了一条连接同时发送多条请求的目标,这就是多路复用的原理。我们看一个例子,在一条TCP连接上同时发送4个Stream,其中Stream1已正确送达,Stream2中的第3个Frame丢失,TCP处理数据时有严格的前后顺序,先发送的Frame要先被处理,这样就会要求发送方重新发送第3个Frame,Stream3和Stream4虽然已到达但却不能被处理,那么这时整条连接都被阻塞。

1655997605605.png

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

1655997621455.png

队头阻塞会导致HTTP/2在更容易丢包的弱网络环境下比HTTP/1.1更慢!

那QUIC是如何解决队头阻塞问题的呢?主要有两点。

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

1655997637710.png

1.6 拥塞控制

拥塞控制的目的是避免过多的数据一下子涌入网络,导致网络超出最大负荷。QUIC的拥塞控制与TCP类似,并在此基础上做了改进。所以我们先简单介绍下TCP的拥塞控制。

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

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

1655997655725.png

QUIC 重新实现了TCP协议的Cubic算法进行拥塞控制,并在此基础上做了不少改进。

1.6.1 热插拔

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

1655997678556.png

1.6.2 前向纠错 FEC

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

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

1655997693662.png

1.6.3 单调递增的 Packet Number

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

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

1655997711717.png

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

1655997747247.png

1.6.4 ACK Delay

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

1655997760910.png

1.6.5 更多的 ACK 块

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

1655997779685.png

1.7 流量控制

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

QUIC只需要建立一条连接,在这条连接上同时传输多条Stream,好比有一条道路,两头分别有一个仓库,道路中有很多车辆运送物资。QUIC的流量控制有两个级别:连接级别(Connection Level)和Stream级别(Stream Level),好比既要控制这条路的总流量,不要一下子很多车辆涌进来,货物来不及处理,也不能一个车辆一下子运送很多货物,这样货物也来不及处理。

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

1655997794468.png

随着数据不断被处理,接收方就有能力处理更多数据。当满足(flow control receive offset - consumed bytes) < (max receive window / 2)时,接收方会发送WINDOW_UPDATE frame告诉发送方你可以再多发送些数据过来。这时flow control receive offset就会偏移,接收窗口增大,发送方可以发送更多数据到接收方。

1655997812576.png

Stream级别对防止接收端接收过多数据作用有限,更需要借助Connection级别的流量控制。理解了Stream流量那么也很好理解 Connection 流控。Stream中,接收窗口(flow control receive window) = 最大接收窗口(max receive window) - 已接收数据(highest received byte offset) ,而对Connection来说:接收窗口 = Stream1 接收窗口 + Stream2 接收窗口 + ... + StreamN 接收窗口 。

2. 总结

QUIC丢掉了TCP、TLS的包袱,基于UDP,并对TCP、TLS、HTTP/2的经验加以借鉴、改进,实现了一个安全高效可靠的HTTP通信协议。凭借着0 RTT建立连接、平滑的连接迁移、基本消除了队头阻塞、改进的拥塞控制和流量控制等优秀的特性,QUIC在绝大多数场景下获得了比 HTTP/2更好的效果

转载知乎https://zhuanlan.zhihu.com/p/143464334

给TA买糖
共{{data.count}}人
人已赞赏
JumpserverLinux

为Jumpserver 配置企业微信

2022-6-23 21:59:47

CephLinux

Linux客户端远程挂载Ceph RBD并设置开机启动自动挂载

2022-7-16 17:06:06

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索