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 elseencode (2^N - 1) on N bitsI = I - (2^N - 1)while I >= 128encode (I % 128 + 128) on 8 bitsI = I / 128encode I on 8 bitsencode (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.1void 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 elseM = 0repeatB = next octetI = I + (B & 127) * 2^MM = M + 7while B & 128 == 128return Idecode 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; // 1NNNNNNNint 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 nodebaos.write(node.symbol);nbits -= node.terminalBits;node = root;} else {// non-terminal nodenbits -= 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|+---+-----------+---+ +---+-----------+---+^ || VInsertion 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.3void 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 trackingfor (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 Nameout.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.5hpackReader.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) { // 10000000throw new IOException("index == 0");} else if ((b & 0x80) == 0x80) { // 1NNNNNNNint index = readInt(b, PREFIX_7_BITS);readIndexedHeader(index - 1);} else if (b == 0x40) { // 01000000readLiteralHeaderWithIncrementalIndexingNewName();} else if ((b & 0x40) == 0x40) { // 01NNNNNNint index = readInt(b, PREFIX_6_BITS);readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1);} else if ((b & 0x20) == 0x20) { // 001NNNNNmaxDynamicTableByteCount = 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。
總結
以上是生活随笔為你收集整理的OkHttp3中的HTTP/2首部压缩的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Caddy Web服务器QUIC部署
- 下一篇: UDT协议实现分析——UDT初始化和销毁