Netty,事件驱动

Netty,事件驱动

前言

本节将围绕事件驱动模型的事件对象、事件源、事件监听者这几个角度,来观察netty中是如何实现这三个抽象的概念,并且通过源码来分析事件驱动的整个过程。

事件驱动模型

  首先来看看事件驱动模型在awt里的应用,模型图如下:

大部分的Gui设计,一般都是采用事件驱动模型。系统触发Click(事件源)时,会将Event(事件对象)加入到事件队列,主线程会从事件队里里取出事件,通知Button(事件监听者)执行这个事件。
netty,对socket上面封装了一层异步事件驱动模型,来触发网络I/O的各种操作,让使用者不在关心网络底层,就可以编写异步非阻塞的代码了。个人理解的模型图,如下:

事件对象

netty中对事件抽象了两种类型:inbound/outbound.

  inbound事件对象,ChannelInboundHandler,它会触发如下事件

  outbound事件对象,ChannelOutboundHandler,它会触发如下事件

事件监听者

在netty中监听IO事件,实现上面的事件对象,将这个对象加入到ChannelPipeline即可
如,监听channel中否有数据可以读,只需要实现ChannelInboundHandler中的channelRead方法
如,监听channel中是否有数据正在写,只需要实现ChannelInboundHandler中的write方法。
这种设计好处,让IO事件与用户的业务逻辑解耦,也提高了扩展性.netty的扩展性在源码中就有体现,如MessageToByteEncoder,总之,这两个事件对象玩起来很灵活,这个在后面会讲述。

事件源

NioEventLoop,这个类在当有IO的事件的时候,会将IO事件发布给事件监听者,来看这个类发布IO事件的源码:

从上面的代码,我们发现了OP_READ,OP_ACCEPT,OP_WRITE,OP_CONNECT事件,这些事件是委托NioUnsafe去完成的。NioUnsafe,会把socket的这些事件封装一层,封装后就变成了netty中的Inbound/Outbound事件。

事件传递

当socket层有事件更新时,netty会把这些事件包装成Inbound/Outbound事件。这些事件的监听者(ChannelHandler)可能是多个的,那么当某个事件发布时,这些事件监听者的执行顺序的规则怎样的呢?在此之前,先来看下这几个类:

HeadHandler

头结点:实现了ChannelOutboundHandler事件类型,被DefaultChannelHandlerContext包装后与TailHandler构成双向链表。

TailHandler

尾结点:实现了ChannelOutboundHandler事件类型,被DefaultChannelHandlerContext包装后与HeadHandler组成双向链表。

DefaultChannelHandlerContext

上下文对象:它包装了DefaultChannelPipeline、ChannelHandler、DefaultChannelHandlerContext next、DefaultChannelHandlerContext prev,用这个对象几乎可以获取到你想要的一切,下面用一张图,来表示他们之间的关系:


DefaultChannelHandlerContext对象,还有几个需要掌握的方法.

构造方法,初始化pipeline、group、handler,并且给布尔类型的inbound与outbound赋值,这个两个布尔值在后面将会用到。

findContextInbound()方法,查找下一个(inbound事件类型的)DefaultChannelHandlerContext,根据inboun这个布尔值去判断是否是Inboun事件类型,代码如下:

findContextInbound()方法,解释同上。

fireChannelxxxxx()方法,终于到fireChannelxxx类型的方法了,netty的事件就是从这个开始传播,以fireChannelActive()为列。如果CPU的调度是给当前线程,那么直接调用这个方法。如果不是当前线程执行的时间,就把这个事件封装成Runnable,放到executor这个线程的队列里,等到CPU调度这个线程时,再调用这个方法。这样做的好处,可以减少一次入队操作。

DefaultChannelPipeline

它内部的数据结构是双向链表,是用来添加ChannelHandler,这些ChanelHandler在pipe里在流转,流转方向是根据inbound/outbound事件类型决定的。

初始化

当一个AbstractChannel(子类有:NioSocketChannel,NioServerSocketChannel)被创建时,会初始化一个DefaultChannelPipeline对象,由下面的代码可以看出AbstractChannel与DefaultChannelPipeline是一对一的关系存在。

添加事件

在DefaultChannelPipeline,调用addLast方法可以添加一个ChannelHandler,从方法名可以看出,是从尾结点加入一个新的结点,在加入到pipe之前,会先把ChannelHandler包装成DefaultChannelHandlerContext,再建立链表的关系后,将ChannelHandler加入到pipe中,下面是源码:

事件流转方向

inbound事件类型流转方向,head->用户自定义ChannelInboundHandler_1->用户自定义ChannelInboundHandler_N+1->tail。

通过源码来看看,inbound事件类型中的channelRead的调用过程(为了节约篇幅,就不贴所有代码了)。下面是当触发了channelRead事件的调用堆栈:

DefaultChannelPipeline的758行的代码如下

head是一个DefaultChannelHandlerContext对象。之所以用DefaultChannelHandlerContext,因为这个对象保存了下个事件的DefaultChannelHandlerContext,通过调用fireChanneXXXX()方法,就会将事件传递到下一个DefaultChannelHandlerContext。

outbound事件类型流转方向:tail->用户自定义ChannelOutboundHandler_M->用户自定义ChannelOutboundHandler_M-1

验证事件流转方向

在Server中,当一个新连接建立后,会往pipe里添加了四个ChannelHandler,在pipe里的顺序为:

head<-->InboundHandlerTest1<-->InboundHandlerTest2<-->OutboundHandlerTest1<-->OutboundHandlerTest2<-->tail

控制台打印:

class event.Server$InboundHandlerTest1>channelRead
class event.Server$InboundHandlerTest2>channelRead
class event.Server$OutboundHandlerTest2>write
class event.Server$OutboundHandlerTest1>write

总结

Netty的事件模型,就讲到这,下一篇讲述Netty高性能。

JackLei
JackLei

我是真的不会修电脑