切换深浅色
第 12 章 QUIC 与 HTTP/3
HTTP/2把逻辑流放在TCP上方,传输层仍只有一条有序字节序列。QUIC把多流、恢复、流量控制和安全握手整合进一种基于UDP承载的传输协议;HTTP/3再把HTTP映射到这些能力上。本章先建立传输服务与数据路径,涉及密码派生时明确前引第16章,不让尚未讲过的算法代替可靠性解释。
12.1 UDP之上怎样重新提供可靠传输
UDP只交付有边界的数据报,不自动保证送达、顺序、去重或拥塞控制。QUIC使用UDP并不代表放弃这些能力,而是由QUIC端点重新实现需要的传输规则。操作系统提供UDP收发,QUIC实现维护连接、包号、流、确认、恢复和发送预算。RFC 9000 §1—2。
本章基线是RFC 9000定义的IETF QUIC版本1,不把历史Google QUIC或其它版本的字段混进同一布局。不同QUIC版本可以改变一些具体格式;讨论某一种实现时仍要另外记录版本和配置,不能只凭“使用UDP”识别协议。
QUIC的分层单元如下:
text
IP包
└─ UDP数据报
└─ 一个或按规则组合的多个QUIC包
└─ 受相应保护的帧序列
├─ STREAM:某流的字节及偏移
├─ ACK:包接收反馈
└─ 其它握手、窗口、路径或关闭控制包和帧不同。QUIC包是接收端可以处理的传输单元,具有包头和包号等上下文;帧表达具体动作,一个包可以携带多个帧。UDP数据报提供外层边界,可以在规范允许的条件下组合多个QUIC包,但不能把任意残缺包当成一份完整报文独立处理。
可靠字节流需要三组机制。第一组标明数据属于哪条流、位于什么偏移,接收端才能重排并识别重复;第二组反馈哪些发送包已经到达,发送端才能推断仍可能缺失的信息;第三组在认为丢失或需要探测时重新发送必要内容,并控制发送速率。仅加一个序号字段,并不足以提供完整可靠服务。
QUIC可靠性主要恢复仍然需要的信息,不要求重发原UDP数据报或原QUIC包。若某包里的流数据丢失,发送者可以把相同流偏移的数据放入新的帧和新包。旧包号不复用,接收方按流偏移恢复应用字节。确认、包号和流偏移各自回答不同问题,后面会用完整布局展开。
多条流的有序性分开维护。流A缺少某段字节,可以阻挡A在该偏移之后的连续交付;若流B的数据已完整到达并可独立处理,它不需要仅因A的缺口等待整个连接补齐。这与HTTP/2下方TCP只有一条交付序列不同。但各流仍共享连接路径和拥塞等资源,不能把“独立交付”写成“某流丢包对其它流绝无影响”。
QUIC也不是HTTP/3的别名。QUIC提供传输服务,HTTP/3规定HTTP请求、控制流与字段压缩如何使用它;其它应用可以在QUIC上建立自己的协议。相反,“应用用了UDP”也不说明它实现了QUIC,更不说明它使用同一种恢复或拥塞算法。
用户态实现让传输逻辑可以随着应用发布更新,而不必把每项变化都放进操作系统TCP栈。这是一种部署与实现选择,不是免费性能保证。用户态包处理、系统调用、批量收发、缓冲和网卡卸载都会影响CPU与吞吐;网络若不允许相应UDP路径,协议再完整也无法凭空获得可达性。
安全握手在QUIC建立过程中提供必要保护材料,握手消息与运输控制也有明确交互。这里先把它看成建立受保护会话的组件;它与可靠流、数据报承载和应用身份分别有自己的职责。第12.5讨论接口,第16章再逐项解释TLS 1.3的实际密码机制。
12.2 连接标识、流类型与STREAM帧
TCP通常按网络端点识别连接,地址或端口变化会影响原来的连接归属。QUIC增加Connection ID,让端点能把收到的包关联到既存连接,而不只依赖当前IP与UDP端口。CID由相应端点选择并让对方在发往它的包中使用;它不是密码,也不单独证明包来自合法对端。路径变化与验证在12.4再讲。
连接内部的Stream ID是另一种编号。QUIC流可以单向或双向,编号低两位表达发起者与方向,其余位区分后续同类流:
| 低两位 | 流类型 | 最先几个编号 |
|---|---|---|
| 00 | 客户端发起,双向 | 0、4、8 |
| 01 | 服务器发起,双向 | 1、5、9 |
| 10 | 客户端发起,单向 | 2、6、10 |
| 11 | 服务器发起,单向 | 3、7、11 |
这些编号不能套用HTTP/2的奇偶规则;QUIC流0是合法的客户端双向流,不是连接控制专用值。编号在连接内不复用,流类型也不因内容换成另一方向。双向流的两个发送方向各有自己的偏移与结束状态。RFC 9000 §2。
QUIC大量字段使用自己的变长整数。首字节高两位表示整个整数占1、2、4或8字节,其余位与后续字节组成大端数值:
| 长度标志 | 总宽度 | 数值位数 |
|---|---|---|
| 00 | 1字节 | 6 |
| 01 | 2字节 | 14 |
| 10 | 4字节 | 30 |
| 11 | 8字节 | 62 |
例如37可以写为一字节 25;150需要两字节,表示为 40 96。这里不是HPACK的前缀加七位续组,也不是Protobuf的逐字节续位。除帧类型等明确要求外,QUIC通用变长整数允许非最短宽度,因此不能把WebSocket的最短长度规则不加区分地搬过来。RFC 9000 §16。
STREAM帧把应用数据定位到具体流偏移。帧类型08—0f中的低三位分别控制是否有Offset、是否有Length,以及是否为该发送方向的最终数据:OFF=04、LEN=02、FIN=01。其后依次为Stream ID、可选Offset、可选Length和数据;Offset缺省为0,Length缺省时数据延伸到当前QUIC包可用的末尾,而不是UDP之后的任意下一包。
假设客户端在流0发送abc并结束方向,完整明文帧可以是:
text
0f 00 00 03 61 62 63类型0f说明OFF、LEN、FIN都设置,流号0、偏移0、长3、内容abc,共七字节。它是加密保护之前或解密之后的帧表示,不是完整线上QUIC包;实际包还需要头部、包号和相应保护,不能把这七字节直接称为合法UDP会话。
流偏移按字节从0递增,与帧被放在哪一个包无关。设消息内容abcdef被两帧传送,接收端先收到末段 0f 00 03 03 64 65 66,知道偏移3起有三字节,且最终大小为6;但开头仍缺失,不能把def当作从0开始的完整内容。稍后收到 0e 00 00 03 61 62 63,补齐abc,才可按顺序恢复abcdef。
FIN表达最终大小,不是“凡此包之前的全部网络数据都已到达”的证明。即使已经看到FIN,缺口仍要恢复。同一偏移重复发送的数据必须相同,接收端可以丢弃已收重复范围;重新传输时帧边界可以变化,不改变应用看到的字节流。RFC 9000 §19.8。
QUIC对流与连接也有流量限制,但其MAX_STREAM_DATA等字段表达的是允许发送到的绝对上界,不是HTTP/2 WINDOW_UPDATE那种每次增量。缺口和最终大小会参与相应预算,不能只按当次新交付应用的字节扣额度。流数量又由单向、双向各自的限制控制,编号可表示很多流不等于对端已经允许打开那么多。
CID、Stream ID、流偏移与包号由此形成四套不同坐标:CID关联连接,Stream ID选择字节通道,偏移定位应用内容,包号支持传输反馈。分清这些坐标,才能解释下一节为何“一个包丢失”与“某段流数据仍缺失”并不是同一状态。
12.3 包号、ACK范围、丢失检测与拥塞预算
QUIC的包号用于反馈发送包是否到达,流偏移用于恢复应用数据。这两个坐标不能互换。一个包可以同时携带多个流的帧;同一段流数据可以先在包N里发送,再在新的包M中恢复。收到M以后,应用字节可能已齐,而N仍未被确认。
QUIC版本1有Initial、Handshake和Application Data三套包号空间,每个发送方向独立编号;0-RTT与1-RTT的应用包使用同一个Application Data空间。空间用于隔离相应反馈与处理状态,不是每条业务流各分一套包号。具体保护级别与TLS接口在12.5解释,先把这些名称视为会话阶段。RFC 9000 §12.3。
ACK可以表达多个已接收包号区间,而不只反馈“连续收到哪里”。一个简化的明文ACK帧为:
text
02 0a 00 01 02 01 01| 字段 | 值 | 解释 |
|---|---|---|
| Type | 02 | 不带ECN计数的ACK |
| Largest Acknowledged | 10 | 最大确认包号 |
| ACK Delay | 0 | 本例无编码后确认延迟 |
| ACK Range Count | 1 | 还有一组额外Gap和Range |
| First ACK Range | 2 | 第一段从10向下包含两项,即8—10 |
| Gap | 1 | 与下一段之间跳过两项6、7 |
| ACK Range Length | 1 | 下一段最大5,再向下包含一项,即4—5 |
全部字段恰好用一字节变长整数,帧总长七字节;它确认4、5、8、9、10。Gap的编码不直接等于缺失包数,下一段最大值按“上一段最小值−Gap−2”计算。ACK没有声明未列出的包全都确定丢失,发送者还要结合发送历史和时间判断。RFC 9000 §19.3。
RFC 9002的丢失检测使用包阈值与时间阈值。较新的包已确认,而某个较早包落后足够多,或已等待足够久,就可认为它丢失并恢复仍需的信息。推荐的初始包阈值是3;推荐时间阈值取最近与平滑RTT较大值的9/8,并受定时器粒度约束。它们用于在乱序与尽快恢复之间取舍,不是网络丢失的数学证明,也不是所有实现永远不能变化的配置常数。
RTT从发送到相关确认到达的间隔估计。对端在收到包后可能故意延迟ACK,因此ACK Delay允许说明其可控等待;发送者按规范条件和上限作修正,不能无限信任它并把RTT减成不合理的小值。系统中未知的排队、调度和路径变化仍会影响估计。
如果没有足够反馈来判丢,PTO用于发送探测,促使对端产生新的确认。典型应用数据阶段的基础估计为:
其中smoothed_rtt是平滑往返估计,rttvar是其波动估计,kGranularity是定时器精度下限,max_ack_delay是适用阶段的对端确认延迟上限。Initial与Handshake的计算不把应用阶段的最大ACK延迟直接照搬;连续PTO还需要退避。PTO到期不等于把所有在途包一律判丢,也不是立即无上限重发全部数据。RFC 9002 §5—6。
恢复的对象取决于信息是否仍有价值。尚未收到的STREAM范围需要重发;窗口上限若已有更新值,应传最新许可,而不是反复重发过时窗口帧;ACK也应反映当前接收状态,不机械重复旧报文。QUIC因此在新包中重新组织帧,而不是把旧UDP负载复制一遍并复用包号。
发送者还必须控制网络负载。拥塞窗口限制正在网络中等待确认的发送量,发送节奏与确认、丢失和路径估计相互作用;恢复数据通常也消耗相应预算。RFC 9002给出一套基于窗口的拥塞算法作为规范基础,传输要求不意味着所有软件只能使用完全相同的策略。选择不同算法需要考虑公平、反馈与适用网络,不能因UDP接口允许发送就无限注入。
流量控制与拥塞控制同时成立:前者防止对端被超出承诺的应用数据淹没,后者避免让共享网络过载。某流接收上限很大,不证明路径能够承受同样大的在途量;网络很空闲,也不允许越过接收者给定的流与连接许可。丢失可能让共享发送预算减少,所以其它流即便没有自身缺口,仍可能减速。
确认也不是业务执行凭证。ACK说明相关包到达QUIC端点并进入协议处理,不证明网站已解码一条HTTP请求,更不证明数据库操作成功。可靠传输把缺字节问题交给协议恢复;业务完成仍需上层响应与状态来确认。
12.4 路径验证与迁移:CID不是地址所有权证明
手机从Wi-Fi切到移动网络,或NAT为同一客户端重新分配出口端口,都会改变服务器看到的地址。QUIC的CID允许新路径上的包仍被关联到既存连接,避免只因四元组变化就丢掉全部会话。但接收端必须同时确认包属于受保护会话,并验证新地址可达,不能只见到一个熟悉CID就向该地址大量发送。
路径验证使用PATH_CHALLENGE和PATH_RESPONSE。挑战携带八字节不可预测数据,对端在相应路径返回相同数据。一个公开教学明文帧对可以是:
text
PATH_CHALLENGE:1a 11 22 33 44 55 66 77 88
PATH_RESPONSE :1b 11 22 33 44 55 66 77 88每个帧九字节,类型分别为1a与1b。数值仅用于解释布局,真实挑战必须按规范重新选择,不复用本例。它们实际位于相应受保护的QUIC包中,不是把这九字节裸发到UDP就完成验证。RFC 9000 §8.2、§19.17—19.18。
返回匹配数据说明相关对端能经被验证路径接收并处理挑战,不是某个用户账户的认证证明。路径验证、会话密码认证和应用身份分别回答不同问题。攻击者只伪造源地址或抄一个CID,不应因此让服务器把大量业务转送给不参与会话的第三方。
这与反放大限制相配合。在地址尚未验证时,服务器向该地址发送的总字节不能超过从它收到的相应数据量三倍。若归属于该连接的UDP负载仅收到1200字节、没有其它许可,未验证阶段可发送预算最多3600字节;这不是一份永久限速,也不是收到每个包就可额外无条件发送3600。发送、接收和验证状态要持续累计。RFC 9000 §8.1。
较大的握手响应可能碰到这份预算,服务器便需要更多客户端数据或完成适用验证。Retry与地址验证token可以用于让客户端证明能收到该地址的返回信息;Retry会增加一次相应往返,但不是所有QUIC连接都必须先经过Retry。地址token也不代替网站证书或应用认证,其适用范围与有效性由协议和服务端处理决定。
主动迁移还有阶段限制。端点不得在握手确认之前主动发起连接迁移;对端发出disable_active_migration时,也要遵守对应限制与规范例外。NAT重绑定并不一定是客户端主动选择,不能把这个参数理解成网络绝不会改变来源端口。新地址未经既有验证时,仍需要路径验证。RFC 9000 §9。
新路径可能有不同RTT、容量与拥塞状况。旧路径上估计的在途量和发送节奏不能无条件搬过去;QUIC需要按迁移规则处理拥塞与RTT状态。保留应用流偏移和会话,不意味着可以忽略新网络条件。迁移过程中尚未确认的数据可能需要恢复,应用也仍可能遇到等待或路径失败。
CID轮换还有隐私作用:在不同路径使用不同CID,可以减少直接按一个固定标识关联的机会;但包的时序、大小、端点观测与其它信息仍可能关联会话。不能因为换CID就声称匿名,也不能因为CID不等于IP就把网络端点看成完全不可见。
QUIC版本1也不提供任意服务器地址变化的通用自由迁移。客户端驱动的新地址迁移、服务器预先提供的preferred address等路径各有条件;不能从手机网络切换可用推导远端服务可随意换IP而无需其它配合。
这些机制说明迁移是一种有条件地延续会话的能力。它保留连接与流状态,但需要路径可达、对端支持、安全状态和资源预算共同成立;若所有可用路径都失败,协议仍可以终止连接,而不会凭借CID自动修复底层网络。
12.5 QUIC使用TLS握手,但不运输TLS记录
在普通HTTPS over TCP中,TLS握手和应用数据经TLS记录层组织,再写进TCP。QUIC采用不同接口:TLS产生与验证握手消息,QUIC用CRYPTO帧可靠承载这些握手字节;TLS输出的会话材料交给QUIC,由QUIC负责包保护。HTTP/3应用内容则进入QUIC流,不先套一层普通TLS记录再套QUIC。RFC 9001 §3—4。
CRYPTO帧具有偏移与长度,但没有Stream ID,也不占普通业务流编号;每个适用保护级别有自己的握手字节序列。一个ClientHello可以跨多个CRYPTO帧,帧又可以跨不同包发送或恢复,TLS仍按自己的握手消息长度解析。CRYPTO不使用普通流的MAX_STREAM_DATA窗口,接收实现需要另外限制握手缓存,不能因此无限接受乱序握手数据。
QUIC版本1使用TLS 1.3的握手。TLS组件提供身份认证和协商结果,并把不同阶段的材料通知QUIC;QUIC组件需要用正确级别发送或恢复对应字节。TLS记录保护不在这里重复执行,TLS告警则映射到适用的QUIC连接错误。这份接口不是“TLS只跑一次、以后任意裸UDP”,而是把持续保护放到了QUIC包层。
| 保护级别 | 常见承载内容 | 关键性质 |
|---|---|---|
| Initial | ClientHello、ServerHello等初期握手 | 保护材料可由公开连接信息导出,不提供对旁观者的真正保密 |
| Handshake | 后续TLS握手消息 | 使用握手过程中产生的材料,维护独立包号空间 |
| 0-RTT | 客户端利用先前会话材料提前发送的应用数据 | 可被拒绝,存在需要上层处理的重放边界 |
| 1-RTT | 正常应用流及适用握手后消息 | 当前连接建立后的主要保护级别 |
四个级别不等于四套包号空间:0-RTT和1-RTT共享Application Data包号空间。服务器不发送0-RTT应用包,客户端的早期包确认也要通过适用的1-RTT路径返回。不能只按某个包里的“早期”标签推断已经完成双向身份和状态确认。
Initial尤其容易误解。QUIC v1用固定版本的公开盐与客户端首次Initial里的目标CID等公开信息,经规定算法导出Initial材料。任何取得这些信息的观察者都能按同样步骤恢复Initial内容;它的包保护过程不等于已经取得会话级保密,也不证明服务器身份。不能把“抓包看起来是密文”当成密码学结论。具体派生算法和为何后续阶段不同,将在第14—16章建立密码学基础后说明。
包保护还分为负载保护与对部分头部位的保护,不是把整个UDP负载的所有字段都隐藏。CID、版本或包类型等可见范围由具体头部与保护级别决定。每个包的处理需要正确上下文,接收端不能在认证失败时仍把解出来的候选字节交给应用。完整算法的密钥、一次性输入和验证性质将在安全篇展开,当前先确定它们位于哪条数据路径。
TLS握手也携带QUIC传输参数,例如适用流量上限等。对端不能只靠一个未认证的普通DATA帧随意改写已经承诺的握手参数;参数交换与握手状态结合,才能让两端对后续运输规则有共同依据。恢复与0-RTT还需处理先前参数与新会话兼容性,不能简单把旧缓存当成新连接无限许可。
“握手完成”和“握手确认”又不同。RFC 9001以本端已经发送Finished并验证对方Finished作为TLS握手完成,双方完成时间可以不同;服务器完成后发送HANDSHAKE_DONE,客户端收到它时确认握手。客户端还可以按规范利用自己1-RTT包的确认判断。这个区分影响何时丢弃旧状态、允许迁移等行为,不能只用一个笼统connected标志覆盖。
首次连接通常需要得到服务器当前握手反馈,客户端才能发送受相应会话保护的应用数据。0-RTT则用先前会话材料,在当前服务器回复之前发送部分早期应用信息;它不代表网络传播耗时为零,也不把重放与业务重复执行问题消除。服务器可以拒绝,应用必须知道哪些操作适合提前,哪些需要等待完整状态。
本节刻意在服务与接口层解释TLS,避免在密码学概念尚未建立前堆出密钥公式。第16章将完整推导TLS 1.3握手、记录与派生;届时再把其输出准确放回这里的QUIC包保护位置,不能把TCP上的TLS记录布局原样套到QUIC。
12.6 HTTP/3:请求流、控制流与QPACK依赖
HTTP/3把HTTP映射到QUIC,不再在UDP之上重复HTTP/2的九字节帧头。一次普通请求响应使用一条客户端发起的双向QUIC流,例如0、4、8;同一请求流内按HTTP/3规则依次传送HEADERS、DATA与可选尾部HEADERS,发送方向的结束由QUIC流结束表达,而不是HTTP/2的END_STREAM位。
HTTP/3帧头只有Type和Length两个QUIC变长整数,Length计算其后帧负载。流编号已经由外层QUIC流确定,所以帧头不再重复它,也没有HTTP/2的Flags字段。例如在合法消息状态下,DATA携带abc的完整明文HTTP/3帧为:
text
00 03 61 62 63Type=00、Length=3,总长五字节。它仍需放进QUIC STREAM等运输表示与受保护的包中;不能拿五字节直接与一个完整IP包比较。HEADERS类型为01,负载则是QPACK编码字段块。RFC 9114 §4、§7。
连接还需要单向流传送控制。单向流开头先有Stream Type,接收者据此选择解释器;这与QUIC流编号的低两位不是同一字段。
| HTTP/3单向流类型值 | 用途 |
|---|---|
| 00 | 控制流,首先发送SETTINGS等连接控制 |
| 01 | 推送流,另有相应推送上下文 |
| 02 | QPACK编码器指令流 |
| 03 | QPACK解码器反馈流 |
每端必须建立自己的控制流,同类关键流不能任意重复;控制流的关闭也不是一个普通请求完成,而会影响连接。QUIC流0本身是客户端双向请求流,不能因HTTP/3控制流类型值也是0,就把两种零混为一谈。
QPACK保留静态表、动态表和字段索引的压缩思路,却重新安排了更新依赖。HPACK把字段更新按一条TCP方向串行处理;QUIC的多个请求流可以独立到达,若直接沿用同一隐式顺序,接收者可能先看到引用、后看到它依赖的表项。QPACK用专门编码器流发送表更新,用解码器流报告接收、字段处理或取消情况,让请求流显式声明需要的状态。RFC 9204 §2。
每个编码字段区前缀含Required Insert Count及与Base有关的信息。前者表示解码该区必须具备的插入进度,其线上值采用规定的环绕编码,要结合容量与当前状态还原,不能当作原样整数。Base为动态相对索引提供参考点,Delta Base和符号位表达其与所需计数的关系。QPACK内部使用HPACK风格的前缀整数,不是把所有字段都改成QUIC通用变长整数。
若请求流的引用依赖尚未到达的表更新,这个字段区就暂时阻塞,直到编码器流补齐。于是QUIC消除了TCP交付缺口对所有流的强制阻挡,却不能消除应用自己引入的跨流依赖。QPACK通过动态表容量、允许阻塞流数量和反馈机制控制这种风险;编码器也可以选择静态表、字面值或已知对端已有的表项,减少阻塞机会。
看一个不引用动态表、不使用Huffman的完整教学块,表达与11.3相同的GET、https、路径/和主机example.com:
text
00 00 d1 d7 c1 50 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d| 字节 | QPACK意义 |
|---|---|
00 00 | 无动态依赖的Required Insert Count与零Delta Base |
d1 | 索引静态表17,:method GET |
d7 | 索引静态表23,:scheme https |
c1 | 索引静态表1,:path / |
50 | 字面值带静态名称引用,名称索引0为:authority |
0b及后十一字节 | 未作Huffman的example.com |
块长十八字节。前面加HTTP/3 HEADERS头 01 12,这一帧共二十字节。该请求若没有内容,可以在帧字节完成后结束客户端发送方向;帧自身没有一个可照搬的END_HEADERS或END_STREAM标志。QPACK静态表从0开始,不能把HPACK的索引2、4、7原封不动移到这里。
本例不新增动态项,因此下一条请求不能像HPACK示例那样凭空用62引用主机。动态压缩必须经过相应编码器指令、容量约定和反馈。缺省的动态容量与允许阻塞数都可能为零;实现仍可用静态和字面表示工作。表越大、引用越积极,可能节省更多重复字段,却增加状态、等待与引用生命周期管理。
HTTP/3仍维护HTTP内容与字段语义,不使用chunked或Transfer-Encoding。一个HTTP/3帧可以跨多个QUIC STREAM帧,多个HTTP/3帧也可以被同一QUIC包承载;请求的内容长度若声明,仍需与实际内容一致。QUIC可靠流保证字节服务,不替HTTP检查字段或消息顺序。
控制流、QPACK流和请求流因此各有职责。把它们一律称为“HTTP/3多路复用”,会掩盖哪一条流上丢数据、哪项状态在等待,以及为什么某个请求尚不能解码。准确定位依赖后,才能判断一次等待来自网络恢复、压缩状态、窗口还是应用处理。
12.7 一个真实标准向量:1200字节Initial里装了什么
QUIC包受保护,不能随意编出一串密文就称为真实报文。这里选用RFC 9001附录A.2的公开Client Initial测试向量,逐字段解析它给出的未保护头部,再对照受保护结果。它是标准向量,不是一次HTTP/3网站访问的生产抓包,不能据此声称其中应用协商就是h3。
未加头部保护的完整包头为二十二字节:
text
c3 00 00 00 01 08 83 94 c8 f0 3e 51 57 08 00 00 44 9e 00 00 00 02| 偏移范围 | 字段 | 本例解释 |
|---|---|---|
[0,1) | 首字节c3 | 长头、固定位置位、Initial类型;未保护时包号长四字节 |
[1,5) | Version | 00000001,QUIC v1 |
[5,6) | DCID Length | 08,八字节目标CID |
[6,14) | DCID | 8394c8f03e515708 |
[14,15) | SCID Length | 00,源CID为空 |
[15,16) | Token Length | 00,无token |
[16,18) | Length | 44 9e,变长整数1182 |
[18,22) | Packet Number | 00000002,包号2 |
Length不是整个包长。44 9e的高两位表示两字节,其余数值为 4×256+158=1182,计算的是从包号开始的剩余部分:四字节包号、1162字节受保护帧内容、十六字节认证标签。包号之前有十八字节,所以完整QUIC包为 18+1182=1200 字节。UDP和IP首部尚未计入。
这个Initial的明文负载包含一条CRYPTO帧,并有足够PADDING填到1162字节。CRYPTO开头为:
text
06 00 40 f1 01 00 00 ed ...此处省略号明确代表未列出的标准握手内容,不是一个完整短帧。06是CRYPTO类型,00是偏移,40 f1为长度241。后续握手字节从 01 00 00 ed 开始:ClientHello类型1及三字节消息长度237,加四字节握手头正好241。CRYPTO自己的头四字节,所以该帧共245字节;剩余 1162−245=917 字节为PADDING。
text
1200字节QUIC包:
包号之前的头 18
包号 4
帧明文 1162 = CRYPTO 245 + PADDING 917
认证标签 16PADDING不是网站内容,也不是TLS握手的一部分。它参与运输尺寸与相关发送预算,不能因为解码应用没有显示它,就从线上字节数中删掉。客户端Initial承载的数据报还有相应最小尺寸要求,不能以这个例子推断每个QUIC包、每个阶段都必须1200字节。
标准向量执行负载保护与头部保护以后,实际包头变为:
text
c0 00 00 00 01 08 83 94 c8 f0 3e 51 57 08 00 00 44 9e 7b 9a ec 34首字节由c3变成c0,包号字段由00000002变成7b9aec34,长度和CID等相应部分保持可见。不能直接用受保护c0的低位断言本包只有一字节包号;应先按规范使用密文样本去头部保护,再恢复正确包号长度与完整包号。负载验证也需要正确上下文,失败不能继续交付猜出的握手内容。RFC 9001附录A.2。
本节没有重新伪造其余1178字节保护结果,完整密文可在该标准附录核对。第14—16章建立算法与派生后,将把密钥、一次性输入和标签验证放回这个流程;这里先用标准提供的输入输出说明哪些范围怎样计算。
Initial的这套长头不能套给所有QUIC包。1-RTT通常使用短头,CID长度来自连接上下文,包号依然使用受保护的截断表示;短头没有同样的Version、CID长度对和Length字段。数据报组合规则必须能确定各包边界,不能只按Initial的偏移不断扫描整个会话。
HTTP/3的HEADERS和DATA位于QUIC流数据中,又是另一套内层边界。看到本例CRYPTO不意味着已经得到一个HTTP请求;解密一个包也不保证某条应用流的所有先前偏移都已齐。完整还原需要先恢复会话与保护级别,再恢复帧与流,最后才运行HTTP/3或其它应用解释器。
12.8 性能比较必须附带路径与状态条件
QUIC常被概括为握手快、丢包表现好、可迁移。这些描述对应具体能力,却不能直接变成任意网络中的吞吐排名。建立、稳态运输和路径变化是不同阶段,各自瓶颈也不同。
先只比较客户端何时可以发送当前会话的常规受保护应用请求。假设地址已知、往返时间为r、新建连接、TLS 1.3、无恢复、无TCP Fast Open、无Retry、无额外认证与排队:TCP先建连约r,再完成客户端所需TLS往返约r,因此约2r;QUIC把传输建立与TLS握手结合,常规路径约r。这个模型解释了一个可能减少的往返,不包含服务器应用处理与请求响应的全部时间。
| 改变的条件 | 需要重新考虑的成本 |
|---|---|
| QUIC需要Retry | 增加相应地址验证往返 |
| 服务器遇反放大预算 | 可能等待更多客户端信息或验证 |
| 会话恢复、早期数据 | 可以提前发送,但有接受、重放与重试边界 |
| 已有连接可复用 | 新建连接成本不再每次支付 |
| 代理还有自己的认证 | 额外控制依赖不能由QUIC握手自动省掉 |
| DNS或节点前置路径未就绪 | 运输握手之前已有等待 |
对短请求,少一次往返可能比减少几十字节更重要;对已经持续传送的大流,初始握手占比可能很小。拿一次新建HTTP/3与一条已热的HTTP/2连接比较,或者反过来,都需要把状态差异明确写出来。
丢包方面,QUIC允许一个流的缺口不阻止另一个已就绪流按字节交付,这是传输服务差异。但拥塞窗口可能因路径反馈整体收缩,QPACK又可能引入表更新依赖,应用也可能在等待共同后端。它避免了一种强制队头阻塞,不承诺所有请求在丢包时都互不影响。
大流吞吐还涉及拥塞算法、接收许可、内核与用户态路径、包批量处理和网卡能力。成熟TCP路径可能受益于操作系统和硬件优化,QUIC实现也可通过批量收发等方法降低成本;具体效果随版本、平台与负载变化。协议格式不能提供一个通用CPU百分比或固定“快多少倍”的结论。
运输尺寸也有约束。HTTP/3一条HEADERS可能很短,经过STREAM帧、QUIC包头、保护标签与UDP/IP封装后,实际数据报更大。QUIC要求路径满足适用最小数据报能力,并进行相应尺寸处理;发送者应避免依赖IP分片来运输过大数据报。把HTTP/2的最大帧尺寸直接当QUIC路径MTU,会把应用分块上限与网络数据报限制混在一起。
UDP可达性是先决条件。有的网络允许TCP网站访问,却限制或整形特定UDP流量;应用若同时实现基于TCP的HTTP路径,可以另行选择回退,但QUIC自身不会把一条UDP连接自动变成TCP。回退是否发生、如何发现失败及何时重试,是应用和实现策略,需要和握手性能分开统计。
迁移改善的是在条件满足时维持会话连续性。新路径可能仍要验证、恢复在途数据和重估发送节奏;若服务端、负载分配或客户端不支持所需条件,业务仍会中断。一次手机切网成功不能证明任意地址变化都可无损迁移。
对代理部署,最终应对齐三项:底层是否允许所选传输、整条链是否提供所需流或数据报服务、实现是否在目标平台上有效处理相应负载。QUIC可以减少某些等待、改善流交付隔离,但不能替代代理认证、目标策略或未提供的网络连通性。
12.9 可靠流与不可靠数据报:同一QUIC连接里的不同服务
QUIC使用UDP承载,基础STREAM却提供可靠有序字节服务;某些实时应用希望相反地保留“不等待旧数据恢复”的行为。RFC 9221因此另定义DATAGRAM扩展,让一条QUIC连接同时运输可靠流和不可靠应用数据报,共享会话与网络拥塞上下文。
使用前,对端要通过max_datagram_frame_size传输参数声明接收能力。缺省值为0,表示不支持;非零值给出愿意接收的最大DATAGRAM帧大小,计入类型、可选长度与负载。它不同于UDP负载最大值,实际数据报还要受QUIC包、路径和其它头部开销约束。RFC 9221 §3。
DATAGRAM有两种帧类型:30不带长度,数据延伸到当前QUIC包末尾;31带QUIC变长整数Length。例如三字节abc:
text
31 03 61 62 63这是五字节明文帧,不含完整包保护;若改用无长度形式,可为 30 61 62 63,但它的终止依赖所在包边界。空数据报允许存在,不能用零长度替代连接关闭。
QUIC层的DATAGRAM没有Stream ID。若应用要区分多个UDP目标、多个逻辑会话或数据种类,就必须在负载里定义自己的关联信息。不能把QUIC可靠流编号自动当作数据报已携带的字段,也不能只看一条QUIC连接就推断它里面只有一个UDP目的地。
| 性质 | QUIC STREAM | QUIC DATAGRAM扩展 |
|---|---|---|
| 交付抽象 | 有序字节流 | 保留边界的应用数据报 |
| 缺失后恢复 | 恢复仍需的流范围 | 不因丢失自动重发原数据报 |
| 关联 | Stream ID与偏移 | 应用负载自行规定会话关联 |
| 接收数据额度 | 流与连接许可 | 不消耗STREAM的这些数据额度 |
| 网络拥塞预算 | 受约束 | 同样受约束 |
DATAGRAM仍是会促使确认的帧。包含它的包可以被ACK,发送者可以据此估计运输结果或向应用报告观察,但丢失后不会为这个不可靠数据报自动恢复原内容。ACK只说明接收运输端处理了相应帧,不保证最终应用成功消费;接收者资源不足时也可能丢弃应用数据报。RFC 9221 §5。
“不重传”不等于“无拥塞控制”。数据报仍共享路径发送预算,过载时必须遵循相关节奏;只是它不使用STREAM那套显式流量额度来保证接收缓存。应用因此需要适当的丢弃、过期和排队策略,而不是把没有接收额度误当无限接收承诺。
一个DATAGRAM帧不能由QUIC自动拆成多个包再可靠重组。若内层应用报文超过可用尺寸,需要应用协议自己处理、调整大小或失败;自己加分片时,又要定义标识、顺序、丢失和资源上限。后文Hysteria2与TUIC会分别分析实际数据报结构,不把一个通用UDP支持开关当作完整机制。
将UDP业务装进可靠QUIC流则是另一种设计。它需要在流里补充报文定界,丢失的旧字节恢复可能推迟后续报文交付;对需要完整到达的数据,这可能符合需求,对过期实时消息则可能增加无意义等待。协议选择改变了内层业务观察到的服务性质,而不只是换了外层端口。
运输能力与安全目标也独立:DATAGRAM可以使用适用的0-RTT或1-RTT保护,不因不可靠就必然明文;可靠STREAM也不替应用说明对端拥有什么权限。至此已经可以区分封装、可靠、认证和保密四类问题。接下来的密码学篇将建立精确安全目标,再解释算法、密钥交换与TLS,最终用于分析VLESS及其现代组合。
本章依据 RFC 9000、RFC 9001、RFC 9002、RFC 9114、RFC 9204 与 RFC 9221。标准向量、构造帧与条件性性能模型均在正文区分,没有把密文外观、一次测量或接口名称当成普遍安全与速度证明。