本卷在于对ChannelHandler,ChannelHandlerContext,ChannelPipeline三大组件的详细讲解,力争做到从架构到源码的深入剖析
Netty整体由以下几大组件组成:
ByteBuf之前几卷中都进行过详细分析,这里只做简单的回顾
从开始学习Java网络编程开始,不知道大家有没有发现API所规定的的数据传输最小单元就是字节,比如NIO中的IntBuffer,LongBuffer等等都是基于ByteBuffer而来的,因此Netty中对NIO中的ByteBuffer类进行了进一步的封装和优化。
ByteBuf的构造如下图所示,它维护了两个指针,一个是读指针,一个是写指针,如果是数据读取操作读指针会自动后移,如果是写操作,写指针会自动后移。因此,这样就不用手动的进行flip()操作了,减少了操作Buffer的复杂性。
它的一些操作方法一般都ByteBuf接口中有所规定的。
ByteBuf的几种模式:
在传统的BIO编程中,我们都会使用Socket进行端口绑定,连接等操作,但是在NIO中我们使用的是SocketChannel(可以简单的理解为Socket+Channel),它也可以进行绑定,连接,读写等操作,也可以完成Channel的关闭操作,因此不难发现Channel的一些增强类提供了一些API让我们不需要直接去使用Socket,减小了开发的复杂性。
下图为netty下的Channel接口里的方法,可以看到它直接调用Unsafe方法来完成Socket的功能
如果对jdk源码有所了解的小伙伴,应该知道jdk底层也有一个Unsafe对象,用来直接和底层操作系统打交道,分配内存的,但是jdk底层的Unsafe对象和这里不是一个对象,不要混淆
进入io.netty.channel包下可以看到它有明显的包结构划分,这也就是常说的Netty给提供的一些数据传输方式
用于处理连接生命周期内所发生的的事件,就不用手动注册和消除事件监听和处理逻辑的调用了。
下图为EventLoop的UML图(后面进行解释)
先说说Channel,EventLoop,EventLoopGroup之间的关系,如下图所示:
可以对上图之间的包含关系做一个整理:
可以从上图中很明显的看到,它的实现是借助了java.util.concurrent包下的线程池来完成的,线程池主要就是用来提供任务执行器,因此netty的EventLoopGroup是netty与jdk的协同设计。
下面这句话是摘抄于书上(将的很透彻):
在这个模型中,一个EventLoop 将由一个永远都不会改变的 Thread 驱动,可以将任务(Runnable 或者Callable)直接提交给EventLoop,以及执行或者调度执行。根据配置和可用核心的不 同,可能会创建多个EventLoop 实例用以优化资源的使用,并且单 个EventLoop 可能会被指派用于服务多个Channel 。
ChannelFuture可以看做是一个线程执行结果的占位符,因为它的执行不确定性因素十分大,谁也不能确定异步信息什么时候会得到结果。
在前文也提到过,在ChannelFuture中扩展了J.U.C的Future,它可以使用addListener()注册一个ChannelFutureListener的一个监听器,以便于可以在某个操作完成之后得到结果(无论是成功还是失败)。
在同一个Channel的异步任务是可以保证它们的顺序调用执行
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事件源码流程,可以参考第五卷
为什么要单独把这个拿出来说一下,是因为这里有坑,很多人会踩到
坑: 使用的channelRead0这个方法,结果服务器端就是不打印,服务器返回的结果,当时客户端是这样写的
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class BaseClientHandler extends SimpleChannelInboundHandler<ByteBuf>{
@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就是常常听到的引导,比如SpringBoot中的启动类上的注解带有BootStrap,因此可以知道BootStrap是一个程序的启动入口,也就是引导的意思。在Netty中引导可以分为两种:服务端引导和客户端引导
也可以从对照下图
BootStrap里只有一个EventLoopGroup,而ServerBootStrap中有两个EventLoopGroup,这是为什么呢?
而客户端不需要本地端口绑定,因此只有连接的Channel,这样更进一步的应征了上一篇文章说得到的ChannelGroup,它可以将不同类型的Channel分为一组。
服务端的两个EventLoopGroup工作流程如下
总结: 在编写服务端的时候需要使用ServerBootStrap来绑定端口和配置一些其他信息,编写客户端是需要使用BootStrap指明发送链接的目的地和其他配置,一个Channel只能绑定一个EventLoop,一个EventLoop可以给多个Channel绑定,通过ChannelFuture来进行异步事件的通知,当事件被触发后,在ChannelPipeline中会有相应的ChannelHandler可以对当前数据进行处理。