夜雨聆风学习资料网

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 解析的一个阶段,状态的转移由输入数据驱动。这种设计的好处:

  1. 天然支持分片到达:TCP 是流式协议,一个 HTTP 请求可能分多个 TCP 包到达。状态机可以停在任意状态等待"更多数据"。
  2. 错误恢复容易:出错了直接 reset() 回初始状态就行。
  3. 逻辑清晰:每个状态的逻辑独立,不会出现一个巨大的 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
推荐做法
REST API 服务器
不太推荐,太原始
配合 HttpObjectAggregator
文件上传服务器
适合,直接处理 HttpContent 流
不加聚合器,自己拼装
WebSocket 升级
需要,解码到升级请求
HttpObjectDecoder 自动进入 UPGRADED 状态
反向代理/网关
非常适合
需要深层定制,HttpObjectDecoder 是基础
简单的 Hello World
不如直接用 Spring WebFlux
教学用途可以

六、总结

HttpObjectDecoder 是 Netty HTTP 协议栈的基石,它用有限状态机的优雅方式解决了 HTTP 协议解析中最棘手的几个问题:

  1. TCP 粘包/半包:状态机天然支持分片到达
  2. 多种编码方式:Content-Length、Chunked、不定长,一个状态机全搞定
  3. 连接复用: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

相关学习资料