http2头部压缩算法如何实现
http2头部压缩算法如何实现
为什么需要头部压缩
在HTTP/1.1中,是不会对头部信息进行压缩的,每个请求和响应都携带着完整的头部信息,并且这些头部信息包含大量的重复数据。例如:
User-Agent
、Accept
、Accept-Language
等头部在同一个tcp连接的多个请求中基本相同- Cookie信息可能非常庞大,且在多个请求中重复出现
- 头部信息都是纯文本格式,没有进行任何压缩
按照http1.1的方式,头部数据可能占据数据传输的很大比例,甚至可能超过实际传输的数据量,这种开销会显著影响性能,占据很大一部分网络带宽。
HTTP/2通过HPACK算法解决了这个问题,它可以将头部压缩率提高到85%以上,大大减少了网络传输开销。
如何实现头部压缩
如果要设计一个头部压缩算法,可以从以下几个角度思考:
1. 字典压缩思路
- 静态字典:预先定义一张表,包含最常用的头部字段名和值,用索引代替完整字符串
- 动态字典:在通信过程中动态维护一张表,存储最近使用过的头部字段,实现更高的压缩率
2. 增量编码思路
- 只传输与之前请求不同的部分
- 对于相同的头部字段,只需要传输一个引用标识
3. 字符串压缩思路
- 对于必须传输的完整字符串,使用Huffman编码等技术进行进一步压缩
- 针对HTTP头部的特点优化编码表
4. 安全性考虑
- 某些敏感头部(如Authorization)不应该被索引,避免安全问题
- 标记敏感字段,而且不会将这些敏感字段存储在动态表中
HPack算法
HPACK(HTTP/2 Header Compression)算法根据RFC 7541的定义,是一种专为HTTP/2设计的高效头部字段压缩格式。它结合了索引表、Huffman编码和增量编码三种核心技术来实现头部压缩。
HPACK的设计目标
根据RFC 7541,HPACK的设计主要解决以下问题:
- 压缩效率:显著减少HTTP头部的传输大小
- 实现简单性:算法复杂度适中,便于实现
- 安全性:避免基于压缩的安全攻击
- 低内存需求:限制压缩上下文的内存占用
- 快速解码:支持增量和流式解码
静态表(Static Table)
静态表是HPACK的基础组件,包含61个预定义的HTTP头部字段。根据RFC 7541附录A的定义:
// RFC 7541附录A:静态表定义(部分)
private static final List<HeaderField> STATIC_TABLE = Arrays.asList(
/* 1 */ new HeaderField(":authority", ""),
/* 2 */ new HeaderField(":method", "GET"),
/* 3 */ new HeaderField(":method", "POST"),
/* 4 */ new HeaderField(":path", "/"),
/* 5 */ new HeaderField(":path", "/index.html"),
/* 6 */ new HeaderField(":scheme", "http"),
/* 7 */ new HeaderField(":scheme", "https"),
/* 8 */ new HeaderField(":status", "200"),
/* 9 */ new HeaderField(":status", "204"),
/* 10 */ new HeaderField(":status", "206"),
/* 11 */ new HeaderField(":status", "304"),
/* 12 */ new HeaderField(":status", "400"),
/* 13 */ new HeaderField(":status", "404"),
/* 14 */ new HeaderField(":status", "500"),
/* 15 */ new HeaderField("accept-charset", ""),
/* 16 */ new HeaderField("accept-encoding", "gzip, deflate"),
/* 17 */ new HeaderField("accept-language", ""),
/* 18 */ new HeaderField("accept-ranges", ""),
/* 19 */ new HeaderField("accept", ""),
/* 20 */ new HeaderField("access-control-allow-origin", ""),
// ... 共61个条目
);
静态表的索引机制:
// 精确匹配:name和value都匹配
static int getIndex(byte[] name, byte[] value) {
// 首先查找name匹配的起始索引
int index = getIndex(name);
if (index == -1) {
return -1;
}
// 查找完全匹配的name-value对
while (index <= STATIC_TABLE.size()) {
HeaderField entry = getEntry(index);
if (!equals(name, entry.name)) {
break; // name不再匹配,停止搜索
}
if (equals(value, entry.value)) {
return index; // 找到完全匹配
}
index++;
}
return -1; // 没有找到完全匹配
}
动态表(Dynamic Table)
动态表是HPACK的核心创新,根据RFC 7541第2.3节的定义,它维护一个先进先出(FIFO)的头部字段列表。
动态表的关键特性:
- 表大小计算(RFC 7541第4.1节):
// 每个条目的大小 = name长度 + value长度 + 32字节开销
int entrySize = name.length + value.length + 32;
- 容量管理(RFC 7541第4.2节):
public void add(HeaderField header) {
int headerSize = header.size();
// 如果单个条目超过表容量,清空整个表
if (headerSize > capacity) {
clear();
return;
}
// 驱逐旧条目直到有足够空间
while (size + headerSize > capacity) {
remove(); // 从表尾删除最老的条目
}
// 在表头添加新条目
headerFields[head++] = header;
size += headerSize;
// 循环数组处理
if (head == headerFields.length) {
head = 0;
}
}
- 索引编址(RFC 7541第2.3.3节):
- 静态表索引:1 到 61
- 动态表索引:62 到 (62 + 动态表长度 - 1)
- 动态表中最新插入的条目索引为62
Huffman编码
HPACK使用RFC 7541附录B定义的静态Huffman编码表,该编码表针对HTTP头部的字符分布进行了优化。
编码实现(基于RFC 7541第5.2节):
public void encode(OutputStream out, byte[] data, int off, int len) throws IOException {
long current = 0;
int n = 0; // 当前累积的位数
for (int i = 0; i < len; i++) {
int symbol = data[off + i] & 0xFF;
int code = huffmanCodes[symbol]; // 获取符号的Huffman编码
int nbits = huffmanLengths[symbol]; // 获取编码位数
// 将新编码添加到累积器
current <<= nbits;
current |= code;
n += nbits;
// 输出完整的字节
while (n >= 8) {
n -= 8;
out.write((int)(current >> n));
}
}
// 处理剩余位数(用EOS符号填充)
if (n > 0) {
current <<= (8 - n);
current |= (0xFF >>> n); // EOS符号填充
out.write((int)current);
}
}
Huffman编码的优势:
- 最常见字符(如空格、字母、数字)使用5-6位编码
- 不常见字符使用更长编码(最长30位)
- 平均压缩率约15-20%
头部字段表示形式
根据RFC 7541第6节,HPACK定义了四种头部字段表示形式:
1. 索引头部字段(Indexed Header Field)
格式:1xxxxxxx(最高位为1)
// 完全索引:直接引用表中的条目
if (tableIndex != -1) {
// 输出:1 + 7位索引
encodeInteger(out, 0x80, 7, tableIndex);
}
2. 增量索引字面量(Literal Header Field with Incremental Indexing)
格式:01xxxxxx(前两位为01)
// 新头部字段,添加到动态表
encodeLiteral(out, name, value, IndexType.INCREMENTAL, nameIndex);
// 模式:01 + 6位名称索引(或0表示新名称)+ 字符串值
3. 非索引字面量(Literal Header Field without Indexing)
格式:0000xxxx(前四位为0000)
// 不添加到动态表的字面量
encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
// 模式:0000 + 4位名称索引 + 字符串值
4. 永不索引字面量(Literal Header Field Never Indexed)
格式:0001xxxx(前四位为0001)
// 敏感信息,永远不加入动态表
encodeLiteral(out, name, value, IndexType.NEVER, nameIndex);
// 模式:0001 + 4位名称索引 + 字符串值
整数编码(Integer Representation)
RFC 7541第5.1节定义了前缀整数编码:
// N位前缀的整数编码
private static void encodeInteger(OutputStream out, int mask, int N, int I) throws IOException {
int limit = (1 << N) - 1; // 2^N - 1
if (I < limit) {
// 小整数:直接编码在前缀中
out.write(mask | I);
} else {
// 大整数:需要多字节编码
out.write(mask | limit);
I = I - limit;
// 128进制变长编码
while (I >= 128) {
out.write((I % 128) + 128);
I = I / 128;
}
out.write(I);
}
}
字符串字面量编码
根据RFC 7541第5.2节,字符串可以选择性地使用Huffman编码:
private void encodeStringLiteral(OutputStream out, byte[] string) throws IOException {
// 计算Huffman编码后的长度
int huffmanLength = Huffman.ENCODER.getEncodedLength(string);
if (huffmanLength < string.length) {
// Huffman编码更短,使用压缩
encodeInteger(out, 0x80, 7, huffmanLength); // H=1标志
Huffman.ENCODER.encode(out, string);
} else {
// 原始编码更短
encodeInteger(out, 0x00, 7, string.length); // H=0标志
out.write(string);
}
}
编码过程详解
1. 数据结构
public final class Encoder {
// 哈希表,用于快速查找动态表条目
private final HeaderEntry[] headerFields = new HeaderEntry[BUCKET_SIZE];
// 双向链表头节点,用于维护插入顺序
private final HeaderEntry head = new HeaderEntry(-1, EMPTY, EMPTY, Integer.MAX_VALUE, null);
// 动态表当前大小和容量
private int size;
private int capacity;
// 配置选项
private final boolean useIndexing; // 是否使用动态表索引
private final boolean forceHuffmanOn; // 强制使用Huffman编码
private final boolean forceHuffmanOff; // 强制不使用Huffman编码
}
2. HeaderEntry结构
private static class HeaderEntry extends HeaderField {
HeaderEntry before, after; // 双向链表指针
HeaderEntry next; // 哈希冲突链表指针
int hash; // 哈希值
int index; // 在动态表中的索引
}
编码流程图
编码过程时序图
关键方法
1. encodeHeader() - 头部编码方法
public void encodeHeader(OutputStream out, byte[] name, byte[] value, boolean sensitive) throws IOException {
// 步骤1: 敏感头部检查
if (sensitive) {
int nameIndex = getNameIndex(name);
encodeLiteral(out, name, value, IndexType.NEVER, nameIndex);
return;
}
// 步骤2: 动态表容量检查
if (capacity == 0) {
// 只使用静态表的逻辑
int staticTableIndex = StaticTable.getIndex(name, value);
if (staticTableIndex == -1) {
int nameIndex = StaticTable.getIndex(name);
encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
} else {
encodeInteger(out, 0x80, 7, staticTableIndex);
}
return;
}
// 步骤3: 头部大小检查
int headerSize = HeaderField.sizeOf(name, value);
if (headerSize > capacity) {
int nameIndex = getNameIndex(name);
encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
return;
}
// 步骤4: 动态表查找
HeaderEntry headerField = getEntry(name, value);
if (headerField != null) {
// 动态表完全匹配
int index = getIndex(headerField.index) + StaticTable.length;
encodeInteger(out, 0x80, 7, index);
} else {
// 步骤5: 静态表查找
int staticTableIndex = StaticTable.getIndex(name, value);
if (staticTableIndex != -1) {
encodeInteger(out, 0x80, 7, staticTableIndex);
} else {
// 步骤6: 编码为字面量
int nameIndex = getNameIndex(name);
if (useIndexing) {
ensureCapacity(headerSize);
}
IndexType indexType = useIndexing ? IndexType.INCREMENTAL : IndexType.NONE;
encodeLiteral(out, name, value, indexType, nameIndex);
if (useIndexing) {
add(name, value);
}
}
}
}
2. encodeLiteral() - 字面量编码
private void encodeLiteral(OutputStream out, byte[] name, byte[] value, IndexType indexType, int nameIndex)
throws IOException {
// 根据索引类型确定掩码和前缀位数
int mask, prefixBits;
switch(indexType) {
case INCREMENTAL: // 01xxxxxx
mask = 0x40; prefixBits = 6; break;
case NONE: // 0000xxxx
mask = 0x00; prefixBits = 4; break;
case NEVER: // 0001xxxx
mask = 0x10; prefixBits = 4; break;
}
// 编码名称索引(0表示新名称)
encodeInteger(out, mask, prefixBits, nameIndex == -1 ? 0 : nameIndex);
// 如果没有名称索引,编码完整名称
if (nameIndex == -1) {
encodeStringLiteral(out, name);
}
// 编码值
encodeStringLiteral(out, value);
}
3. encodeStringLiteral() - 字符串编码
private void encodeStringLiteral(OutputStream out, byte[] string) throws IOException {
// 计算Huffman编码长度
int huffmanLength = Huffman.ENCODER.getEncodedLength(string);
// 选择最优编码方式
if ((huffmanLength < string.length && !forceHuffmanOff) || forceHuffmanOn) {
// 使用Huffman编码
encodeInteger(out, 0x80, 7, huffmanLength); // H=1
Huffman.ENCODER.encode(out, string);
} else {
// 使用原始编码
encodeInteger(out, 0x00, 7, string.length); // H=0
out.write(string, 0, string.length);
}
}
4. 动态表管理
查找操作
private HeaderEntry getEntry(byte[] name, byte[] value) {
if (length() == 0 || name == null || value == null) {
return null;
}
int h = hash(name); // 计算哈希值
int i = index(h); // 计算桶索引
// 在哈希桶中查找
for (HeaderEntry e = headerFields[i]; e != null; e = e.next) {
if (e.hash == h &&
HpackUtil.equals(name, e.name) &&
HpackUtil.equals(value, e.value)) {
return e;
}
}
return null;
}
添加操作
private void add(byte[] name, byte[] value) {
int headerSize = HeaderField.sizeOf(name, value);
// 检查容量
if (headerSize > capacity) {
clear();
return;
}
// 驱逐旧条目
while (size + headerSize > capacity) {
remove();
}
// 创建新条目
name = Arrays.copyOf(name, name.length); // 深拷贝
value = Arrays.copyOf(value, value.length);
int h = hash(name);
int i = index(h);
HeaderEntry old = headerFields[i];
HeaderEntry e = new HeaderEntry(h, name, value, head.before.index - 1, old);
// 更新哈希表
headerFields[i] = e;
// 更新双向链表(维护FIFO顺序)
e.addBefore(head);
size += headerSize;
}
编码输出格式
1. 索引头部字段
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 1 | Index (7+) |
+---+---------------------------+
2. 增量索引字面量
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | Index (6+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
3. 非索引字面量
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | Index (4+) |
+---+---+---+---+---------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
4. 永不索引字面量
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | Index (4+) |
+---+---+---+---+---------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
性能优化点
- 双重索引结构:使用哈希表快速查找 + 双向链表维护顺序
- 智能Huffman编码:动态选择最优编码方式
- 容量预检查:避免不必要的动态表操作
- 深拷贝保护:防止外部修改影响动态表状态
- 分层查找策略:动态表 → 静态表 → 字面量编码
解码过程详解
1. 数据结构
public final class Decoder {
// 动态表,与编码器保持同步
private final DynamicTable dynamicTable;
// 解码状态
private State state;
private IndexType indexType;
private int index;
private boolean huffmanEncoded;
private byte[] name;
// 大小限制
private int maxHeaderSize;
private int maxDynamicTableSize;
private int encoderMaxDynamicTableSize;
// 当前解码进度
private long headerSize;
private int nameLength;
private int valueLength;
private int skipLength;
}
2. 状态机枚举
private enum State {
READ_HEADER_REPRESENTATION, // 读取头部表示类型
READ_MAX_DYNAMIC_TABLE_SIZE, // 读取动态表大小更新
READ_INDEXED_HEADER, // 读取索引头部字段
READ_INDEXED_HEADER_NAME, // 读取索引头部名称
READ_LITERAL_HEADER_NAME_LENGTH_PREFIX, // 读取字面量名称长度前缀
READ_LITERAL_HEADER_NAME_LENGTH, // 读取字面量名称长度
READ_LITERAL_HEADER_NAME, // 读取字面量名称
SKIP_LITERAL_HEADER_NAME, // 跳过字面量名称
READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX, // 读取字面量值长度前缀
READ_LITERAL_HEADER_VALUE_LENGTH, // 读取字面量值长度
READ_LITERAL_HEADER_VALUE, // 读取字面量值
SKIP_LITERAL_HEADER_VALUE // 跳过字面量值
}
解码器状态机流程图
解码过程时序图
关键方法
1. decode() - 主解码方法
public void decode(InputStream in, HeaderListener headerListener) throws IOException {
while (in.available() > 0) {
switch(state) {
case READ_HEADER_REPRESENTATION:
byte b = (byte) in.read();
// 检查动态表大小变更要求
if (maxDynamicTableSizeChangeRequired && (b & 0xE0) != 0x20) {
throw MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED;
}
if (b < 0) {
// 索引头部字段 (1xxxxxxx)
index = b & 0x7F;
if (index == 0) {
throw ILLEGAL_INDEX_VALUE;
} else if (index == 0x7F) {
state = State.READ_INDEXED_HEADER; // 需要读取更多字节
} else {
indexHeader(index, headerListener);
}
} else if ((b & 0x40) == 0x40) {
// 增量索引字面量 (01xxxxxx)
indexType = IndexType.INCREMENTAL;
index = b & 0x3F;
processLiteralHeader(index);
} else if ((b & 0x20) == 0x20) {
// 动态表大小更新 (001xxxxx)
index = b & 0x1F;
processDynamicTableSizeUpdate(index);
} else {
// 非索引/永不索引字面量 (0000xxxx/0001xxxx)
indexType = ((b & 0x10) == 0x10) ? IndexType.NEVER : IndexType.NONE;
index = b & 0x0F;
processLiteralHeader(index);
}
break;
// 其他状态处理...
}
}
}
2. indexHeader() - 索引头部解码
private void indexHeader(int index, HeaderListener headerListener) throws IOException {
if (index <= StaticTable.length) {
// 静态表查找
HeaderField headerField = StaticTable.getEntry(index);
addHeader(headerListener, headerField.name, headerField.value, false);
} else if (index - StaticTable.length <= dynamicTable.length()) {
// 动态表查找
HeaderField headerField = dynamicTable.getEntry(index - StaticTable.length);
addHeader(headerListener, headerField.name, headerField.value, false);
} else {
throw ILLEGAL_INDEX_VALUE;
}
}
3. readStringLiteral() - 字符串解码
private byte[] readStringLiteral(InputStream in, int length) throws IOException {
byte[] buf = new byte[length];
if (in.read(buf) != length) {
throw DECOMPRESSION_EXCEPTION;
}
if (huffmanEncoded) {
// Huffman解码
return Huffman.DECODER.decode(buf);
} else {
// 原始字节
return buf;
}
}
4. insertHeader() - 字面量头部处理
private void insertHeader(HeaderListener headerListener, byte[] name, byte[] value, IndexType indexType) {
// 添加到输出
addHeader(headerListener, name, value, indexType == IndexType.NEVER);
// 根据索引类型决定是否添加到动态表
switch (indexType) {
case NONE:
case NEVER:
break; // 不添加到动态表
case INCREMENTAL:
dynamicTable.add(new HeaderField(name, value)); // 添加到动态表
break;
}
}
Huffman解码过程
Huffman解码器结构
Huffman解码算法
public byte[] decode(byte[] buf) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Node node = root;
int current = 0;
int bits = 0;
for (int i = 0; i < buf.length; i++) {
int b = buf[i] & 0xFF;
current = (current << 8) | b; // 累积位
bits += 8;
while (bits >= 8) {
int c = (current >>> (bits - 8)) & 0xFF; // 取高8位
node = node.children[c]; // 树遍历
bits -= node.bits; // 消耗匹配的位数
if (node.isTerminal()) {
if (node.symbol == HUFFMAN_EOS) {
throw EOS_DECODED;
}
baos.write(node.symbol); // 输出符号
node = root; // 重置到根节点
}
}
}
// 处理剩余位(填充检查)
while (bits > 0) {
int c = (current << (8 - bits)) & 0xFF;
node = node.children[c];
if (node.isTerminal() && node.bits <= bits) {
bits -= node.bits;
baos.write(node.symbol);
node = root;
} else {
break;
}
}
// 验证填充
int mask = (1 << bits) - 1;
if ((current & mask) != mask) {
throw INVALID_PADDING;
}
return baos.toByteArray();
}
解码示例分析
示例:解码压缩的HTTP响应头部
假设我们收到以下编码数据(十六进制):
88 C1 61 96 D0 7A BE 94 10 54 D4 44 A8 20 05 95 04 0B 81 66 E0 84 A6 2D 1B FF
逐步解码过程
字节1: 88 (10001000)
// 1xxxxxxx -> 索引头部字段
// index = 8 -> 静态表索引8 -> :status: 200
indexHeader(8, headerListener);
// 输出: :status: 200
字节2: C1 (11000001)
// 1xxxxxxx -> 索引头部字段
// index = 65 -> 动态表索引(65-61=4) -> cache-control: private
indexHeader(65, headerListener);
// 输出: cache-control: private
字节3: 61 (01100001)
// 01xxxxxx -> 增量索引字面量
// indexType = INCREMENTAL
// index = 33 -> 静态表索引33 -> date
readName(33); // name = "date"
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
字节4: 96 (10010110)
// 读取值长度前缀
// huffmanEncoded = true (最高位为1)
// valueLength = 22
state = READ_LITERAL_HEADER_VALUE;
字节5-26: Huffman编码的日期值
// 读取22字节的Huffman编码数据
byte[] huffmanData = readBytes(22);
byte[] value = Huffman.DECODER.decode(huffmanData);
// 解码结果: "Mon, 21 Oct 2013 20:13:22 GMT"
insertHeader(headerListener, "date", value, IndexType.INCREMENTAL);
// 输出: date: Mon, 21 Oct 2013 20:13:22 GMT
// 添加到动态表索引62
动态表同步机制
编码器-解码器状态同步
错误处理机制
1. 大小限制检查
private boolean exceedsMaxHeaderSize(long size) {
if (size + headerSize <= maxHeaderSize) {
return false;
}
// 标记截断,在endHeaderBlock时报告
headerSize = maxHeaderSize + 1;
return true;
}
2. 索引越界检查
private void indexHeader(int index, HeaderListener headerListener) throws IOException {
if (index <= StaticTable.length) {
// 静态表范围内
} else if (index - StaticTable.length <= dynamicTable.length()) {
// 动态表范围内
} else {
throw ILLEGAL_INDEX_VALUE; // 索引超出范围
}
}
3. 数值溢出保护
// 检查数值溢出
if (maxSize > Integer.MAX_VALUE - index) {
throw DECOMPRESSION_EXCEPTION;
}
HPACK解码器使用状态机处理输入流(RFC 7541第4节):
public void decode(InputStream in, HeaderListener headerListener) throws IOException {
while (in.available() > 0) {
switch(state) {
case READ_HEADER_REPRESENTATION:
byte b = (byte) in.read();
if ((b & 0x80) == 0x80) {
// 索引头部字段 (1xxxxxxx)
index = b & 0x7F;
if (index == 0x7F) {
state = State.READ_INDEXED_HEADER; // 需要读取更多字节
} else {
indexHeader(index, headerListener);
state = State.READ_HEADER_REPRESENTATION;
}
} else if ((b & 0x40) == 0x40) {
// 增量索引字面量 (01xxxxxx)
indexType = IndexType.INCREMENTAL;
index = b & 0x3F;
processLiteralHeader(index);
} else if ((b & 0x20) == 0x20) {
// 动态表大小更新 (001xxxxx)
index = b & 0x1F;
processDynamicTableSizeUpdate(index);
} else {
// 非索引字面量 (0000xxxx 或 0001xxxx)
indexType = ((b & 0x10) == 0x10) ? IndexType.NEVER : IndexType.NONE;
index = b & 0x0F;
processLiteralHeader(index);
}
break;
// 其他状态处理...
}
}
}
动态表大小管理
RFC 7541第4.2节详细说明了动态表大小管理:
// 设置动态表最大大小
public void setDynamicTableSize(int maxSize) throws IOException {
if (maxSize < 0) {
throw new IllegalArgumentException("Invalid size: " + maxSize);
}
this.maxDynamicTableSize = maxSize;
// 如果当前大小超过新限制,需要驱逐条目
evictToFit(0); // 驱逐直到大小满足限制
// 通知对端大小变更
encodeInteger(outputStream, 0x20, 5, maxSize);
}
// 驱逐条目以适应大小限制
private void evictToFit(int additionalSize) {
int targetSize = maxDynamicTableSize - additionalSize;
while (currentDynamicTableSize > targetSize && !isEmpty()) {
HeaderField evicted = removeOldest();
currentDynamicTableSize -= evicted.size();
}
}
HPACK的问题与QPACK的改进
虽然HPACK在HTTP/2中表现良好,但当应用到HTTP/3时暴露出一个关键问题:队头阻塞(Head-of-Line Blocking)。
HPACK存在的核心问题
1. 动态表状态依赖问题
在HTTP/2中,HPACK的动态表更新是严格有序的:
- 所有头部字段必须按照接收顺序进行解码
- 动态表的状态必须在编码器和解码器之间保持完全同步
- 如果某个数据包延迟或丢失,后续所有头部解码都会被阻塞
问题示例:
请求1: 添加 "custom-header: value1" 到动态表索引62
请求2: 引用动态表索引62
请求3: 引用动态表索引62
如果请求1的数据包丢失,请求2和请求3都无法解码!
2. 流阻塞传播
HTTP/3基于QUIC协议,不同流之间本来是独立的,但HPACK的设计导致:
- 一个流的头部解码失败会影响所有后续流
- 动态表状态的不一致会在所有流之间传播
- 这违背了HTTP/3流独立性的设计原则
QPACK的创新解决方案
QPACK(RFC9204)作为HPACK的改进版本,专门为HTTP/3设计,解决了上述问题:
1. 引入独立的编码器和解码器流
QPACK将动态表操作从数据流中分离:
HPACK架构:
HTTP请求流 = 头部数据 + 动态表操作(混合在一起)
QPACK架构:
编码器流:专门用于动态表插入操作
解码器流:专门用于确认和取消操作
数据流:只包含头部引用,不包含动态表操作
2. 异步动态表更新
QPACK允许动态表更新与头部解码异步进行:
// QPACK伪代码示例
class QPACKDecoder {
// 可以独立处理数据流,不必等待动态表同步
void decodeHeaders(Stream dataStream) {
if (canDecodeWithCurrentTable(dataStream)) {
// 立即解码
decodeImmediately(dataStream);
} else {
// 标记为阻塞,等待动态表更新
markAsBlocked(dataStream);
}
}
// 从编码器流接收动态表更新
void processDynamicTableUpdate(EncoderStream encoderStream) {
updateDynamicTable(encoderStream);
// 尝试解码之前阻塞的流
processBlockedStreams();
}
}
3. 流的独立性保证
QPACK通过以下机制确保流的独立性:
基点(Base)机制:
- 每个编码的头部块都有一个基点,表示依赖的动态表状态
- 只有当动态表达到相应状态时,才解码该头部块
- 不同流可以有不同的基点,实现独立解码
插入计数(Insert Count):
// QPACK的插入计数机制
class FieldBlock {
int requiredInsertCount; // 需要的动态表插入计数
int base; // 基点
byte[] encodedFields; // 编码的字段数据
boolean canDecode(int currentInsertCount) {
return currentInsertCount >= requiredInsertCount;
}
}
4. 风险评估与选择性索引
QPACK引入了更智能的索引策略:
阻塞风险评估:
// QPACK编码器的智能决策
class QPACKEncoder {
void encodeField(String name, String value, int streamId) {
if (isHighRiskOfBlocking(streamId)) {
// 使用静态表或字面量,避免阻塞
encodeWithoutDynamicTable(name, value);
} else {
// 可以安全使用动态表
encodeWithDynamicTable(name, value);
}
}
boolean isHighRiskOfBlocking(int streamId) {
// 考虑网络延迟、流优先级等因素
return networkLatency > threshold || isHighPriorityStream(streamId);
}
}
HPACK与QPACK的核心差异对比
特性 | HPACK (HTTP/2) | QPACK (HTTP/3) |
---|---|---|
动态表更新方式 | 内联在头部块中 | 独立的编码器流 |
解码依赖性 | 严格顺序依赖 | 基于插入计数的条件依赖 |
流阻塞影响范围 | 影响所有后续流 | 仅影响依赖相同动态表状态的流 |
错误恢复能力 | 错误会传播到所有流 | 流之间相互独立 |
压缩效率 | 高(但有阻塞风险) | 高(通过智能策略平衡) |
实现复杂度 | 中等 | 较高(需要管理多个流) |
QPACK的技术优势
- 消除队头阻塞:流之间真正独立,一个流的问题不会影响其他流
- 更好的并发性:可以并行处理多个流的头部解码
- 网络友好:适应网络延迟和数据包丢失
- 向后兼容:保留了HPACK的核心压缩技术
- 智能化:根据网络状况和流特性动态调整压缩策略
实际应用中的改进效果
根据RFC9204的设计目标,QPACK在实际应用中带来了显著改进:
- 延迟降低:消除了因动态表同步导致的额外延迟
- 吞吐量提升:并行处理能力显著提高
- 可靠性增强:单个流的错误不再影响整个连接
- 移动网络友好:更好地适应高延迟、高丢包率的移动网络环境