Netty 从netty角度分析http协议解析(一)HttpServerCodec

Netty 从netty角度分析http协议解析,本文主要研究 HttpServerCodec 工作原理。

本文以netty 自带sample为例进行分析:
https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/helloworld

HttpServerCodec

先看看 HttpServerCodec 的类定义:

public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
        implements HttpServerUpgradeHandler.SourceCodec {
}

CombinedChannelDuplexHandler 是结合了 ChannelInboundHandlerChannelOutboundHandler 特性的 ChannelHandler
即他结合了解码与编码为一体。
CombinedChannelDuplexHandler 定义:

public class CombinedChannelDuplexHandler<I extends ChannelInboundHandler, O extends ChannelOutboundHandler>
        extends ChannelDuplexHandler {

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(CombinedChannelDuplexHandler.class);

    private DelegatingChannelHandlerContext inboundCtx;
    private DelegatingChannelHandlerContext outboundCtx;
    private volatile boolean handlerAdded;

    private I inboundHandler;
    private O outboundHandler;

里面定义了inbound和outbound两个handler以及对应的 handlerContext。

总体而言, HttpServerCodec 就是作为一个门面,具体实现由泛型传入的 I 和 O来实现。
对于 HttpServerCodec,则是 HttpRequestDecoderHttpResponseEncoder

HttpRequestDecoder

http 请求解码器:

public class HttpRequestDecoder extends HttpObjectDecoder {
...
}

默认的参数为 :

      maxInitialLineLength = 4096,
      maxHeaderSize = 8192
      maxChunkSize = 8192.

主要实现都在其父类 HttpObjectDecoder 中维护。

HttpObjectDecoder

HttpObjectDecoder 是一个抽象类,它继承自ByteToMessageDecoder:

public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
}

具体的http解码操作就是这里,解码出的结果为 HttpMessage
这个方法很长,但是可以从里面了解更深层次的http协议,本文将从里面case 语句进行一步一步分析。

第一段
        if (resetRequested) {
            resetNow();
        }

如果reset了,说明在某个解析阶段,触发了HttpExpectationFailedEvent事件, 解析报错了,则重置HttpObjectDecoder 变量信息。

第二段

接下来就是判断 currentState 状态。

        switch (currentState) {
        case SKIP_CONTROL_CHARS:
            // Fall-through
        case READ_INITIAL: try {
            AppendableCharSequence line = lineParser.parse(buffer);
            if (line == null) {
                return;
            }
            String[] initialLine = splitInitialLine(line);
            if (initialLine.length < 3) {
                // Invalid initial line - ignore.
                currentState = State.SKIP_CONTROL_CHARS;
                return;
            }

            message = createMessage(initialLine);
            currentState = State.READ_HEADER;
            // fall-through
        } catch (Exception e) {
            out.add(invalidMessage(buffer, e));
            return;
        }
  1. 初始读,READ_INITIAL 处理的会读取http的请求行,即:方法,协议版本,uri。
    initialLineGET, /, HTTP/1.1
    如果 initialLine 不足3个,则说明是一个非法的请求行,那么重置状态为下一次准备。
    调用 DefaultHttpRequest 创建一个 HttpMessage 返回,默认返回是一个 DefaultHttpRequest
        return new DefaultHttpRequest(
                HttpVersion.valueOf(initialLine[2]),
                HttpMethod.valueOf(initialLine[0]), initialLine[1], validateHeaders);

最后将状态变为 READ_HEADER

第三段

本段主要为读取header信息,由于http协议中,header有多行,并且每个header都会有属性会影响,所以会在读取中影响判断的逻辑。

        case READ_HEADER: try {
            State nextState = readHeaders(buffer);
            if (nextState == null) {
                return;
            }
            currentState = nextState;
            switch (nextState) {
            case SKIP_CONTROL_CHARS:
                // fast-path
                // No content is expected.
                out.add(message);
                out.add(LastHttpContent.EMPTY_LAST_CONTENT);
                resetNow();
                return;
            case READ_CHUNK_SIZE:
                if (!chunkedSupported) {
                    throw new IllegalArgumentException("Chunked messages not supported");
                }
                // Chunked encoding - generate HttpMessage first.  HttpChunks will follow.
                out.add(message);
                return;
            default:
                /**
                 * <a href="https://tools.ietf.org/html/rfc7230#section-3.3.3">RFC 7230, 3.3.3</a> states that if a
                 * request does not have either a transfer-encoding or a content-length header then the message body
                 * length is 0. However for a response the body length is the number of octets received prior to the
                 * server closing the connection. So we treat this as variable length chunked encoding.
                 */
                long contentLength = contentLength();
                if (contentLength == 0 || contentLength == -1 && isDecodingRequest()) {
                    out.add(message);
                    out.add(LastHttpContent.EMPTY_LAST_CONTENT);
                    resetNow();
                    return;
                }

                assert nextState == State.READ_FIXED_LENGTH_CONTENT ||
                        nextState == State.READ_VARIABLE_LENGTH_CONTENT;

                out.add(message);

                if (nextState == State.READ_FIXED_LENGTH_CONTENT) {
                    // chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT state reads data chunk by chunk.
                    chunkSize = contentLength;
                }

                // We return here, this forces decode to be called again where we will decode the content
                return;
            }
        } catch (Exception e) {
            out.add(invalidMessage(buffer, e));
            return;
        }

readHeaders方法 中处理逻辑如下

  1. readHeaders 方法主要为读取header信息,一行一行循环的读,并将其加入到 HttpMessage 的headers中。
  2. 读取完后,会读取content-length 字段,有则会记录在 contentLength 用于读http消息体信息。
  3. 判断如果既有 transfer-encoding 头,又有 content-length,则根据http协议,会将 content-length 删掉,并且以 trasfer-encoding 为主。
    并且将 当前解析状态为 State.READ_CHUNK_SIZE,等待下一次解码时,直接进入到 State.READ_CHUNK_SIZE 的case 中执行。
  4. 如果contentlenght >0,则返回 State.READ_FIXED_LENGTH_CONTENT
  5. 否则返回 State.READ_VARIABLE_LENGTH_CONTENT,进行下一次实际解析具体content-length中数据。

再回看第三段:

  1. 如果是返回 SKIP_CONTROL_CHARS,则会添加 message即 DefaultHttpRequest 和一个LastHttpContent 标识为空的http信息返回。并执行 resetNow 方法,标识本次解析已失败。
  2. 如果返回 READ_CHUNK_SIZE,则由于会进一步解析成 HttpChunk 数据,所以并未执行 resetNow 方法。
  3. 否则如果content-length =0,则直接返回并重置解析。
  4. 如果返回 READ_FIXED_LENGTH_CONTENT 信息,则说明需要解析 content-length信息,就会执行:
chunkSize = contentLength;

给chunkSize赋值供下次使用。

总的来看,HttpObjectDecoder 解析出来的out集合,解析的http协议,会包括多份数据,这一段只会返回 一个DefaultHttpMessage以及一个 空的 HttpContent。

第四段
        case READ_VARIABLE_LENGTH_CONTENT: {
            // Keep reading data as a chunk until the end of connection is reached.
            int toRead = Math.min(buffer.readableBytes(), maxChunkSize);
            if (toRead > 0) {
                ByteBuf content = buffer.readRetainedSlice(toRead);
                out.add(new DefaultHttpContent(content));
            }
            return;
        }

本段开始解析可变长度的 content-length,由此可见,解析content-length时,并不会判断是get还是post。直接会读取content的信息。

  1. 获取一个toRead长度,一次性读取的最大maxChunkSize数据,默认为8192,即8kb。
  2. 读取完之后,直接将字节容器 ByteBuf 传入到 一个 DefaultHttpContent 并out返回。
    注意,此处并没有更改currentState状态,所以下一次仍然是READ_VARIABLE_LENGTH_CONTENT,一直会读到这次http协议终止。
    由于http协议是无状态的,所以会一直读,读一次会放一个 DefaultHttpContent

如果没有退出,又为 State.READ_VARIABLE_LENGTH_CONTENT,则会按照这种方式一直读。

第五段
        case READ_FIXED_LENGTH_CONTENT: {
            int readLimit = buffer.readableBytes();

            // Check if the buffer is readable first as we use the readable byte count
            // to create the HttpChunk. This is needed as otherwise we may end up with
            // create an HttpChunk instance that contains an empty buffer and so is
            // handled like it is the last HttpChunk.
            //
            // See https://github.com/netty/netty/issues/433
            if (readLimit == 0) {
                return;
            }

            int toRead = Math.min(readLimit, maxChunkSize);
            if (toRead > chunkSize) {
                toRead = (int) chunkSize;
            }
            ByteBuf content = buffer.readRetainedSlice(toRead);
            chunkSize -= toRead;

            if (chunkSize == 0) {
                // Read all content.
                out.add(new DefaultLastHttpContent(content, validateHeaders));
                resetNow();
            } else {
                out.add(new DefaultHttpContent(content));
            }
            return;
        }

本节为读取 固定content-length内容处理逻辑:

  1. 判断当前ByteBuf是否有可读信息,利用maxChunkSize,得到一个最大读取值,设置一个本次读取最大限制。
  2. 每次读完之后,都会变更 chunkSize 值,chunkSize 则是 header中的content-length数据,读完就不会读了。
  3. 同样,如果是读完了,就会填充一个 DefaultLastHttpContent 放在末尾,否则会填充一个普通的 DefaultHttpContent 信息。
第六段

本段主要读取chunk相关配置项。
transfer-encoding:chunked 含义是什么呢?

Transfer-Encoding: chunked 表示输出的内容长度不能确定,普通的静态页面、图片之类的基本上都用不到这个。
但动态页面就有可能会用到,但我也注意到大部分asp,php,asp.net动态页面输出的时候大部分还是使用Content-Length,没有使用Transfer-Encoding: chunked。
不过如果结合:Content-Encoding: gzip 使用的时候,Transfer-Encoding: chunked还是比较有用的。
记得以前实现:Content-Encoding: gzip 输出时,先把整个压缩后的数据写到一个很大的字节数组里(如 ByteArrayOutputStream),然后得到数组大小 -> Content-Length。
如果结合Transfer-Encoding: chunked使用,就不必申请一个很大的字节数组了,可以一块一块的输出,更科学,占用资源更少。

即一段一段的传输,减少一次性申请一个大的数据。
分块传输的规则:

  1. 每个分块包含一个 16 进制的数据长度值和真实数据。
  2. 数据长度值独占一行,和真实数据通过 CRLF(\r\n) 分割。
  3. 数据长度值,不计算真实数据末尾的 CRLF,只计算当前传输块的数据长度。
  4. 最后通过一个数据长度值为 0 的分块,来标记当前内容实体传输结束。

详细可以结合 https://www.cnblogs.com/jamesvoid/p/11297843.html 理解。

读取chunkSize
        case READ_CHUNK_SIZE: try {
            AppendableCharSequence line = lineParser.parse(buffer);
            if (line == null) {
                return;
            }
            int chunkSize = getChunkSize(line.toString());
            this.chunkSize = chunkSize;
            if (chunkSize == 0) {
                currentState = State.READ_CHUNK_FOOTER;
                return;
            }
            currentState = State.READ_CHUNKED_CONTENT;
            // fall-through
        } catch (Exception e) {
            out.add(invalidChunk(buffer, e));
            return;
        }

首先从buffer中,读取一行,并将其从16进制转为int类型,记录chunkSize。

  1. 如果chunkSize=0,则标记currentState为 State.READ_CHUNK_FOOTER;,表明读到底了。
  2. 否则读完chunkSize,就要去读chunkContent了,标记为 State.READ_CHUNKED_CONTENT;
读取chunkContent
        case READ_CHUNKED_CONTENT: {
            assert chunkSize <= Integer.MAX_VALUE;
            int toRead = Math.min((int) chunkSize, maxChunkSize);
            toRead = Math.min(toRead, buffer.readableBytes());
            if (toRead == 0) {
                return;
            }
            HttpContent chunk = new DefaultHttpContent(buffer.readRetainedSlice(toRead));
            chunkSize -= toRead;

            out.add(chunk);

            if (chunkSize != 0) {
                return;
            }
            currentState = State.READ_CHUNK_DELIMITER;
            // fall-through
        }

根据 Math.min((int) chunkSize, maxChunkSize) 来获取一个可读值。

  1. 如果读取成功,则封装一个 DefaultHttpContent 加入到content中。
  2. 如果读完了,直接返回,注意此时状态仍然是 READ_CHUNKED_CONTENT,所以仍然会一直读到connect结束。
  3. 如果没有读完,说明http指定的chunkSize太大了,一个maxChunkSize大小的 DefaultHttpContent装不下,所以需要继续读,直接返回。
  4. 如果读完了,即chunkSize=0了,就说明这个chunk读完了, 将 currentState 设置为 State.READ_CHUNK_DELIMITER
读取chunk的delimiter
        case READ_CHUNK_DELIMITER: {
            final int wIdx = buffer.writerIndex();
            int rIdx = buffer.readerIndex();
            while (wIdx > rIdx) {
                byte next = buffer.getByte(rIdx++);
                if (next == HttpConstants.LF) {
                    currentState = State.READ_CHUNK_SIZE;
                    break;
                }
            }
            buffer.readerIndex(rIdx);
            return;
        }

这一段是读取分隔符,即会一直读到writeIndex位置,指导读到 byte 值=10的分割付位置,然后将 重新设置currentState为State.READ_CHUNK_SIZE,表示读取下一个chunk块大小。

读取chunk的footer
        case READ_CHUNK_FOOTER: try {
            LastHttpContent trailer = readTrailingHeaders(buffer);
            if (trailer == null) {
                return;
            }
            out.add(trailer);
            resetNow();
            return;
        } catch (Exception e) {
            out.add(invalidChunk(buffer, e));
            return;
        }

本文则是读取到一个footer块,从而标识本次http协议读取结束,并构造一个 LastHttpContent 信息返回。

读取 bad request
        case BAD_MESSAGE: {
            // Keep discarding until disconnection.
            buffer.skipBytes(buffer.readableBytes());
            break;
        }

如果http报文不符合规范,则 currentState会被设置为 BAD_MESSAGE,直接会跳过所有课读取信息,直接返回。

解析UPGRADED
        case UPGRADED: {
            int readableBytes = buffer.readableBytes();
            if (readableBytes > 0) {
                // Keep on consuming as otherwise we may trigger an DecoderException,
                // other handler will replace this codec with the upgraded protocol codec to
                // take the traffic over at some point then.
                // See https://github.com/netty/netty/issues/2173
                out.add(buffer.readBytes(readableBytes));
            }
            break;
        }

解析到了upgrade,http返回101的websocket升级协议,则直接将ByteBuf添加到out集合中。

总结

HttpServerCodec 解析过程中,有以下特点:

  1. netty的http解析过程,是一个循环往复的过程,根据http协议特点,会解析request,也会解析response,会首先构造一个 HttpMessage 来存储request或者response的 请求行header信息
  2. 在HttpMessage后面,会有一个或者多个HttpContent信息,HttpContent中主要会包ByteBuf信息。
  3. 在结尾,会带上一个LastHttpContent 类型的作为结尾。

关注博主公众号: 六点A君。
哈哈哈,一起研究Netty:
在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值