Netty源码学习-HttpChunkAggregator-HttpRequestEncoder-HttpResponseDecoder

本文详细解析了Netty中用于实现HTTP服务器的关键组件,包括HttpRequestDecoder、HttpResponseEncoder及HttpChunkAggregator的工作原理与流程,特别关注了它们如何处理HTTP请求与响应。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天看Netty如何实现一个Http Server
org.jboss.netty.example.http.file.HttpStaticFileServerPipelineFactory:

pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("aggregator", new HttpChunkAggregator(65536));
pipeline.addLast("encoder", new HttpResponseEncoder());

分析一下这三个ChannalHandler

HttpResponseEncoder比较简单,没什么可说的
按照Http协议规定的Response的格式
把org.jboss.netty.handler.codec.http.HttpResponse转为ChannelBuffer就可以了
注意添加必要的CR(Carriage Return)和LF(Line Feed)

HttpRequestDecoder
如何解析HttpRequest?很容易想到要用ReplayingDecoder:
所以 HttpRequestDecoder extends ReplayingDecoder
根据HttpRequest的消息结构,定义State:

protected static enum State {
SKIP_CONTROL_CHARS,
READ_INITIAL, //请求行
READ_HEADER, //请求头
READ_VARIABLE_LENGTH_CONTENT,
READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS,
READ_FIXED_LENGTH_CONTENT, //请求主体
READ_FIXED_LENGTH_CONTENT_AS_CHUNKS,
READ_CHUNK_SIZE,
READ_CHUNKED_CONTENT,
READ_CHUNKED_CONTENT_AS_CHUNKS,
READ_CHUNK_DELIMITER,
READ_CHUNK_FOOTER;
}

后面的几个定义与“http chunk”有关,稍后再分析
简单地看一看decode方法:

protected Object decode(...) {
switch (state) {
//...
case READ_INITIAL: {
String[] initialLine = splitInitialLine(readLine(buffer, maxInitialLineLength));
message = createMessage(initialLine);
checkpoint(State.READ_HEADER);
}
case READ_HEADER: {
State nextState = readHeaders(buffer);
checkpoint(nextState);
}
}
//...
}
private State readHeaders(ChannelBuffer buffer) throws TooLongFrameException {
final HttpMessage message = this.message;
String line = readHeader(buffer);
String name = null;
String value = null;
//...
message.addHeader(name, value);
}

很好理解。ReplayingDecoder使用时,“可以认为”数据已经接收完整,因此,按行读入并解析就可以了
那如何“断行”呢?别忘了,HttpResponse时会写入CRLF,因此readLine时,遇到CRLF就表示读到行尾了

真正的难题在HttpChunkAggregator
这个ChannelHandler有什么用?

首先看看“http chunk”是什么
当Server返回的HttpResponse是动态生成,无法“一开始”就确定Content-Length时,
可以采用“http chunk”

举例:
一般形式的HttpResponse:

HTTP/1.1 200 OK
Date: Mon, 22 Mar 2004 11:15:03 GMT
Content-Type: text/html
Content-Length: 129
Expires: Sat, 27 Mar 2004 21:12:00 GMT

<html><body><p>The file you requested is 3,400 bytes long and was last modified: Sat, 20 Mar 2004 21:12:00 GMT.</p></body></html>

对应的“http chunk”形式HttpResponse:

HTTP/1.1 200 OK
Date: Mon, 22 Mar 2004 11:15:03 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Trailer: Expires

29
<html><body><p>The file you requested is
5
3,400
23
bytes long and was last modified:
1d
Sat, 20 Mar 2004 21:12:00 GMT
13
.</p></body></html>
0
Expires: Sat, 27 Mar 2004 21:12:00 GMT

简单地说,“http chunk”就是
1.在header加入“Transfer-Encoding: chunked”,
2.把body分成多段(每段的格式是:字节流的长度+CRLF+字节流+CRLF)
长度为0的段表示结束
注意表示长度的数字是十六进制,计算长度时不包括末尾的CRLF
详见《HTTP权威指南》

HttpChunkAggregator的作用就是把“http chunk”形式转为一般形式

如何实现?
与“http chunk”对着干就可以了:
1.把“Transfer-Encoding: chunked”去掉
2.把分段的数据组合成一段

public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
throws Exception {

Object msg = e.getMessage();
HttpMessage currentMessage = this.currentMessage;

if (msg instanceof HttpMessage) {
HttpMessage m = (HttpMessage) msg;

//处理Expect: 100-continue请求
if (is100ContinueExpected(m)) {
write(ctx, succeededFuture(ctx.getChannel()), CONTINUE.duplicate());
}

if (m.isChunked()) {
//移除 'Transfer-Encoding'
List<String> encodings = m.getHeaders(HttpHeaders.Names.TRANSFER_ENCODING);
encodings.remove(HttpHeaders.Values.CHUNKED);
if (encodings.isEmpty()) {
m.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
}
m.setChunked(false);
this.currentMessage = m;
} else {
// Not a chunked message - pass through.
this.currentMessage = null;
ctx.sendUpstream(e);
}
} else if (msg instanceof HttpChunk) {
HttpChunk chunk = (HttpChunk) msg;
ChannelBuffer content = currentMessage.getContent();
appendToCumulation(chunk.getContent());

if (chunk.isLast()) {
this.currentMessage = null;
//“http chunk”有时候会把一个“拖挂”(例如上面例子里面的Expires)放到最后
//要读取这个值并设置header
if (chunk instanceof HttpChunkTrailer) {
HttpChunkTrailer trailer = (HttpChunkTrailer) chunk;
for (Entry<String, String> header: trailer.getHeaders()) {
currentMessage.setHeader(header.getKey(), header.getValue());
}
}

//设置Content-Length,这个值就是各段大小的总和
currentMessage.setHeader(
HttpHeaders.Names.CONTENT_LENGTH,
String.valueOf(content.readableBytes()));

Channels.fireMessageReceived(ctx, currentMessage, e.getRemoteAddress());
}
} else {
ctx.sendUpstream(e);
}
}

从上面r的if-else判断也可看到,HttpChunkAggregator是把“http chunk”分成两大部分来decode的:
HttpMessage和HttpChunk
这两部分的分割是谁来做?当然是HttpRequestDecoder
这就是为什么API里面会说要把HttpChunkAggregator放在HttpRequestDecoder的后面

回头细看一下HttpRequestDecoder的decode方法:

case READ_HEADER: {
State nextState = readHeaders(buffer);
checkpoint(nextState);
if (nextState == State.READ_CHUNK_SIZE) {
// Chunked encoding
message.setChunked(true);
// Generate HttpMessage first. HttpChunks will follow.
return message;
}
case READ_CHUNK_SIZE: {
String line = readLine(buffer, maxInitialLineLength);
int chunkSize = getChunkSize(line);
this.chunkSize = chunkSize;
//...
checkpoint(State.READ_CHUNKED_CONTENT);
}
case READ_CHUNKED_CONTENT: {
HttpChunk chunk = new DefaultHttpChunk(buffer.readBytes((int) chunkSize));
checkpoint(State.READ_CHUNK_DELIMITER);
return chunk;
}

可以看到,HttpRequestDecoder把接收到的每一个分段数据组装成一个HttpChunk返回给
下一个Handler(这里是HttpChunkAggregator)处理
所以HttpChunkAggregator要把多个HttpChunk读取完毕后再组装在一起
另外HttpChunkAggregator组装时如果发现content-length超过maxContentLength,
就会抛TooLongFrameException

另外,上面还提到了http的100-continue:
有时Client发送数据前会先发送Expect: 100-continue,如果Server愿意接受数据,则返回类似如下的响应:

HTTP/1.1 100 Continue

HTTP/1.1 200 OK
Date: Fri, 31 Dec 1999 23:59:59 GMT
Content-Type: text/plain
Content-Length: 42
some-footer: some-value
another-footer: another-value

abcdefghijklmnoprstuvwxyz1234567890abcdef

这是两个HttpResponse
第二个才是真实的、响应的数据,Client可以直接忽略第一个
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值