Netty对HPACK头部压缩的支持-CSDN博客

65 阅读2分钟

前言

HTTP2终于支持对头部进行压缩传输了,Netty很早就支持HTTP2了,看下Netty对HPACK的实现源码,可以对HPACK理解的更深一下。

HpackDecoder

Netty内置的编解码器Http2FrameCodec专门用来对HTTP2的各种Frame进行编解码,其中就包含将ByteBuf解码为HeadersFrame,解码的工作最终交给了io.netty.handler.codec.http2.HpackDecoder

状态

HpackDecoder维护了一组状态常量,代表的是当前对Header的读取状态,不同的状态做的事情是不一样的,HpackDecoder通过一个While循环来读取Header,因为一个Frame里面包含若干个Header,根据读取到的数据判断,State会不断变化流转,理解了这些State,再看代码就简单的多了。

State说明
READ_HEADER_REPRESENTATION读取header的初试状态
READ_INDEXED_HEADER读取被索引的完整header,即name和value均被索引
READ_INDEXED_HEADER_NAME读取被索引的header name
READ_LITERAL_HEADER_NAME_LENGTH_PREFIXname未被索引,读取name长度前缀,判断是否使用哈夫曼编码
READ_LITERAL_HEADER_NAME_LENGTHname未被索引,读取name长度
READ_LITERAL_HEADER_NAMEname未被索引,读取header name
READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX读取value长度前缀,判断是否使用哈夫曼编码
READ_LITERAL_HEADER_VALUE_LENGTH读取value长度
READ_LITERAL_HEADER_VALUEvalue未被索引,读取value

decode()

解码的方法是io.netty.handler.codec.http2.HpackDecoder#decode(),这里直接贴出源码,核心代码已写注释:

private void decode(ByteBuf in, Http2HeadersSink sink) throws Http2Exception {
    int index = 0;
    int nameLength = 0;
    int valueLength = 0;
    byte state = READ_HEADER_REPRESENTATION;// 初始状态 准备读取Header
    boolean huffmanEncoded = false;// 是否使用哈夫曼编码 长度的第1个Bit来标记
    AsciiString name = null;
    IndexType indexType = IndexType.NONE;// 索引类型
    while (in.isReadable()) {// 只要有数据,就循环读
        switch (state) {
            case READ_HEADER_REPRESENTATION:
                byte b = in.readByte();// 读取首字节
                if (maxDynamicTableSizeChangeRequired && (b & 0xE0) != 0x20) {
                    // HpackEncoder MUST signal maximum dynamic table size change
                    throw MAX_DYNAMIC_TABLE_SIZE_CHANGE_REQUIRED;
                }
                if (b < 0) {// 小于0 即最高位是1,代表name和value均被索引
                    // Indexed Header Field
                    index = b & 0x7F;
                    switch (index) {
                        case 0:
                            throw DECODE_ILLEGAL_INDEX_VALUE;
                        case 0x7F: // 索引号超过了1字节,需要继续读取 才能获取最终索引号
                            state = READ_INDEXED_HEADER;
                            break;
                        default:// 索引号没超 直接从静态/动态表读取即可
                            HpackHeaderField indexedHeader = getIndexedHeader(index);
                            sink.appendToHeaderList(
                                    (AsciiString) indexedHeader.name,
                                    (AsciiString) indexedHeader.value);
                    }
                } else if ((b & 0x40) == 0x40) {// 01开头 header需要加入到动态表
                    // Literal Header Field with Incremental Indexing
                    indexType = IndexType.INCREMENTAL;
                    index = b & 0x3F;
                    switch (index) {
                        case 0:// name未被索引,读取name长度 再读name
                            state = READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;
                            break;
                        case 0x3F:// name 索引号超了1字节 需要继续读
                            state = READ_INDEXED_HEADER_NAME;
                            break;
                        default:
                            // name被索引,继续读value
                            name = readName(index);
                            nameLength = name.length();
                            state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
                    }
                } else if ((b & 0x20) == 0x20) {
                    // Dynamic Table Size Update
                    // See https://www.rfc-editor.org/rfc/rfc7541.html#section-4.2
                    throw connectionError(COMPRESSION_ERROR, "Dynamic table size update must happen " +
                        "at the beginning of the header block");
                } else {
                    // Literal Header Field without Indexing / never Indexed
                    indexType = (b & 0x10) == 0x10 ? IndexType.NEVER : IndexType.NONE;
                    index = b & 0x0F;
                    switch (index) {
                        case 0:
                            state = READ_LITERAL_HEADER_NAME_LENGTH_PREFIX;
                            break;
                        case 0x0F:
                            state = READ_INDEXED_HEADER_NAME;
                            break;
                        default:
                            // Index was stored as the prefix
                            name = readName(index);
                            nameLength = name.length();
                            state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
                    }
                }
                break;

            case READ_INDEXED_HEADER:// 被索引的header,读取可变长度的索引号,从表中获取header
                HpackHeaderField indexedHeader = getIndexedHeader(decodeULE128(in, index));
                sink.appendToHeaderList(
                        (AsciiString) indexedHeader.name,
                        (AsciiString) indexedHeader.value);
                state = READ_HEADER_REPRESENTATION;
                break;

            case READ_INDEXED_HEADER_NAME:
                // Header Name matches an entry in the Header Table
                name = readName(decodeULE128(in, index));
                nameLength = name.length();
                state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
                break;

            case READ_LITERAL_HEADER_NAME_LENGTH_PREFIX:// 读取name前缀 判断是否使用哈夫曼编码
                b = in.readByte();
                huffmanEncoded = (b & 0x80) == 0x80;
                index = b & 0x7F;
                if (index == 0x7f) {
                    state = READ_LITERAL_HEADER_NAME_LENGTH;
                } else {
                    nameLength = index;
                    state = READ_LITERAL_HEADER_NAME;
                }
                break;

            case READ_LITERAL_HEADER_NAME_LENGTH:
                // Header Name is a Literal String
                nameLength = decodeULE128(in, index);

                state = READ_LITERAL_HEADER_NAME;
                break;

            case READ_LITERAL_HEADER_NAME:
                // Wait until entire name is readable
                if (in.readableBytes() < nameLength) {
                    throw notEnoughDataException(in);
                }

                name = readStringLiteral(in, nameLength, huffmanEncoded);

                state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
                break;

            case READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX:
                b = in.readByte();
                huffmanEncoded = (b & 0x80) == 0x80;
                index = b & 0x7F;
                switch (index) {
                    case 0x7f:
                        state = READ_LITERAL_HEADER_VALUE_LENGTH;
                        break;
                    case 0:
                        insertHeader(sink, name, EMPTY_STRING, indexType);
                        state = READ_HEADER_REPRESENTATION;
                        break;
                    default:
                        valueLength = index;
                        state = READ_LITERAL_HEADER_VALUE;
                }

                break;

            case READ_LITERAL_HEADER_VALUE_LENGTH:
                // Header Value is a Literal String
                valueLength = decodeULE128(in, index);

                state = READ_LITERAL_HEADER_VALUE;
                break;

            case READ_LITERAL_HEADER_VALUE:
                // Wait until entire value is readable
                if (in.readableBytes() < valueLength) {
                    throw notEnoughDataException(in);
                }

                AsciiString value = readStringLiteral(in, valueLength, huffmanEncoded);
                insertHeader(sink, name, value, indexType);
                state = READ_HEADER_REPRESENTATION;
                break;

            default:
                throw new Error("should not reach here state: " + state);
        }
    }

    if (state != READ_HEADER_REPRESENTATION) {
        throw connectionError(COMPRESSION_ERROR, "Incomplete header block fragment.");
    }
}

该方法做的事情:

  1. 读取首字节,判断最高位是否是1开头。如果是代表name和value都是被索引的,继续读取索引号从静态/动态表获取header即可。
  2. 判断高位是否是01开头,是的话就需要把读取到的header添加到动态表中,进行维护。
  3. 如果name被索引,则读取索引号,从静态/动态表获取。如果没有被索引,则判断长度的第1Bit是否是1,如果是代表使用了哈夫曼编码,否则使用常规的ASCII字符编码。
  4. 读取value的规则类似,也是先判断是否使用哈夫曼编码,再按规则读取。

静态表

Netty通过io.netty.handler.codec.http2.HpackStaticTable类来维护静态表,硬编码写死的,不支持动态修改。

final class HpackStaticTable {

    static final int NOT_FOUND = -1;

    // Appendix A: Static Table
    // https://tools.ietf.org/html/rfc7541#appendix-A
    private static final List<HpackHeaderField> STATIC_TABLE = Arrays.asList(
    /*  1 */ newEmptyHeaderField(":authority"),
    /*  2 */ newHeaderField(":method", "GET"),
    /*  3 */ newHeaderField(":method", "POST"),
    /*  4 */ newHeaderField(":path", "/"),
    /*  5 */ newHeaderField(":path", "/index.html"),
    /*  6 */ newHeaderField(":scheme", "http"),
    /*  7 */ newHeaderField(":scheme", "https"),
    /* 61 */ newEmptyHeaderField("www-authenticate")
	。。。。。。
    );
}

动态表

Netty通过io.netty.handler.codec.http2.HpackDynamicTable类来维护动态表,动态表初始是空的,只有读到01开头的Header才会加入到动态表中维护。

final class HpackDynamicTable {

    // a circular queue of header fields
    HpackHeaderField[] hpackHeaderFields;
    int head;
    int tail;
    private long size;
    private long capacity = -1;

    public void add(HpackHeaderField header) {
        int headerSize = header.size();
        if (headerSize > capacity) {
            clear();
            return;
        }
        while (capacity - size < headerSize) {
            remove();
        }
        hpackHeaderFields[head++] = header;
        size += headerSize;
        if (head == hpackHeaderFields.length) {
            head = 0;
        }
    }
    。。。。。。
}