OkHttp3中的HTTP/2首部壓縮
當前網路環境中,同一個頁面發出幾十個HTTP請求已經是司空見慣的事情了。在HTTP/1.1中,請求之間完全相互獨立,使得請求中冗餘的首部欄位不必要地浪費了大量的網路頻寬,並增加了網路延時。以對某站點的一次頁面訪問為例,直觀地看一下這種狀況:
Header 1
Header 2
如上圖,同一個頁面中對兩個資源的請求,請求中的頭部欄位絕大部分是完全相同的。"User-Agent" 等頭部欄位通常還會消耗大量的頻寬。
首部壓縮正是為了解決這樣的問題而設計。
首部壓縮是HTTP/2中一個非常重要的特性,它大大減少了網路中HTTP請求/響應頭部傳輸所需的頻寬。HTTP/2的首部壓縮,主要從兩個方面實現,一是首部表示,二是請求間首部欄位內容的複用。
首部表示
在HTTP中,首部欄位是一個名值隊,所有的首部欄位組成首部欄位列表。在HTTP/1.x中,首部欄位都被表示為字串,一行一行的首部欄位字串組成首部欄位列表。而在HTTP/2的首部壓縮HPACK演算法中,則有著不同的表示方法。
HPACK演算法表示的物件,主要有原始資料型別的整型值和字串,頭部欄位,以及頭部欄位列表。
整數的表示
在HPACK中,整數用於表示 頭部欄位的名字的索引,頭部欄位索引 或 字串長度。整數的表示可在位元組內的任何位置開始。但為了處理上的優化,整數的表示總是在位元組的結尾處結束。
整數由兩部分表示:填滿當前位元組的字首,以及在字首不足以表示整數時的一個可選位元組列表。如果整數值足夠小,比如,小於2^N-1,那麼把它編碼進字首即可,而不需要額外的空間。如:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| ? | ? | ? | Value |
+---+---+---+-------------------+
在這個圖中,字首有5位,而要表示的數足夠小,因此無需更多空間就可以表示整數了。
當前綴不足以表示整數時,字首的所有位被置為1,再將值減去2^N-1之後用一個或多個位元組編碼。每個位元組的最高有效位被用作連續標記:除列表的最後一個位元組外,該位的值都被設為1。位元組中剩餘的位被用於編碼減小後的值。
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | ? | ? | ? | 1 1 1 1 1 | +---+---+---+-------------------+ | 1 | Value-(2^N-1) LSB | +---+---------------------------+ ... +---+---------------------------+ | 0 | Value-(2^N-1) MSB | +---+---------------------------+
要由位元組列表解碼出整數值,首先需要將列表中的位元組順序反過來。然後,移除每個位元組的最高有效位。連線位元組的剩餘位,再將結果加2^N-1獲得整數值。
字首大小N,總是在1到8之間。從位元組邊界處開始編碼的整數值其字首為8位。
這種整數表示法允許編碼無限大的值。
表示整數I的虛擬碼如下:
if I < 2^N - 1, encode I on N bits
else
encode (2^N - 1) on N bits
I = I - (2^N - 1)
while I >= 128
encode (I % 128 + 128) on 8 bits
I = I / 128
encode I on 8 bits
encode (I % 128 + 128) on 8 bits 一行中,加上128的意思是,最高有效位是1。如果要編碼的整數值等於 (2^N - 1),則用字首和緊跟在字首後面的值位0的一個位元組來表示。
OkHttp中,這個演算法的實現在 okhttp3.internal.http2.Hpack.Writer 中:
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-4.1.1
void writeInt(int value, int prefixMask, int bits) {
// Write the raw value for a single byte value.
if (value < prefixMask) {
out.writeByte(bits | value);
return;
}
// Write the mask to start a multibyte value.
out.writeByte(bits | prefixMask);
value -= prefixMask;
// Write 7 bits at a time 'til we're done.
while (value >= 0x80) {
int b = value & 0x7f;
out.writeByte(b | 0x80);
value >>>= 7;
}
out.writeByte(value);
}
這裡給最高有效位置 1 的方法就不是加上128,而是與0x80執行或操作。
解碼整數I的虛擬碼如下:
decode I from the next N bits
if I < 2^N - 1, return I
else
M = 0
repeat
B = next octet
I = I + (B & 127) * 2^M
M = M + 7
while B & 128 == 128
return I
decode I from the next N bits 這一行等價於一個賦值語句 *I = byteValue & (2^N - 1)
OkHttp中,這個演算法的實現在 okhttp3.internal.http2.Hpack.Reader :
int readInt(int firstByte, int prefixMask) throws IOException {
int prefix = firstByte & prefixMask;
if (prefix < prefixMask) {
return prefix; // This was a single byte value.
}
// This is a multibyte value. Read 7 bits at a time.
int result = prefixMask;
int shift = 0;
while (true) {
int b = readByte();
if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255].
result += (b & 0x7f) << shift;
shift += 7;
} else {
result += b << shift; // Last byte.
break;
}
}
return result;
}
儘管HPACK的整數表示方法可以表示無限大的數,但實際的實現中並不會將整數當做無限大的整數來處理。
字串字面量的編碼
頭部欄位名和頭部欄位值可使用字串字面量表示。字串字面量有兩種表示方式,一種是直接用UTF-8這樣的字串編碼方式表示,另一種是將字串編碼用Huffman 碼錶示。 字串表示的格式如下:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| H | String Length (7+) |
+---+---------------------------+
| String Data (Length octets) |
+-------------------------------+
先是標記位 H + 字串長度,然後是字串的實際資料。各部分說明如下:
- H: 一位的標記,指示字串的位元組是否為Huffman編碼。
- 字串長度: 編碼字串字面量的位元組數,一個整數,編碼方式可以參考前面 整數的表示 的部分,一個7位字首的整數編碼。
- 字串資料: 字串的實際資料。如果H是'0',則資料是字串字面量的原始位元組。如果H是'1',則資料是字串字面量的Huffman編碼。
在OkHttp3中,總是會使用直接的字串編碼,而不是Huffman編碼, okhttp3.internal.http2.Hpack.Writer 中編碼字串的過程如下:
void writeByteString(ByteString data) throws IOException {
writeInt(data.size(), PREFIX_7_BITS, 0);
out.write(data);
}
OkHttp中,解碼字串在 okhttp3.internal.http2.Hpack.Reader 中實現:
/** Reads a potentially Huffman encoded byte string. */
ByteString readByteString() throws IOException {
int firstByte = readByte();
boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN
int length = readInt(firstByte, PREFIX_7_BITS);
if (huffmanDecode) {
return ByteString.of(Huffman.get().decode(source.readByteArray(length)));
} else {
return source.readByteString(length);
}
}
字串編碼沒有使用Huffman編碼時,解碼過程比較簡單,而使用了Huffman編碼時會藉助於Huffman類來解碼。
Huffman編碼是一種變長位元組編碼,對於使用頻率高的位元組,使用更少的位數,對於使用頻率低的位元組則使用更多的位數。每個位元組的Huffman碼是根據統計經驗值分配的。為每個位元組分配Huffman碼的方法可以參考 哈夫曼(huffman)樹和哈夫曼編碼 。
哈夫曼樹的構造
Huffman 類被設計為一個單例類。物件在建立時構造一個哈夫曼樹以用於編碼和解碼操作。
private static final Huffman INSTANCE = new Huffman();
public static Huffman get() {
return INSTANCE;
}
private final Node root = new Node();
private Huffman() {
buildTree();
}
......
private void buildTree() {
for (int i = 0; i < CODE_LENGTHS.length; i++) {
addCode(i, CODES[i], CODE_LENGTHS[i]);
}
}
private void addCode(int sym, int code, byte len) {
Node terminal = new Node(sym, len);
Node current = root;
while (len > 8) {
len -= 8;
int i = ((code >>> len) & 0xFF);
if (current.children == null) {
throw new IllegalStateException("invalid dictionary: prefix not unique");
}
if (current.children[i] == null) {
current.children[i] = new Node();
}
current = current.children[i];
}
int shift = 8 - len;
int start = (code << shift) & 0xFF;
int end = 1 << shift;
for (int i = start; i < start + end; i++) {
current.children[i] = terminal;
}
}
......
private static final class Node {
// Null if terminal.
private final Node[] children;
// Terminal nodes have a symbol.
private final int symbol;
// Number of bits represented in the terminal node.
private final int terminalBits;
/** Construct an internal node. */
Node() {
this.children = new Node[256];
this.symbol = 0; // Not read.
this.terminalBits = 0; // Not read.
}
/**
* Construct a terminal node.
*
* @param symbol symbol the node represents
* @param bits length of Huffman code in bits
*/
Node(int symbol, int bits) {
this.children = null;
this.symbol = symbol;
int b = bits & 0x07;
this.terminalBits = b == 0 ? 8 : b;
}
}
OkHttp3中的 哈夫曼樹 並不是一個二叉樹,它的每個節點最多都可以有256個位元組點。OkHttp3用這種方式來優化Huffman編碼解碼的效率。用一個圖來表示,將是下面這個樣子的:
Huffman Tree
Huffman 編碼
void encode(byte[] data, OutputStream out) throws IOException {
long current = 0;
int n = 0;
for (int i = 0; i < data.length; i++) {
int b = data[i] & 0xFF;
int code = CODES[b];
int nbits = CODE_LENGTHS[b];
current <<= nbits;
current |= code;
n += nbits;
while (n >= 8) {
n -= 8;
out.write(((int) (current >> n)));
}
}
if (n > 0) {
current <<= (8 - n);
current |= (0xFF >>> n);
out.write((int) current);
}
}
逐個位元組地編碼資料。編碼的最後一個位元組沒有位元組對齊時,會在低位填充1。
Huffman 解碼
byte[] decode(byte[] buf) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Node node = root;
int current = 0;
int nbits = 0;
for (int i = 0; i < buf.length; i++) {
int b = buf[i] & 0xFF;
current = (current << 8) | b;
nbits += 8;
while (nbits >= 8) {
int c = (current >>> (nbits - 8)) & 0xFF;
node = node.children[c];
if (node.children == null) {
// terminal node
baos.write(node.symbol);
nbits -= node.terminalBits;
node = root;
} else {
// non-terminal node
nbits -= 8;
}
}
}
while (nbits > 0) {
int c = (current << (8 - nbits)) & 0xFF;
node = node.children[c];
if (node.children != null || node.terminalBits > nbits) {
break;
}
baos.write(node.symbol);
nbits -= node.terminalBits;
node = root;
}
return baos.toByteArray();
}
配合Huffman樹的構造過程,分幾種情況來看。Huffman碼自己對齊時;Huffman碼沒有位元組對齊,最後一個位元組的最低有效位包含了資料流中下一個Huffman碼的最高有效位;Huffman碼沒有位元組對齊,最後一個位元組的最低有效位包含了填充的1。
有興趣的可以參考其它文件對Huffman編碼演算法做更多瞭解。
首部欄位及首部塊的表示
首部欄位主要有兩種表示方法,分別是索引表示和字面量表示。字面量表示又分為首部欄位的名字用索引表示值用字面量表示和名字及值都用字面量表示等方法。
說到用索引表示首部欄位,就不能不提一下HPACK的動態表和靜態表。
HPACK使用兩個表將 頭部欄位 與 索引 關聯起來。 靜態表 是預定義的,它包含了常見的頭部欄位(其中的大多數值為空)。 動態表 是動態的,它可被編碼器用於編碼重複的頭部欄位。
靜態表由一個預定義的頭部欄位靜態列表組成。它的條目在 HPACK規範的 附錄 A 中定義。
動態表由以先進先出順序維護的 頭部欄位列表 組成。動態表中第一個且最新的條目索引值最低,動態表最舊的條目索引值最高。
動態表最初是空的。條目隨著每個頭部塊的解壓而新增。
靜態表和動態表被組合為統一的索引地址空間。
在 (1 ~ 靜態表的長度(包含)) 之間的索引值指向靜態表中的元素。
大於靜態表長度的索引值指向動態表中的元素。通過將頭部欄位的索引減去靜態表的長度來查詢指向動態表的索引。
對於靜態表大小為 s,動態表大小為 k 的情況,下圖展示了完整的有效索引地址空間。
<---------- Index Address Space ---------->
<-- Static Table --> <-- Dynamic Table -->
+---+-----------+---+ +---+-----------+---+
| 1 | ... | s | |s+1| ... |s+k|
+---+-----------+---+ +---+-----------+---+
^ |
| V
Insertion Point Dropping Point
用索引表示頭部欄位
當一個頭部欄位的名-值已經包含在了靜態表或動態表中時,就可以用一個指向靜態表或動態表的索引來表示它了。表示方法如下:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 1 | Index (7+) |
+---+---------------------------+
頭部欄位表示的最高有效位置1,然後用前面看到的表示整數的方法表示索引,即索引是一個7位字首編碼的整數。
用字面量表示頭部欄位
在這種表示法中,頭部欄位的值是用字面量表示的,但頭部欄位的名字則不一定。根據名字的表示方法的差異,以及是否將頭部欄位加進動態表等,而分為多種情況。
增量索引的字面量表示
以這種方法表示的頭部欄位需要被 加進動態表中。在這種表示方法下,頭部欄位的值用索引表示時,頭部欄位的表示如下:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | Index (6+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
頭部欄位的名字和值都用字面量表示時,表示如下:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
增量索引的字面量頭部欄位表示以'01' 的2位模式開始。
如果頭部欄位名與靜態表或動態表中儲存的條目的頭部欄位名匹配,則頭部欄位名稱可用那個條目的索引表示。在這種情況下,條目的索引以一個具有6位字首的整數 表示。這個值總是非0。否則,頭部欄位名由一個字串字面量 表示,使用0值代替6位索引,其後是頭部欄位名。
兩種形式的 頭部欄位名錶示 之後是字串字面量表示的頭部欄位值。
無索引的字面量頭部欄位
這種表示方法不改變動態表。頭部欄位名用索引表示時的頭部欄位表示如下:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
頭部欄位名不用索引表示時的頭部欄位表示如下:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
無索引的字面量頭部欄位表示以'0000' 的4位模式開始,其它方面與 增量索引的字面量表示 類似。
從不索引的字面量頭部欄位
這種表示方法與 無索引的字面量頭部欄位 類似,但它主要影響網路中的中間節點。頭部欄位名用索引表示時的頭部欄位如:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
頭部欄位名不用索引表示時的頭部欄位如:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
首部列表的表示
各個首部欄位表示合併起來形成首部列表。在 okhttp3.internal.framed.Hpack.Writer 的writeHeaders() 中完成編碼首部塊的動作:
/** This does not use "never indexed" semantics for sensitive headers. */
// http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-6.2.3
void writeHeaders(List<Header> headerBlock) throws IOException {
if (emitDynamicTableSizeUpdate) {
if (smallestHeaderTableSizeSetting < maxDynamicTableByteCount) {
// Multiple dynamic table size updates!
writeInt(smallestHeaderTableSizeSetting, PREFIX_5_BITS, 0x20);
}
emitDynamicTableSizeUpdate = false;
smallestHeaderTableSizeSetting = Integer.MAX_VALUE;
writeInt(maxDynamicTableByteCount, PREFIX_5_BITS, 0x20);
}
// TODO: implement index tracking
for (int i = 0, size = headerBlock.size(); i < size; i++) {
Header header = headerBlock.get(i);
ByteString name = header.name.toAsciiLowercase();
ByteString value = header.value;
Integer staticIndex = NAME_TO_FIRST_INDEX.get(name);
if (staticIndex != null) {
// Literal Header Field without Indexing - Indexed Name.
writeInt(staticIndex + 1, PREFIX_4_BITS, 0);
writeByteString(value);
} else {
int dynamicIndex = Util.indexOf(dynamicTable, header);
if (dynamicIndex != -1) {
// Indexed Header.
writeInt(dynamicIndex - nextHeaderIndex + STATIC_HEADER_TABLE.length, PREFIX_7_BITS,
0x80);
} else {
// Literal Header Field with Incremental Indexing - New Name
out.writeByte(0x40);
writeByteString(name);
writeByteString(value);
insertIntoDynamicTable(header);
}
}
}
}
HPACK的規範描述了多種頭部欄位的表示方法,但並沒有指明各個表示方法的適用場景。
在OkHttp3中,實現了3種表示頭部欄位的表示方法:
- 頭部欄位名在靜態表中,頭部欄位名用指向靜態表的索引表示,值用字面量表示。頭部欄位無需加入動態表。
- 頭部欄位的 名-值 對在動態表中,用指向動態表的索引表示頭部欄位。
- 其它情況,用字面量表示頭部欄位名和值,頭部欄位需要加入動態表。
如果頭部欄位的 名-值 對在靜態表中,OkHttp3也不會用索引表示。
請求間首部欄位內容的複用
HPACK中,最重要的優化就是消除請求間冗餘的首部欄位。在實現上,主要有兩個方面,一是前面看到的首部欄位的索引表示,另一方面則是動態表的維護。
HTTP/2中資料傳送方向和資料接收方向各有一個動態表。通訊的雙方,一端傳送方向的動態表需要與另一端接收方向的動態表保持一致,反之亦然。
HTTP/2的連線複用及請求併發執行指的是邏輯上的併發。由於底層傳輸還是用的TCP協議,因而,傳送方傳送資料的順序,與接收方接收資料的順序是一致的。
資料傳送方在傳送一個請求的首部資料時會順便維護自己的動態表,接收方在收到首部資料時,也需要立馬維護自己接收方向的動態表,然後將解碼之後的首部欄位列表dispatch出去。
如果通訊雙方同時在進行2個HTTP請求,分別稱為Req1和Req2,假設在傳送方Req1的頭部欄位列表先發送,Req2的頭部欄位後傳送。接收方必然先收到Req1的頭部欄位列表,然後是Req2的。如果接收方在收到Req1的頭部欄位列表後,沒有立即解碼,而是等Req2的首部欄位列表接收並處理完成之後,再來處理Req1的,則兩端的動態表必然是不一致的。
這裡來看一下OkHttp3中的動態表維護。
傳送方向的動態表,在 okhttp3.internal.framed.Hpack.Writer 中維護。在HTTP/2中,動態表的最大大小在連線建立的初期會進行協商,後面在資料收發過程中也會進行更新。
在編碼頭部欄位列表的 writeHeaders(List<Header> headerBlock) 中,會在需要的時候,將頭部欄位插入動態表,具體來說,就是在頭部欄位的名字不在靜態表中,同時 名-值對不在動態表中的情況。
將頭部欄位插入動態表的過程如下:
private void clearDynamicTable() {
Arrays.fill(dynamicTable, null);
nextHeaderIndex = dynamicTable.length - 1;
headerCount = 0;
dynamicTableByteCount = 0;
}
/** Returns the count of entries evicted. */
private int evictToRecoverBytes(int bytesToRecover) {
int entriesToEvict = 0;
if (bytesToRecover > 0) {
// determine how many headers need to be evicted.
for (int j = dynamicTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) {
bytesToRecover -= dynamicTable[j].hpackSize;
dynamicTableByteCount -= dynamicTable[j].hpackSize;
headerCount--;
entriesToEvict++;
}
System.arraycopy(dynamicTable, nextHeaderIndex + 1, dynamicTable,
nextHeaderIndex + 1 + entriesToEvict, headerCount);
Arrays.fill(dynamicTable, nextHeaderIndex + 1, nextHeaderIndex + 1 + entriesToEvict, null);
nextHeaderIndex += entriesToEvict;
}
return entriesToEvict;
}
private void insertIntoDynamicTable(Header entry) {
int delta = entry.hpackSize;
// if the new or replacement header is too big, drop all entries.
if (delta > maxDynamicTableByteCount) {
clearDynamicTable();
return;
}
// Evict headers to the required length.
int bytesToRecover = (dynamicTableByteCount + delta) - maxDynamicTableByteCount;
evictToRecoverBytes(bytesToRecover);
if (headerCount + 1 > dynamicTable.length) { // Need to grow the dynamic table.
Header[] doubled = new Header[dynamicTable.length * 2];
System.arraycopy(dynamicTable, 0, doubled, dynamicTable.length, dynamicTable.length);
nextHeaderIndex = dynamicTable.length - 1;
dynamicTable = doubled;
}
int index = nextHeaderIndex--;
dynamicTable[index] = entry;
headerCount++;
dynamicTableByteCount += delta;
}
動態表佔用的空間超出限制時,老的頭部欄位將被移除。在OkHttp3中,動態表是一個自後向前生長的表。
在資料的接收防線,okhttp3.internal.http2.Http2Reader 的 nextFrame(Handler handler) 會不停從網路讀取一幀幀的資料:
public boolean nextFrame(Handler handler) throws IOException {
try {
source.require(9); // Frame header size
} catch (IOException e) {
return false; // This might be a normal socket close.
}
/* 0 1 2 3
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Length (24) |
* +---------------+---------------+---------------+
* | Type (8) | Flags (8) |
* +-+-+-----------+---------------+-------------------------------+
* |R| Stream Identifier (31) |
* +=+=============================================================+
* | Frame Payload (0...) ...
* +---------------------------------------------------------------+
*/
int length = readMedium(source);
if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) {
throw ioException("FRAME_SIZE_ERROR: %s", length);
}
byte type = (byte) (source.readByte() & 0xff);
byte flags = (byte) (source.readByte() & 0xff);
int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit.
if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags));
switch (type) {
case TYPE_DATA:
readData(handler, length, flags, streamId);
break;
case TYPE_HEADERS:
readHeaders(handler, length, flags, streamId);
break;
讀到頭部塊時,會立即維護本地接收方向的動態表:
private void readHeaders(Handler handler, int length, byte flags, int streamId)
throws IOException {
if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0");
boolean endStream = (flags & FLAG_END_STREAM) != 0;
short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0;
if ((flags & FLAG_PRIORITY) != 0) {
readPriority(handler, streamId);
length -= 5; // account for above read.
}
length = lengthWithoutPadding(length, flags, padding);
List<Header> headerBlock = readHeaderBlock(length, padding, flags, streamId);
handler.headers(endStream, streamId, -1, headerBlock);
}
private List<Header> readHeaderBlock(int length, short padding, byte flags, int streamId)
throws IOException {
continuation.length = continuation.left = length;
continuation.padding = padding;
continuation.flags = flags;
continuation.streamId = streamId;
// TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20.
// http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.1.2.5
hpackReader.readHeaders();
return hpackReader.getAndResetHeaderList();
}
okhttp3.internal.http2.Hpack.Reader的readHeaders()如下:
static final class Reader {
private final List<Header> headerList = new ArrayList<>();
private final BufferedSource source;
private final int headerTableSizeSetting;
private int maxDynamicTableByteCount;
// Visible for testing.
Header[] dynamicTable = new Header[8];
// Array is populated back to front, so new entries always have lowest index.
int nextHeaderIndex = dynamicTable.length - 1;
int headerCount = 0;
int dynamicTableByteCount = 0;
Reader(int headerTableSizeSetting, Source source) {
this(headerTableSizeSetting, headerTableSizeSetting, source);
}
Reader(int headerTableSizeSetting, int maxDynamicTableByteCount, Source source) {
this.headerTableSizeSetting = headerTableSizeSetting;
this.maxDynamicTableByteCount = maxDynamicTableByteCount;
this.source = Okio.buffer(source);
}
int maxDynamicTableByteCount() {
return maxDynamicTableByteCount;
}
private void adjustDynamicTableByteCount() {
if (maxDynamicTableByteCount < dynamicTableByteCount) {
if (maxDynamicTableByteCount == 0) {
clearDynamicTable();
} else {
evictToRecoverBytes(dynamicTableByteCount - maxDynamicTableByteCount);
}
}
}
private void clearDynamicTable() {
headerList.clear();
Arrays.fill(dynamicTable, null);
nextHeaderIndex = dynamicTable.length - 1;
headerCount = 0;
dynamicTableByteCount = 0;
}
/** Returns the count of entries evicted. */
private int evictToRecoverBytes(int bytesToRecover) {
int entriesToEvict = 0;
if (bytesToRecover > 0) {
// determine how many headers need to be evicted.
for (int j = dynamicTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) {
bytesToRecover -= dynamicTable[j].hpackSize;
dynamicTableByteCount -= dynamicTable[j].hpackSize;
headerCount--;
entriesToEvict++;
}
System.arraycopy(dynamicTable, nextHeaderIndex + 1, dynamicTable,
nextHeaderIndex + 1 + entriesToEvict, headerCount);
nextHeaderIndex += entriesToEvict;
}
return entriesToEvict;
}
/**
* Read {@code byteCount} bytes of headers from the source stream. This implementation does not
* propagate the never indexed flag of a header.
*/
void readHeaders() throws IOException {
while (!source.exhausted()) {
int b = source.readByte() & 0xff;
if (b == 0x80) { // 10000000
throw new IOException("index == 0");
} else if ((b & 0x80) == 0x80) { // 1NNNNNNN
int index = readInt(b, PREFIX_7_BITS);
readIndexedHeader(index - 1);
} else if (b == 0x40) { // 01000000
readLiteralHeaderWithIncrementalIndexingNewName();
} else if ((b & 0x40) == 0x40) { // 01NNNNNN
int index = readInt(b, PREFIX_6_BITS);
readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1);
} else if ((b & 0x20) == 0x20) { // 001NNNNN
maxDynamicTableByteCount = readInt(b, PREFIX_5_BITS);
if (maxDynamicTableByteCount < 0
|| maxDynamicTableByteCount > headerTableSizeSetting) {
throw new IOException("Invalid dynamic table size update " + maxDynamicTableByteCount);
}
adjustDynamicTableByteCount();
} else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit.
readLiteralHeaderWithoutIndexingNewName();
} else { // 000?NNNN - Ignore never indexed bit.
int index = readInt(b, PREFIX_4_BITS);
readLiteralHeaderWithoutIndexingIndexedName(index - 1);
}
}
}
public List<Header> getAndResetHeaderList() {
List<Header> result = new ArrayList<>(headerList);
headerList.clear();
return result;
}
private void readIndexedHeader(int index) throws IOException {
if (isStaticHeader(index)) {
Header staticEntry = STATIC_HEADER_TABLE[index];
headerList.add(staticEntry);
} else {
int dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.length);
if (dynamicTableIndex < 0 || dynamicTableIndex > dynamicTable.length - 1) {
throw new IOException("Header index too large " + (index + 1));
}
headerList.add(dynamicTable[dynamicTableIndex]);
}
}
// referencedHeaders is relative to nextHeaderIndex + 1.
private int dynamicTableIndex(int index) {
return nextHeaderIndex + 1 + index;
}
private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException {
ByteString name = getName(index);
ByteString value = readByteString();
headerList.add(new Header(name, value));
}
private void readLiteralHeaderWithoutIndexingNewName() throws IOException {
ByteString name = checkLowercase(readByteString());
ByteString value = readByteString();
headerList.add(new Header(name, value));
}
private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex)
throws IOException {
ByteString name = getName(nameIndex);
ByteString value = readByteString();
insertIntoDynamicTable(-1, new Header(name, value));
}
private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException {
ByteString name = checkLowercase(readByteString());
ByteString value = readByteString();
insertIntoDynamicTable(-1, new Header(name, value));
}
private ByteString getName(int index) {
if (isStaticHeader(index)) {
return STATIC_HEADER_TABLE[index].name;
} else {
return dynamicTable[dynamicTableIndex(index - STATIC_HEADER_TABLE.length)].name;
}
}
private boolean isStaticHeader(int index) {
return index >= 0 && index <= STATIC_HEADER_TABLE.length - 1;
}
/** index == -1 when new. */
private void insertIntoDynamicTable(int index, Header entry) {
headerList.add(entry);
int delta = entry.hpackSize;
if (index != -1) { // Index -1 == new header.
delta -= dynamicTable[dynamicTableIndex(index)].hpackSize;
}
// if the new or replacement header is too big, drop all entries.
if (delta > maxDynamicTableByteCount) {
clearDynamicTable();
return;
}
// Evict headers to the required length.
int bytesToRecover = (dynamicTableByteCount + delta) - maxDynamicTableByteCount;
int entriesEvicted = evictToRecoverBytes(bytesToRecover);
if (index == -1) { // Adding a value to the dynamic table.
if (headerCount + 1 > dynamicTable.length) { // Need to grow the dynamic table.
Header[] doubled = new Header[dynamicTable.length * 2];
System.arraycopy(dynamicTable, 0, doubled, dynamicTable.length, dynamicTable.length);
nextHeaderIndex = dynamicTable.length - 1;
dynamicTable = doubled;
}
index = nextHeaderIndex--;
dynamicTable[index] = entry;
headerCount++;
} else { // Replace value at same position.
index += dynamicTableIndex(index) + entriesEvicted;
dynamicTable[index] = entry;
}
dynamicTableByteCount += delta;
}
HTTP/2中資料收發兩端的動態表一致性主要是依賴TCP來實現的。
Done。