跳转至

HTTP3解析

2022年,HTTP/3 标准正式被提出,在这个标准里面,正式抛弃了HTTP对传输层TCP的依赖,并提出了为HTTP服务的新的传输协议QUIC,将其作为HTTP新的传输层,下文是RFC有关HTTP3的必要性说明:

HTTP/1.1 ([HTTP/1.1]) uses whitespace-delimited text fields to convey HTTP messages. While these exchanges are human readable, using whitespace for message formatting leads to parsing complexity and excessive tolerance of variant behavior.

Because HTTP/1.1 does not include a multiplexing layer, multiple TCP connections are often used to service requests in parallel. However, that has a negative impact on congestion control and network efficiency, since TCP does not share congestion control across multiple connections.

HTTP/2 ([HTTP/2]) introduced a binary framing and multiplexing layer to improve latency without modifying the transport layer. However, because the parallel nature of HTTP/2's multiplexing is not visible to TCP's loss recovery mechanisms, a lost or reordered packet causes all active transactions to experience a stall regardless of whether that transaction was directly impacted by the lost packet.

The QUIC transport protocol incorporates stream multiplexing and per-stream flow control, similar to that provided by the HTTP/2 framing layer. By providing reliability at the stream level and congestion control across the entire connection, QUIC has the capability to improve the performance of HTTP compared to a TCP mapping. QUIC also incorporates TLS 1.3 ([TLS]) at the transport layer, offering comparable confidentiality and integrity to running TLS over TCP, with the improved connection setup latency of TCP Fast Open ([TFO]).

简单总结下上文就是,针对历史的主流版本HTTP/1.1和HTTP/2仍然存在很多不足,且从协议层面并没有一个很好的解决方式:

针对于HTTP/1.1

  • 消息整体格式使用了ASCLL编码,且消息内容采用空格进行分段。虽然这些机制保证了human-readable, 但是对计算机来说,这无疑大大增加了复杂性(需要处理各种可能的空格和换行符组合),并且缺乏一个统一的标准,每个应用的实现都也可能大相径庭;
  • 单个连接只能传输单个数据流,缺乏多路复用的设计,这不利于数据的并发传输,虽然使用多个TCP连接能解决这个问题,但是这也带来了更多的时间和资源开销;

针对于HTTP/2,不再使用ASCLL文本, 而是使用二进制文本作为HTTP消息的内容格式(binary framing),并且也设计了multiplexing layer,使得多个请求可以复用同一个连接, HTTP/2允许多个请求和响应在同一TCP连接上**并发传输**(即多路复用)。这些请求和响应被拆分为多个**数据帧(frames)**,并通过同一个TCP连接发送。这个改进大大提升了连接的利用率和传输带宽,同时也节省了系统资源。

但这么做并不是万事大吉了,HTTP/2的多路复用只体现在HTTP协议本身,针对其使用的传输层TCP依旧如初。如果TCP发生丢包和网络拥塞,HTTP协议层不得不进行等待这些关键数据的到来,依赖这个连接的所有的活动事务不得不停止。

Note

为什么说所有活动事务都会受到影响?

  1. 由于TCP将HTTP/2的多路复用连接视为一个整体,当某个数据包丢失时,TCP无法区分哪些数据帧属于哪个具体的HTTP事务。因此,TCP会**暂停整个连接的数据传输**,直到丢失的数据包被恢复。
  2. 即使某些HTTP事务并未直接受到数据包丢失的影响,它们也会因为TCP的暂停机制而**被迫停滞**。这是因为这些事务的数据帧可能被阻塞在TCP的发送缓冲区中,无法继续传输。

基于以上问题, HTTP/3 提出了新的协议模型QUIC:一种使用UDP作为底层传输协议,并在其基础上合并实现了TCP的流控,拥塞控制类似机制,并提供多路复用技术来支持并行化的数据传输。

QUIC协议

QUIC(Quick UDP Internet Connections)协议最开始由Google公司提出,并且在2021年,被IEFT组织标准化。QUIC协议提出的为了构建一种新的HTTP技术标准,来达成一种更快,更安全,更可靠的网络数据传输,从而替换掉基于TCP + TLS的主流HTTP/2标准。

QUIC也可以被理解为一种传输层协议,不同于传统的传输层,QUIC是基于UDP构建,严格意义来讲,它也可以算是一种应用层协议了。

Note

值得注意的是,QUIC实际有两个版本,google版本和IEFT,前者被称为GQUIC,后者被称为IQUIC,现在指的QUIC协议一般指后者,我们这里也是主要分析IQUIC协议

在安全性方面,GQUIC与IQUIC采用了两个完全不同的方式,前者使用了自己的一套加密方式QUIC Crypto,后者还是延续使用了TLS1.3.

image-20251117223317738

目前wireshark只能解密IQUIC协议

它是一种**面向连接的(connection-oriented)**,有状态的(stateful),用于解决**client-server间通信**的一个**安全**传输协议。后文将详细介绍基于这几个属性,QUIC协议是如何实现的。

QUIC的几大核心特点包括了,0-RTT恢复连接, 安全传输,以及解决了**队头阻塞**问题。首先,0-RTT的实现原理是在QUIC建立连接的握手(handshake)阶段就完成了加密通信和传输参数的协商,避免了基于TCP的三次握手和TLS的四次握手的多次RTT耗费,在两次握手完成了所有的协商过程(第二次握手的ACK报文已经可以携带数据载荷(payload), 所以将其叫做0-RTT,当前这需要实现特殊的配置开启下才能启用)。

每个QUIC协议的数据包(packets)都将QUIC协议以帧(frame)结构来划分,每一帧既包含了协议的控制信息,也包含了应用数据, 这一部分,我们通常把它叫做帧的载荷(payload)。基于QUIC的应用协议数据在QUIC里面是流式传输的,这意味着所有的上层应用数据都会经过一系列的操作将其序列化后交由QUIC协议发送。

QUIC提供的是可靠数据传输,并提供了拥塞检测和避免机制,与TCP最大的不同是,它还实现了数据丢失的检测和恢复机制,而不是直接通过数据重传来实现,极大提升了数据的传输效率,很大程度上解决了**队头阻塞**的问题。

v-0

Note

队头阻塞:这描述的是串行化任务执行过程中,由于前置任务没有及时完成,导致后续任务无法继续执行的一种现象。在HTTP/3之前,HTTP协议中并没有一个好的设计来解决批量的请求处理,所有的请求在服务端只能顺序处理,由于其底层协议是使用TCP协议,一旦TCP出现拥塞和丢包,基于这个连接的所有请求被迫进入等待,知道数据传输恢复,这就出现了所谓的队头阻塞问题。

Streams(流机制)

Streams是QUIC协议中最核心,最具创新的机制之一。它不仅解决了TCP中的队头阻塞,同时也为应用层(HTTP/3)提供了高效的并发传输能力。

流的定义

QUIC 的流是一种 轻量级的、独立的、双向或单向的数据通道,运行在 QUIC 连接之上。每个连接可以包含多个流,彼此之间 完全独立,互不干扰。简单理解,流就是基于单个QUIC连接上的抽象数据通道,一个连接允许同时存在多个流,且同时可以操作任意数量的流发送任意数量大小的数据,以此来实现高效的并行数据传输。

流的分类

类型 描述 应用场景
双向流(Bidirectional Stream) 双方都可以发送和接收数据 适用于 HTTP 请求和响应
单向流(Unidirectional Stream) 只有一方发送,另一方接收 适用于服务器推送、控制信息传输等

Stream ID

每个流都有一个唯一的 Stream ID,并且 ID 的奇偶性表示流的发起方:

  • 客户端发起的流:ID 为偶数
  • 服务器发起的流:ID 为奇数

在报文中,流ID是一个可变长度的整数,流 ID 的第二个最低有效位(0x02)区分双向流(该位设置为 0)和单向流(该位设置为 1)。

数据帧(Stream frames)

在流中的数据会被切分为固定大小的分片,这些分片被称之为帧, 每个帧都将携带流ID以及分片的索引(offset, 可以理解为分片编号), 用于接收端的数据重组,使用offset保证数据包按序组装后,传递给上层应用协议。 一个大致的帧结构如下:

Bash
STREAM Frame {
  Type (1 byte)                // 包含标志位
  Stream ID (varint)           // 流的唯一标识符
  [Offset (varint)]            // 数据在流中的偏移量(可选)
  [Length (varint)]            // 数据长度(可选)
  [FIN (flag)]                 // 是否为流的最后一帧(可选)
  Data (bytes)                 // 实际传输的数据
}

与TCP的对比

img

从上图可以看出,针对QUIC协议,单个连接可以提供多个并发工作的流来模拟并行数据传输,对于TCP来说,单个流只能串行化传输业务数据,单个数据包丢失或者延迟抵达接收端会导致基于该连接的所有业务阻塞。

流量控制

流量控制的核心目标都是防止发送方发送过多的数据,导致接收方缓冲区溢出造成数据丢失。 QUIC在连接和流两个层面都实现了流控机制,这就意味着每个流有自己的流量限制,流所在的连接也有一个总体限制。

双层流控

层级 控制对象 描述
流级别(Stream-level) 单个流的数据量 控制每个流能发送多少数据
连接级别(Connection-level) 所有流的总数据量 控制整个连接中所有流的累计数据量

关键帧机制

QUIC 使用以下帧来实现流控:

帧类型 作用
MAX_STREAM_DATA 告诉发送方:某个流最多可以发送多少字节
MAX_DATA 告诉发送方:整个连接最多可以发送多少字节
STREAM_DATA_BLOCKED 发送方通知接收方:当前流被流控阻塞了
DATA_BLOCKED 发送方通知接收方:整个连接被流控阻塞了

当然,这些帧是动态协商的,随着数据传输,接收方可以逐步提升限制,让发送方继续发送更多数据。 与TCP不同的是,QUIC的流控实并不是像TCP那样,是一个滑动的窗口,它是一个固定的窗口,如果客户端很快就发送好了最开始接收方告诉发送方指定窗口大小的数据,发送方不得不立即停止发送,直到接收发送新的窗口大小消息过来,发送方才可以继续发送。

QUIC流控 VS TCP流控

维度 TCP 流控 QUIC 流控
控制粒度 整个连接 每个流 + 整个连接
机制类型 滑动窗口 显式窗口更新(MAX_STREAM_DATA / MAX_DATA)
反馈方式 隐式(ACK 驱动) 显式帧通知
多路复用支持 不支持(单一字节流) 支持并发多流,每个流独立流控
协议层位置 内核 TCP 栈 用户态 QUIC 实现
阻塞影响范围 整个连接阻塞 单个流阻塞不影响其他流

流量分析

image-20251117232145963

首先仍然使用TLS v1.3进行密钥协商

image-20251117232624062

随后进行初始化建立会话,可以看到这里发送了帧和流的最大窗口

image-20251117232309109

建立会话后,每一个流都有一个ID标识,随后进行数据payload的传输

发展趋势

根据HTTP Archive: State of the Web上的统计:

image-20251117222649232

目前移动端和PC端都有约⅓的网站支持HTTP3协议,但由于目前很多的防火墙等设备还不支持HTTP3,所以HTTP3目前还只能说是未来可期

参考文章

HTTP/3 QUIC 在线测试 - 免费在线工具

QUIC协议详解 | 编程之旅✨

QUIC Protocol - GeeksforGeeks

HTTP3协议的安全优势与挑战 - FreeBuf网络安全行业门户