`
cqupt123
  • 浏览: 66181 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

Netty从入门到精通——概念篇

阅读更多

  前一篇blog,讲解了如何快速启动netty服务,并通过telnet命令来访问的简单过程。其中用到了netty中常用的几个类和方法,本文将做一一介绍(其中翻译了netty的api文档,同时结合自己的理解)。
  首先,看类:ServerBootstrap,Server的启动过程就是从这里开始的。通过简单的构造方法注入ChannelFactory后设置ChannelPiplineFactory,再调用bind方法,服务器便启动起来了。这里重点关注一下两个工厂类,从类名可以看出是用来产出Channel和ChannelPipline的。Channel和ChannelPipline都是netty的核心概念,贯穿了服务的整个过程。
  NIO中的通道
  那么Channel在netty中扮演了一个怎么样的角色呢?顾名思义,Channel即是通道的意思。提到Channel,首先会想到NIO。在Nio中,废弃了面向Socket和ServerSocket编程的方式,引入了通道和字节缓冲区(ByteBuffer)的概念。通道关联着某个文件描述符(FD)和字节缓冲区,来将缓冲区的数据写入FD关联的文件或者套接字,或将文件或套接字的内容读入ByteBuffer。所以通道分为读、写通道,分别实现了ReadableByteChannel、WritableByteChannel,当然同时实现了两个接口后便是双向的通道了。不过NIO中已经为我们定义好了很多好用的通道,能够解决我们遇到的大多数问题,不用自己去重新实现。那么在众多的实现中,与网络通信相关联的通道有哪些呢,来看看类图。



   从下往上看,ServerSocketChannelSocketChannelDatagramChannel是我们需要打交道的面向连接和无连接的通道。它们的继承关系是这样的:AbstractSelectableChannel —-> … --> InterruptibleChannel –-> Channel

  这里又需要引入一个概念:selector,它是Channel的最佳搭档。我们知道NIOOIO相比,很大优势在于NIO可以选择非阻塞模式处理I/O事件,从而避免了线程阻塞的情况。尤其是在高并发的情况下,使用传统Socket,往往需要为每一个连接创建一个线程,如果不这样,当工作线程都阻塞了,来了新的请求就没人干活了。然而,创建大量的线程带来的消耗是巨大的,例如:上下文切换等。而在NIO中,可以启用非阻塞的模式来进行,例如,在某个连接(Connect1)中读取消息时,如果此时没有消息到达,读取线程可以立即返回,无需等待读取成功,这样就不怕工作线程都阻塞而导致没有工作人员的情况了。但是仅仅靠非阻塞来处理高并发是不够的,当工作线程去处理Connect2时,将Connect1放在哪里呢、当Connect1中有了新的消息时怎么通知到工作线程呢?selector的加入,完美的解决了这个问题。Selector融合了linux中的selectpoll或者epoll模型,通过reactor模式来达到I/O多路复用的目的(在下一篇文章中将会做详细的介绍)。在初始化时,告知selector管理器,当前的通道是哪一个(即关联的socket)、当该通道上面发生了xx事件时需要被记录下来。这个时候,与该通道关联的线程可以去做其他事情(例如:处理其他通道的消息),在必要的时候,该线程去询问selector管理器,自己感兴趣的事件中哪些已经发生了,如果发生了就加入到自己的处理队列中,做好处理的就绪工作。当然,你也可以选择nettyOIO的实现,不过对高并发的处理上,性能相对会低很多。

  通道接口InterruptibleChannel表示该通道是可以被中断的,当工作线程在某通道上被阻塞的时候,该线程被中断了,那么通道将会关闭,此线程也会产生一个ClosedByInterruptException异常。假设一个线程的中断状态被设置后,再去访问某通道,此时通道也会被关闭,同时抛出ClosedByInterruptException异常。如果一个通道被关闭,休眠在该通道上面的所有线程都会被唤醒,同时收到一个AsynchronousCloseException异常。从上图可以看出,我们用到的几个socket相关的通道都是可中断的通道。

  在类的继承中,ServerSocketChannelSocketChannelDatagramChannel是有所不同的。SocketChannelDatagramChannel同时继承了ReadableByteChannelWritableByteChannel,可用于读和写。这是由于ServerSocketChannel本身并不会读写,专门用于接收connect,收到connect后,由SocketChannel来处理消息。

   Netty中的通道

  同样netty中也引入了通道的概念,netty框架在其nio的实现过程中,实际上是对nio的通道进行了上层的封装,它是关联网络socket或者能够用于I/O操作(比如:读、写、连接和绑定)的组件。先看一下NioServerSocketChannel的继承关系:



     1.   ServerChannel:用于接收连接请求的通道,它通过accept()方法来创建子通道,例如其子类ServerSocketChannel

2.   SocketChannelTCP/IP socket 通道,它通常被serverSocketChannelaccept()方法或者ClientSocketChannelFactory类创建。

3.   AbstractChannelChannel的抽象实现。

4.   DatagramChannelUDP/IP通道,通过DatagramChannelFactory创建。

5.    LocalChannel:用于本地传输的通道

 

  Channel给我们提供了:

    1.         通道目前的状态(如:是否打开?是否已连接?)

    2.         通道的配置参数(如:用于接收消息的buffer的大小)

    3.         通道提供的I/O操作(如:写、连接、绑定等)

    4.         还提供了用于处理与通道相关联的I/O事件和I/O请求的ChannelPipline

  通道中所有的I/O操作都是异步进行的

  这意味着所有的I/O调用在结束的时候都不能保证该I/O操作已经完成了。相反的,这个时候用户需要返回一个ChannelFuture实例,当这个请求成功、失败或者取消的时候,Futrue就会通知你。

   通道是分层级的

   一个通道是否有父通道取决于它的创建方式。例如:通过ServerSocketChannel收到连接时(ServerSocketChannel.accepted()方法)创建的SocketChannel,在channelgetParent()方法中就会返回他的父通道ServerSocketChannel

   分层结构的意义在于你需要的通道是属于哪种传输方式。例如:你可以写一个新通道的实现方式,这个通道和它的子通道共享一个socket连接,例如BEEP协议和SSH协议的实现。

  向下转换解决特殊的传输方式

   一些网络传输需要附加一些特殊的操作。这时可以通过继承的方式,在子类中去实现这些操作。例如:用OIO的方式处理报文传输,在DatagramChannel中就实现了广播joinleave的操作。

  感兴趣事件(InterestOps)

  通道有一个被称作InterestOps的属性,这和NIO中的SelectionKey相似。它是由两个标志组成的bit field来表示的。

1.    OP_READ:如果设置了这个标志,那么从远端发送来的消息将会被立即读到。相反,如果没有设置,就有等到被设置过后才能读取远端的消息了。

2.    OP_WRITE:如果设置了这个标志,写请求就不会发送到远端,而是停留在队列中,直到清除了这个标志为止。如果没有设置,写请求就会被尽快的进行出队列的操作。

3.    OP_READ_WRITE:这个标志关联了OP_READOP_WRITE,含义是只有写请求才会被挂起。

4.    OP_NONE:这个标志关联了非OP_READ和非OP_WRITE,含义是只有读请求才会被挂起。

  用户可以通过setReadable(boolean)函数来设置或者清除OP_READ来挂起和恢复读操作。

  需要注意的是,不能像设置或者清除OP_READ一样来处理OP_WRITE,它是只读的,用于告诉应用挂起的写请求是否达到了临界值,避免放入过多挂起的写请求导致内存溢出。比如:在用NIO传输的NioSocketChannelConfig中使用writeBufferLowWaterMarkwriteBufferHighWaterMark属性来决定何时可以放入或者清除OP_WRITE标志。

    事件

  通道封装了NIOChannel,用于接收连接或者读取消息,收到连接或者消息就代表一个事件发生了,在netty中同样做了相应的映射,抽象出ChannelEvent的概念,表示:和某通道关联的I/O事件或者I/O请求。来看看事件的类图结构:



  事件分为UpStream事件和downStream事件,一个事件的处理流向如果是从ChannelPipline中的第一个(head)Handler(后文讲解)开始到最后一个(tail)Handler,那么就称这个事件为UpStream事件,相反,如果一个事件的处理流向是从ChannelPipline中的最后一个Handler开始到第一个Handler,就称这个事件为downStream事件。

 

  当服务器端收到来自客户端的消息时,携带消息的事件是一个Upstream事件。当服务器端向客户端发送消息或者回应客户端的时候,这个事件就为downStream事件。当然,站在客户端的角度看也是一样。Upstream事件往往是由外向内获取资源等操作后触发的,例如:InputStream.read(byte[])等事件发生后通知handler去处理读到的消息,downStream事件往往是由内向外发送请求时所触发的,例如:OutputStream.write(byte[]),Socket.connect (SocketAddress), and Socket.close()等请求会触发handler进行写、连接、关闭socket等操作。

  个人理解:upStream事件是事件发生之后,用于通知handler做相应的处理,这时事件已经发生;downStream事件是通知handler去做相应的请求操作,是为了处理该事件所发起的请求。

  UpStream事件包括:

 

事件名称

事件类型与发生条件

含义

备注

messageReceived

MessageEvent

表示从远端接收到了消息(eg:ChannelBuffer

 

exceptionCaught

ExceptionEvent

表示在某handler或者I/O线程中发生了异常

 

channelOpen

ChannelStateEvent

state=OPEN,value=true

表示某通道打开了,但是还没有绑定或者链接成功

注意:这个事件是由Boss 线程内部触发的,所以不要对它做一些重量级的操作,否则会阻塞其他worker线程的调度

channelClosed

ChannelStateEvent

state=OPEN,value=false

表示关联的通道已经关闭和相关资源已经释放

 

channelBound

ChannelStateEvent

state=BOUND,value=socketAddress

表示通道已经绑定到本地地址,但还没有连接

注意:同channelOpen

channelUnbound

ChannelStateEvent

state=BOUND,value=null

表示已从当前地址解除绑定

 

channelConnected

ChannelStateEvent

state=CONNECTED,value=socketAddress

表示当前通道已经打开、绑定了本地地址、并与远程地址连接成功

注意:同channelOpen

writeComplete

WriteCompletionEvent

表示有消息被写到了远端

 

channelDisconnected

ChannelStateEvent

state=CONNECTED,value=socketAddress

表示通道与远端的连接断开

 

channelInterestChanged

ChannelStateEvent

state= INTEREST_OPS

表示修改了通道感兴趣的事件

 

 

  有两种事件只被用于有子通道的通道,比如:ServerSocketChannel

事件名称

事件类型与发生条件

含义

备注

childChannelOpen

ChildChannelStateEvent

childChannel.isOpen() = true

当子通道发生OPEN事件的时候,例如:当serverChannel接到连接时

 

childChannelClosed

ChildChannelStateEvent

childChannel.isOpen() = false

当子通道发生CLOSE事件的时候,例如:接收到的连接关闭

 

 

  downStream事件包括:

 

事件名称

事件类型与发生条件

含义

备注

write

MessageEvent

向通道发送消息

 

bind

ChannelStateEvent

state=BOUND,value=socketAddress

将通道绑定到value所指向的地址

 

unbind

ChannelStateEvent

state=BOUND,value=null

请求解除与关联地址的绑定关系

 

connect

ChannelStateEvent

state=CONNECTED,value=socketAddress

请求连接到value所指定的地址

 

unconnect

ChannelStateEvent

state=CONNECTED,value=null

请求与当前地址解除连接关系

 

close

ChannelStateEvent

state=OPEN,value=false

关闭通道

 

  需要注意的是在downStream事件中没有提到open事件,这是因为ChannelFactory在创建通道的时候它就处于open状态了。

 

  Handler

  当接收到一个ChannelEvent时,我们应该做怎么样的处理,比如:在消息被Channel读入的时候我们应该怎么处理,在回复客户端之前应该干点什么,这些都是应该由我们的应用程序来控制的业务逻辑。可以看到,在上一篇文章中,Server中包含了一个内部类MyChannelHandler,在接收到连接时输出当前Channel的信息、接收到消息时回复客户端等操作就是我们的业务逻辑。在netty中,为我们封装了ChannelHandler接口,用于处理或拦截ChannelEvent,并且传递这个事件给所在ChannelPipline中的下一个handler

 

 子类

  ChannelHandler接口没有实现任何方法。用于处理事件的Handler需要去继承它的子接口。以下的两个子接口用于处理接收到的事件,一个是处理upStream事件的,另一个是用来处理downStream事件的。

    1.         ChannelUpstreamHandler:用于处理upStream事件。

 

  通常被用于工作者线程拦截到I/O请求中转换(编码等处理)消息或者其它相关的业务逻辑。

SimpleChannelUpstreamHandler是实现中最常用的一个类,因为它已经实现了关于各个事件最基础的方法。当然,遇到特殊的需求,也可以直接实现这个接口来做处理。

2.         ChannelDownstreamHandler:用于处理downStream事件。

 

  ChannelPipline

  前面介绍了handler,通过在Handler中注入业务逻辑。但是我们对业务逻辑的处理往往不像前一篇文章中讲到的那么简单,例如:在接收到消息时,先进行解码,得到我们需要的数据结构,再对该数据结构进行真正的逻辑处理等。这时,我们就可以将这两个逻辑放到两个handler中,一个用于解码,另一个用于处理业务,并且规定handler的执行顺序,先解码后处理业务。这样我们就可以把工作拆分开来,代码看起来干净、简洁。尤其是在我们需要做的事情很多时,将任务拆解是一种很好的方式。这就是即将隆重推出的ChannelPipline。在ChannelPipline中注入我们实现好的handlernetty就会在谋事件发生的时候依次执行handler



 其中headtail对应的类:DefaultChannelHandlerContext,是整个处理流程的上下文。以下为类图:



   Context中定义了当前的handler实例,并且根据ChannelHandler的类型记录是用于处理upstream事件还是downstream事件的,分别以两个boolean变量表示。再看看nextprev成员变量,很明显这是一个双向链表的结构,通过next找到下一个handler,通过prev找到上一个handler

 

  ContextChannelPipline中的重要角色,被定义为两个变量:headtail。也就是说可以从ChannelPipline中找到头部和尾部的context即可找到对应的handler。而通过该contextsendUpstream(ChannelEvent)sendDownstream(ChannelEvent)方法又可以将事件传递给其上下的handler处理,从而串起了upstream事件和downstream事件的整个流程。

 

 

 

 

 

 

 

  • 大小: 63.7 KB
  • 大小: 52.3 KB
  • 大小: 61.8 KB
  • 大小: 28.5 KB
  • 大小: 26.9 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics