目录
QUIC
QUIC(Quick UDP Internet Connections,快速 UDP 互联网连接),顾名思义,是一种基于 UDP 协议的快速传输层协议。
最初的 QUIC 协议由 Google 的 Jim Roskind 在 2012 年设计并实现,Google 最初的目的是实现一种强调性能的可靠传输层协议来代替 TCP/HTTP 协议。
后来 QUIC 交由 Internet 工程任务组(IETF)进行标准化,为了让 QUIC 协议更具有普适性,IETF 将其拆分为了 2 层:
- QUIC 传输层;
- HTTP over QUIC 应用层。
2018 年,HTTP over QUIC 被更名为 HTTP/3,正式被确定为下一代 Web 网络规范。
QUIC 的特性
QUIC 借鉴了 TCP、UDP 和 TLS(用于加密)的原理和功能,在这个基础上优化了传输的速度。QUIC 的数据传输从第一个数据包传送(0-RTT)开始立即开始,从而减少了应用程序延迟时间。并且可以在数据量已满时调整管理流程(拥塞控制),从而更快更安全。QUIC 协议在登录成功、推拉流成功的耗时,大幅低于 TCP 协议,优化百分比在 30% 以上,极端场景甚至超过 90%。
基于 UDP
QUIC 是基于 UDP 在用户空间实现的传输协议。如果不观察细节,你会觉得 QUIC 跟 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 的单个数据流可以保证有序交付,但多个数据流之间可能乱序。这意味着单个数据流的传输是按序的,但是多个数据流中接收方收到的顺序可能与发送方的发送顺序不同。
举个例子:服务器传送流 A 和 B 到客户端。流 A 先启动,然后是流 B。在 QUIC 中,丢包只会影响该包所处的流。如果流 A 发生了一次丢包,而流 B 没有,流 B 将继续传输直到结束,而流 A 将会进行丢包重传过程。而在 HTTP/2 中这不可能发生。
下图展示了连通两个 QUIC 端点的单一连接中的黄色与蓝色的数据流。它们互相独立,所以可能乱序到达,但是每个流内的信息将按序可靠到达。
快速握手
与 TCP 的 3 次握手相比,QUIC 提供了 0-RTT 和 1-RTT 的握手,这减少了协商和建立新连接时所需的时间。其中更快的 0-RTT 仅在两个主机之间建立过连接且缓存了该连接的 Secret 时可以使用。
此外,QUIC 提供了提早传输更多数据的 “早期数据”(early data)特性,并且它的使用比 TCP 快速打开(TCP Fast Open)更加简便。同时,因为数据流概念的引入,客户端不用等待前一个连接结束,便可以与同一个主机建立另一个逻辑连接。
安全性
QUIC 使用 TLS 1.3 传输层安全协议,并且 QUIC 协议没有明文的版本,所以想要建立一个 QUIC 连接,就必须通过 TLS 1.3 来安全地建立一个加密连接。如上文所说,加密可以避免协议僵化等拦截和特殊处理。这也使 QUIC 具有了 Web 用户所期望的所有的 HTTPS 安全特性。QUIC 只在加密协议协商时会发送几个明文传送的初始握手报文。
连接迁移
TCP 连接基于四元组(源 IP、源端口、目的 IP、目的端口),切换网络时至少会有一个因素发生变化,导致连接发生变化。当连接发生变化时,如果还使用原来的 TCP 连接,则会导致连接失败,就得等原来的连接超时后重新建立连接,所以我们有时候发现切换到一个新网络时,即使新网络状况良好,但内容还是需要加载很久。如果实现得好,当检测到网络变化时立刻建立新的 TCP 连接,即使这样,建立新的连接还是需要几百毫秒的时间。
QUIC 的连接不受四元组的影响,当这四个元素发生变化时,原连接依然维持。那这是怎么做到的呢?道理很简单,QUIC 连接不以四元组作为标识,而是使用一个 64 位的随机数,这个随机数被称为 Connection ID,即使 IP 或者端口发生变化,只要 Connection ID 没有变化,那么连接依然可以维持。
解决队头阻塞
- QUIC 的传输单元是 Packet,加密单元也是 Packet,整个加密、传输、解密都基于 Packet,这样就能避免 TLS 的队头阻塞问题;
- QUIC 基于 UDP,UDP 的数据包在接收端没有处理顺序,即使中间丢失一个包,也不会阻塞整条连接,其他的资源会被正常处理。
HTTP/3 支撑
HTTP/3 协议使用了 QPACK 进行 HTTP 头部压缩,这和 HTTP/2 中使用 HPACK 压缩头部类似。HPACK 算法依赖于数据流的有序交付,由于 HTTP/3 的数据流之间可能乱序,所以该算法需要修改才能使用。QPACK 可被视作适用于 QUIC 版本的 HPACK。
QUIC 的工作原理
连接
QUIC 连接是两个 QUIC 端点之间的单次会话(conversation)过程。QUIC 建立连接时,加密算法的版本协商与传输层握手合并完成,以减小延迟。在连接上实际传输数据时需要建立并使用一个或多个数据流。
每个连接过程都有一组连接标识符,或称连接 ID,该 ID 用以识别该连接。每个端点各自选择连接 ID。每个端点选择对方使用的连接 ID。连接 ID 的基本功能是确保底层协议(UDP、IP 及其底层协议)的寻址变更不会使 QUIC 连接传输数据到错误的端点。
利用连接 ID 的优势,连接可以在 IP 地址和网络接口迁移的情况下得到保持,这是 TCP 永远也做不到的。举例来说,当用户的设备连接到一个 Wi-Fi 网络时,将进行中的下载从蜂窝网络连接转移到更快速的 Wi-Fi 连接。与此类似,当 Wi-Fi 连接不再可用时,将连接转移到蜂窝网络连接。
由于 QUIC 基于 UDP 建立,因此使用 16 比特的 UDP 端口号字段来区分传入的不同连接。
在初始的数据包建立连接之后,连接发起者会马上发一个加密的帧以开始安全层握手。安全层使用 TLS 1.3 协议。
数据流
数据流(Streams)在 QUIC 中提供了一个轻量级、有序的字节流的抽象化。QUIC 中有两种基本的数据流类型:
- 从发起者到对等端(Peer)的单向数据流。
- 双向均可发出数据的双向数据流。
连接端点的任意一方都可以建立这两种数据流,数据流之间可并行、交错地传输,并且可以被取消。通过 QUIC 发送数据需要建立一个或多个数据流。
- 流量控制(Flow control):每个数据流都有独立的流量控制,端点可以通过此实现内存控制和反压(back pressure)。数据流的创建本身也有流量控制,连接双方可以声明最多愿意创建几个流 ID。
- 流标识符:数据流通过一个无符号的 62 比特整数标识,也称流 ID。流 ID 的最低 2 位比特用于识别流的类型(单向或双向)和流的发起者。流 ID 的最低 1 位比特(0x1)用于识别流的发起者。客户端发起双数(最低位置 0)流,服务器发起单数(最低位置 1)流。第 2 个比特(0x2)识别单/双向流。单向流始终置 1,双向流则置 0。
- 流并发:QUIC 允许任意数量的并发流。端点通过闲置最大流 ID 来控制并发活动的传入流数量。每个端点指定自己的最大流 ID 数,并只对对等端端点有效。
- 收发数据:端点使用流来收发数据,这是流的最终用途。QUIC 数据流是有序的字节流抽象。但是,不同流之间是无序的。
- 流优先度:如果正确设置了各流的优先度,流复用机制可以显著提升应用的效率。使用其他多路复用协议(如 HTTP/2)的经验表明,有效的优先度划分策略对效率具有显著的正面影响。QUIC 本身没有提供交换优先度信息的报文。接收优先度信息依赖于使用 QUIC 的应用层。应用层可以定义所有复合其语义的优先度方案。基于 QUIC 使用 HTTP/3 时,优先度信息在 HTTP 层完成。
用户态实现
在用户空间中实现一个传输层协议有助于协议的快速迭代,协议的演进更为容易,不需要客户端和服务器更新其操作系统内核才能部署新的版本。同时,QUIC 本身没有固有的东西阻碍未来在操作系统内核中实现和提供 QUIC 协议。
API
常规 TCP 与程序最成功的因素之一便是标准化的套接字(Socket)API。其 API 有着定义良好的功能,使用它能让你轻松地在各操作系统之间移植程序,因为 TCP 采用同样的方式运作。
但 QUIC 不是如此。QUIC 目前没有标准化的 API。使用 QUIC 时,你需要选择一个现有的库实现,并坚持使用它的 API。这在某种程度上把应用 “绑定” 到了单一的库上。换库意味着使用另外一套 API,这可能带来相当的工作量。
另外,由于 QUIC 一般在用户空间中实现,所以它不像现有的 TCP 和 UDP 套接字 API 那样能轻松扩展。使用 QUIC 意味着选择了套接字 API 之外的另一套 API。
参考文档
- https://mp.weixin.qq.com/s/0hsWDE3lqow5Vqa802J3qA
- https://mp.weixin.qq.com/s/zWin_9zhUrB1mFDnn9rmQg
- https://mp.weixin.qq.com/s/TU36OLc5pE-vbsEo0aHZGA
- https://mp.weixin.qq.com/s/_Sa3QSN8PnM61qMG_0gtSw
- https://mp.weixin.qq.com/s/HuK5kpz7QhIvxN5X0Z_z2A
- https://mp.weixin.qq.com/s/vT3OGlymecYp4fu7AY7qVQ
- https://mp.weixin.qq.com/s/gBnot5CjGtvUV3RVR7ISQQ