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
是结合了 ChannelInboundHandler
和 ChannelOutboundHandler
特性的 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,则是 HttpRequestDecoder
和 HttpResponseEncoder
。
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;
}
- 初始读,
READ_INITIAL
处理的会读取http的请求行,即:方法,协议版本,uri。
initialLine
:GET, /, 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方法 中处理逻辑如下
readHeaders
方法主要为读取header信息,一行一行循环的读,并将其加入到HttpMessage
的headers中。- 读取完后,会读取
content-length
字段,有则会记录在contentLength
用于读http消息体信息。 - 判断如果既有
transfer-encoding
头,又有content-length
,则根据http协议,会将content-length
删掉,并且以trasfer-encoding
为主。
并且将 当前解析状态为State.READ_CHUNK_SIZE
,等待下一次解码时,直接进入到State.READ_CHUNK_SIZE
的case 中执行。 - 如果contentlenght >0,则返回
State.READ_FIXED_LENGTH_CONTENT
。 - 否则返回
State.READ_VARIABLE_LENGTH_CONTENT
,进行下一次实际解析具体content-length中数据。
再回看第三段:
- 如果是返回
SKIP_CONTROL_CHARS
,则会添加 message即DefaultHttpRequest
和一个LastHttpContent
标识为空的http信息返回。并执行resetNow
方法,标识本次解析已失败。 - 如果返回
READ_CHUNK_SIZE
,则由于会进一步解析成 HttpChunk 数据,所以并未执行resetNow
方法。 - 否则如果content-length =0,则直接返回并重置解析。
- 如果返回
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的信息。
- 获取一个toRead长度,一次性读取的最大maxChunkSize数据,默认为8192,即8kb。
- 读取完之后,直接将字节容器 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内容处理逻辑:
- 判断当前ByteBuf是否有可读信息,利用maxChunkSize,得到一个最大读取值,设置一个本次读取最大限制。
- 每次读完之后,都会变更
chunkSize
值,chunkSize
则是 header中的content-length数据,读完就不会读了。 - 同样,如果是读完了,就会填充一个 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使用,就不必申请一个很大的字节数组了,可以一块一块的输出,更科学,占用资源更少。
即一段一段的传输,减少一次性申请一个大的数据。
分块传输的规则:
- 每个分块包含一个 16 进制的数据长度值和真实数据。
- 数据长度值独占一行,和真实数据通过 CRLF(\r\n) 分割。
- 数据长度值,不计算真实数据末尾的 CRLF,只计算当前传输块的数据长度。
- 最后通过一个数据长度值为 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。
- 如果chunkSize=0,则标记currentState为
State.READ_CHUNK_FOOTER;
,表明读到底了。 - 否则读完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)
来获取一个可读值。
- 如果读取成功,则封装一个 DefaultHttpContent 加入到content中。
- 如果读完了,直接返回,注意此时状态仍然是 READ_CHUNKED_CONTENT,所以仍然会一直读到connect结束。
- 如果没有读完,说明http指定的chunkSize太大了,一个maxChunkSize大小的 DefaultHttpContent装不下,所以需要继续读,直接返回。
- 如果读完了,即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
解析过程中,有以下特点:
- netty的http解析过程,是一个循环往复的过程,根据http协议特点,会解析request,也会解析response,会首先构造一个 HttpMessage 来存储request或者response的 请求行和header信息
- 在HttpMessage后面,会有一个或者多个HttpContent信息,HttpContent中主要会包ByteBuf信息。
- 在结尾,会带上一个LastHttpContent 类型的作为结尾。
关注博主公众号: 六点A君。
哈哈哈,一起研究Netty: