一. 前言
代码语言:javascript复制自 2015 年以来,QUIC 协议开始在 IETF 进行标准化并被国内外各大厂商相继落地。
鉴于 QUIC 具备“0RTT 建连”、“支持连接迁移”等诸多优势,即将成为下一代互联网协议。
阅读完本文你将了解和学习到:
- HTTP协议发展史
- HTTP各版本存在的问题,以及各版本解决了哪些问题
- QUIC协议特性
- 再也不怕面试官问HTTP相关的问题了!
行文思路: 从历史使用最广泛的HTTP1.1开始,介绍各版本存在的问题,以及新版本如何解决旧版本存在的问题
二. HTTP协议发展史
- HTTP 0.9(1991年)只支持get方法不支持请求头
- HTTP 1.0(1996年)基本成型,支持请求头、富文本、状态码、缓存、连接无法复用
- HTTP 1.1(1999年)支持连接复用、分块发送、断点续传
- HTTP 2.0(2015年)二进制分帧传输、多路复用、头部压缩、服务器推送等
- HTTP 3.0(2018年)QUIC 于2013年实现;2018年10月,IETF的HTTP工作组和QUIC工作组共同决定将QUIC上的HTTP映射称为 "HTTP/3[1]",以提前使其成为全球标准
HTTP1.1存在的问题
1. 单向请求
只能单向请求,必须由客户端先发起请求,服务器才能发送数据给客户端,不能主动推送数据给客户端。
2. 协议开销大
header里携带的内容过大,且不能压缩,增加了传输的成本。 举个栗子:客户端每次发起请求,都要在请求头里带上 Cache-Control: no-cache。每次请求都重复带上这些字段,其实是带宽浪费的
3. 队头阻塞
下个请求必须在前一个请求返回后才能发出,导致带宽无法被充分利用,后续请求被阻塞(HTTP 1.1 尝试使用流水线(Pipelining)技术,但先天 FIFO(先进先出)机制导致当前请求的执行依赖于上一个请求执行的完成,容易引起队头阻塞,并没有从根本上解决问题) 举个栗子:请求 A 和请求 B。A 先被发起,此时 server 端接收到了 A 请求,正在处理。同时 B 请求也发过来了。但是 A 请求还没被返回,此时 B 请求只能等待。
基于HTTP1.1存在的这些问题,HTTP2在2015年被提出。HTTP2解决了HTTP1.1的哪些问题?又有哪些新的特性?
HTTP2特性以及存在的问题
HTTP2特性
1. 二进制分帧
在 HTTP 2.0 中,它把数据报的两大部分分成了 header frame 和 data frame。也就是头部帧和数据体帧。帧的传输最终在流中进行,流中的帧,头部(header)帧 和 data 帧可以分为多个片段帧,例如data帧即是可以 data = data_1 data_2 ... data_n
举个栗子:请求a.js和b.css,a.js对应的stream的id为1,b.css对应的stream的id为2,a.js的head帧为head1,数据帧为data1,b.js的head帧为head2,数据帧为data2。浏览器可以将head1、data1、head2、data2同时放入TCP信道进行报文传输,在TCP层,可能会进一步对这些数据进行拆分,拆成不同报文序号进行传输,但是可以无需关注这层是如何拆分、组装的。因为可以在HTTP2.0的二进制帧层进行有序处理,将接收到的stream的id为1的放一起处理,接收到的stream的id为2的放一起处理。
2. 多路复用
HTTP 2.0 的多路复用其实是 HTTP 1.1 中长链接的升级版本,在 HTTP 1.1 中,一次链接成功后,只要该链接还没断开,那么 client 端可以在这么一个链接中有序地发起多个请求,并以此获得每个请求对应的响应数据。它的缺点是,一次请求与响应的交互必须要等待前面的请求交互完成,否则后面的只能等待,这个就是HTTP层面的头阻塞。在 HTTP 2.0 中,一次链接成功后,只要链接还没断开,那么 client 端就可以在一个链接中并发地发起多个请求,每个请求及该请求的响应不需要等待其他的请求,某个请求任务耗时严重,不会影响到其它连接的正常执行
3. 流优先级
WEB应用的资源有重要性的区别,优先加载重要资源,可以尽快渲染页面,提升用户体验。HTTP2中,所有资源通过一个连接传输,为了避免队头阻塞,这时候资源传输的顺序就更重要了。
举个栗子:如果一个网页上有一堆图片,还有一个外部样式表。如果浏览器首先下载了所有图片并且最后加载了样式表,在所有内容都加载完毕前,页面将完全是空白页,这谁受得了啊?
4. Header压缩
减少请求中的冗余数据,降低开销,使用的压缩算法为HPACK,这种算法通过服务端和客户端个字维护索引表来实现。
举个例子:客户端和服务器通信,类似于 A 和 B 讲话,A 每天中午都要和 B 说:“走去万达广场吃饭了”,每天都这么说就很累,A 不想每次都说这么多字,就和 B 约定好,以后 A 说 “吃”,就代表 “走去万达广场吃饭了”,从那以后每天中午吃饭 A 就只用跟 B 说一个“吃”字,B 就知道 A 叫他去万达吃饭了。
5. 服务端主动推送
提前给客户端推送必要的资源,这样就可以相对减少一点延迟时间
举个栗子:当客户端请求一个HTML文件,服务器返回这个HTML之前,其实是可以解析出这个HTML引用了哪些JS文件和CSS文件的,那服务器就可以主动推送这些静态资源文件给客户端,而不用等客户端收到HTML之后,解析HTML引用的静态资源,再请求后端,这样就节省了一些时间。
HTTP2存在的问题
1. 建立连接耗时长
建连耗时长,主要指的是TCP的三次握手,还有TLS建连耗时长,这里简单了解就行。在下文QUIC 0RTT 建连的部分会把HTTP2和QUIC进行对比,并深入讲解
2. 队头阻塞
实际上多路复用只是解决了HTTP层面的队头堵塞,TCP层面的队头堵塞依然存在,在下文QUIC解决队头阻塞的部分会把HTTP2和QUIC进行对比,并深入讲解
基于HTTP2存在的这些问题,google另辟蹊径,设计了QUIC协议,并在2018年被正式提议为HTTP3
三. QUIC协议
什么是QUIC?
QUIC(Quick UDP Internet Connection)是谷歌推出的一套基于UDP的传输协议,它实现了TCP HTTPS HTTP/2的功能,目的是保证可靠性的同时降低网络延迟。 从上图可以看出来,QUIC运行在不可靠的 UDP 协议之上。但是,这并不意味着 QUIC 本身也是不可靠的!在某种程度上,QUIC 应该被看作是一个 TCP 2.0。它包括 TCP 的所有特性(可靠性、拥塞控制、流量控制、排序等)的最佳版本,以及更多其他特性。QUIC还完全集成了TLS,不允许未加密的连接。
QUIC协议特性
1. 基于UDP
HTTP2.0及之前的版本,传输层都是使用TCP的,而也正是因为使用了TCP,建立连接时就必须进行TCP的三次握手,导致建连耗时过长。 QUIC是基于UDP的,而UDP本身的特性就是无链接,这样就节省了建连时间
2. 低连接延时
HTTP2建连耗时高的问题
image.png
由上图可以看出,HTTP2在首次建立连接时,需要3次RTT时间,而在非首次建连(已经交换过TLS密钥),并且使用最快的TLS1.3也至少需要1次RTT时间
举个栗子:一次简单的浏览器访问,在地址栏中输入某个网址按下回车,实际会产生以下动作:
- DNS递归查询www.abc.com,获取地址解析的对应IP;
- TCP握手,我们熟悉的TCP三次握手需要1个RTT(也可以算作1.5,因为是1个半往返延时);
- TLS握手,以目前应用最广泛的TLS 1.2而言,需要2个RTT。对于非首次建连,可以选择启用会话重用,则可缩小握手时间到1个RTT。由于本文核心内容是HTTP,不会详细讲解TLS建连的过程,不懂的自行google即可
- HTTP业务数据交互,假设abc.com的数据在一次交互就能取回来。那么业务数据的交互需要1个RTT;经过上面的过程分析可知,要完成一次简短的HTTPS业务数据交互,需要经历:新连接 3RTT DNS;会话重用 1RTT DNS。
所以,对于数据量小的请求而言,单一次的请求握手就占用了大量的时间,对于用户体验的影响非常大。同时,在用户网络不佳的情况下,RTT延时会变得较高,极其影响用户体验。
QUIC的0-RTT建立连接
image.png
QUIC基于UDP,其实本身是不需要建立连接的,建连主要是为了交换TLS密钥。这里建连主要分两个场景:
- 首次建立连接(1RTT),需要交换加密密钥,再发送业务数据
- 非首次建立链接(0RTT),已经交换过加密密钥,直接发送业务数据 而非首次实现1RTT建连的核心是使用了Diffie-Hellman算法进行密钥交换,DH算法是一个密钥协商算法,双方最终协商出一个共同的密钥,而这个密钥不会通过网络传输。 如下图栗子:
Alice和Bob要交换数据,Alice首先选择一个素数p,底数g,随机数小a,然后计算A=g^a mod p,生成大A,然后Alice把底数g,素数p,和计算出来的大A发送给Bob;Bob收到后,也选择一个随机数b,然后计算B=g^b mod p,计算出大B,乙再同时计算K=A^b mod p, Bob把计算出来的大B发给Alice,Alice通过这个公式,就能计算出K,计算结果与Bob算出的结果一样
我们把小a看成Alice的私钥,大A看成Alice的公钥,小b看成Bob的私钥,大B看成Bob的公钥,DH算法的本质就是双方各自生成自己的私钥和公钥,私钥仅对自己可见,然后交换公钥,并根据自己的私钥和对方的公钥,生成最终的密钥secretKey,DH算法通过数学定律保证了双方各自计算出的secretKey是相同的。
可以发现最终的密钥K并没有在网络中传输过,但是双方仅通过交换公钥,就可以计算协商出加密数据用的K
3. 连接迁移
HTTP2基于四元组标识连接
当四元组中的任何一个元素变化,都会导致连接断开,需要重新建立连接 举个例子:当用户从 WIFI 切换到 4G 场景,基于 TCP 的 HTTP 协议无法保持连接的存活,因为四元组里的元素变化了
QUIC基于ConnectionID标识连接
那 QUIC 是如何做到连接迁移呢?很简单,QUIC是基于UDP协议的,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。
4. 可自定义的拥塞控制
传输层协议如 TCP 和 QUIC 包括一种称为拥塞控制(Congestion Control)的机制。比如:慢启动,拥塞避免,快速重传,快速恢复。 拥塞控制器的主要工作是确保网络不会同时被过多的数据过载。如果没有缓冲区的话,数据包就会溢出, 所以,它通常只发送一点数据(通常是 14KB),看看是否能通过。如果数据到达,接收方将确认发送回发送方。只要所有发送的数据都得到确认,发送方就在每次 RTT 时将其发送速率加倍,直到观察到丢包事件(这意味着网络过载(1 位),它需要后退(1 位))。这就是 TCP 连接如何“探测”其可用带宽
HTTP2.0 在TCP底层固化使用了Cubic拥塞控制算法,无法改变,灵活性差
QUIC 的传输控制不再依赖内核的拥塞控制算法,而是实现在应用层上,这意味着我们根据不同的业务场景,实现和配置不同的拥塞控制算法以及参数。GOOGLE 提出的 BBR 拥塞控制算法与 CUBIC 是思路完全不一样的算法,在弱网和一定丢包场景,BBR 比 CUBIC 更不敏感,性能也更好。在 QUIC 下我们可以根据业务随意指定拥塞控制算法和参数,甚至同一个业务的不同连接也可以使用不同的拥塞控制算法。
5. 无队头阻塞
HTTP2存在队头阻塞问题
如上图的例子,HTTP2 在一个 TCP 连接上同时发送 4 个 Stream。其中 Stream1 已经正确到达,并被应用层读取。但是 Stream2 的第三个 tcp segment 丢失了,TCP 为了保证数据的可靠性,需要发送端重传第 3 个 segment 才能通知应用层读取接下去的数据,虽然这个时候 Stream3 和 Stream4 的全部数据已经到达了接收端,但都被阻塞住了,这就是TCP层面的队头阻塞问题
请深刻理解这句话:虽然我们和浏览器都知道我们正在获取 JavaScript 和 CSS 文件,但 HTTP/2 不需要知道这一点。它只知道它在使用来自不同资源流 id (stream id)的块。然而,TCP 甚至不知道它在传输 HTTP!
怎么解决TCP队头阻塞的问题?其实解决方案很简单:我们“只是”需要让传输层知道不同的、独立的流。这样,如果一个流的数据丢失,传输层本身就知道它不需要阻塞其他流。尽管这个解决方案概念简单,但在现实中却很难实现。由于各种原因,改变 TCP 本身使其具有流意识(stream-aware)已经非常困难了
QUIC解决了HTTP2的队头阻塞问题
QUIC 受到 HTTP2 帧方式(framing-approach)的启发,还添加了自己的帧(frames)。流id(stream id)以前在 HTTP2 的数据帧(DATA frame)中,现在被下移到传输层的 QUIC 流帧(STREAM frame)中,同时QUIC 使用的Packet Number 单调递增的设计,可以让数据包不再像TCP 那样必须有序确认,QUIC 支持乱序确认,当数据包Packet N 丢失后,只要有新的已接收数据包确认,当前窗口就会继续向右滑动。待发送端获知数据包Packet N 丢失后,会将需要重传的数据包放到待发送队列,重新编号比如数据包Packet N M 后重新发送给接收端,对重传数据包的处理跟发送新的数据包类似,这样就不会因为丢包重传将当前窗口阻塞在原地,从而解决了队头阻塞问题。那么,既然重传数据包的Packet N M 与丢失数据包的Packet N 编号并不一致,我们怎么确定这两个数据包的内容一样呢?QUIC使用Stream ID 来标识当前数据流属于哪个资源请求,这同时也是数据包多路复用传输到接收端后能正常组装的依据。重传的数据包Packet N M 和丢失的数据包Packet N 单靠Stream ID 的比对一致仍然不能判断两个数据包内容一致,还需要再新增一个字段Stream Offset,标识当前数据包在当前Stream ID 中的字节偏移量。有了Stream Offset 字段信息,属于同一个Stream ID 的数据包也可以乱序传输了(HTTP/2 中仅靠Stream ID 标识,要求同属于一个Stream ID 的数据帧必须有序传输),通过两个数据包的Stream ID 与 Stream Offset 都一致,就说明这两个数据包的内容一致。
总结
现今网络带宽已经大幅提升的情况下,传输的数据大小已经不是主要的性能瓶颈,反而网络延时变得更为重要。基于这个背景,QUIC 不再使用TCP作为传输层协议,而是另辟蹊径采用了UDP,适应了时代网络状况的变化。在某种程度上,QUIC 应该被看作是一个 TCP 2.0。它包括 TCP 的所有特性(可靠性、拥塞控制、流量控制、排序等)的最佳版本,以及更多其他特性。QUIC还完全集成了TLS,不允许未加密的连接。同时拥有 0 RTT 建立连接、平滑的连接迁移、基本消除了队头阻塞、改进的拥塞控制和流量控制等特性,我相信QUIC前途无量。
参考文章
- HTTP/3 From A To Z: Core Concepts
- QUIC官网