整体架构
ByteBufChannelEventLoop和EventLoopGroupChannelFutureChannelHandler和ChannelPipelineSimpleChannelInboundHandler关于入站和出站处理器是如何识别的问题BootStrap
本卷在于对ChannelHandler,ChannelHandlerContext,ChannelPipeline三大组件的详细讲解,力争做到从架构到源码的深入剖析
整体架构
Netty整体由以下几大组件组成:
ByteBuf
ByteBuf之前几卷中都进行过详细分析,这里只做简单的回顾
从开始学习Java网络编程开始,不知道大家有没有发现API所规定的的数据传输最小单元就是字节,比如NIO中的IntBuffer,LongBuffer等等都是基于ByteBuffer而来的,因此Netty中对NIO中的ByteBuffer类进行了进一步的封装和优化。
ByteBuf的构造如下图所示,它维护了两个指针,一个是读指针,一个是写指针,如果是数据读取 *** 作读指针会自动后移,如果是写 *** 作,写指针会自动后移。因此,这样就不用手动的进行flip() *** 作了,减少了 *** 作Buffer的复杂性。
它的一些 *** 作方法一般都ByteBuf接口中有所规定的。
ByteBuf的几种模式:
- 堆缓冲区: 将数据存在JVM堆里的一个字节数组,在没有使用内存池的情况下可以提供快速的分配和释放,也被成为支撑数组(backing array)直接缓冲区:直接缓冲区模式可以避免不必要的中间内存拷贝,它的内存分配并不在JVM堆里,相对于堆来说它的分配和释放代价比较大,而且如果要对它里面的数据进行 *** 作的话还需要将里面的内容拷贝到一个数组里 *** 作。它比较适合大数据量的传输且不带有数据处理的一些 *** 作。复合缓冲区:它可以看做是多个ByteBuf聚合后的视图,可以根据需要进行ByteBuf实例的添加和删除(这个在JDK中的复合缓冲区是没有这个特性的),Netty中通过CompositeByteBuf(ByteBuf的子类)实现了这个模式,里面可以同时包含有直接缓冲区和非直接缓冲区。它的结构如下图所示,相当于是一个链表结构,它的内部也带有迭代器。但是它也有缺点,也就是它不支持直接访问数组,和直接缓冲区一样,需要先转化为数组,才能进行 *** 作。
Channel
在传统的BIO编程中,我们都会使用Socket进行端口绑定,连接等 *** 作,但是在NIO中我们使用的是SocketChannel(可以简单的理解为Socket+Channel),它也可以进行绑定,连接,读写等 *** 作,也可以完成Channel的关闭 *** 作,因此不难发现Channel的一些增强类提供了一些API让我们不需要直接去使用Socket,减小了开发的复杂性。
下图为netty下的Channel接口里的方法,可以看到它直接调用Unsafe方法来完成Socket的功能
如果对jdk源码有所了解的小伙伴,应该知道jdk底层也有一个Unsafe对象,用来直接和底层 *** 作系统打交道,分配内存的,但是jdk底层的Unsafe对象和这里不是一个对象,不要混淆
进入io.netty.channel包下可以看到它有明显的包结构划分,这也就是常说的Netty给提供的一些数据传输方式
EventLoop和EventLoopGroup
用于处理连接生命周期内所发生的的事件,就不用手动注册和消除事件监听和处理逻辑的调用了。
下图为EventLoop的UML图(后面进行解释)
先说说Channel,EventLoop,EventLoopGroup之间的关系,如下图所示:
可以对上图之间的包含关系做一个整理:
因此,在前文说到EventLoop很像是一个Selector。
可以从上图中很明显的看到,它的实现是借助了java.util.concurrent包下的线程池来完成的,线程池主要就是用来提供任务执行器,因此netty的EventLoopGroup是netty与jdk的协同设计。
下面这句话是摘抄于书上(将的很透彻):
在这个模型中,一个EventLoop 将由一个永远都不会改变的 Thread 驱动,可以将任务(Runnable 或者Callable)直接提交给EventLoop,以及执行或者调度执行。根据配置和可用核心的不 同,可能会创建多个EventLoop 实例用以优化资源的使用,并且单 个EventLoop 可能会被指派用于服务多个Channel 。
ChannelFuture
ChannelFuture可以看做是一个线程执行结果的占位符,因为它的执行不确定性因素十分大,谁也不能确定异步信息什么时候会得到结果。
在前文也提到过,在ChannelFuture中扩展了J.U.C的Future,它可以使用addListener()注册一个ChannelFutureListener的一个监听器,以便于可以在某个 *** 作完成之后得到结果(无论是成功还是失败)。
在同一个Channel的异步任务是可以保证它们的顺序调用执行
ChannelHandler和ChannelPipeline
ChannelHandler,它可以对出站入站的数据进行处理,相应的网络事件的出发也就伴随着相应的ChannelHandler的执行。
ChannelPipeline则是它的容器,在ChannelPipeline中包含有处理对应Channel的所有ChannelHandler,在这个ChannelPipeline中规定有数据的进站和出站处理的一些列ChannelHandler,如下图所示
采用的是责任链模式,这种模式简化了手动的逻辑判断,比如在Tomcat中也是使用到了责任链模式
下面详细剖析一下这些组件:
首先先分析一下ChannelHandler,ChannelHandler是我们日常开发中使用最多的组件了,大概我们平时写的最多的组件就是Handler了,继承图如下
我们平时继承的最多的就是ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter,这两个不是接口也不是抽象类,所以我们可以仅仅重写我们需要的方法,没有必须要实现的方法,当然我们也会使用SimpleChannelInboundHandler,这个类我们上个小节也稍微讲了它的优缺点,这里不赘述
ChannelHandler,ChannelHandlerContext,ChannelPipeline这三者的关系很特别,相辅相成,
一个ChannelPipeline中可以有多个ChannelHandler实例,而每一个ChannelHandler实例与ChannelPipeline之间的桥梁就是ChannelHandlerContext实例,如图所示:
看图就知道,ChannelHandlerContext的重要性了,如果你获取到了ChannelHandlerContext的实例的话,你可以获取到你想要的一切,你可以根据ChannelHandlerContext执行ChannelHandler中的方法,我们举个例子来说,我们可以看下ChannelHandlerContext部分API:
这几个API都是使用比较频繁的,都是调用当前handler之后同一类型的channel中的某个方法,这里的同一类型指的是同一个方向,比如inbound调用inbound,outbound调用outbound类型的channel,一般来说,都是一个channel的ChannnelActive方法中调用fireChannelActive来触发调用下一个handler中的ChannelActive方法
ChannelHandlerContext负责包装一个ChannelHandler对象,然后pipeline使用双向链表的形式将这一个一个ChannelHandlerContext串联在一起,每当有一个客户端连接传入时,所有工作线程共享这一套pipeline工作流体系
这里我们追踪一下源码:
可以知道findContextOutbound返回的是拥有下一个channelHanlder的channelHanlderContext对象
next.invokeChannelActive()这里当前的this对象已经发生了改变,这在下面条件不满足时,继续寻找链表上下一个ChannelHandlerContext的findContextInBound()方法中起到了作用
不满足时会继续找下一个ContextHandlerContext.,然后执行其内部维护的ChannelHandler的channelActive方法
上面以active事件为切入点进行了调用链分析,其他事件类似,大家可以参考
分析了那么多,下面讲讲pipeline的作用体现
我们下面看看是怎么放进去的
剩余代码不做分析,大家可以自行去看源码
目前来说这样做的好处:
1)每一个handler只需要关注自己要处理的方法,如果你不关注channelActive方法时,你自定义的channelhandler就不需要重写channelActive方法
2)异常处理,如果 exceptionCaught方法每个handler都重写了,只需有一个类捕捉到然后做处理就可以了,不需要每个handler都处理一遍
3)灵活性。例如如下图所示:
如图所示在业务逻辑处理中,也许左侧第一个ChannelHandler根本不需要管理某个业务逻辑,但是从第二个ChannelHandler就需要关注处理某个业务需求了,那么就可以很灵活地从第二个ChannelHandler开始处理业务,不需要从channel中的第一个ChannelHandler开始处理,这样会使代码显得让人看不懂~
初步看懂的ChannelHandler,ChannelHandlerContext,ChannelPipeline之间的关系就是如上总结的
这里纠正一下上面源码剖析的一个错误结论:每个客户端的socketChannel对象都会创建一个自己的piepline,并且互相拥有对方的引用,下面源码证明:
每个客户端连接上来后,都会用NioSocktChannel包装原生的SocketChannel引用,下面源码论证:
详细的accept事件源码流程,可以参考第五卷
SimpleChannelInboundHandler
为什么要单独把这个拿出来说一下,是因为这里有坑,很多人会踩到
坑: 使用的channelRead0这个方法,结果服务器端就是不打印,服务器返回的结果,当时客户端是这样写的
import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; public class baseClientHandler extends SimpleChannelInboundHandler{ @Override protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception { System.out.println("Client channelRead0 received:" + msg); } // @Override // public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // System.out.println("Client channelRead received:" + msg); // // } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
原因:SimpleChannelInboundHandler是继承于ChannelInboundHandlerAdapter,重写了channelRead方法
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { boolean release = true; try { if (acceptInboundMessage(msg)) { @SuppressWarnings("unchecked") I imsg = (I) msg; channelRead0(ctx, imsg); } else { release = false; ctx.fireChannelRead(msg); } } finally { if (autoRelease && release) { ReferenceCountUtil.release(msg); } } }
SimpleChannelInboundHandler后面指定了处理类型,也就是源码中的"I",acceptInboundMessage方法判断msg是不是SimpleChannelInboundHandler中指定的类型,我们这边指定的是ByteBuf,感觉没啥问题啊,但是我们忽略了一个问题,我们客户端中有3个处理器,两个inbound类型的处理器,其中一个就是HelloWorldClientHandler,还有一个就是StringDecoder,上一个处理器已经把服务器端的信息转化成String,还用ByteBuf来接收,显然不能处理
这里如果想处理,把类型换成String即可,下面我们来看看书上是怎么说的
SimpleChannelInboundHandler的channelRead0还有一个好处就是你不用关心释放资源,因为源码中已经帮你释放了,所以如果你保存获取的信息的引用,是无效的~
关于入站和出站处理器是如何识别的问题
首先pipeline将所有入站和出站处理器串联在一起,并没有搞出两条链表进行区分,那么pipeline是如何识别入站和出站处理器的呢?
下面分析:
上面分析过active事件,这里是触发read事件的时候,会挨个调用每个handler的channelRead方法,但是别忘了这里有我们之前没讲的一个方法:
分析完毕
BootStrap
BootStrap就是常常听到的引导,比如SpringBoot中的启动类上的注解带有BootStrap,因此可以知道BootStrap是一个程序的启动入口,也就是引导的意思。在Netty中引导可以分为两种:服务端引导和客户端引导
也可以从对照下图
BootStrap里只有一个EventLoopGroup,而ServerBootStrap中有两个EventLoopGroup,这是为什么呢?
而客户端不需要本地端口绑定,因此只有连接的Channel,这样更进一步的应征了上一篇文章说得到的ChannelGroup,它可以将不同类型的Channel分为一组。
服务端的两个EventLoopGroup工作流程如下
总结: 在编写服务端的时候需要使用ServerBootStrap来绑定端口和配置一些其他信息,编写客户端是需要使用BootStrap指明发送链接的目的地和其他配置,一个Channel只能绑定一个EventLoop,一个EventLoop可以给多个Channel绑定,通过ChannelFuture来进行异步事件的通知,当事件被触发后,在ChannelPipeline中会有相应的ChannelHandler可以对当前数据进行处理。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)