在HTTP/3中,QPACK负责压缩HTTP头部。它借鉴了HPACK的静态表和动态表思路,但必须适配QUIC的传输特性。QUIC的某个流可以单独被重置,且不同流之间的数据没有固定先后顺序。如果动态表更新放在一条流上,而引用该更新的头部块放在另一条流上,解码器完全可能先收到头部块,后收到动态表插入项。更麻烦的是,携带动态表插入项的流一旦被取消,所有更新都会丢失。这就需要一个明确的丢失检测机制,让编码器知道解码器到底看到了哪些更新,让解码器知道自己还缺哪些更新。

QPACK丢失检测的计数同步模型
QPACK定义了两种单向流:编码器流负责传输动态表插入、容量变更等指令;解码器流负责传输表状态同步、头部块确认等。编码器为每一次动态表插入分配连续递增的插入计数,解码器收到后同样维护总插入计数。为了反馈,解码器可以发送Insert Count Increment指令,其值表示已经连续接收到的插入总数。编码器维护Known Received Count,即它确认解码器已经收到的插入数量。这两个值的差值就是未确认的插入范围。
头部块编码时,编码器会在前缀中写入Required Insert Count,表示解码器为了解码该头部块至少需要具备的动态表条目数。若解码器发现自己当前的插入计数小于Required Insert Count,说明有更新尚未到达或者已经丢失。此时解码器不能直接解码,需要等待缺失的动态表更新,或发送流取消请求。这种机制让丢失检测从被动定时变成了主动比较计数。
需要明确,QUIC流内数据是可靠有序的,所以编码器流上的指令不会出现部分丢失但顺序乱掉的情况。但整条流如果被重置,之前已发送但未被确认的数据就全部失效。丢失检测的重点就是识别这种流级丢失以及计数不连续,而不是处理单个包的丢包重传。
用Ruby实现动态表状态跟踪与丢失判断
在Ruby里,可以设计一个DynamicTable类,内部维护三个关键集合:所有插入条目、未确认的插入条目、以及已知接收计数。每条插入记录至少包含插入序号、名称、值和确认状态。为了简化,可以用Struct定义Entry。
class QPACKDynamicTable
Entry = Struct.new(:insert_count, :name, :value, :confirmed, keyword_init: true)
def initialize(max_capacity: 4096)
@max_capacity = max_capacity
@entries = []
@insert_count = 0
@known_received_count = 0
@unconfirmed = {}
end
def insert(name, value)
@insert_count += 1
entry = Entry.new(
insert_count: @insert_count,
name: name,
value: value,
confirmed: false
)
@entries << entry
@unconfirmed[entry.insert_count] = entry
entry
end
def on_insert_count_increment(increment)
# 解码器反馈: 连续收到的插入总数为 increment
while @known_received_count < increment
@known_received_count += 1
if (entry = @unconfirmed.delete(@known_received_count))
entry.confirmed = true
end
end
end
def missing_updates_after_required(count)
# 如果 required count 大于已知接收计数, 说明存在未确认更新
count > @known_received_count
end
end上面的insert方法为每次动态表插入分配一个递增序号,并放入未确认哈希。on_insert_count_increment模拟解码器发来的Insert Count Increment,逐步推进Known Received Count,把对应条目标记为已确认。missing_updates_after_required则给编码器一个判断入口:准备引用的动态表条目的Required Insert Count如果超过Known Received Count,就不能假设解码器已经拥有这些更新,需要等待或重发。这个简单实现已经能够覆盖大部分丢失检测需求。
但要注意,动态表有容量限制,插入过多条目会触发淘汰。QPACK规定只有在条目被确认后才能安全淘汰。因此还应该在确认后检查容量,执行淘汰逻辑。此外,如果解码器反馈的increment值小于当前的@known_received_count,说明出现重复或过期反馈,可以忽略。实际实现还需要处理编码器流上的Duplicate指令,Duplicate会增加插入计数但不增加新条目,此时未确认追踪需要区分。
结合流取消事件实现安全重发
QUIC允许应用层取消单个流,编码器流如果被取消,所有未确认的动态表更新都会失效。Ruby的QPACK实现通常运行在一个事件循环里,可以监听流的reset或cancel回调。当检测到编码器流被重置时,需要遍历未确认哈希,把这些条目标记为丢失,并从当前动态表中移除引用,然后重新排队发送。这时不能再简单依赖Insert Count Increment,因为解码器可能已经增加过计数,而编码器因为流取消丢失了某些插入,计数器会出现空洞。
处理空洞的一种做法是维护一个待重发队列,不是用原来的插入序号重发,而是重新分配新的插入计数。旧序列号作废,解码器也会在处理新插入时更新自己的总计数。此时编码器需要重新编码之前依赖这些条目的头部块,尤其是那些已经发送但尚未确认的请求流。为了减少重发范围,可以在动态表条目上记录反向引用:哪些头部块使用了该条目。一旦条目丢失,立刻把这些头部块加入重发队列。
def handle_encoder_stream_reset
lost_entries = @unconfirmed.values
affected_blocks = []
lost_entries.each do |entry|
entry.confirmed = false
affected_blocks.concat(entry.referenced_by)
end
@unconfirmed.clear
# 这里 @known_received_count 保持不变, 但后续重发的插入会继续递增
requeue_entries(lost_entries)
requeue_header_blocks(affected_blocks.uniq)
endhandle_encoder_stream_reset中的逻辑体现了丢失检测与恢复的结合。@known_received_count虽然没有变化,但未确认集合被清空,等待重发的插入会分配新序号,从而重新建立计数同步。affected_blocks收集所有受影响的头部块,避免解码器拿着过期的Required Insert Count等待永远不会到达的更新。这个方案比单纯依赖超时重传更精确,因为它直接利用QUIC流取消事件,把丢失检测的时间点从协议层对齐到应用层。
验证Ruby丢失检测逻辑的测试方法
Ruby项目里可以使用minitest或RSpec来测试QPACK动态表的丢失检测。需要模拟两个对象:一个编码器实例和一个解码器实例,让它们通过内存队列交换指令。测试用例可以覆盖正常确认、乱序确认、流取消重发、Required Insert Count不足等场景。重点验证状态机在收到Insert Count Increment后是否正确推进Known Received Count,以及在流取消后未确认集合是否清空并重新入队。
下面给出一个minitest风格的例子,测试正常确认流程。
require 'minitest/autorun'
class DynamicTableTest < Minitest::Test
def test_confirm_updates_in_order
table = QPACKDynamicTable.new
e1 = table.insert('accept', 'text/html')
e2 = table.insert('user-agent', 'ruby-client')
table.on_insert_count_increment(1)
assert_equal 1, table.instance_variable_get(:@known_received_count)
assert e1.confirmed
assert_equal false, e2.confirmed
table.on_insert_count_increment(2)
assert_equal 2, table.instance_variable_get(:@known_received_count)
assert e2.confirmed
assert_equal({}, table.instance_variable_get(:@unconfirmed))
end
end测试中通过instance_variable_get读取内部状态,实际生产代码可以暴露只读访问器。还需要补充一个测试,模拟编码器流在插入两个条目后立即被取消,确认两个条目都被重新排队,且affected_blocks不再为空。只有覆盖这些边界情况,丢失检测逻辑才能在真实HTTP/3连接中稳定工作。经过这样验证后,再与Ruby的QUIC栈对接时会更有把握。