目录
Pipeline是一条Netty管道流水线,一条管道需要很多Handler处理器来处理业务。Netty的业务处理器流水线ChannelPipeline是基于责任链设计模式(Chain of Responsibility)来设计的,内部是一个双向链表结构,能够支持动态地添加和删除Handler业务处理器。
Channel通道
通道是Netty的核心概念之一,代表着网络连接,由它负责同对端进行网络通信,可以写入数据到对端,也可以从对端读取数据。Netty通道抽象类AbstractChannel的构造函数如下:
protected AbstractChannel(Channel parent) {
this.parent = parent; //父通道
id = newId();
unsafe = newUnsafe(); //新建一个底层的 NIO 通道,完成实际的 IO 操作
pipeline = newChannelPipeline(); //新建一条通道流水线
}
AbstractChannel内部有一个pipeline属性,表示处理器的流水线。Netty在对通道进行初始化的时候,将pipeline属性初始化为DefaultChannelPipeline的实例。以上代码表明,每个通道拥有一条ChannelPipeline处理器流水线。
EmbeddedChannel嵌入式通道
在Netty的实际开发中,底层通信传输的基础工作Netty已经替大家完成。实际上,更多的工作是设计和开发ChannelHandler业务处理器。处理器开发完成后,需要投入单元测试。一般单元测试的大致流程是:
- 需要将Handler业务处理器加入到通道的Pipeline流水线中;
- 接下来先后启动Netty服务器、客户端程序;
- 相互发送消息,测试业务处理器的效果。
这些复杂的工序存在一个问题,如果每开发一个业务处理器,都进行服务器和客户端的重复启动,这整个的过程是非常的烦琐和浪费时间的。如何解决这种徒劳的、低效的重复工作呢?Netty提供了一个专用通道——名字叫EmbeddedChannel(嵌入式通道)。
EmbeddedChannel仅仅是模拟入站与出站的操作,底层不进行实际的传输,不需要启动Netty服务器和客户端。除了不进行传输之外,EmbeddedChannel的其他的事件机制和处理流程和真正的传输通道是一模一样的。因此,使用EmbeddedChannel,开发人员可以在单元测试用例中方便、快速地进行ChannelHandler业务处理器的单元测试。
名称 | 说明 |
---|---|
writeInbound(…) | 向通道写入入站数据,模拟真实通道收到数据的场景。也就是说,这些写入的数据会被流水线上的入站处理器所处理。 |
readInbound(…) | 从EmbeddedChannel中读取入站数据,返回经过流水线最后一个入站处理器处理完成之后的入站数据。如果没有数据,则返回 null。 |
writeOutbound(…) | 向通道写入出站数据,模拟真实通道发送数据。也就是说,这些写入的数据会被流水线上的出站处理器处理。 |
readOutbound(…) | 从EmbeddedChannel中读取出站数据,返回经过流水线最后一个出站处理器处理之后的出站数据。如果没有数据,则返回 null。 |
finish() | 结束EmbeddedChannel,它会调用通道的close 方法。 |
Handler业务处理器
整个的IO处理操作环节的前后两个环节,包括从通道读数据包和由通道发送到对端,由Netty的底层负责完成,不需要用户程序负责。
用户程序主要涉及的Handler环节为:数据包解码、业务处理、目标数据编码、把数据包写到通道中。
ChannelInboundHandler入站处理器
ChannelInboundHandler的主要操作如下:
-
channelRegistered
当通道注册完成后,Netty会调用fireChannelRegistered方法,触发通道注册事件。而在通道流水线注册过的入站处理器Handler的channelRegistered回调方法,将会被调用到。
-
channelActive
当通道激活完成后,Netty会调用fireChannelActive方法,触发通道激活事件。而在通道流水线注册过的入站处理器的channelActive回调方法,会被调用到。
-
channelRead
当通道缓冲区可读,Netty会调用fireChannelRead,触发通道可读事件。而在通道流水线注册过的入站处理器的channelRead回调方法,会被调用到,以便完成入站数据的读取和处理。
-
channelReadComplete
当通道缓冲区读完,Netty会调用fireChannelReadComplete,触发通道缓冲区读完事件。而在通道流水线注册过的入站处理器的channelReadComplete回调方法,会被调用到。
-
channelInactive
当连接被断开或者不可用时,Netty会调用fireChannelInactive,触发连接不可用事件。而在通道流水线注册过的入站处理器的channelInactive回调方法,会被调用到。
-
exceptionCaught
当通道处理过程发生异常时,Netty会调用fireExceptionCaught,触发异常捕获事件。而在通道流水线注册过的入站处理器的exceptionCaught方法,会被调用到。注意,这个方法是在通道处理器中ChannelHandler定义的方法,入站处理器、出站处理器接口都继承到了该方法。
ChannelOutboundHandler出站处理器
当业务处理完成后,需要发送数据时,通过一系列ChannelOutboundHandler出站处理器处理,完成底层Socket操作。ChannelOutboundHandler接口操作如下:
-
bind
监听地址(IP+端口)绑定:完成底层Java IO通道的IP地址绑定。如果使用TCP传输协议,这个方法用于服务器端。
-
connect
连接服务端:完成底层Java IO通道的服务器端的连接操作。如果使用TCP传输协议,这个方法用于客户端。
-
write
写数据到底层:完成Netty通道向底层Java IO 通道的数据写入操作。此方法仅仅是触发一下操作而已,并不是完成实际的数据写入操作。
-
flush
将底层缓存区的数据腾空,立即写出到对端。
-
read
从底层读数据:完成Netty通道从Java IO通道的数据读取。
-
disConnect
断开服务器连接:断开底层Java IO通道的socket连接。如果使用TCP传输协议,此方法主要用于客户端。
-
close
主动关闭通道:关闭底层的通道,例如服务器端的新连接监听通道。
Pipeline通道流水线
前面讲到,一条Netty通道需要很多的Handler业务处理器来处理业务。每条通道内部都有一条流水线(Pipeline)将Handler装配起来。Netty的业务处理器流水线ChannelPipeline是基于责任链设计模式(Chain of Responsibility)来设计的,内部是一个双向链表结构,能够支持动态地添加和删除Handler业务处理器。
每个节点是一个ChannelHandlerContext,每个context包含一个Handler。在Netty的设计中Handler是无状态的,不保存和Channel有关的信息。Handler的目标,是将自己的处理逻辑做得很通用,可以给不同的Channel使用。与Handler有不同的是,Pipeline是有状态的,保存了Channel的关系。于是乎,Handler和Pipeline之间,需要一个中间角色,把他们联系起来。这个中间角色是谁呢?这就是——ChannelHandlerContext 。
不管我们定义的是哪种类型的Handler业务处理器, 最终它们都是以双向链表的方式保存在流水线中。这里流水线的节点类型,并不是前面的Handler业务处理器基类,而是其包装类型:ChannelHandlerContext通道处理器上下文类。当Handler业务处理器被添加到流水线中时,会为其专门创建一个通道处理器上下文ChannelHandlerContext实例,主要封装了ChannelHandler通道处理器和ChannelPipeline通道流水线之间的关联关系。
所以流水线ChannelPipeline中的双向链接,实质是一个由ChannelHandlerContext组成的双向链表。而无状态的Handler,作为Context的成员,关联在ChannelHandlerContext中。
ChannelHandlerContext中包含了有许多方法,主要可以分为两类:第一类是获取上下文所关联的Netty组件实例,如所关联的通道、所关联的流水线、上下文内部Handler业务处理器实例等;第二类是入站和出站处理方法。
如果通过Channel或ChannelPipeline的实例来调用这些出站和入站处理方法,它们就会在整条流水线中传播。然而,如果是通过ChannelHandlerContext上下文调用出站和入站处理方法,就只会从当前的节点开始,往同类型的下一站处理器传播,而不是在整条流水线从头至尾进行完整的传播。
比如context.channel.writeAndFlush( )和context.writeAndFlush( )所传播的路径不同
HeadContext和TailContext
实际上,通道流水线在没有加入任何处理器之前,装配了两个默认的处理器上下文:一个头部上下文叫做HeadContext、一个尾部上下文叫做TailContext。pipeline的创建、初始化除了保存一些必要的属性外,核心就在于创建了HeadContext头节点和TailContext尾节点。
每个pipeline中双向链表结构,从一开始就存在了HeadContext和TailContext两个节点,后面添加的处理器上下文节点,都在添加在HeadContext实例和TailContext实例之间。在添加了一些必要的解码器、业务处理器、编码器之后,一条流水线的结构大致如下图所示:
-
TailContext
流水线尾部的TailContext不仅仅是一个上下文类,而且是一个入站处理器类,实现了所有入站处理回调方法,这些回调实现的主要工作,基本上都是收尾处理的,如释放缓冲区对象、完成异常处理等。
-
HeadContext
流水线头部的HeadContext则比TailContext复杂得多,既是一个出站处理器、也是一个入站处理器,还保存了一个unsafe(完成实际通道传输的类)实例,也就是HeadContext还需要负责最终的通道传输工作。