协议--QUIC理解

参考文献及连接

  • [1] 网络编程懒人入门(十):一泡尿的时间,快速读懂QUIC协议
  • [2] QUIC协议–对QUIC的特性讲解非常此详细
  • [3] QUIC 简明教程
  • [4] QUIC协议规范
  • [5] QUIC协议是如何做到0RTT加密传输的(addons)
  • [6] QUIC协议和HTTP3.0技术研究-对QUIC的特征介绍的非常详细清楚
  • [7] 关于队头阻塞,看这一篇就足够了–详细讲解了HTTP/2中的流以及多路复用
  • [8] HTTP/2 协议规范
  • [9] HTTP/2笔记之流和多路复用–对HTTP/2中重要点的讲解也非常清楚
  • [10] HTTP发展史
  • [11] 谈谈QUIC协议原理|网络硬核系列
  • [12] QUIC中的流量控制
  • [13] 让互联网更快的“快”—QUIC协议原理分析–非常好的一篇文章,详细解释了QUIC协议的一些特点
  • [14] QUIC传输格式规范–必读

1. QUIC介绍

1.1 QUIC是什么

QUIC :Quick UDP Internet Connections;是一种新的默认加密的互联网通信协议,它提供了许多改进,旨在加速HTTP通信,同时使其变得更加安全,其最终目的是在web上代替TCP和TLS协议。QUIC 协议也是整合了 TCP 协议的可靠性和 UDP 协议的速度和效率。

1.2 QUIC出现的背景

传统的传输层协议是TCP和UDP,但是TCP和UDP存在很多问题,因而促进了QUIC的诞生。

1.2.1 TCP协议的不足

(1) TCP的不足

  • 握手导致的连接成本
  • 队头阻塞
  • 连接不能迁移(手机以及Wifi的场景)
  • 协议历史悠久导致中间设备僵化
  • 依赖于操作系统的实现导致协议本身僵化。

(2) UDP的不足

  • 不稳定
  • 不可靠

(3) 具体缺点的分析

a) 握手导致的连接成本

不管是 HTTP1.0/1.1 还是 HTTPS,HTTP2,都使用了 TCP 进行传输。HTTPS 和 HTTP2 还需要使用 TLS 协议来进行安全传输,因此TCP的缺点HTTP及TLS都有。

两个握手延迟:

  • TCP 三次握手导致的 TCP 连接建立的延迟,TCP的三次握手至少需要1个RTT;
  • TLS 完全握手需要至少 2 个 RTT 才能建立,简化握手需要 1 个 RTT。

对于很多短连接场景,这样的握手延迟影响很大,且无法消除。

b) TCP的可靠成本

在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。

c) 中间设备的僵化

TCP 协议使用得太久,也非常可靠。所以我们很多中间设备,包括防火墙、NAT 网关,整流器等出现了一些约定俗成的动作。

比如有些防火墙只允许通过 80 和 443,不放通其他端口。NAT 网关在转换网络地址时重写传输层的头部,有可能导致双方无法使用新的传输格式。整流器和中间代理有时候出于安全的需要,会删除一些它们不认识的选项字段。

TCP 协议本来是支持端口、选项及特性的增加和修改。但是由于 TCP 协议和知名端口及选项使用的历史太悠久,中间设备已经依赖于这些潜规则,所以对这些内容的修改很容易遭到中间环节的干扰而失败。而这些干扰,也导致很多在 TCP 协议上的优化变得小心谨慎,步履维艰。

d) 依赖于操作系统的实现导致协议僵化

TCP 是由操作系统在内核系统层面实现的,应用程序只能使用,不能直接修改。虽然应用程序的更新迭代非常快速和简单。但是 TCP 的迭代却非常缓慢,原因就是操作系统升级很麻烦。

服务端系统不依赖用户升级,但是由于操作系统升级涉及到底层软件和运行库的更新,所以也比较保守和缓慢。这也就意味着即使 TCP 有比较好的特性更新,也很难快速推广。比如 TCP Fast Open。它虽然 2013 年就被提出了,但是 Windows 很多系统版本依然不支持它。

e) 队头阻塞

队头阻塞主要是 TCP 协议的可靠性机制引入的。TCP 使用序列号来标识数据的顺序,数据必须按照顺序处理,如果前面的数据丢失,后面的数据就算到达了也不会通知应用层来处理。

另外 TLS 协议层面也有一个队头阻塞,因为 TLS 协议都是按照 record 来处理数据的,如果一个 record 中丢失了数据,也会导致整个 record 无法正确处理。

概括来讲,TCP 和 TLS1.2 之前的协议存在着结构性的问题,如果继续在现有的 TCP、TLS 协议之上实现一个全新的应用层协议,依赖于操作系统、中间设备还有用户的支持。部署成本非常高,阻力非常大。

由于TCP存在以上的缺陷,促进了QUIC的诞生。QUIC是基于 UDP实现的一种协议,因为 UDP 本身没有连接的概念,不需要三次握手,优化了连接建立的握手延迟。同时QUIC在应用程序层面实现了 TCP 的可靠性,TLS 的安全性和 HTTP2 的并发性,只需要用户端和服务端的应用程序支持 QUIC 协议,完全避开了操作系统和中间设备的限制。

1.2.2 基于UDP实现的QUIC协议

QUIC协议是基于UDP协议实现。和与TCP 相反,UDP 协议是无连接协议。客户端发出 UDP 数据包后,只能“假设”这个数据包已经被服务端接收。这样的好处是在网络传输层无需对数据包进行确认,但存在的问题就是为了确保数据传输的可靠性,应用层协议需要自己完成包传输情况的确认。

QUIC 非常类似于在 UDP 上实现的 TCP + TLS + HTTP/2。由于 TCP 是在操作系统内核和中间件固件中实现的,因此对 TCP 进行重大更改几乎是不可能的(TCP 协议栈通常由操作系统实现)。但是,由于 QUIC 建立在 UDP 之上,因此没有这种限制。QUIC 可以实现可靠传输,而且相比于 TCP,它的流控功能在用户空间而不在内核空间,那么使用者就不受限于 CUBIC 或是 BBR,而是可以自由选择,甚至根据应用场景自由调整优化。

QUIC 与现有 TCP + TLS + HTTP/2 方案相比,有以下几点主要特征:
1)利用缓存,显著减少连接建立时间;
2)改善拥塞控制,拥塞控制从内核空间到用户空间;
3)没有队头阻塞的多路复用;
4)前向纠错,减少重传;
5)连接平滑迁移,网络状态的变更不会影响连接断线。
协议--QUIC理解_第1张图片
从图上可以看出,QUIC 底层通过 UDP 协议替代了 TCP,上层只需要一层用于和远程服务器交互的 HTTP/2 API。这是因为 QUIC 协议已经包含了多路复用和连接管理,HTTP API 只需要完成 HTTP 协议的解析即可。

1.2.3 QUIC的基本特征

  • 低延迟的连接建立
  • 更加灵活的拥塞控制
  • 没有队头阻塞的多路复用
  • 流和连接的流量控制
  • 前项纠错
  • 连接迁移
  • 身份认证和加密的头部和负载

1.3 QUIC重要特征详解

1.3.1 低延迟的连接建立

  • QUIC协议是如何做到0RTT加密传输的(addons)
  • QUIC协议和HTTP3.0技术研究-对QUIC的特征介绍的非常详细清楚

有在HTTPS协议中,由于TCP+TLS 需要4~5个RTT,导致连接建立过程较为复杂和耗时,降低了HTTPS的效率。QUIC在握手过程中使用Diffie-Hellman算法协商初始密钥,初始密钥依赖于服务器存储的一组配置参数,该参数会周期性的更新。初始密钥协商成功后,服务器会提供一个临时随机数,双方根据这个数再生成会话密钥。
具体握手过程如下:
(1) 客户端判断本地是否已有服务器的全部配置参数,如果有则直接跳转到(5),否则继续
(2) 客户端向服务器发送inchoate client hello(CHLO)消息,请求服务器传输配置参数
(3) 服务器收到CHLO,回复rejection(REJ)消息,其中包含服务器的部分配置参数
(4) 客户端收到REJ,提取并存储服务器配置参数,跳回到(1)
(5) 客户端向服务器发送full client hello消息,开始正式握手,消息中包括客户端选择的公开数。此时客户端根据获取的服务器配置参数和自己选择的公开数,可以计算出初始密钥。
(6) 服务器收到full client hello,如果不同意连接就回复REJ,同(3);如果同意连接,根据客户端的公开数计算出初始密钥,回复server hello(SHLO)消息,SHLO用初始密钥加密,并且其中包含服务器选择的一个临时公开数。
(7) 客户端收到服务器的回复,如果是REJ则情况同(4);如果是SHLO,则尝试用初始密钥解密,提取出临时公开数
(8) 客户端和服务器根据临时公开数和初始密钥,各自基于SHA-256算法推导出会话密钥
(9) 双方更换为使用会话密钥通信,初始密钥此时已无用,QUIC握手过程完毕。之后会话密钥更新的流程与以上过程类似,只是数据包中的某些字段略有不同。
协议--QUIC理解_第2张图片
如上所述, DH密钥协商需要通行双方各自生成自己的非对称公私钥对。server端与客户端的关系是1对N的关系,明显server端生成一份公私钥对, 让N个客户端公用, 能明显减少生成开销, 降低管理成本。server端的这份公私钥对就是专门用于握手使用的,客户端一经获取,就可以缓存下来后续建连时继续使用, 这个就是达成0-RTT握手的关键, 因此server生成的这份公钥称为0-RTT握手公钥。client端首次握手时对server一无所知,需要1个RTT来询问server端的握手公钥(实际的握手交互还会发送诸如版本等其他数据)并缓存下来,本步骤只在首次建连时发生(0-RTT握手公钥的过期也会导致需要重走这一步),但这种情况很少发生。

另外,前面提到在初始密钥后,还会再协商出一个最终的会话密钥,这么做的目的是为了获取所谓的前向安全特性: 因为server端的后面生成的这份公私钥是临时生成的,不会保存下来,也就杜绝了密钥泄漏导致会话数据被恶意收集后的被解密掉的风险。
协议--QUIC理解_第3张图片
由上图可以看出,在C和S首次通信的1RTT后就发送了应用数据,这是实现1RTT建立链接的真正原因。在C和S进行非首次通信时,C端已经缓存了S端的基本配置,即可以直接进行业务数据的发送。QUIC使用的加密算法是DH算法。

QUIC建立链接的过程
(1) step0: 配置服务器S密钥对
在S生成一个素数 p p p和一个整数 g g g,生成一个随机数 K p r i K_{pri} Kpri。根据上述三元组生成 K p u b K_{pub} Kpub
K p u b = ( g K p r i ) m o d ( p ) K_{pub}=(g^{K_{pri}})mod(p) Kpub=(gKpri)mod(p)
根据生成的 K p u b K_{pub} Kpub打包 { p , g , K p u b } \{p,g,K_{pub}\} {p,g,Kpub}为服务端的config。

(2) step1: C端首次发起链接
C发送简单的Client Hello到S。
(3) step2: S首次响应C
S把config封装称一个数据包回复给C,内部含有 { p , g , K p u b } \{p,g,K_{pub}\} {p,g,Kpub}元组。
(4) step3: C发送加密数据
C收到 { p , g , K p u b } \{p,g,K_{pub}\} {p,g,Kpub}后随机生成一个数 K c _ p r i K_{c\_pri} Kc_pri做如下计算:

  • 计算公钥: K c _ p u b = ( g K c _ p r i ) m o d ( p ) K_{c\_pub} = (g^{K_{c\_pri}})mod(p) Kc_pub=(gKc_pri)mod(p)
  • 计算对称密钥: K 1 = ( K p u b K c _ p r i ) m o d ( p ) K1=(K^{Kc\_pri}_{pub} )mod (p) K1=(KpubKc_pri)mod(p)

准备业务数据payload1,设加密函数为 E n c ( k e y , d a t a ) Enc(key,data) Enc(key,data),将下列元组D1发送给S:
D 1 = { K c _ p u b , E n c ( K 1 , p a y l o a d 1 ) } D_1=\{K_{c\_pub},Enc(K_1,payload1)\} D1={Kc_pub,Enc(K1,payload1)}
该阶段开始,payload便是加密传输的。

(5) step4: S发送加密数据
S收到 D 1 D_1 D1后,做如下计算:

  • 计算对称密钥: K 1 ′ = ( K c _ p u b K p r i ) m o d ( p ) K_{1}^{'}=(K_{c\_pub}^{K_{pri}})mod(p) K1=(Kc_pubKpri)mod(p)

利用计算计算得到的 K 1 ′ K_{1}^{'} K1可以解密 E n c ( K 1 , p a y l o a d 1 ) Enc(K_1,payload1) Enc(K1,payload1)

S在发送自己的payload2之前,随机生成一个数 K n _ p r i K_{n\_pri} Kn_pri,做如下计算:

  • 计算新的通信公钥: K n _ p u b = ( g K n _ p r i ) m o d ( p ) K_{n\_pub}=(g^{K_{n\_pri}})mod(p) Kn_pub=(gKn_pri)mod(p)
  • 计算新的通信对称密钥: K 2 = ( K c _ p u b K n _ p r i ) m o d ( p ) K_{2}=(K_{c\_pub}^{K_{n\_pri}})mod(p) K2=(Kc_pubKn_pri)mod(p)

计算出 K 2 K_2 K2后,可以把 D 2 = { K n _ p u b , E n c ( K 2 , p a y l o a d 2 ) } D_2=\{ K_{n\_pub},Enc(K_2,payload2)\} D2={Kn_pub,Enc(K2,payload2)}发送到C。
(6) step5: C收到S的 D 2 D_2 D2
C收到S发来的 D 2 D_2 D2后,解出其中的 K n _ p u b K_{n\_pub} Kn_pub,做如下运算:

  • 计算新的通信密钥: K 2 ′ = ( K c _ p r i n _ p u b ) m o d ( p ) K_2^{′}=(K_{c\_pri}^{n\_pub} )mod(p) K2=(Kc_prin_pub)mod(p)

K 2 ′ K_2^{′} K2可以正确解密出payload2。其中 K 2 ′ K_2^{′} K2 K 2 K_2 K2等价。
此后的通讯,S和C便可以用K2做通信对称密钥了。
(7) step 6:C和S断开连接
S和C之间通信一会儿后,断开连接。

(8) step 7:C直接发送加密数据
一段时间后,C和S再次通信。此时C已经有了S的config元组 p , g , K p u b {p,g,K_{pub}} p,g,Kpub,直接从Step 3开始数据传输。

(9) step 8:S发送加密数据
这里在S收到C的加密数据后,重复step 4重新计算出一个新的“安全对称密钥”作为之后数据传输的对称密钥。

几个问题:
Q1. 为什么QUIC可以实现0RTT建立连接
0 RTT 的效果是因为QUIC的客户端会缓存服务器端发的令牌和证书,当有数据需要再次发送的时候,客户端可以直接使用旧的令牌和证书,这样子就实现了 0 RTT 了。对于没有缓存的情况,服务器端会直接拒绝请求,并且返回新生产的令牌和证书。 所以当令牌失效或者没有缓存的情况下,QUIC还是需要一次握手才能开始传输数据。

Q2 密钥的生成以及DH加密算法是什么?

Tips:
首先明确一个概念是 RTT(round-trip time),顾名思义,就是服务器和终端一次交互需要的时间。RTT一般用于衡量网络延迟。传统的TCP协议,我们需要进行3次握手,也就是1.5 RTT,才开始传输数据。TCP的三次握手的时间也可以是1RTT,因为客户端发送ACK同时,就可以直接下发业务报文数据。

1.3.2 更灵活的拥塞控制

QUIC协议当前默认使用TCP协议的Cubic拥塞控制算法。看似QUIC协议只是把TCP的拥塞算法重新实现了一遍,其实不然。QUIC协议在TCP拥塞算法基础上做了些改进:

(1) 可插拔

什么叫可插拔呢?就是能够非常灵活地生效,变更和停止。

  • 应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,这在产品快速迭代,网络爆炸式增长的今天,显然有点满足不了需求。
  • 单个应用程序的不同连接也能支持配置不同的拥塞控制。就算是一台服务器,接入的用户网络环境也千差万别,结合大数据及人工智能处理,我们能为各个用户提供不同的但又更加精准更加有效的拥塞控制。比如 BBR 适合,Cubic 适合。
  • 应用程序不需要停机和升级就能实现拥塞控制的变更。我们在服务端只需要修改一下配置,reload 一下,完全不需要停止服务就能实现拥塞控制的切换。

(2) 单调递增的Packet Number

  • QUIC并没有使用TCP的基于字节序号及ACK来确认消息的有序到达,QUIC使用的是Packet Number,每个Packet Number严格递增,所以如果Packet N丢失了,重传Packet N的Packet Number已不是N,而是一个大于N的值。 这样就很容易解决TCP的重传歧义问题。

(3) 更多的ACK块

  • QUIC ACK帧支持256个ACK块,相比TCP的SACK在TCP选项中实现,有长度限制,最多只支持3个ACK块

(4) 精确计算RTT时间

  • QUIC ACK包同时携带了从收到包到回复ACK的延时,这样结合递增的包序号,能够精确的计算RTT。

1.3.3 无队头阻塞的多路复用

基于TCP的HTTP2的一个较大问题是队头阻塞问题。应用程序将TCP连接看作字节流,当TCP数据包丢失时,HTTP2连接上的流不能继续进行,直到数据包被重传并被远端接收——甚至当这些流的数据包已经到达并在缓冲区中等待时也不能。

因为QUIC是专门为多路复用操作而设计的,丢失的数据包携带单个流的数据通常只影响那个特定的流。每个流帧可以在到达时立即分配到该流,因此没有丢失的流可以继续重新组装并在应用程序中继续进行。
协议--QUIC理解_第4张图片
协议--QUIC理解_第5张图片

补充:
(1) TCP的流量控制策略
TCP 保证了数据的有序和可达性,所以原则上是数据按照序号依次发送和接收,下一个包的发送需要等到上一个包 ACK 到达。这样的话,在相邻两个包的发送间隙存在很长时间的空闲等待,好在 TCP 采用了滑动窗口机制来减少了排队等待时间,双方约定一定大小的窗口,在这个窗口内的包都可以同步发送,接收方收到一个 packet 时会回复 ACK 给发送方,发送方收到 ACK 后移动发送窗口,发送后续数据。
但是如果某个 packet 丢失或者其对应的 ACK 包丢失,同样会出现一方不必要的等待。如下图情况,packet 5的 ACK 包丢失,导致发送方无法移动发送窗口,但接收方已经在等待后面的包了。必须等到接收方超时重传这个 ACK 包,接收方超收到这个 ACK 包后,发送窗口才会移动,继续后面的发送行为。
协议--QUIC理解_第6张图片
协议--QUIC理解_第7张图片

1.3.4前向冗余纠错

为了在不等待重传的情况下从丢失的数据包中恢复,QUIC可以用FEC数据包补充一组数据包。很像RAID-4, FEC包包含FEC组中包的奇偶校验。如果组中的一个包丢失,则可以从FEC包和组中的其余包中恢复该包的内容。发送方可以决定是否发送FEC数据包来优化特定场景(例如,请求的开始和结束)。但是这种机制可能会造成传输数据包的冗余,目前已经舍弃

1.3.5 连接迁移

QUIC连接由客户端随机生成的64位连接ID标识。相比之下,TCP连接由源地址、源端口、目的地址和目的端口的4元组标识。这意味着,如果客户端更改了IP地址(例如,从Wi-Fi范围转移到蜂窝网络)或端口(如果NAT映射丢失并重新绑定端口关联),那么任何活动的TCP连接都不再有效。当QUIC客户端更改IP地址时,它可以继续使用来自新IP地址的旧连接ID,而不会中断任何正在运行的请求。

1.3.6 流量控制

首先分两个定义,Stream和Connections:

  • Stream 可以认为就是一条 HTTP 请求。
  • Connection 可以类比一条 TCP 连接。多路复用意味着在一条 Connetion 上会同时存在多条 Stream。既需要对单个 Stream 进行控制,又需要针对所有 Stream 进行总体控制。

(1) 博客一种对QUIC流量控制的解释

  • QUIC中的流量控制

当客户端与服务端进行发送数据的时候,有可能因为发送者发送的速度太快,导致接收者来不及接收,因此会出现分组的丢失,因此为了解决这个问题,解决的根本应该是控制发送者的速度,因此服务端在进行TCP通信时,使用滑动窗口协议。

quic的流量控制是在此基础上的改进,分为了两类,第一类是连接上的流量控制,可以类比成TCP连接。第二类是逻辑流上的流量控制,可以类比为HTTP请求,quic对这两种类型分别进行流量控制。
协议--QUIC理解_第8张图片
如上图,对于quic中的可用窗口也会分为两类:

  • 针对Connection:可用窗口 = 最大窗口数 - 接收到的最大偏移数;

  • 针对流:可用窗口 = stream1可用 + stream2 + … + streamN

(2) 博客二对流量控制的解释

  • 跟坚哥学 QUIC 系列:流量控制(Flow Control)

QUIC 是基于 UDP 传输的,而 UDP 没有流量控制,因此 QUIC 实现了自己的流量控制机制,分为 Stream 和 Connection 两种级别:

1) Stream级别的流控

通过限制 stream 可以发送的数据量,防止单个 stream 消耗连接(connection)的全部缓冲区。与 TCP 不同,就算此前有 packet 没有接收到,它的滑动只取决于接收到的最大偏移字节数(highest received byte offset)。只要还有可用窗口,发送方可以继续发送数据。

  • 在握手时,接收方通过传输参数(transport parameters)设置 stream 的初始限制。
  • 发送方根据这个值进行流量控制,大致过程跟 TCP 相似。
  • 如果发送方达到限制,(可选)则可以发送STREAM_DATA_BLOCKED帧给接收方,以告示它有数据要发送,但被流量控制限制阻止。
  • 接收方如果有更大的窗口值,可以发送MAX_STREAM_DATA帧通知发送方增加。
  • 如果发送方违反流量控制的限制,接收方可以关闭连接并返回FLOW_CONTROL_ERROR错误。

可用窗口 = 最大窗口数 - 接收到的最大偏移数

协议--QUIC理解_第9张图片

2) Connections级别的流量控制
Connections级别的流量控制:对connection 中所有 streams 相加起来的总字节数进行限制,防止发送方超过 connection 的缓冲(buffer)容量。

  • 在握手时,接收方通过传输参数(transport parameters)设置 connection 的初始限制。

  • 发送方根据计算 connection 中所有 streams 的可用窗口,与这个连接窗口值对比进行流量控制。

  • 如果发送方达到限制,(可选)则可以发送STREAM_BLOCKED帧给接收方,以告示它有数据要发送,但被流量控制限制阻止。

  • 接收方如果有更大的窗口值,可以发送MAX_DATA帧通知发送方增加。

  • 如果发送方违反流量控制的限制,接收方可以关闭连接并返回FLOW_CONTROL_ERROR错误。
    下图所示的例子,所有 streams 的最大窗口数为 120,其中:

      stream 1 的最大接收偏移为 100,可用窗口 = 120 - 100 = 20
      stream 2 的最大接收偏移为 90,可用窗口 = 120 - 90 = 30
      stream 3 的最大接收偏移为 110,可用窗口 = 120 - 110 = 10
      那么整个 Connection 的可用窗口 = 20 + 30 + 10 = 60
    

协议--QUIC理解_第10张图片
协议--QUIC理解_第11张图片
可用窗口 = stream 1 可用窗口 + stream 2 可用窗口 +… + stream N 可用窗口

1.4 QUIC协议包类型和格式

QUIC 具有特殊包和普通包。有两种类型特殊包:版本协商包 (Version Negotiation Packets) 和 公共复位包 (Public Reset Packets),普通包包含帧。
所有 QUIC 包的大小应该适配在路径的 MTU 以避免IP分片。路径 MTU 发现是正在进行中的工作,而当前 QUIC 实现为 IPv6 使用 1350 字节的最大QUIC包大小,IPv4 使用1370字节。两个大小都没有 IP 和 UDP 过载。

1.4.1 QUIC公共包头

公共包头的格式如下:

--- src
     0        1        2        3        4            8
+--------+--------+--------+--------+--------+---    ---+
| Public |    Connection ID (64)    ...                 | ->
|Flags(8)|      (optional)                              |
+--------+--------+--------+--------+--------+---    ---+

     9       10       11        12   
+--------+--------+--------+--------+
|      QUIC Version (32)            | ->
|         (optional)                |                           
+--------+--------+--------+--------+


    13       14       15        16      17       18       19       20
+--------+--------+--------+--------+--------+--------+--------+--------+
|                        Diversification Nonce                          | ->
|                              (optional)                               |
+--------+--------+--------+--------+--------+--------+--------+--------+

    21       22       23        24      25       26       27       28
+--------+--------+--------+--------+--------+--------+--------+--------+
|                   Diversification Nonce Continued                     | ->
|                              (optional)                               |
+--------+--------+--------+--------+--------+--------+--------+--------+

    29       30       31        32      33       34       35       36
+--------+--------+--------+--------+--------+--------+--------+--------+
|                   Diversification Nonce Continued                     | ->
|                              (optional)                               |
+--------+--------+--------+--------+--------+--------+--------+--------+

    37       38       39        40      41       42       43       44
+--------+--------+--------+--------+--------+--------+--------+--------+
|                   Diversification Nonce Continued                     | ->
|                              (optional)                               |
+--------+--------+--------+--------+--------+--------+--------+--------+


    45      46       47        48       49       50
+--------+--------+--------+--------+--------+--------+
|           Packet Number (8, 16, 32, or 48)          |
|                  (variable length)                  |
+--------+--------+--------+--------+--------+--------+

---

(1) 公共标记(Public Flags)

  • 0x01 = PUBLIC_FLAG_VERSION。这个标记的含义与包是由服务器还是客户端发送的有关。当由客户端发送时,设置它表示头部包含 QUIC 版本 (参考下面的说明)。客户端必须在所有的包中设置这个位,直到客户端收到来自服务器的确认,同意所提议的版本。服务器通过发送不设置该位的包来表示同意版本。当这个位由服务器设置时,包是版本协商包。版本协商在后面更详细地描述。
  • 0x02 = PUBLIC_FLAG_RESET。设置来表示包是公共复位包。
  • 0x04 = 表明在头部中存在 32字节的多样化随机数。
  • 0x08 = 表明包中存在完整的8字节连接ID。必须为所有包设置该位,直到为给定方向协商出不同的值 (比如,客户端可以请求包含更少字节的连接ID)。
  • 0x30 处的两位表示每个包中存在的数据包编号的低位字节数。这些位只用于帧包。没有包号的公共复位和版本协商包 (由服务器发送) ,不使用这些位,且必须被设置为0。这2位的掩码:
    • 0x30 表示包号占用6个字节。
    • 0x20 表示包号占用4个字节。
    • 0x10 表示包号占用2个字节。
    • 0x00 表示包号占用1个字节。
    • 0x40 为多路径使用保留。
    • 0x80 当前未使用,且必须被设置为0。

(2) 连接ID
这是客户端选择的无符号64位统计随机数,该数字是连接的标识符。由于 QUIC 的连接被设计为,即使客户端漫游,连接依然保持建立状态,因而 IP 4元组(源IP,源端口,目标IP,目标端口)可能不足以标识连接。对每个传输方向,当4元组足以标识连接时,连接ID可以省略。

(3) QUIC版本
表示 QUIC 协议版本的32位不透明标记。只有在公共标记包含 FLAG_VERSION(比如 public_flags & FLAG_VERSION !=0) 时才存在。客户端可以设置这个标记,并 准确 包含一个提议版本,同时包含任意的数据(与该版本一致)。当客户端提议的版本不支持时,服务器可以设置这个标记,并可以提供一个可接受版本的列表(0或多个),但 一定不能(MUST not) 在版本信息之后包含任何数据。最近的实验版本的版本值示例包括 “Q025”,它对应于 byte 9 包含 ‘Q”,byte 10 包含 ‘0”,等等。[参考本文末尾的不同版本变化列表。]

(4) 包号
包号的低 8,16,32,或 48 位,基于公共标记的 FLAG_?BYTE_SEQUENCE_NUMBER 标记被设置为什么。每个普通包(与特别的公共复位和版本协商包相反)由发送者分配包号。由某一端发送的首包包号应该为1,后续每个包的包号应该比前一个的大1。

1.4.2 特殊包

(1) 版本协商包
只有服务器会发送版本协商包。版本协商包以8位的公共标记和64位的连接ID开始。公共标记必须设置PUBLIC_FLAG_VERSION,并指明64位的连接ID。版本协商包的其余部分是服务器支持的版本的4字节列表:

--- src
     0        1        2        3        4        5        6        7       8
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| Public |    Connection ID (64)                                                 | ->
|Flags(8)|                                                                       |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+

     9       10       11        12       13      14       15       16       17
+--------+--------+--------+--------+--------+--------+--------+--------+---...--+
|      1st QUIC version supported   |     2nd QUIC version supported    |   ...
|      by server (32)               |     by server (32)                |             
+--------+--------+--------+--------+--------+--------+--------+--------+---...--+

---

(2) 公共复位包
公共复位包以8位的公共标记和64位的连接ID开始。公共标记必须设置 PUBLIC_FLAG_RESET,并表明64位的连接ID。公共复位包的其余部分像标记 PRST 的加密握手消息那样编码(参考[QUIC-CRYPTO]):


--- src
     0        1        2        3        4         8
+--------+--------+--------+--------+--------+--   --+
| Public |    Connection ID (64)                ...  | ->
|Flags(8)|                                           |
+--------+--------+--------+--------+--------+--   --+

     9       10       11        12       13      14       
+--------+--------+--------+--------+--------+--------+---
|      Quic Tag (32)                |  Tag value map      ... ->
|         (PRST)                    |  (variable length)                         
+--------+--------+--------+--------+--------+--------+---
---

标记值映射(Tag value map):标记值映射包含如下的标记值:

  • RNON (public reset nonce proof) - 一个64位的无符号整数。必须。
  • RSEQ (rejected packet number) - 一个64位的包号。必须。
  • CADR (client address) - 观察到的客户端IP地址和端口号。它当前只被用于调试,因而是可选的。
    (TODO:公共复位包应该包含认证的(目标)服务器 IP/端口。)

1.4.3 普通包

普通包已经过认证和加密。公共头部已认证但未加密,从第一帧开始的包的其余部分已加密。紧随公共头部之后,普通包包含 AEAD(authenticated encryption and associated data)数据。要解释内容,这些数据必须先解密。解密之后,明文由一系列帧组成。

(1) 帧包
帧包具有一个载荷,它是一系列的类型前缀帧。帧类型的格式将在本文档的后面定义,但帧包的通用格式如下:

--- src
+--------+---...---+--------+---...---+
| Type   | Payload | Type   | Payload |
+--------+---...---+--------+---...---+
---

1.5 QUIC帧类型

QUIC帧包由帧填充。它具有一个帧类型字节,它本身具有一个依赖类型的解释,后面是依赖类型的帧首部字段。所有的帧被包含在单独的QUIC包中,且没有帧可以跨越QUIC包边界。QUIC帧类型字节有两种解释,导致两种帧类型:特殊帧类型和普通帧类型。特殊帧类型在帧类型字节中同时编码帧类型和对应的标记,而普通帧类型简单地使用帧类型字节。

特殊帧类型的定义如下:

--- src
   +------------------+-----------------------------+
   | Type-field value |     Control Frame-type      |
   +------------------+-----------------------------+
   |     1fdooossB    |  STREAM                     |
   |     01ntllmmB    |  ACK                        |
   |     001xxxxxB    |  CONGESTION_FEEDBACK        |
   +------------------+-----------------------------+
---

普通帧类型的定义如下:

--- src
   +------------------+-----------------------------+
   | Type-field value |     Control Frame-type      |
   +------------------+-----------------------------+
   | 00000000B (0x00) |  PADDING                    |
   | 00000001B (0x01) |  RST_STREAM                 |
   | 00000010B (0x02) |  CONNECTION_CLOSE           |
   | 00000011B (0x03) |  GOAWAY                     |
   | 00000100B (0x04) |  WINDOW_UPDATE              |
   | 00000101B (0x05) |  BLOCKED                    |
   | 00000110B (0x06) |  STOP_WAITING               |
   | 00000111B (0x07) |  PING                       |
   +------------------+-----------------------------+
---

(1) STREAM 帧
STREAM帧被隐式地创建流和在流上发送数据,他的格式如下:

--- src
     0        1       …               SLEN
+--------+--------+--------+--------+--------+
|Type (8)| Stream ID (8, 16, 24, or 32 bits) |
|        |    (Variable length SLEN bytes)   |
+--------+--------+--------+--------+--------+

  SLEN+1  SLEN+2     …                                         SLEN+OLEN   
+--------+--------+--------+--------+--------+--------+--------+--------+
|   Offset (0, 16, 24, 32, 40, 48, 56, or 64 bits) (variable length)    |
|                    (Variable length: OLEN  bytes)                     |
+--------+--------+--------+--------+--------+--------+--------+--------+

  SLEN+OLEN+1   SLEN+OLEN+2
+-------------+-------------+
| Data length (0 or 16 bits)|
|  Optional(maybe 0 bytes)  |
+------------+--------------+
---

STREAM帧首部中的字段如下:

  • 帧类型:帧类型字节是一个包含多种标记 (1fdooossB) 的8位值:
    • 最左边的位必须被设为 1 以指明这是一个STREAM帧。
    • ‘f’ 位是FIN位。当被设置为 1 时,这个位表明发送者已经完成在流上的发送并希望 “half-close(半关闭)”(稍后将详细描述)。本文档的后面将更详细地描述。
    • ‘d’ 位表明STREAM头部中是否包含数据长度。当设为0时,这个字段表明STREAM帧扩展至包的结尾。
    • 接下来的三个’ooo’位编码Offset头部字段的长度为0,16,24,32,40,48,56,或64位长。
    • 接下来的两个 ‘ss’ 位编码流 ID头部字段的长度为 8,16,24,或32位长。
  • 流 ID:一个大小可变的流唯一的无符号ID。
  • 偏移:一个大小可变的无符号数字指定流中这块数据的字节偏移。
  • 数据长度:一个可选的16位无符号数字指定这个流帧中数据的长度。只有当包是 “全大小(full-sized)” 包时,才应该省略长度,来避免填充破坏的风险。

一个流帧必须总是要么具有非零的数据长度,要么设置了FIN位。

(2) ACK帧

1.6 QUIC的传输参数

2.基础协议

QUIC 非常类似于在 UDP 上实现的 TCP + TLS + HTTP/2,因此对此几种基础的协议进行记录

2.1 TCP协议

2.2 UDP协议

2.3 SSL/TLS

  • 图解SSL/TLS协议

TLS协议旨在为所有正在使用它的应用程序提供三种服务,即:加密,身份验证和完整性。从技术上讲,并非所有三种都可以使用,但在实践中,为了确保安全,通常使用所有三种。

TLS1.3(2018年8月发布)的交互流程如下图所示,需要1RTT,TCP+TLS1.3的时间就是2RTT。
协议--QUIC理解_第12张图片
TLS1.3会话恢复时的流程如下图所示,需要0RTT,此时TCP+TLS1.3的时间是1RTT。对话恢复过程中,在客户端发送ClientHello时就已经可以发送数据了。
协议--QUIC理解_第13张图片

2.4 HTTP/2

  • HTTP/2 协议规范
  • HTTP发展史

HTTP/2的目标是回到单个 TCP 连接,解决队头阻塞问题,具体如何解决的可以产看参考文献[7]

HTTP1.1主要存在的问题是无法进行多路复用,每进行一次传输都需要建立一个TCP连接,这中开销是巨大的。HTTP/2主要是提出了多路复用使用一个TCP连接来传输多个数据流。假设有两个流A和B,其中A比较大,B比较小。当两个流使用一个TCP连接进行传输时,并且先传输A,就可能导致B巨大的延迟。比如传输的方式为AAAAAAAAABB,B必须在A传输完成后才能传输。HTTP2通过为每个包添加帧并采用多路复用的方式进解决了上述问题,使得传输变成了ABABAAAAAA这种形式。但是这只是在应用层的层次解决了队头阻塞问题,TCP本身依旧存在阻塞问题,HTTP/2并没有给予解决。

2.4.1 HTTP发展史

  • HTTP/0.9
  • HTTP/1.0
  • HTTP/1.1
  • HTTP/2

a) HTTP0.9

HTTP 是基于 TCP/IP 协议的应用层协议。它不涉及数据包(packet)传输,主要规定了客户端和服务器之间的通信格式,默认使用 80 端口。
最早版本是 1991 年发布的 0.9 版。该版本极其简单,只有一个命令 GET。

GET /index.html

上面命令表示,TCP 连接(connection)建立后,客户端向服务器请求(request)网页 index.html。协议规定,服务器只能回应 HTML 格式的字符串,不能回应别的格式。


  Hello World

复制代码服务器发送完毕,就关闭 TCP 连接。

b) HTTP/1.0

1996 年 5 月,HTTP/1.0 版本发布,内容大大增加。相对于 HTTP/0.9 大致增加了如下几点:

  • 首先,任何格式的内容都可以发送。这使得互联网不仅可以传输文字,还能传输图像、视频、二进制文件。这为互联网的大发展奠定了基础。
  • 其次,除了 GET 命令,还引入了 POST 命令和 HEAD 命令,丰富了浏览器与服务器的互动手段。
  • 再次,HTTP 请求和回应的格式也变了。除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。
  • 其他的新增功能还包括状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。

当时其实也存在一些别的问题如下:

HTTP/1.0 版的主要缺点是,每个 TCP 连接只能发送一个请求。发送数据完毕,连接就关闭,如果还要请求其他资源,就必须再新建一个连接。TCP 连接的新建成本很高,因为需要客户端和服务器三次握手,并且开始时发送速率较慢(slow start)。

c) HTTP/1.1

相对于 HTTP/1.0 版本 HTTP/1.1 做了一些优化大致如下:

  • 长连接: HTTP 1.1 支持长连接(Persistent Connection)和请求的流水线(Pipelining)处理,在一个 TCP 连接上可以传送多个 HTTP 请求和响应,减少了建立和关闭连接的消耗和延迟,在 HTTP1.1 中默认开启 Connection: keep-alive,一定程度上弥补了 HTTP1.0 每次请求都要创建连接的缺点。

  • 缓存处理:在 HTTP1.0 中主要使用 header 里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。

  • 带宽优化及网络连接的使用,HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了range 头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

  • 错误通知的管理,在 HTTP1.1 中新增了24 个错误状态响应码,如 409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。

  • Host 头处理,在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。HTTP1.1 的请求消息和响应消息都应支持 Host 头域,且请求消息中如果没有 Host 头域会报告一个错误(400 Bad Request)。

但是同时也存在一些问题如下:

  • 虽然 1.1 版允许复用 TCP 连接,但是同一个 TCP 连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。这称为"队头堵塞"(Head-of-line blocking)。
  • HTTP1.x 在传输数据时,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份,这在一定程度上无法保证数据的安全性。
  • HTTP1.x 在使用时,header 里携带的内容过大,在一定程度上增加了传输的成本,并且每次请求 header 基本不怎么变化,尤其在移动端增加用户流量。
  • 虽然 HTTP1.x 支持了 keep-alive,来弥补多次创建连接产生的延迟,但是 keep-alive 使用多了同样会给服务端带来大量的性能压力,并且对于单个文件被不断请求的服务(例如图片存放网站),keep-alive 可能会极大的影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间。

d) SPYD协议

2009 年,谷歌公开了自行研发的 SPDY 协议,主要解决 HTTP/1.1 效率不高的问题。这个协议在 Chrome 浏览器上证明可行以后,就被当作 HTTP/2 的基础,主要特性都在 HTTP/2 之中得到继承。SPDY 可以说是综合了 HTTPS 和 HTTP 两者优点于一体的传输协议,主要解决:

  • 降低延迟,针对 HTTP 高延迟的问题,SPDY 优雅的采取了多路复用(multiplexing)多路复用通过多个请求 stream 共享一个 tcp 连接的方式,解决了队头阻塞的问题,降低了延迟同时提高了带宽的利用率
  • 请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞SPDY 允许给每个 request 设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的 html 内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
  • header 压缩。前面提到 HTTP1.x 的 header 很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
  • 基于 HTTPS 的加密协议传输,大大提高了传输数据的可靠性。
  • 服务端推送(server push),采用了 SPDY 的网页,例如我的网页有一个 sytle.css 的请求,在客户端收到 sytle.css 数据的同时,服务端会将 sytle.js 的文件推送给客户端,当客户端再次尝试获取 sytle.js 时就可以直接从缓存中获取到,不用再发请求了。

SPDY 构成图:
协议--QUIC理解_第14张图片

e) HTTP/2.0

HTTP/2 可以说是 SPDY 的升级版(其实原本也是基于 SPDY 设计的),但是,HTTP2.0 跟 SPDY 仍有不同的地方,主要是以下两点:

  • HTTP2.0 支持明文 HTTP 传输,而 SPDY 强制使用 HTTPS
  • HTTP2.0 消息头的压缩算法采用 HPACK,而非 SPDY 采用的 DEFLATE

HTTP/2 的新特性:

  • 二进制分帧:HTTP/2 的所有帧都采用二进制编码
  • 多路复用 (Multiplexing)
  • 请求优先级
  • header 压缩
  • 服务端推送

2.4.2 HTTP/2中的基本术语

(1) 流 (stream)
流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符(1、2…N)。下图就是把两个流分拆开打包进两个TCP Packet。
协议--QUIC理解_第15张图片
流的组成如下图所示
协议--QUIC理解_第16张图片
(2) 数据帧 (Data Frame)
区别于HTTP/1并不能在一个TCP链接中区分两个数据流,HTTP/2为每个数据流添加了一个流id,为每一个TCP Packet的头部添加了一个数据源帧,该数据帧包含两个重要信息,分别是:1) 下面的块属于哪个资源。每个资源的“字节流(bytestream)”都被分配了一个唯一的数字,即流id(stream id);2) 块的大小是多少。如上图的示意图所示。

(3) 消息
是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。

(4) 帧
客户端与服务器通过交换帧来通信,帧是基于这个新协议通信的最小单位。

(5) 帧、流、消息的关系
每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。 帧是流中的数据单位,一个数据报的 header 帧可以分成多个 header 帧,data 帧可以分成多个 data 帧。其实流的的

(6) 对流和帧的抽象理解

  • 每一个帧可看做是一个学生,流可以认为是组(流标识符为帧的属性值),一个班级(一个连接)内学生被分为若干个小组,每一个小组分配不同的具体任务。
  • HTTP/1.* 一次请求-响应,建立一个连接,用完关闭;每一个小组任务都需要建立一个班级,多个小组任务多个班级,1:1比例
  • HTTP/1.1 Pipeling解决方式为,若干个小组任务排队串行化单线程处理,后面小组任务等待前面小组任务完成才能获得执行机会,一旦有任务处理超时等,后续任务只能被阻塞,毫无办法,也就是人们常说的线头阻塞
  • HTTP/2多个小组任务可同时并行(严格意义上是并发)在班级内执行。一旦某个小组任务耗时严重,但不会影响到其它小组任务正常执行
  • 针对一个班级资源维护要比多个班级资源维护经济多了,这也是多路复用出现的原因

其他blog上对帧和流的解释
(1) 帧(frame)
HTTP/2 中数据传输的最小单位,因此帧不仅要细分表达 HTTP/1.x 中的各个部份,也优化了 HTTP/1.x 表达得不好的地方,同时还增加了 HTTP/1.x 表达不了的方式。
每一帧都包含几个字段,有length、type、flags、stream identifier、frame playload等,其中type 代表帧的类型,在 HTTP/2 的标准中定义了 10 种不同的类型,包括上面所说的 HEADERS frame 和 DATA frame。此外还有:
PRIORITY(设置流的优先级)
RST_STREAM(终止流)
SETTINGS(设置此连接的参数)
PUSH_PROMISE(服务器推送)
PING(测量 RTT)
GOAWAY(终止连接)
WINDOW_UPDATE(流量控制)
CONTINUATION(继续传输头部数据)
在 HTTP 2.0 中,它把数据报的两大部分分成了 header frame 和 data frame。也就是头部帧和数据体帧。

(2) 流(stream)
流: 存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数 ID。
HTTP/2 长连接中的数据包是不按请求-响应顺序发送的,一个完整的请求或响应(称一个数据流 stream,每个数据流都有一个独一无二的编号)可能会分成非连续多次发送。它具有如下几个特点:

  • 双向性:同一个流内,可同时发送和接受数据。
  • 有序性:流中被传输的数据就是二进制帧 。帧在流上的被发送与被接收都是按照顺序进行的。
  • 并行性:流中的 二进制帧 都是被并行传输的,无需按顺序等待。
  • 流的创建:流可以被客户端或服务器单方面建立, 使用或共享。
  • 流的关闭:流也可以被任意一方关闭。
  • HEADERS 帧在 DATA 帧前面。
  • 流的 ID 都是奇数,说明是由客户端发起的,这是标准规定的,那么服务端发起的就是偶数了。

2.4.3 HTTP/2 VS HTTP/1

http/1中的每个请求都会建立一个单独的连接,除了在每次建立连接过程中的三次握手之外,还存在TCP的慢启动导致的传输速度低。其实大部分的http请求传送的数据都很小,就导致每一次请求基本上都没有达到正常的传输速度。

在http1.1中默认开启keep-alive,解决了上面说到的问题,但是http的传输形式是一问一答的形式,一个请求对应一个响应(http2中已经不成立,一个请求可以有多个响应,server push),在keep-alive中,必须等上一个请求接受才能发起下一个请求,所以会收到前面请求的阻塞。

使用pipe-line可以连续发送一组没有相互依赖的请求而不必等到上一个请求先结束,看似pipe-line是个好东西,但是到目前为止我还没见过这种类型的连接,也间接说明这东西比较鸡肋。pipe-line依然没有解决阻塞的问题,因为请求响应的顺序必须和请求发送的顺序一致,如果中间有某个响应花了很长的时间,后面的响应就算已经完成了也要排队等阻塞的请求返回,这就是线头阻塞。

http2的多路复用就很好的解决了上面所提出的问题。http2的传输是基于二进制帧的。每一个TCP连接中承载了多个双向流通的流,每一个流都有一个独一无二的标识和优先级,而流就是由二进制帧组成的。二进制帧的头部信息会标识自己属于哪一个流,所以这些帧是可以交错传输,然后在接收端通过帧头的信息组装成完整的数据。这样就解决了线头阻塞的问题,同时也提高了网络速度的利用率。

HTTP/2的新特征

  • 二进制分帧:HTTP/2 的所有帧都采用二进制编码
  • 多路复用 (Multiplexing)
  • 请求优先级
  • header 压缩
  • 服务端推送

2.4.4 HTTP/2中的多路复用

  • 关于队头阻塞(Head-of-Line blocking),看这一篇就足够了
  • HTTP发展史

多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。即连接共享,即每一个 request 都是是用作连接共享机制的。一个 request 对应一个 id,这样一个连接上可以有多个 request,每个连接的 request 可以随机的混杂在一起,接收方可以根据 request 的 id 将 request 再归属到各自不同的服务端请求里面。

多路复用原理图:
协议--QUIC理解_第17张图片

2.4.5 请求优先级

  • 把 HTTP 消息分解为很多独立的帧之后,就可以通过优化这些帧的交错和传输顺序,每个流都可以带有一个 31 比特的优先值:0 表示最高优先级;2 的 31 次方-1 表示最低优先级。
  • 服务器可以根据流的优先级,控制资源分配(CPU、内存、带宽),而在响应数据准备好之后,优先将最高优先级的帧发送给客户端。
  • HTTP 2.0 一举解决了所有这些低效的问题:浏览器可以在发现资源时立即分派请求,指定每个流的优先级,让服务器决定最优的响应次序。这样请求就不必排队了,既节省了时间,也最大限度地利用了每个连接。

2.4.6 header 压缩

HTTP1.x 的 header 带有大量信息,而且每次都要重复发送,HTTP/2 使用 encoder 来减少需要传输的 header 大小,通讯双方各自cache 一份 header fields 表,既避免了重复 header 的传输,又减小了需要传输的大小。 为了减少这块的资源消耗并提升性能, HTTP/2 对这些首部采取了压缩策略:

  • HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,不再重复发送 header
  • 首部表在 HTTP/2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
  • 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。

两次请求不相同的 header,传说的 header 如下图所示:
协议--QUIC理解_第18张图片

2.4.7 服务端推送

Server Push 即服务端能通过 push 的方式将客户端需要的内容预先推送过去,也叫“cache push”。
服务器可以对一个客户端请求发送多个响应。服务器向客户端推送资源无需客户端明确地请求,服务端可以提前给客户端推送必要的资源,这样可以减少请求延迟时间,例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不是等到 HTML 解析到资源时发送请求,大致过程如下图所示:
协议--QUIC理解_第19张图片

2.4.8HTTP/2总结

相对于HTTP/1.1,HTTP/2主要基于流和数据帧的概念提出了多路复用机制。这有效解决了HTTP/1中N个请求对应N个TCP链接的缺陷,进而极大的节省了建立TCP连接的开销。

2.5 HTTP/3

  • HTTP/3协议规范

2.6 QUIC

  • QUIC协议规范

3. QUIC协议规范

  • QUIC协议规范

3.1 常用术语

贯穿本文档使用的一些术语定义如下。

  • “客户端”:初始化 QUIC 连接的端点。
  • “服务器”:接受进入的 QUIC 连接的端点。
  • “端点”:连接的客户端或服务器端。
  • “流”:QUIC 连接中穿过一个逻辑通道的双向字节流。
  • “连接”:两个 QUIC 端点之间的会话,它具有一个单独的加密上下文且包含多路复用流。
  • “连接ID”:QUIC 连接的标识符。
  • “QUIC包”:经过良好格式化的 UDP 载荷,可由 QUIC 接收者解析。本文档中的 QUIC 包大小指 UDP 载荷大小。

3.2 QUIC概述

4. HTTP/3协议规范

HTTP2协议虽然大幅提升了HTTP/1.1的性能,然而,基于TCP实现的HTTP2遗留下3个问题:

  • 有序字节流引出的 队头阻塞(Head-of-line blocking),使得HTTP2的多路复用能力大打折扣;
  • TCP与TLS叠加了握手时延,建链时长还有1倍的下降空间;
  • 基于TCP四元组确定一个连接,这种诞生于有线网络的设计,并不适合移动状态下的无线网络,这意味着IP地址的频繁变动会导致TCP连接、TLS会话反复握手,成本高昂。

HTTP3协议解决了这些问题:

  • HTTP3基于UDP协议重新定义了连接,在QUIC层实现了无序、并发字节流的传输,解决了队头阻塞问题(包括基于QPACK解决了动态表的队头阻塞);
  • HTTP3重新定义了TLS协议加密QUIC头部的方式,既提高了网络攻击成本,又降低了建立连接的速度(仅需1个RTT就可以同时完成建链与密钥协商);
  • HTTP3 将Packet、QUIC Frame、HTTP3 Frame分离,实现了连接迁移功能,降低了5G环境下高速移动设备的连接维护成本。

本文将会从HTTP3协议的概念讲起,从连接迁移的实现上学习HTTP3的报文格式,再围绕着队头阻塞问题来分析多路复用与QPACK动态表的实现。

3.1 HTTP/2回顾

HTTP/2的一个主要特性是使用多路复用(multiplexing),因而它可以通过同一个TCP连接发送多个逻辑数据流。复用使得很多事情变得更快更好,它带来更好的拥塞控制、更充分的带宽利用、更长久的TCP连接。这些都比以前更好了,链路能更容易实现全速传输。标头压缩技术也减少了带宽的用量。
采用HTTP/2后,浏览器对每个主机一般只需要 一个 TCP连接,而不是以前常见的 六个 连接。事实上,HTTP/2使用的连接聚合(connection coalescing)和“去分片”(desharding)技术还可以进一步缩减连接数。
HTTP/2解决了HTTP的队头拥塞(head of line blocking)问题,客户端必须等待一个请求完成才能发送下一个请求的日子过去了。

3.2 为什么需要QUIC

QUIC协议特点

图X:HTTP/2与HTTP/3的对比
基于UDP
QUIC是基于UDP在用户空间实现的传输协议。如果不观察细节,你会觉得QUIC跟UDP报文差不多。
基于UDP意味着它使用UDP端口号来识别指定机器上的特定服务器。
目前已知的所有QUIC实现都位于用户空间,这使它能得到更快速的迭代(相较于内核空间中的实现)。
稳定性
虽然UDP不提供可靠的传输,但QUIC在基于UDP之时增加了一层带来可靠性的层。它提供了数据包重传、拥塞控制、调整传输节奏(pacing)以及其他一些TCP中存在的特性。只要连接没有中断,从QUIC一端传输的数据迟早会出现在另一端。
数据流
类似SCTP、SSH和HTTP/2,QUIC在同一物理连接上可以有多个独立的逻辑数据流。这些数据流并行在同一个连接上传输,不影响其他流。
连接在两个端点之间经过类似TCP连接的方式协商建立。QUIC连接基于UDP端口和IP地址建立,而一旦建立,连接通过其“连接ID”(connection ID)关联。
在已建立的连接上,双方均可以建立传输给对方的数据流。单一数据流的传输是可靠、有序的,但不同的数据流间可能无序传送。
QUIC可对连接和数据流分别进行流量控制(flow control)。
快速握手
QUIC提供0-RTT和1-RTT的连接建立,这意味着QUIC在最佳情况下不需要任何的额外往返时间便可建立新连接。其中更快的0-RTT仅在两个主机之间建立过连接且缓存了该连接的“秘密”(secret)时可以使用。
QUIC工作原理
连接
QUIC连接是两个QUIC端点之间的单次会话(conversation)过程。QUIC建立连接时,加密算法的版本协商与传输层握手合并完成,以减小延迟。
在连接上实际传输数据时需要建立并使用一个或多个数据流。
(1)连接ID(Connection ID)
每个连接过程都有一组连接标识符,或称连接ID,该ID用以识别该连接。每个端点各自选择连接ID。每个端点选择对方使用的连接ID。
连接ID的基本功能是确保底层协议(UDP、IP及其底层协议)的寻址变更不会使QUIC连接传输数据到错误的端点。
利用连接ID的优势,连接可以在IP地址和网络接口迁移的情况下得到保持,而这TCP永远做不到。举例来说,当用户的设备连接到一个Wi-Fi网络时,将进行中的下载从蜂窝网络连接转移到更快速的Wi-Fi连接。与此类似,当Wi-Fi连接不再可用时,将连接转移到蜂窝网络连接。
(2)端口号
QUIC基于UDP建立,因此使用16比特的UDP端口号字段来区分传入的不同连接。
(3)版本协商
客户端的QUIC连接请求会告知服务器所希望使用的QUIC协议版本,服务器端会回复一系列支持的版本供客户端选择。

问题补充
HTTP/2中的多路复用
http/1中的每个请求都会建立一个单独的连接,除了在每次建立连接过程中的三次握手之外,还存在TCP的慢启动导致的传输速度低。其实大部分的http请求传送的数据都很小,就导致每一次请求基本上都没有达到正常的传输速度。
在http1.1中默认开启keep-alive,解决了上面说到的问题,但是http的传输形式是一问一答的形式,一个请求对应一个响应(http2中已经不成立,一个请求可以有多个响应,server push),在keep-alive中,必须等下上一个请求接受才能发起下一个请求,所以会收到前面请求的阻塞。
使用pipe-line可以连续发送一组没有相互依赖的请求而不比等到上一个请求先结束,看似pipe-line是个好东西,但是到目前为止我还没见过这种类型的连接,也间接说明这东西比较鸡肋。pipe-line依然没有解决阻塞的问题,因为请求响应的顺序必须和请求发送的顺序一致,如果中间有某个响应花了很长的时间,后面的响应就算已经完成了也要排队等阻塞的请求返回,这就是线头阻塞。
http2的多路复用就很好的解决了上面所提出的问题。http2的传输是基于二进制帧的。每一个TCP连接中承载了多个双向流通的流,每一个流都有一个独一无二的标识和优先级,而流就是由二进制帧组成的。二进制帧的头部信息会标识自己属于哪一个流,所以这些帧是可以交错传输,然后在接收端通过帧头的信息组装成完整的数据。这样就解决了线头阻塞的问题,同时也提高了网络速度的利用率。
队头拥塞问题
https://zhuanlan.zhihu.com/p/330300133
HTTP1.1主要存在的问题是无法进行多路复用,每进行一次传输都需要建立一个TCP连接,这中开销是巨大的。HTTP/2主要是提出了多路复用使用一个TCP连接来传输多个数据流。假设有两个流A和B,其中A比较大,B比较小。当两个流使用一个TCP连接进行传输时,并且先传输A,就可能导致B巨大的延迟。比如传输的方式为AAAAAAAAABB,B必须在A传输完成后才能传输。HTTP2通过为每个包添加帧并采用多路复用的方式进解决了上述问题,使得传输变成了ABABAAAAAA这种形式。但是这只是在应用层的层次解决了队头阻塞问题,TCP本身依旧存在阻塞问题,HTTP/2并没有给予解决。
(1)HTTP/2解决了什么问题
HTTP/2使用多路复用解决了HTTP1中每个流需要开启一个TCP连接进行传输的问题。基于HTTP2协议,多个流可以采用一个TCP连接进行传输。这也就是解决了客户端必须等待一个请求完成才能发送下一个请求的问题。
(2)TCP存在的队头阻塞问题

基于HTTP/2的多路复用进行数据传输,假设A流为绿色,B流为红色。由于TCP本身的可靠传输机制,有可能使得单个A的包被阻塞。当单个A包被阻塞后,后面的各个包依旧被阻塞。这种单个数据包造成的阻塞,就是TCP上的队头阻塞。

参考文献

(1)[HTTP/3 explained]https://http3-explained.haxx.se/zh/why-quic/why-tcphol#du-li-de-shu-ju-liu-bi-mian-zu-sai-wen-ti
(2)https://zhuanlan.zhihu.com/p/330300133

你可能感兴趣的:(HTTP协议栈,后端,网络,计算机网络)