今天看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可以直接忽略第一个