netty 系列之:netty 初探
简介我们常用浏览器来访问 web 页面得到相关的信息,通常来说使用的都是 HTTP 或者 HTTPS 协议,这些协议的本质上都是 IO,客户端的请求就是 In,服务器的返回就是 Out。但是在目前的协议框架中,并不能完全满足我们所有的需求。比如使用 HTTP 下载大文件,可能需要长连接等待等。我们也知道 IO 方式有多种多样的,包括同步 IO,异步 IO,阻塞 IO 和非阻塞 IO 等。不同的 IO 方式其性能也是不同的,而 netty 就是一个基于异步事件驱动的 NIO 框架。
本系列文章将会探讨 netty 的详细使用,通过原理+例子的具体结合,让大家了解和认识 netty 的魅力。
netty 介绍 netty 是一个优秀的 NIO 框架,大家对 IO 的第一映像应该是比较复杂,尤其是跟各种 HTTP、TCP、UDP 协议打交道,使用起来非常复杂。但是 netty 提供了对这些协议的友好封装,通过 netty 可以快速而且简洁的进行 IO 编程。netty 易于开发、性能优秀同时兼具稳定性和灵活性。如果你希望开发高性能的服务,那么使用 netty 总是没错的。
netty 的最新版本是 4.1.66.Final,事实上这个版本是官方推荐的最稳定的版本,netty 还有 5.x 的版本,但是官方并不推荐。
如果要在项目中使用,则可以引入下面的代码:
下面我们将会从一个最简单的例子,体验 netty 的魅力。
netty 的第一个服务器什么叫做服务器?能够对外提供服务的程序就可以被称为是服务器。建立服务器是所有对外服务的第一步,怎么使用 netty 建立一个服务器呢?服务器主要负责处理各种服务端的请求,netty 提供了一个 ChannelInboundHandlerAdapter 的类来处理这类请求,我们只需要继承这个类即可。
在 NIO 中每个 channel 都是客户端和服务器端沟通的通道。ChannelInboundHandlerAdapter 定义了在这个 channel 上可能出现一些事件和情况,如下图所示:
如上图所示,channel 上可以出现很多事件,比如建立连接,关闭连接,读取数据,读取完成,注册,取消注册等。这些方法都是可以被重写的,我们只需要新建一个类,继承 ChannelInboundHandlerAdapter 即可。
这里我们新建一个 FirstServerHandler 类,并重写 channelRead 和 exceptionCaught 两个方法,第一个方法是从 channel 中读取消息,第二个方法是对异常进行处理。
public class FirstServerHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {// 对消息进行处理 ByteBuf in = (ByteBuf) msg;try {log.info("收到消息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII));}finally {ReferenceCountUtil.release(msg);}}
}
上面例子中,我们收到消息后调用 release()方法将其释放,并不进行实际的处理。调用 release 方法是在消息使用完成之后常用的做法。上面代码将 msg 进行了 ByteBuf 的强制转换,如果并不想进行转换的话,可以直接这样使用:
在异常处理方法中,我们打印出异常信息,并关闭异常的上下文。
有了 Handler,我们需要新建一个 Server 类用来使用 Handler 创建 channel 和接收消息。接下来我们看一下 netty 的消息处理流程。
在 netty 中,对 IO 进行处理是使用多线程的 event loop 来实现的。netty 中的 EventLoopGroup 就是这些 event loop 的抽象类。
我们来观察一下 EventLoopGroup 的类结构。
可以看出 EventLoopGroup 继承自 EventExecutorGroup,而 EventExecutorGroup 继承自 JDK 自带的 ScheduledExecutorService。
所以 EventLoopGroup 本质是是一个线程池服务,之所以叫做 Group,是因为它里面包含了很多个 EventLoop,可以通过调用 next 方法对 EventLoop 进行遍历。
EventLoop 是用来处理注册到该 EventLoop 的 channel 中的 IO 信息,一个 EventLoop 就是一个 Executor,通过不断的提交任务进行执行。当然,一个 EventLoop 可以注册多个 channel,不过一般情况下并不这样处理。
EventLoopGroup 将多个 EventLoop 组成了一个 Group,通过其中的 next 方法,可以对 Group 中的 EventLoop 进行遍历。另外 EventLoopGroup 提供了一些 register 方法,将 Channel 注册到当前的 EventLoop 中。
从上图可以看到,register 的返回结果是一个 ChannelFuture,Future 大家都很清楚,可以用来获得异步任务的执行结果,同样的 ChannelFuture 也是一个异步的结果承载器,可以通过调用 sync 方法来阻塞 Future 直到获得执行结果。
可以看到,register 方法还可以传入一个 ChannelPromise 对象,ChannelPromise 它同时是 ChannelFuture 和 Promise 的子类,Promise 又是 Future 的子类,它是一个特殊的可以控制 Future 状态的 Future。
EventLoopGroup 有很多子类的实现,这里我们使用 NioEventLoopGroup,Nio 使用 Selector 对 channel 进行选择。还有一个特性是 NioEventLoopGroup 可以添加子 EventLoopGroup。
对于 NIO 服务器程序来说,我们需要两个 Group,一个 group 叫做 bossGroup,主要用来监控连接,一个 group 叫做 worker group,用来处理被 boss accept 的连接,这些连接需要被注册到 worker group 中才能进行处理。
将这两个 group 传给 ServerBootstrap,就可以从 ServerBootstrap 启动服务了,相应的代码如下:
//建立两个 EventloopGroup 用来处理连接和消息 EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {@Overridepublic void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new FirstServerHandler());}}).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);
我们最开始创建的 FirstServerHandler 最作为 childHandler 的处理器在初始化 Channel 的时候就被添加进去了。
这样,当有新建立的 channel 时,FirstServerHandler 就会被用来处理该 channel 的数据。
上例中,我们还指定了一些 ChannelOption,用于对 channel 的一些属性进行设定。
最后,我们绑定了对应的端口,并启动服务器。
netty 的第一个客户端上面我们已经写好了服务器,并将其启动,现在还需要一个客户端和其进行交互。
如果不想写代码的话,可以直接 telnet localhost 8000 和 server 端进行交互即可,但是这里我们希望使用 netty 的 API 来构建一个 client 和 Server 进行交互。
构建 netty 客户端的流程和构建 netty server 端的流程基本一致。首先也需要创建一个 Handler 用来处理具体的消息,同样,这里我们也继承 ChannelInboundHandlerAdapter。
上一节讲到了 ChannelInboundHandlerAdapter 里面有很多方法,可以根据自己业务的需要进行重写,这里我们希望当 Channel active 的时候向 server 发送一个消息。那么就需要重写 channelActive 方法,同时也希望对异常进行一些处理,所以还需要重写 exceptionCaught 方法。如果你想在 channel 读取消息的时候进行处理,那么可以重写 channelRead 方法。
创建的 FirstClientHandler 代码如下:
@Slf4jpublic class FirstClientHandler extends ChannelInboundHandlerAdapter {
}上面的代码中,我们首先从 ChannelHandlerContext 申请了一个 ByteBuff,然后调用它的 writeBytes 方法,写入要传输的数据。最后调用 ctx 的 writeAndFlush 方法,向服务器输出消息。
接下来就是启动客户端服务了,在服务端我们建了两个 NioEventLoopGroup,是兼顾了 channel 的选择和 channel 中消息的读取两部分。对于客户端来说,并不存在这个问题,这里只需要一个 NioEventLoopGroup 即可。
服务器端使用 ServerBootstrap 来启动服务,客户端使用的是 Bootstrap,其启动的业务逻辑基本和服务器启动一致:
运行服务器和客户端有了上述的准备工作,我们就可以运行了。首先运行服务器,再运行客户端。
如果没有问题的话,应该会输出下面的内容:
[nioEventLoopGroup-3-1] INFO com.flydean01.FirstServerHandler - 收到消息:Hello flydean.com
总结一个完整的服务器,客户端的例子就完成了。我们总结一下 netty 的工作流程,对于服务器端,首先建立 handler 用于对消息的实际处理,然后使用 ServerBootstrap 对 EventLoop 进行分组,并绑定端口启动。对于客户端来说,同样需要建立 handler 对消息进行处理,然后调用 Bootstrap 对 EventLoop 进行分组,并绑定端口启动。
有了上面的讨论就可以开发属于自己的 NIO 服务了。是不是很简单? 后续文章将会对 netty 的架构和背后的原理进行深入讨论,敬请期待。
本文的例子可以参考:learn-netty4
本文已收录于 http://www.flydean.com/01-netty-startup/
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
版权声明: 本文为 InfoQ 作者【程序那些事】的原创文章。
原文链接:【http://xie.infoq.cn/article/76f091e56eebf91f61990dbff】。文章转载请联系作者。
评论