ARTICLE · 1226959
Netty源码分析(20) — Netty 与 HTTP 协议:HttpObjectDecoder 解析
Netty源码分析(20) — Netty 与 HTTP 协议:HttpObjectDecoder 解析
一、背景与问题
HTTP 协议可以说是互联网的基石,我们每天都在和它打交道。但说实话,HTTP 协议看起来简单——不就是请求-响应吗?真正去解析的时候才发现事情没那么简单。
想象一下,当一条 TCP 连接上源源不断地传来数据时,你怎么知道哪里是一个 HTTP 请求的结束、哪里是下一个 HTTP 请求的开始?尤其 HTTP 还有 Keep-Alive、chunked 编码、Content-Length 缺失等各种让人头疼的情况。如果让你手写一个 HTTP 解析器,你可能会写出一堆 if-else 嵌套,自己都看不下去。
这就是 Netty 的 HttpObjectDecoder 存在的意义——它是 Netty HTTP 协议栈的心脏,负责把原始的字节流(ByteBuf)解码成有意义的 HTTP 对象(HttpRequest、HttpResponse、HttpContent)。Netty 没有重新发明轮子,而是把 HTTP 协议解析以状态机的方式优雅地实现了。
今天我们就来扒一扒 HttpObjectDecoder 的源码,看看它是怎么处理各种 HTTP 协议的边界情况的。
二、核心源码分析
2.1 类继承体系
首先看下 HttpObjectDecoder 在 Netty 中的定位:
// HttpObjectDecoder 继承 ByteToMessageDecoder,这是 Netty 标准的解码器基类// 也就是说它遵循入站处理流程:数据到达 -> decode() 被反复调用 -> 产出 HTTP 对象publicabstractclassHttpObjectDecoderextendsByteToMessageDecoder {// 最大起始行长度,比如请求行 "GET / HTTP/1.1 "privatestaticfinalintDEFAULT_MAX_INITIAL_LINE_LENGTH=4096;// 最大头部大小,防止恶意构造的大头部privatestaticfinalintDEFAULT_MAX_HEADER_SIZE=8192;// 最大分块编码的块大小privatestaticfinalintDEFAULT_MAX_CHUNK_SIZE=8192;// 解码器当前处于什么状态?(核心状态机!)privatevolatileDecoderStatecurrentState= DecoderState.SKIP_CONTROL_CHARS;// 是否解码请求(true=HttpRequestDecoder, false=HttpResponseDecoder)privatefinalboolean isDecodingRequest;// HTTP 消息是否已经构建好了起始行和头部privateboolean headerSent;// ... 省略其他字段}我当初第一次看这个类时,最困惑的是它的状态机设计。HTTP 解析为什么需要状态机?等你看到请求行、头部、空行、消息体这一系列步骤后,就明白了。
2.2 核心状态机
// HttpObjectDecoder 内部定义了完整的状态枚举enumDecoderState {// 跳过前导空白控制字符(CRLF等),用于处理持久连接的"粘包"问题 SKIP_CONTROL_CHARS,// 正在读取起始行(请求行 GET /path HTTP/1.1 或状态行 HTTP/1.1 200 OK) READ_INITIAL,// 正在读取请求/响应头 READ_HEADER,// 正在读取消息体(非chunked编码) READ_VARIABLE_LENGTH_CONTENT,// 正在读取消息体(chunked编码) READ_CHUNK_SIZE,// 正在读取chunked的块扩展(少见但存在) READ_CHUNKED_CONTENT,// 读取chunked尾随CRLF READ_CHUNK_DELIMITER,// 读取chunked尾随头部(Trailing Headers) READ_CHUNK_TRAILING_HEADERS,// 跳过固定长度的内容 READ_FIXED_LENGTH_CONTENT,// 发送完最后一个HTTP消息后的状态 BAD_MESSAGE,// 升级协议后(比如WebSocket),不再进行HTTP解码 UPGRADED}这里每个状态的迁移都不是瞎写的。以chunked传输编码为例,HTTP协议规定:
chunk-size(HEX) CRLFchunk-data CRLFchunk-size(HEX) CRLF...0 CRLFtrailer CRLF对应到状态机就是: READ_CHUNK_SIZE → READ_CHUNKED_CONTENT → READ_CHUNK_DELIMITER → (循环) → READ_CHUNK_TRAILING_HEADERS
2.3 decode() 方法的主循环
这是核心中的核心,整个 HTTP 解析的门面:
@Overrideprotectedvoiddecode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out)throws Exception {// 如果 buffer 可读字节为0,直接跳过if (buffer.readableBytes() == 0) {return; }// 循环解析,一次 decode 调用可能产出多个 HTTP 对象// 比如一条 TCP 数据包包含两个完整的 HTTP 请求do {// 保存当前状态快照,如果解析到一半数据不够了可以回滚intreadableBefore= buffer.readableBytes();switch (currentState) {case SKIP_CONTROL_CHARS:// 跳过开头无意义的 CR/LF/空字符// 这是为了处理 HTTP 持久连接时,前一个请求结束后// TCP 缓冲区可能还残留换行符if (!skipControlCharacters(buffer)) {return; // 数据不够,等待更多数据 }// 跳过后进入读取起始行状态 currentState = DecoderState.READ_INITIAL;// 注意这里没有 break,直接 fall throughcase READ_INITIAL:try {// 解析请求行或状态行// 比如 "GET /index.html HTTP/1.1 "// 或者 "HTTP/1.1 200 OK "AbstractHttpMessagemessage= createMessage( readInitialLine(buffer) );// 解析成功,进入读取头部状态 currentState = DecoderState.READ_HEADER;// 记录这是新消息的开始 headerSent = false;// 这里有个小细节——先不把消息加到out里// 等头部解析完了再一起发,保证消息的完整性 } catch (Exception e) {// 起始行解析失败... reset();throw e; }break;case READ_HEADER:try {// 解析HTTP头部,直到遇到空行// 这里处理头部续行(HTTP/1.0中允许头部跨多行) readHeaders(buffer, currentMessage);// 头部解析完成!开始处理消息体// 注意:这里把HttpRequest/HttpResponse对象加到输出列表// 但消息体还没解析完,后面跟着的是HttpContent对象 out.add(currentMessage); headerSent = true;// 根据头部信息决定消息体解析策略// 关键判断:Content-Length、Transfer-Encoding、chunked等if (isContentAlwaysEmpty(currentMessage)) {// 比如 HEAD 响应、304 Not Modified、1xx 响应// 这些消息体为空,直接重置进入下一个请求 currentState = DecoderState.SKIP_CONTROL_CHARS; currentMessage = null; } elseif (isTransferEncodingChunked(currentMessage)) {// Chunked 编码! currentState = DecoderState.READ_CHUNK_SIZE; } elseif (contentLength >= 0) {// 有 Content-Length,固定长度消息体 currentState = DecoderState.READ_FIXED_LENGTH_CONTENT; } else {// 没有 Content-Length 也没有 chunked?// 那只能读到连接关闭为止了 currentState = DecoderState.READ_VARIABLE_LENGTH_CONTENT; } } catch (Exception e) { reset();throw e; }break;// ... 各种消息体读取状态 }// 如果 buffer 没有推进,说明数据不够,退出循环等更多数据if (readableBefore == buffer.readableBytes()) {break; }// 处理 UPGRADED 状态(WebSocket 升级后不再解码) } while (currentState != DecoderState.UPGRADED && buffer.isReadable() && currentState != DecoderState.BAD_MESSAGE);}你发现没?这个 decode() 方法其实不是一次性的。Netty 的 ByteToMessageDecoder 基类会在有新数据到达时反复调用 decode(),所以 currentState 必须用成员变量来持久化状态。这个设计非常实用——TCP 数据包是分片到达的,不可能假设一次就收到完整的 HTTP 请求。
2.4 起始行解析:readInitialLine()
private String[] readInitialLine(ByteBuf buffer) {// 在 buffer 中找 intcrlf= findCRLF(buffer);if (crlf == -1) {// 没找到换行,数据不够returnnull; }// 提取起始行内容(不包含 )byte[] line = newbyte[crlf - buffer.readerIndex()]; buffer.readBytes(line);// 跳过 buffer.skipBytes(2); // CRLF 或 LFStringlineStr=newString(line, CharsetUtil.US_ASCII);if (isDecodingRequest) {// 请求起始行: METHOD URI VERSION// 例如: "GET /index.html HTTP/1.1" String[] parts = lineStr.split(" ", 3);if (parts.length != 3) {thrownewIllegalArgumentException("malformed request line: " + lineStr); }return parts; // [0]="GET", [1]="/index.html", [2]="HTTP/1.1" } else {// 响应起始行: VERSION STATUS_CODE REASON_PHRASE// 例如: "HTTP/1.1 200 OK"intspace1= lineStr.indexOf(' ');intspace2= lineStr.indexOf(' ', space1 + 1);// ... 解析版本和状态码returnnewString[] { ... }; }}说实话,这部分代码看着简单,但实际 HTTP 协议里坑不少。比如请求行里 URI 可能包含空格(虽然不常见),响应行里的 Reason Phrase 也可能包含空格("200 OK" 没问题,"404 Not Found" 就得小心了)。
2.5 头部解析:逐行处理
privatevoidreadHeaders(ByteBuf buffer, AbstractHttpMessage message) {HttpHeadersheaders= message.headers();while (true) {// 找到头部的行结束符 intcrlf= findCRLF(buffer);if (crlf == -1) {// 数据不完整,等待更多return; }intlineLength= crlf - buffer.readerIndex();if (lineLength == 0) {// 碰到了空行!说明头部结束 buffer.skipBytes(2); // 跳过 break; }// 读取头部行byte[] lineBytes = newbyte[lineLength]; buffer.readBytes(lineBytes); buffer.skipBytes(2); // 跳过换行StringheaderLine=newString(lineBytes, CharsetUtil.US_ASCII);// 检查是不是续行(HTTP/1.0支持)// 续行以空格或制表符开头if (headerLine.charAt(0) == ' ' || headerLine.charAt(0) == ' ') {// 续行:追加到上一个头的值后面// (这个特性现在基本没人用了,但Netty还是处理了)StringpreviousValue= headers.get(lastHeaderName); headers.set(lastHeaderName, previousValue + headerLine.trim()); } else {// 正常头部行:Header-Name: valueintcolon= headerLine.indexOf(':');if (colon == -1) {thrownewIllegalArgumentException("bad header: " + headerLine); }Stringname= headerLine.substring(0, colon).trim();Stringvalue= headerLine.substring(colon + 1).trim(); headers.add(name, value); lastHeaderName = name; }// 防止头部大小超过限制(安全考虑)if (headers.size() > maxHeaderSize) {thrownewTooLongHttpHeaderException(...); } }}2.6 Chunked 编码解析:最复杂的部分
privatevoidreadChunkSize(ByteBuf buffer, List<Object> out) {// 找 intcrlf= findCRLF(buffer);if (crlf == -1) return;// 读取分块大小(十六进制),忽略 chunk-extensions// chunk-size [chunk-ext] CRLFStringchunkSizeHex= readLine(buffer, crlf);// 去掉 chunk-extensions(如果有分号分隔的话)// 比如 "1a;extension=value" → 取 "1a"intsemicolon= chunkSizeHex.indexOf(';');if (semicolon >= 0) { chunkSizeHex = chunkSizeHex.substring(0, semicolon).trim(); }intchunkSize= Integer.parseInt(chunkSizeHex, 16);if (chunkSize == 0) {// 最后一个块!Size为0表示结束 currentState = DecoderState.READ_CHUNK_TRAILING_HEADERS; } else {// 非零,准备读取真正的块内容 currentState = DecoderState.READ_CHUNKED_CONTENT;// 记录这个分块需要读取多少字节 chunkSizeRemaining = chunkSize; }}privatevoidreadChunkedContent(ByteBuf buffer, List<Object> out) {// 检查是否有足够的字节if (buffer.readableBytes() < remainingChunkSize + 2) {return; // 还不够,等更多数据 }// 读取分块数据(不包含末尾的 CRLF)ByteBufchunkData= buffer.readRetainedSlice(remainingChunkSize); buffer.skipBytes(2); // 跳过后面的 // 发送 HttpContent 对象 out.add(newDefaultHttpContent(chunkData));// 回到读取下一个分块的大小 currentState = DecoderState.READ_CHUNK_SIZE;}这里有个值得一提的点:readRetainedSlice 不会拷贝数据,只是创建一个共享同一块内存的视图。这是 Netty 零拷贝思想的体现。
三、原理分析
3.1 状态机模式
HttpObjectDecoder 本质上是一个有限状态机(FSM)。每个状态对应 HTTP 解析的一个阶段,状态的转移由输入数据驱动。这种设计的好处:
天然支持分片到达:TCP 是流式协议,一个 HTTP 请求可能分多个 TCP 包到达。状态机可以停在任意状态等待"更多数据"。 错误恢复容易:出错了直接 reset()回初始状态就行。逻辑清晰:每个状态的逻辑独立,不会出现一个巨大的 if-else 函数。
3.2 解码流程总图
TCP 字节流进入 │ ▼┌─────────────────┐ 跳过前导 ┌─────────────────┐│ SKIP_CONTROL │ ──────────────────────────→ │ READ_INITIAL ││ _CHARS │ │ (读起始行) │└─────────────────┘ └────────┬────────┘ │ 解析完起始行 ▼ ┌─────────────────┐ │ READ_HEADER │ │ (逐行读头部) │ └────────┬────────┘ │ 遇到空行,头部结束 ▼ ┌──────────────────────┐ │ 判断消息体编码方式 │ │ │ │ Content-Length? │──→ READ_FIXED_LENGTH_CONTENT │ Transfer: chunked? │──→ READ_CHUNK_SIZE ←──┐ │ 无Content-Length │──→ READ_VARIABLE_ │ │ 且非chunked? │ LENGTH_CONTENT │ └──────────────────────┘ │ │ ┌───────────────────────────────────────────────────────────────┘ ▼ ┌──────────────────────────┐ │ 下一个请求/响应 │ │ (回到 SKIP_CONTROL_CHARS)│ └──────────────────────────┘3.3 关键设计亮点
1) 零拷贝读取头部
HttpObjectDecoder 解析头部时,并没有把每个头部行都 copy 到新字符串再解析。它直接操作 ByteBuf 的 readerIndex,通过偏移量来定位分割点。只有在真正需要 String 的时候才创建对象。
2) 消息聚合器搭档
HttpObjectDecoder 产出的是一系列对象:先是 HttpRequest/HttpResponse,然后是多个 HttpContent(每个 chunk 一个),最后可能还有个 LastHttpContent。实际业务场景中通常需要一个完整的 HTTP 消息,所以 Netty 提供了 HttpObjectAggregator 来把这些碎片拼起来:
// Pipeline 中的典型配置pipeline.addLast("decoder", newHttpRequestDecoder()); // 解码器pipeline.addLast("aggregator", newHttpObjectAggregator( // 聚合器65536// 最大消息体大小));pipeline.addLast("handler", newMyHttpHandler()); // 业务处理器3) 设计模式:责任链 + 观察者
HttpObjectDecoder 本身是一个 ChannelInboundHandler,放入 ChannelPipeline 后,当数据到达时 Netty 自动调用它的 channelRead() 方法(基类 ByteToMessageDecoder 的模板方法),解析出 HTTP 对象后再传递给下一个 Handler。这就是 Netty 引以为傲的责任链模式。
3.4 与其他解码器的配合
单独用 HttpObjectDecoder 比较原始,实际项目中通常搭配使用:
// 典型的 HTTP 服务器 pipelinepipeline.addLast(newHttpRequestDecoder()); // 字节 → HTTP请求对象pipeline.addLast(newHttpResponseEncoder()); // HTTP响应对象 → 字节(出站)pipeline.addLast(newHttpObjectAggregator(65536)); // 拼装完整消息pipeline.addLast(newHttpContentCompressor()); // 压缩(可选)pipeline.addLast(newMyServerHandler()); // 你的业务逻辑HttpRequestDecoder 就是继承自 HttpObjectDecoder 的子类,重写了 createMessage() 方法来创建 HttpRequest 对象。
四、使用示例
看一个完整的 HTTP 服务器示例,感受一下 HttpObjectDecoder 在实际中的用法:
import io.netty.bootstrap.ServerBootstrap;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.*;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.http.*;import io.netty.util.CharsetUtil;/** * 基于Netty原生的HTTP服务器(不使用聚合器,直接处理HttpObjectDecoder的输出) * * 这个例子演示了HttpObjectDecoder产出的"碎片化"消息 * 你会看到 HttpRequest 和 HttpContent 是分开到达的 */publicclassNettyHttpServerRaw {publicstaticvoidmain(String[] args)throws Exception {EventLoopGroupbossGroup=newNioEventLoopGroup(1);EventLoopGroupworkerGroup=newNioEventLoopGroup();try {ServerBootstrapb=newServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(newChannelInitializer<SocketChannel>() {@OverrideprotectedvoidinitChannel(SocketChannel ch) {ChannelPipelinep= ch.pipeline();// HttpObjectDecoder 在这里!它把字节解码成HTTP消息 p.addLast("httpDecoder", newHttpRequestDecoder(4096, // maxInitialLineLength8192, // maxHeaderSize8192// maxChunkSize ));// 编码器(出站方向:HttpResponse → ByteBuf) p.addLast("httpEncoder", newHttpResponseEncoder());// 业务处理器 p.addLast("handler", newSimpleChannelInboundHandler<Object>() {private HttpRequest request;privateStringBuildercontentBuilder=newStringBuilder();@OverrideprotectedvoidchannelRead0(ChannelHandlerContext ctx, Object msg) {if (msg instanceof HttpRequest) {// 收到请求头!this.request = (HttpRequest) msg; System.out.println("收到请求: " + request.method() + " " + request.uri()); System.out.println("请求头: " + request.headers());// 检查是否是100 Continueif (HttpUtil.is100ContinueExpected(request)) { ctx.write(newDefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE )); } contentBuilder.setLength(0); // 清空 }if (msg instanceof HttpContent) {// 收到请求体的一部分!HttpContenthttpContent= (HttpContent) msg;ByteBufcontent= httpContent.content(); contentBuilder.append(content.toString(CharsetUtil.UTF_8)); content.release();if (msg instanceof LastHttpContent) {// 请求接收完毕,开始处理!StringrequestBody= contentBuilder.toString(); System.out.println("请求体内容: " + requestBody);// 构建响应StringresponseBody="<html><body>" + "<h1>Hello Netty HTTP!</h1>" + "<p>Your request: " + request.method() + " " + request.uri() + "</p>" + "<p>Body: " + requestBody + "</p>" + "</body></html>";FullHttpResponseresponse=newDefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(responseBody, CharsetUtil.UTF_8) ); response.headers() .set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8") .set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); ctx.writeAndFlush(response);// 如果不是Keep-Alive,关闭连接if (!HttpUtil.isKeepAlive(request)) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) .addListener(ChannelFutureListener.CLOSE); } } } } }); } });ChannelFuturef= b.bind(8080).sync(); System.out.println("HTTP Server started on http://localhost:8080"); f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }}用 curl 测试:
# 测试GET请求curl -v http://localhost:8080/hello# 测试POST请求带bodycurl -v -X POST http://localhost:8080/submit \ -H "Content-Type: application/json" \ -d '{"name":"netty","type":"framework"}'如果你觉得处理 HttpRequest 和 HttpContent 分开太麻烦,可以用 HttpObjectAggregator 让它们合体:
// 使用聚合器后,收到的就是 FullHttpRequest 了pipeline.addLast("httpDecoder", newHttpRequestDecoder());pipeline.addLast("aggregator", newHttpObjectAggregator(1024 * 1024)); // 1MBpipeline.addLast("handler", newSimpleChannelInboundHandler<FullHttpRequest>() {@OverrideprotectedvoidchannelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {// 现在 request 一次性包含所有内容Stringbody= request.content().toString(CharsetUtil.UTF_8); System.out.println("收到完整请求: " + request.uri() + ", body=" + body);// 处理完后一定要释放! request.release(); }});五、注意事项
5.1 常见坑
1) 内存泄漏风险
HttpObjectDecoder 产出的 HttpContent 对象(包括 DefaultHttpContent 和 DefaultLastHttpContent)使用了 Netty 的引用计数机制。如果你消费了 content() 的内容,必须调用 release(),否则会造成内存泄漏。用 SimpleChannelInboundHandler 可以自动释放,但如果你用 ChannelInboundHandlerAdapter,就得自己手动释放。
// 正确姿势if (msg instanceof HttpContent) {HttpContentcontent= (HttpContent) msg;try {ByteBufdata= content.content();// ... 处理数据 } finally { content.release(); }}2) 100 Continue 处理
如果你不处理 Expect: 100-continue 请求头,客户端可能一直等待你的 100 Continue 响应后才发送请求体。默认情况下 Netty 不会自动处理,你得自己在 Handler 中检查并响应:
if (HttpUtil.is100ContinueExpected(request)) { ctx.write(newDefaultFullHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE));}3) 大请求体 vs 聚合器
HttpObjectAggregator 虽然方便,但会把整个请求体放在内存中。对超大上传(比如几百MB的文件),千万不要用聚合器,否则 OOM 警告⚠️。这种情况下应该直接处理 HttpContent 碎片,边收边往磁盘写。
// 大文件上传:不要用聚合器!pipeline.addLast("httpDecoder", newHttpRequestDecoder());// 不添加 HttpObjectAggregator!pipeline.addLast("handler", newStreamingFileUploadHandler());4) 请求 URI 解码
HttpObjectDecoder 不会帮你 decode URI。URI 中的 %20 之类的编码需要你自己处理:
StringrawUri= request.uri();try {StringdecodedUri= java.net.URLDecoder.decode(rawUri, "UTF-8");// 现在 decodedUri 中的 %20 变成了空格} catch (UnsupportedEncodingException e) {// UTF-8 永远存在,不用太担心}5.2 性能考量
池化缓冲区:配合 PooledByteBufAllocator使用,减少 GC 压力Direct Memory:对于高吞吐场景,使用 DirectBuffer 避免内核态到用户态的数据拷贝 HttpObjectDecoder 线程安全:不!它是与 Channel 绑定的,同一个 Channel 的请求串行处理 maxInitialLineLength 限制:默认 4KB,如果遇到特别大的 URI(比如带大量参数的 GET 请求),需要调大这个参数
5.3 适用场景
六、总结
HttpObjectDecoder 是 Netty HTTP 协议栈的基石,它用有限状态机的优雅方式解决了 HTTP 协议解析中最棘手的几个问题:
TCP 粘包/半包:状态机天然支持分片到达 多种编码方式:Content-Length、Chunked、不定长,一个状态机全搞定 连接复用:Keep-Alive 的边界处理
下次你用 Spring Boot 写 Controller 的时候,底层的 Tomcat 或者 Netty 其实也在做着同样的事情——把原始的字节流变成你可以直接使用的 HttpServletRequest。只不过 Netty 给了你更多控制权,代价是你得处理更多的底层细节。
说实话,Netty 这个 HttpObjectDecoder 我翻来覆去看了好几遍,最大的感受就是:状态机真的很适合做协议解析。如果你有机会写自己的协议解析器,别上来就写 if-else,先画个状态转移图,会清晰很多。
下一篇我们继续深入,讲 Netty 的 WebSocket 协议支持和协议升级源码,看看 HttpObjectDecoder 如何优雅地切换到 UPGRADED 状态。
参考源码:Netty 4.1.x io.netty.handler.codec.http.HttpObjectDecoder















































































