Netty简介
Netty 是最流行的 NIO 框架,是基于 Java NIO 的异步事件驱动的网络应用框架。Netty 提供了简单易用的API从网络处理代码中解耦业务逻辑。Netty 是完全基于 NIO 实现的,所以整个 Netty 都是异步的。
许多框架和开源组件的底层 rpc 都是使用的 Netty,如 Dubbo、Elasticsearch 等等。下面是官网给出的一些 Netty 的特性:
设计方面
- 对各种传输协议提供统一的 API(使用阻塞和非阻塞套接字时候使用的是同一个 API,只是需要设置的参数不一样)。
- 基于一个灵活、可扩展的事件模型来实现关注点清晰分离。
- 高度可定制的线程模型——单线程、一个或多个线程池。
- 真正的无数据报套接字(UDP)的支持(since 3.1)。
易用性
- 完善的 Javadoc 文档和示例代码。
- 不需要额外的依赖,JDK 5 (Netty 3.x) 或者 JDK 6 (Netty 4.x) 已经足够。
性能
- 更好的吞吐量,更低的等待延迟。
- 更少的资源消耗。
- 最小化不必要的内存拷贝。
安全性
- 完整的 SSL/TLS 和 StartTLS 支持
对于初学者,上面的特性我们在脑中有个简单了解和印象即可, 下面开始我们的实战部分。
Netty实战
实现Http 服务器
开发环境: IDEA+Gradle+Netty4
添加依赖: compile 'io.netty:netty-all:4.1.26.Final'
第一个示例我们使用 Netty 编写一个 Http 服务器的程序,启动服务我们在浏览器输入网址来访问我们的服务,便会得到服务端的响应。功能很简单,下面我们看看具体怎么做?
服务启动类
public class HttpServer {
public static void main(String[] args) {
//构造两个线程组
EventLoopGroup bossrGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//服务端启动辅助类
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new HttpServerInitializer());
ChannelFuture future = bootstrap.bind(8080).sync();
//等待服务端口关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
注:
在编写 Netty 程序时,一开始都会生成 NioEventLoopGroup
的两个实例,分别是 bossGroup
和 workerGroup
,也可以称为 parentGroup
和 childGroup
,为什么创建这两个实例,作用是什么?可以这么理解,bossGroup
和 workerGroup
是两个线程池, 它们默认线程数为 CPU 核心数乘以 2,bossGroup
用于接收客户端传过来的请求,接收到请求后将后续操作交由 workerGroup
处理。
接下来我们生成了一个服务启动辅助类的实例 bootstrap
,boostrap
用来为 Netty 程序的启动组装配置一些必须要组件,例如上面的创建的两个线程组。channel 方法用于指定服务器端监听套接字通道 NioServerSocketChannel
,其内部管理了一个 Java NIO 中的ServerSocketChannel
实例。
channelHandler
方法用于设置业务职责链,责任链是我们下面要编写的,责任链具体是什么,它其实就是由一个个的 ChannelHandler
串联而成,形成的链式结构。正是这一个个的 ChannelHandler
帮我们完成了要处理的事情。
接着我们调用了 bootstrap 的 bind 方法将服务绑定到 8080 端口上,bind 方法内部会执行端口绑定等一系列操,使得前面的配置都各就各位各司其职,sync 方法用于阻塞当前 Thread,一直到端口绑定操作完成。接下来一句是应用程序将会阻塞等待直到服务器的 Channel 关闭。
启动类的编写大体就是这样了,下面要编写的就是上面提到的责任链了。如何构建一个链,在 Netty 中很简单,不需要我们做太多,代码如下:
public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel sc) throws Exception {
ChannelPipeline pipeline = sc.pipeline();
//处理http消息的编解码
pipeline.addLast("httpServerCodec", new HttpServerCodec());
//添加自定义的ChannelHandler
pipeline.addLast("httpServerHandler", new HttpServerHandler());
}
}
我们自定义一个类 HttpServerInitializer
继承 ChannelInitializer
并实现其中的 initChannel
方法。
ChannelInitializer
继承 ChannelInboundHandlerAdapter
,用于初始化 Channel
的 ChannelPipeline
。通过 initChannel
方法参数 sc
得到 ChannelPipeline
的一个实例。
当一个新的连接被接受时, 一个新的 Channel
将被创建,同时它会被自动地分配到它专属的 ChannelPipeline
。
ChannelPipeline
提供了 ChannelHandler
链的容器,推荐读者仔细自己看看 ChannelPipeline
的 Javadoc,文章后面也会继续说明 ChannelPipeline
的内容。
Netty 是一个高性能网络通信框架,同时它也是比较底层的框架,想要 Netty 支持 Http(超文本传输协议),必须要给它提供相应的编解码器。
所以我们这里使用 Netty 自带的 Http 编解码组件 HttpServerCodec
对通信数据进行编解码,HttpServerCodec
是 HttpRequestDecoder
和 HttpResponseEncoder
的组合,因为在处理 Http 请求时这两个类是经常使用的,所以 Netty 直接将他们合并在一起更加方便使用。所以对于上面的代码:
pipeline.addLast("httpServerCodec", new HttpServerCodec())
通过 addLast
方法将一个一个的 ChannelHandler
添加到责任链上并给它们取个名称(不取也可以,Netty 会给它个默认名称),这样就形成了链式结构。在请求进来或者响应出去时都会经过链上这些 ChannelHandler
的处理。
最后再向链上加入我们自定义的 ChannelHandler
组件,处理自定义的业务逻辑。下面就是我们自定义的 ChannelHandler
。
public class HttpServerChannelHandler0 extends SimpleChannelInboundHandler<HttpObject> {
private HttpRequest request;
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (msg instanceof HttpRequest) {
request = (HttpRequest) msg;
request.method();
String uri = request.uri();
System.out.println("Uri:" + uri);
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
ByteBuf buf = content.content();
System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8));
ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
ctx.writeAndFlush(response);
}
}
}
至此一个简单的 Http 服务器就完成了。首先我们来看看效果怎样,我们运行 HttpServer 中的 main 方法。让后使用 Postman 这个工具来测试下,使用 post 请求方式(也可以 get,但没有请求体),并一个 json 格式数据作为请求体发送给服务端,服务端返回给我们一个hello world字符串。
总结
对于自定义的 ChannelHandler
, 一般会继承 Netty 提供的SimpleChannelInboundHandler
类,并且对于 Http 请求我们可以给它设置泛型参数为 HttpOjbect
类,然后覆写 channelRead0
方法,在 channelRead0
方法中编写我们的业务逻辑代码,此方法会在接收到服务器数据后被系统调用。
Netty 的设计中把 Http 请求分为了 HttpRequest
和 HttpContent
两个部分,HttpRequest 主要包含请求头、请求方法等信息,HttpContent 主要包含请求体的信息。
所以上面的代码我们分两块来处理。在 HttpContent
部分,首先输出客户端传过来的字符,然后通过 Unpooled 提供的静态辅助方法来创建未池化的 ByteBuf 实例, Java NIO 提供了 ByteBuffer 作为它的字节容器,Netty 的 ByteBuffer 替代品是 ByteBuf。
接着构建一个 FullHttpResponse 的实例,并为它设置一些响应参数,最后通过 writeAndFlush 方法将它写回给客户端。
上面这样获取请求和消息体则相当不方便,Netty 又提供了另一个类 FullHttpRequest
,FullHttpRequest
包含请求的所有信息,它是一个接口,直接或者间接继承了 HttpRequest 和 HttpContent,它的实现类是 DefalutFullHttpRequest
。
因此我们可以修改自定义的 ChannelHandler
如下:
public class HttpServerChannelHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
ctx.channel().remoteAddress();
FullHttpRequest request = msg;
System.out.println("请求方法名称:" + request.method().name());
System.out.println("uri:" + request.uri());
ByteBuf buf = request.content();
System.out.print(buf.toString(CharsetUtil.UTF_8));
ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
response.headers().add(HttpHeaderNames.CONTENT_TYPE, "text/plain");
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
ctx.writeAndFlush(response);
}
}
这样修改就可以了吗,如果你去启动程序运行看看,是会抛异常的。前面说过 Netty 是一个很底层的框架,对于将请求合并为一个 FullRequest 是需要代码实现的,然而这里我们并不需要我们自己动手去实现,Netty 为我们提供了一个 HttpObjectAggregator 类,这个 ChannelHandler作用就是将请求转换为单一的 FullHttpReques。
所以在我们的 ChannelPipeline 中添加一个 HttpObjectAggregator 的实例即可。
public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel sc) {
ChannelPipeline pipeline = sc.pipeline();
//处理http消息的编解码
pipeline.addLast("httpServerCodec", new HttpServerCodec());
pipeline.addLast("aggregator", new HttpObjectAggregator(65536));
//添加自定义的ChannelHandler
pipeline.addLast("httpServerHandler", new HttpServerChannelHandler0());
}
}
实现Netty 客户端
上面的两个示例中我们都是以 Netty 做为服务端,接下来看看如何编写 Netty 客户端,以第一个 Http 服务的例子为基础,编写一个访问 Http 服务的客户端。
public class HttpClient {
public static void main(String[] args) throws Exception {
String host = "127.0.0.1";
int port = 8080;
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(65536));
pipeline.addLast(new HttpClientHandler());
}
});
// 启动客户端.
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
客户端启动类编写基本和服务端类似,在客户端我们只用到了一个线程池,服务端使用了两个,因为服务端要处理 n 条连接,而客户端相对来说只处理一条,因此一个线程池足以。
然后服务端启动辅助类使用的是 ServerBootstrap,而客户端换成了 Bootstrap。通过 Bootstrap 组织一些必要的组件,为了方便,在 handler 方法中我们使用匿名内部类的方式来构建 ChannelPipeline 链容器。最后通过 connect 方法连接服务端。
接着编写 HttpClientHandler 类。
public class HttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
URI uri = new URI("http://127.0.0.1:8080");
String msg = "Are you ok?";
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
uri.toASCIIString(), Unpooled.wrappedBuffer(msg.getBytes("UTF-8")));
// 构建http请求
// request.headers().set(HttpHeaderNames.HOST, "127.0.0.1");
// request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());
// 发送http请求
ctx.channel().writeAndFlush(request);
}
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) {
FullHttpResponse response = msg;
response.headers().get(HttpHeaderNames.CONTENT_TYPE);
ByteBuf buf = response.content();
System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8));
}
}
在 HttpClientHandler 类中,我们覆写了 channelActive 方法,当连接建立时,此方法会被调用,我们在方法中构建了一个 FullHttpRequest 对象,并且通过 writeAndFlush 方法将请求发送出去。
channelRead0 方法用于处理服务端返回给我们的响应,打印服务端返回给客户端的信息。至此,Netty 客户端的编写就完成了,我们先开启服务端,然后开启客户端就可以看到效果了。
希望通过前面介绍的几个例子能让大家基本知道如何编写 Netty 客户端和服务端,下面我们来说说 Netty 程序为什么是这样编写的,这也是 Netty 中最为重要的一部分知识,可以让你在编写 netty 程序时做到心中有数。