干货丨Netty框架初探
Posted 中兴开发者社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了干货丨Netty框架初探相关的知识,希望对你有一定的参考价值。
来源丨中兴大数据
Netty简介
Netty作为当前最流行的NIO框架,在游戏、大数据通讯,互联网领域都有广泛的应用。它是一个异步非阻塞的、事件驱动型的高性能网络开发框架。通过对Socket的封装,简化和流线化了网络应用的编程开发过程。例如,TCP、UDP和文件传输等网络应用的开发。Netty通过精心的项目设计,支持多种协议的开发,如FTP,SMTP,HTTP,各种二进制,自定义消息体结构的的开发。上面这些优点可以帮助我们快速开发出一个网络应用程序。
范例
下面我通过一个简单的helloworld程序,简单描述一下Netty的整体架构和工作流程。
服务端
启动程序:服务端Handler:
客户端
客户端启动:
客户端handler:
范例讲解
BootStrap
在Netty框架中。Netty程序的开始,用于配置EventLoopGroup,Channel,Handler,启动端口号,连接配置参数options。
group(...):定义通信框架使用何种线程模型,输入参数EventLoopGroup是Netty实现线程异步执行的基础。例如范例中服务端通过简单的设置两个NioEventLoopGroup,boss和worker就实现了一个Reactor的线程运行方式。
.channel(...):定义了网络服务框架应用层的实现方式。范例中使用的是NIO方式。
.handler(...):用于向Channl中添加用于ChannelHandler,范例中使用ChannelInitializer向Channel中的ChannelPipeline中添加ChannelHandler,每个ChannelHandler处理自己感兴趣的事件,我们的业务逻辑都是写在handler之中。
线程模型EventLoopGroup
EventLoopGroup,把这个词拆开Event(事件),Loop(循环),group(组)他就是一组事件的循环执行。根据是阻塞还是非阻塞的他有不同的实现子类。OioEventLoopGroup对应的是阻塞式,NioEventLoopGroup对应的非阻塞的,Netty官方也是推荐后者。如下图:
服务端的范例中定义了boss和worker两个NioEventLoopGroup,它的执行原理就是专门有个NIO线程boss用于监听服务端,接受client端的连接请求。当连接建立后就交给worker线程去负责消息的读取,解码,编码和发送。打个比方,就像现实生活中企业的老板负责谈业务接单子,有了订单就交给手下的工人去干。客户端使用的BootStrap只需定义一个Group线程。而通过这种方式就能实现了Reactor线程模型。不需要我们在做额外的开发工作。
它的线程模型如下图所示:
在Netty框架中,无论是I/O操作还是非I/O都会被放在EventLoopGroup中去执行,实际上EventLoopGroup是一组EventLoop集合。观察NioEventLoopGroup和NioEventLoop的继承关系图可以看出他们两个有是一个共同的父类EventExecutorGroup。EventLoopGroup的父类MultithreadEventExecutorGroup的构造函数中可以看出,他会根据nThreads,默认情况下nThreads的取值是是当前cpu的个数\*2,创建一个由NioEventExecutor组成的数组,每个元素都设置了selector选择器和拒绝策略。由此可见至少io操作是放在NioEventExecutor`中执行的。
流程控制的ChannelPipeline
ChannelPipeline实际上是ChannelHandler的容器,采用了职责链的设计模式。它负责ChannelHandler的增加,删除,修改和删除,通过ChannelHandlerContext将ChannelHandler组成一个双向链表。
它还通过自身的inbound和outbound事件触发消息在依次在ChannelHandler的之间传输。但是pipeline并不是直接同chandler进行交互,而是通过ChannelHandlerContext控制handler的执行。
如何构建pipeLine,范例中使用ChannelInitializer来构建一个简单的pipeline。
pipeline中有两个内部channelHandlerContext:head和tail。范例中通过ChannelInitializer类添加进来的ChannelHandler都在这两个内部channelHandlerContext之间。
一个消息在ChannelPipeline中大致处理顺序是:
由前面提到的AbstractNioMessageChannel中的NioMessageUnsafe.read()方法触发I/O事件,然后调用ChannelPipeline.fireChannelRead方法,将消息传入到pipeline中,开始在ChannelHandler的链表表中执行。
消息的执行顺序是从headContext开始,然后依次执行自定义的ChannelInboundHandler1,ChannelInboundHandler2..ChannelInboundHandlerN中执行,最后到TailContext中收尾。
如果有事件调用了ChannelHandlerContext中的write方法,消息将会从tail开始,从ChannelOutboundHandlerN,ChannelOutboundHandlerN-1..ChannelOutboundHandler1,headHandler的顺序执行。
接口ChannelPipeline中的API中的一张图很清楚的说明了这种调度关系,如下图:
当向pipeline中添加handler时,pipeline自动用ChannelHandlerContext将ChannelHandler封装起来,存放到吱声中。如下面的代码段,从代码中可以看出,会将NioEventLoopGroup(EventExecutorGroup的子类)也封装到ChannelHandlerContext中去,供handler调用。
ChannelHandlerContext
ChannelHandlerContext接口及其子类,我们在开发中不会直接接触到,但当我们向channel的pipeline中添加handler的时候,Netty会将pipeline和handler绑定在一起。
Netty中代表上下文关系的一个接口。比如在其子类AbstractChannelHandlerContext中定义了:
当我们开发时向pipeline中增加handler时,Netty会将这个handler通过context与前和后的handler相关联,组成一个双向链表。
在范例的ServerHandler中
结合前面Handler中提到的,ctx.write调用的是ChannelOutboundHandler中的事件,实际上就是向pipeline中待发送区写入消息。比如ctx.write(in)就是向client端发送数据。如果把这句注释掉,client端就不会接收到任何数据。
如果需要调用的是ctx.fireChannelRead(in)就会寻找下一个ChannelInboundHandler的ChannelRead方法。从下图的代码中可以看出,操作最终是放到EventExecutor中执行的,前面提到过EventExecutor又是EventExecutorLoop的父类,这里就解释了Netty的所有操作都是放到线程中去执行的。
ChannelHandler
ChannelHandler类似拦截器,他是基于事件驱动的,他会根据自身的事件选择性地拦截在pipeline中传输的数据,比如ServerHandler中只对channelRead,channelReadComplete和exceptionCaught。也可以直接终止整个事件的传输,比如exceptionCaught方法中当发生异常情况就直接调用ctx.close()关闭连接。
ChannelHandler有两个重要的子接口:
ChannelInboundHandler
是指当某个I/O事件如链路的建立和关闭,数据的读取所触发事件。
为了方便用户开发,推荐直接继承ChannelInboundHandlerAdapter,然后对自己感兴趣的事件进行覆盖重写即可。比如范例中的ServerHandler,就覆盖了channelRead、channelReadComplete,exceptionCaught三个方法。
ChannelOutboundHandler
Netty或者用户触发的I/O都被称为OutBound事件。比如bind,connect,disconnnect,read,write事件等。
同样也有一个ChannelOutboundHandlerAdapter,方便自定义开发。
小结
ChannelPipeline,ChannelHandler,ChannelHandlerContext三者的权责关系是非常清晰的,符合了单一职责的原则。他们就像一条流水线,ChannelPipeline负责整个流水线的正常运转,ChannelHandler就像一个工人随时准备处理流水线的产品,ChannelHandlerContext就像传送带,在工人之间传输产品。
Netty推荐我们在开发业务handler时直接继承ChannelOutboundHandlerAdapter和ChannelInboundHandlerAdapter,因为他们对每种事件都有默认的处理方法,不需要我们重写全部的时间方法。
Channel
Netty的Channel采用了Facade的模式进行了统一封装。将JDK的socketChannel和SeverSocketChannel统一封装在一起,同时为了适应前面提到的Netty框架中的ChannelPipeline,EventLoopGroup等功能,它使用聚合的方式把这些也归纳进来,为的就是为外界提供一个统一的方便的接口。比如范例中使用到的NioserverSocketChannel就是,它的关系继承图如下:
通过观察下图中抽象类AbstractChannel的子类,就可以很清晰的看出每个子类对应的不同的应用场景。我们在开发的时候只需要根据不同的场景使用不同的子类即可。
需要注意的是,每种Channel必须使用对应的EventLoopGroup,AbstratctNioChannel只能使用NioEventLoopGroup。AbstractOioSocket只能使用OioEventLoopGroup。
在channel中有一个重要的内部类Unsafe,每种channel的unsafe都不一样,channel使用unsafe来实现最底层的网络通信。如AbstractNioMessageChannel中的Unsafe。Netty取名unsafe不是说他不安全,我觉得而是让后面的开发者别乱改变他。
总结
到此为止,我们把Netty框架中最基础的概念稍稍讲解了一遍。Netty作为一款优秀的网络开发的框架,我们可以通过阅读他的源码学习他在编程技巧和编程思想上优点,开拓自己的编程思维,并可以将这些思维应用到平时的开发过程当中。
以上是关于干货丨Netty框架初探的主要内容,如果未能解决你的问题,请参考以下文章
BIO/NIO 线程模型以及高性能通讯框架 Netty Reactor 模型初探
#yyds干货盘点# 基于Netty,20分钟手写一个RPC框架
Netty 简介《Netty In Action》 #yyds干货盘点#