前一篇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中已经为我们定义好了很多好用的通道,能够解决我们遇到的大多数问题,不用自己去重新实现。那么在众多的实现中,与网络通信相关联的通道有哪些呢,来看看类图。
从下往上看,ServerSocketChannel、SocketChannel、DatagramChannel是我们需要打交道的面向连接和无连接的通道。它们的继承关系是这样的:AbstractSelectableChannel —-> … --> InterruptibleChannel –-> Channel。
这里又需要引入一个概念:selector,它是Channel的最佳搭档。我们知道NIO与OIO相比,很大优势在于NIO可以选择非阻塞模式处理I/O事件,从而避免了线程阻塞的情况。尤其是在高并发的情况下,使用传统Socket,往往需要为每一个连接创建一个线程,如果不这样,当工作线程都阻塞了,来了新的请求就没人干活了。然而,创建大量的线程带来的消耗是巨大的,例如:上下文切换等。而在NIO中,可以启用非阻塞的模式来进行,例如,在某个连接(Connect1)中读取消息时,如果此时没有消息到达,读取线程可以立即返回,无需等待读取成功,这样就不怕工作线程都阻塞而导致没有工作人员的情况了。但是仅仅靠非阻塞来处理高并发是不够的,当工作线程去处理Connect2时,将Connect1放在哪里呢、当Connect1中有了新的消息时怎么通知到工作线程呢?selector的加入,完美的解决了这个问题。Selector融合了linux中的select、poll或者epoll模型,通过reactor模式来达到I/O多路复用的目的(在下一篇文章中将会做详细的介绍)。在初始化时,告知selector管理器,当前的通道是哪一个(即关联的socket)、当该通道上面发生了xx事件时需要被记录下来。这个时候,与该通道关联的线程可以去做其他事情(例如:处理其他通道的消息),在必要的时候,该线程去询问selector管理器,自己感兴趣的事件中哪些已经发生了,如果发生了就加入到自己的处理队列中,做好处理的就绪工作。当然,你也可以选择netty中OIO的实现,不过对高并发的处理上,性能相对会低很多。
通道接口InterruptibleChannel表示该通道是可以被中断的,当工作线程在某通道上被阻塞的时候,该线程被中断了,那么通道将会关闭,此线程也会产生一个ClosedByInterruptException异常。假设一个线程的中断状态被设置后,再去访问某通道,此时通道也会被关闭,同时抛出ClosedByInterruptException异常。如果一个通道被关闭,休眠在该通道上面的所有线程都会被唤醒,同时收到一个AsynchronousCloseException异常。从上图可以看出,我们用到的几个socket相关的通道都是可中断的通道。
在类的继承中,ServerSocketChannel与SocketChannel、DatagramChannel是有所不同的。SocketChannel、DatagramChannel同时继承了ReadableByteChannel、WritableByteChannel,可用于读和写。这是由于ServerSocketChannel本身并不会读写,专门用于接收connect,收到connect后,由SocketChannel来处理消息。
Netty中的通道
同样netty中也引入了通道的概念,netty框架在其nio的实现过程中,实际上是对nio的通道进行了上层的封装,它是关联网络socket或者能够用于I/O操作(比如:读、写、连接和绑定)的组件。先看一下NioServerSocketChannel的继承关系:
1. ServerChannel:用于接收连接请求的通道,它通过accept()方法来创建子通道,例如其子类ServerSocketChannel。
2. SocketChannel:TCP/IP socket 通道,它通常被serverSocketChannel的accept()方法或者ClientSocketChannelFactory类创建。
3. AbstractChannel:Channel的抽象实现。
4. DatagramChannel:UDP/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,在channel的getParent()方法中就会返回他的父通道ServerSocketChannel。
分层结构的意义在于你需要的通道是属于哪种传输方式。例如:你可以写一个新通道的实现方式,这个通道和它的子通道共享一个socket连接,例如BEEP协议和SSH协议的实现。
向下转换解决特殊的传输方式
一些网络传输需要附加一些特殊的操作。这时可以通过继承的方式,在子类中去实现这些操作。例如:用OIO的方式处理报文传输,在DatagramChannel中就实现了广播join和leave的操作。
感兴趣事件(InterestOps)
通道有一个被称作InterestOps的属性,这和NIO中的SelectionKey相似。它是由两个标志组成的bit field来表示的。
1. OP_READ:如果设置了这个标志,那么从远端发送来的消息将会被立即读到。相反,如果没有设置,就有等到被设置过后才能读取远端的消息了。
2. OP_WRITE:如果设置了这个标志,写请求就不会发送到远端,而是停留在队列中,直到清除了这个标志为止。如果没有设置,写请求就会被尽快的进行出队列的操作。
3. OP_READ_WRITE:这个标志关联了OP_READ和OP_WRITE,含义是只有写请求才会被挂起。
4. OP_NONE:这个标志关联了非OP_READ和非OP_WRITE,含义是只有读请求才会被挂起。
用户可以通过setReadable(boolean)函数来设置或者清除OP_READ来挂起和恢复读操作。
需要注意的是,不能像设置或者清除OP_READ一样来处理OP_WRITE,它是只读的,用于告诉应用挂起的写请求是否达到了临界值,避免放入过多挂起的写请求导致内存溢出。比如:在用NIO传输的NioSocketChannelConfig中使用writeBufferLowWaterMark和writeBufferHighWaterMark属性来决定何时可以放入或者清除OP_WRITE标志。
事件
通道封装了NIO的Channel,用于接收连接或者读取消息,收到连接或者消息就代表一个事件发生了,在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中注入我们实现好的handler,netty就会在谋事件发生的时候依次执行handler。
其中head和tail对应的类:DefaultChannelHandlerContext,是整个处理流程的上下文。以下为类图:
Context中定义了当前的handler实例,并且根据ChannelHandler的类型记录是用于处理upstream事件还是downstream事件的,分别以两个boolean变量表示。再看看next、prev成员变量,很明显这是一个双向链表的结构,通过next找到下一个handler,通过prev找到上一个handler。
Context是ChannelPipline中的重要角色,被定义为两个变量:head、tail。也就是说可以从ChannelPipline中找到头部和尾部的context即可找到对应的handler。而通过该context的sendUpstream(ChannelEvent)和sendDownstream(ChannelEvent)方法又可以将事件传递给其上下的handler处理,从而串起了upstream事件和downstream事件的整个流程。