简介我们经常使用浏览器访问网页来获取相关信息。通常,使用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版本,不过官方不推荐。如果想在项目中使用,可以引入如下代码:io.nettynetty-all4.1.66.Final下面我们就从一个最简单的例子来感受netty的魅力。netty的第一个服务器叫什么服务器?能够对外提供服务的程序就可以称为服务器。建立服务器是所有对外服务的第一步。如何使用netty建立服务器?服务器主要负责处理各种服务器请求。Netty提供了ChannelInboundHandlerAdapter类来处理此类请求。我们只需要继承这个类。NIO中的每个通道都是客户端和服务器之间的一个通信通道。ChannelInboundHandlerAdapter定义了这个通道上可能发生的一些事件和情况,如下图所示:、注册、注销等等。这些方法是可以重写的,我们只需要新建一个类,继承ChannelInboundHandlerAdapter即可。这里我们新建了一个FirstServerHandler类,重写了channelRead和exceptionCaught两个方法。第一种方法是从通道中读取消息,第二种方法是处理异常。publicclassFirstServerHandlerextendsChannelInboundHandlerAdapter{@OverridepublicvoidchannelRead(ChannelHandlerContextctx,Objectmsg){//处理消息ByteBufin=(ByteBuf)msg??;try{log.info("收到消息:{}",in.toString(io.netty.util.CharsetUtil.US_ASCII));}最后{ReferenceCountUtil.release(msg);}}@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause){//异常处理log.error("Anexceptionoccurred",cause);ctx.close();}}在上面的例子中,我们在收到消息后调用release()方法释放消息,并没有真正进行处理。在消息被消费后调用release方法是一种常见的做法。上面的代码将msg转换为ByteBuf。如果不想转换,可以直接这样使用:try{//messageprocessing}finally{ReferenceCountUtil.release(msg);在异常处理方法中,我们打印出异常信息,并关闭异常上下文。有了Handler,我们需要新建一个Server类来使用Handler创建通道和接收消息。接下来我们看一下netty的消息处理流程。在netty中,IO处理是使用多线程事件循环实现的。netty中的EventLoopGroup就是这些事件循环的抽象类。我们观察一下EventLoopGroup的类结构。可以看出EventLoopGroup继承自EventExecutorGroup,EventExecutorGroup继承自JDK自带的ScheduledExecutorService。所以EventLoopGroup本质上是一个线程池服务。之所以叫Group,是因为它包含了很多EventLoop,可以通过调用next方法来遍历。EventLoop用于处理在EventLoop的channel中注册的IO信息。一个EventLoop就是一个Executor,通过不断的提交任务来执行。当然一个EventLoop可以注册多个channel,但是一般情况下不是这样的。EventLoopGroup将多个EventLoop组成一个Group,通过next方法可以遍历Group中的EventLoop。另外EventLoopGroup提供了一些register方法将Channel注册到当前EventLoop中。从上图可以看出,register的返回结果是一个ChannelFuture。大家都知道Future可以用来获取异步任务的执行结果。同样ChannelFuture也是一个异步的结果载体,可以通过调用sync方法来阻塞。直到得到执行结果。可见register方法也可以传入一个ChannelPromise对象。ChannelPromise是ChannelFuture和Promise的子类。Promise是Future的子类。它是一个特殊的Future,可以控制Future的状态。EventLoopGroup有很多子类实现。这里我们使用NioEventLoopGroup,Nio使用Selector选择通道。另一个特点是NioEventLoopGroup可以添加子EventLoopGroup。对于NIO服务器程序,我们需要两个组,一个组叫bossGroup,主要用来监听连接,另一个组叫worker组,用来处理boss接受的连接。这些连接需要在工作组中注册才能继续。处理。将这两个组传递给ServerBootstrap以从ServerBootstrap启动服务。对应代码如下://创建两个EventloopGroup,分别处理连接和消息EventLoopGroupbossGroup=newNioEventLoopGroup();EventLoopGroupworkerGroup=newNioEventLoopGroup();尝试{ServerBootstrapb=newServerBootstrap();b.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(newChannelInitializer(){@OverridepublicvoidinitChannel(SocketChannelch)throwsException{ch.pipeline().addLast(newFirstServerHandler()});}}).option(ChannelOption.SO_BACKLOG,128).childOption(ChannelOption.SO_KEEPALIVE,true);//绑定端口并开始接收连接ChannelFuturef=b.bind(port).sync();我们一开始创建的FirstServerHandler作为Channel初始化时的childHandler处理器这样添加,当有新创建的通道时,会使用FirstServerHandler来处理通道的数据。在上面的例子中,我们还指定了一些ChannelOptions来设置通道的一些属性。最后我们绑定对应的端口,启动服务器。上面netty的第一个客户端我们已经写好了服务端并启动了,现在还需要一个客户端与之交互。如果不想写代码,可以直接telnetlocalhost8000与服务端进行交互,但是这里我们希望利用netty的API搭建一个客户端与服务端进行交互。构建netty客户端的过程与构建netty服务器的过程基本相同。首先,我们需要创建一个Handler来处理特定的消息。同样,这里我们也继承ChannelInboundHandlerAdapter。上一节提到ChannelInboundHandlerAdapter中有很多方法,可以根据自己业务的需要改写。这里我们希望在Channel激活时向服务器发送消息。那么你需要重写channelActive方法,同时你还想对异常做一些处理,所以你也需要重写exceptionCaught方法。如果要在通道读取消息时进行处理,可以重写channelRead方法。创建的FirstClientHandler代码如下:@Slf4jpublicclassFirstClientHandlerextendsChannelInboundHandlerAdapter{privateByteBufcontent;私有ChannelHandlerContextctx;@OverridepublicvoidchannelActive(ChannelHandlerContextctx){this.ctx=ctx.xdirct2.allect(6)writeBytes("Helloflydean.com".getBytes(StandardCharsets.UTF_8));//发送消息sayHello();}@OverridepublicvoidexceptionCaught(ChannelHandlerContextctx,Throwablecause){//异常处理log.error("Anexceptionoccurred",cause);ctx.close();}privatevoidsayHello(){//向服务器输出消息ctx.writeAndFlush(content.retain());}}上面的代码中,我们首先从ChannelHandlerContext中申请了一个ByteBuff,然后调用它的writeBytes方法写入要传输的数据。最后调用ctx的writeAndFlush方法向服务端输出消息。下一步是启动客户端服务。在服务端,我们构建了两个NioEventLoopGroup,它兼顾了频道的选择和频道内消息的读取。对于client来说,这个问题是不存在的,这里只需要一个NioEventLoopGroup即可。服务端使用ServerBootstrap启动服务,客户端使用Bootstrap。其启动的业务逻辑与服务端基本相同:EventLoopGroupgroup=newNioEventLoopGroup();试试{Bootstrapb=newBootstrap();b.group(group).channel(NioSocketChannel.class).handler(newChannelInitializer(){@OverrideprotectedvoidinitChannel(SocketChannelch)throwsException{ChannelPipelinep=ch.pipeline();p.addLast(新FirstClientHandler());}});//连接到服务器ChannelFuturef=b.connect(HOST,PORT).sync();运行服务器和客户端有了上面的准备工作,我们就可以运行了。先运行服务器,再运行客户端。如果没有问题,应该会输出如下内容:[nioEventLoopGroup-3-1]INFOcom.flydean01.FirstServerHandler-Receivedmessage:Helloflydean.com总结一个完整的服务器,客户端示例完成。下面总结一下netty的工作流程。对于server端来说,首先建立一个handler用于消息的实际处理,然后使用ServerBootstrap对EventLoop进行分组,并绑定端口启动。对于client,还需要建立一个handler来处理消息,然后调用Bootstrap对EventLoop进行分组,并绑定端口启动。通过以上讨论,您可以开发自己的NIO服务。是不是很简单?后续文章将对netty的架构及其背后的原理进行深入探讨,敬请期待。这篇文章的例子可以参考:learn-netty4本文已经收录在http://www.flydean.com/01-netty-startup/最通俗的解读,最深刻的干货,最简洁的教程,很多你不知道的小技巧等你来发现!欢迎关注我的公众号:《程序那些事儿》,懂技术,更懂你!