Netty系列之——HTTP服务器

1. HTTP编解码器

1.1 内置编解码器

我们都知道,对于一个简单的HTTP请求来说,报文都包括了:请求行、请求头部、请求数据(POST等方法才有,GET是没有的),并且请求头部的每一行都以回车符 + 换行符分隔:

1
2
3
4
GET / HTTP/1.1
Host: server.example.com
Connection: keep-alive
Accept:text/html,application/json

作为一个HTTP服务器,必须具备解析请求报文中的数据的功能,知道客户端需要请求的URI、请求方法、接收的数据格式等等。除此之外,还有其他的复杂工作要自己实现。

所以,这时候就体现出Netty的强大之处了,因为它已经内置了关于HTTP的解码器、编码器、以及更加方便的编解码器。也就是说Netty会将这些请求数据封装成相应的POJO实现,而我们也就不必编写处理这些数据的自定义编解码器了。

在Netty中,一个HTTP请求/相应被分割成下图的组成——由多个数据部分组成,并且总是以一个LastHttpContent部分作为结束。而FullHttpRequestFullHttpResponse则是特殊的子类型,代表了完成的请求和响应:

TIM截图20181106130258.png

http_response.png

Netty内置了处理和生成这些消息的HTTP编码器和解码器;

名称 HTTP编码器和解码器
HttpRequestEncoder 将HttpRequest、HttpContent 和 LastHttpContent 消息编码为字节
HttpResponseEncoder 将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节
HttpRequestDecoder 将字节解码为HttpRequest、HttpContent 和 LastHttpContent 消息
HttpResponseDecoder 将字节解码为HttpResponse、HttpContent 和LastHttpContent 消息

1.2 支持HTTP

所以,如果想让服务器支持HTTP,只需要将这些编码器添加到ChannelPipeline中即可。

客户端(编码Request,解码Response):

1
2
pipeline.addLast("decoder", new HttpResponseDecoder());
pipeline.addLast("encoder", new HttpRequestEncoder());

服务器(编码Response,解码Request):

1
2
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("encoder", new HttpResponseEncoder());

另外,Netty还提供更加方便的编解码器:

1
2
3
4
5
// 客户端
pipeline.addLast(new HttpClientCodec());

// 服务端
pipeline.addLast(new HttpServerCodec());

1.3 聚合HTTP消息

前面说到,一个完整的请求会被分割成多个组成部分。举例来说,如果POST一个接口,服务端将会两次调用channelRead方法,分别传入DefaultHttpRequestDefaultLastHttpContent消息类型:

TIM截图20181106204732.png

因此还需要聚合它们以形成完整的消息,但是Netty还提供一个聚合器,可以将多个消息部分合并为FullHttpRequest 或者 FullHttpResponse 消息:

1
2
pipeline.addLast("aggregator",
new HttpObjectAggregator(512 * 1024)); // 最大消息大小为512KB

2. 实现

2.1 简单功能

上面介绍了Netty为我们内置提供的编解码器,让我们能快速开发出自己的HTTP服务器,所以接下来,我们开始实现一个简单的HTTP服务器。

首先创建服务端引导,在ChannelPipeline中添加HTTP编解码器和用于聚合消息的HttpObjectAggregator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @author Pushy
* @since 2018/11/6 10:39
*/
public class HttpServer {

private static final String host = "localhost";
private static final int port = 8080;

public static void main(String[] args) {
try {
ServerBootstrap b = new ServerBootstrap();
b.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpServerCodec()); // HTTP解编码器
pipeline.addLast(new HttpObjectAggregator(512 * 1024)); // 聚合HTTP消息
pipeline.addLast(new HttpServerHandler());
}
});
ChannelFuture f = b.bind(host, port).sync();
f.channel().closeFuture().sync();

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

接着创建HttpServerHandler,继承自SimpleChannelInboundHandler,指定入站的消息类型为FullHttpRequest,并在channelRead0方法中处理客户端的所有请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author Pushy
* @since 2018/11/6 10:50
*/
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.OK, Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));
ctx.writeAndFlush(response)
.addListener(ChannelFutureListener.CLOSE);
}
}

为了符合HTTP的协议的响应,我们需要创建FullHttpResponse对象,并指定协议的版本、响应状态码、响应内容。Netty也提供了最简单的DefaultFullHttpResponse实现。所以我们只需要将该FullHttpResponse写入并冲刷到Channel中即可。

最后,通过writeAndFlush返回的ChannelFutureaddListener方法关闭当前的客户端连接。这样,最简单的HTTP服务器就实现了!

2.2 传输大文件

例如我们想要在HTTP应用中想要响应HTML文件,由于Netty是一个NIO的异步框架,因此在写大块数据时是一个特殊的问题。在《Netty实战》中提到:

因为网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题。由于写操作是非阻塞的,所以即使没有写出所有的数据,写操作也会在完成时返回并通知 ChannelFuture。当这种情况发生时,如果仍然不停地写入,就有内存耗尽的风险。所以在写大型数据时,需要准备好处理到远程节点的连接是慢速连接的情况,这种情况会导致内存释放的延迟。

那么如何来实现大块数据的传输呢?得益于Netty的优秀的封装和NIO的零拷贝特性(消除将文件的内容从文件系统移动到网络栈的复制过程),我们可以使用FileRegion接口的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
File file = new File(path);
// 写入response对象
ctx.write(new DefaultHttpResponse(
request.protocolVersion(), HttpResponseStatus.OK));

// 以该文件的长度创建一个DefaultFileRegion
FileInputStream in = new FileInputStream(file);
FileRegion region = new DefaultFileRegion(
in.getChannel(), 0, file.length());
ctx.write(region);

// 写入LastHttpContent并冲刷数据
ChannelFuture future = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
future.addListener(ChannelFutureListener.CLOSE);

由于使用的是零拷贝特性的原因,直接将文件从文件系统传输到网络中,是对文件内容的直接传输。如果我们想要将文件系统复制到用户内存中,并对数据做一些处理,例如使用SSL进行加密数据的操作。那么则可以使用ChunkedWriteHandler,同样支持异步写大型数据流,不会导致大量的内存消耗。

Netty提供了如下四个将由ChunkedWriteHandler处理不定长度的数据流,并实现了ChunkedInput接口:

名称 描述
ChunkedFile 从文件中逐块获取数据,当你的平台不支持零拷贝或者你需要转换数据时使用
ChunkedNioFile 和 ChunkedFile 类似,只是它使用了 FileChannel
ChunkedStream 从 InputStream 中逐块传输内容
ChunkedNioStream 从 ReadableByteChannel 中逐块传输内容

现在,我们想要将文件写入到网络,并在传输之前由SslHandler进行加密。首先,我们创建ChunkedWriteHandlerInitializer,配置ChannelPipeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ChunkedWriteHandlerInitializer {

private final SslContext sslCtx;

public ChunkedWriteHandlerInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}

@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 将SslHandler添加到ChannelPipeline实现数据的加密
pipeline.addLast(new SslHandler(sslCtx.newEngine(ch.alloc());
// 添加ChunkedWriteHandler,以处理作为ChunkedInput传入的数据
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new WriteStreamHandler()); // 处理写的逻辑Hanlder
}
}

ChannelPipeline的添加顺序可以看出,在出站过程中,由WriteStreamHandler处理写的逻辑之后,ChunkedWriteHandler处理数据流,最后再由SslHandler进行加密数据后传输给客户端。

接着创建WriteStreamHandler类来处理写逻辑,在连接建立之后,使用ChunkedInput来写文件数据:

1
2
3
4
5
6
7
8
public final class WriteStreamHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
ctx.writeAndFlush(
new ChunkedStream(new FileInputStream(file)));
}
}

一个HTTP的基本服务器就实现完成了,总的来说,得益于Netty的强大性,实现HTTP服务器非常的简单。如果让我们自己用底层Socket来实现HTTP服务器,则需要实现报文解析和生成的功能,还需要考虑到多线程并发的问题。总之,使用Netty来搭建一个HTTP服务器,让我们可以将心思花在业务代码上,可以自己实现一个应用级别的框架。

最后,老规矩,代码已上传到Github,欢迎start :) !!!

Pushy wechat
欢迎订阅我的微信公众号