上一篇文章学习了Channel,它屏蔽了许多底层的java.net.Socket
的操作,
那么,当有了数据流之后,就到了如何处理它的时候,那么本篇文章先看ChannelPipeline和ChannelHandler。
Netty里面的ChannelPipeline就类似于一根管道,而ChannelHandler类似于里面的拦截器,当一个拦截器拦截完后,可以向后传递,或者跳过。
ChannelPipeline提供了提供了ChannelHandler链上的容器,并定义了用于该链的传播入站和出站的流API。ChannelPipeline持有I/O事件拦截器ChannelHandler的链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便地通过新增和删除ChannelHandler来实现不同的业务逻辑定制
很简单的理解就是编码器和解码器都是ChannelHandler,当数据在网络上传输时候,通信双方会定义协议和格式,此时到了Channel端,则需要进行解码,从而再进行业务处理,而处理完,再进行编码发送出去。当然这是例子,实际中可以定一个多个ChannelHandler,这些ChannelHandler将以链表的形式存在,再进行事件传递。
当创建一个新的Channel
时,都会分配了一个新的ChannelPipeline
,该关联是永久的,该通道既不能附加另一个ChannelPipeline
也不能分离当前的ChannelPipeline
。
下面分别介绍ChannelPipline
ChannelPipline可以理解为管家,对ChannelHandler进行拦截和调度。
当一个消息被ChannelPipeline的Handler链拦截和处理过程是怎样的呢?
在上文中的HelloClientHandler
的channelActive
打一个端点,分析其执行流程
先来分析下:
Server.java
,随后启动Client.java
EventLoopGroup
分配线程作为Selector,等待到了事件变化,并将事件按照类别处理,如下: private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); //获取channel的unsafe内部类
if (!k.isValid()) { //key可用
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop(); //从Channel中获取对应的EventLoop
} catch (Throwable ignored) {
// 如果抛出异常则直接返回,因为原因可能是还没有EventLoop
return;
}
// Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop
// and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
// still healthy and should not be closed.
// See https://github.com/netty/netty/issues/5125
if (eventLoop != this || eventLoop == null) {
return;
}
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps(); // 获取Selector的keys
// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
// the NIO JDK channel implementation may throw a NotYetConnectedException.
if ((readyOps & SelectionKey.OP_CONNECT) != 0) { // 可读并且连接的事件
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect(); //先调用finishConnect,否则jdk会抛出NotYetConnectedException
}
// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) { //可读并且写事件
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read(); //读事件
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
unsafe
的read()
方法,而在read
方法里面,将调用pipeline
的fireChannelRead
方法,将事件流传递,如read
中下面代码: int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
fireChannel*
方法将消息向后传递向后传递,利用ChannelHandlerContext
来依次传递给channelHandler1,channelHandler2,channelHandler3…整个read事件就如上。
那么write事件呢?可以理解为和read相反,消息从tailHandler开始,途经channelHandlerN……channelHandler1, 最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回等。
事件主要分为inbound和outbound,即入站和出站事件
如fireChannelActive
,fireChannelRead
,fireChannelReadComplete
等则为入站事件,而
write
,flush
,disconnect
则为出战事件。
看ChannelInbound和ChannelOutbound结构图:
即一般实现ChannelHandler,继承相应的装饰器类Adapter即可,然后重写需要的方法。
ChannelPipline接口提供了ChannelHandler链的容器,并定义了用于在该链上传播入站和出战的事件流API。当Channel被创建时, 它会自动的分配到它专属的ChannelPipline中。
而且并不用程序员去创建一个ChannelPipline,只需要往这个容器中丢东西就好了,记得在BootStrap启动时:
pipeline = ch.pipeline();
pipeline.addLast("decoder", new MyProtocolDecoder());
pipeline.addLast(new HelloClientHandler());
pipeline.addLast("encoder", new MyProtocolEncoder());
ChannelPipeline的机制和和Map很相似,以简直对的方式讲ChannelHandler管理起来,增删改查,但是此时会有问题,类似与Map有ConcurrentHashMap一类并发容器,理论上,ChannelPipeline会有IO线程和用户线程之间的并发情况,以及用户之间的并发情况,那么ChannelPipeline并发下怎么解决呢?
在ChannelPipeline中有四个方法:
DefaultChannelPipeline
的源码不难发现,这四个方法都使用Synchronized(this)
来加锁,将当前整个ChannelPipeline给锁起来,这样依赖就很好的避免了更改Pipeline内部链表结构时候出现的并发问题。checkDuplicateName(name);
进行同名校验,从表头循环到表尾进行校验: private AbstractChannelHandlerContext context0(String name) {
AbstractChannelHandlerContext context = head.next;
while (context != tail) {
if (context.name().equals(name)) {
return context;
}
context = context.next;
}
return null;
}
DefaultChannelPipeline
是ChannelPipline的默认实现,能够满足大多数ChannelPipline的需求,其父类实现了ChannelInboundInvoker
和 ChannelOutboundInvoker
用于能够分别给不同类型的事件发送通知。
ChannelPipline
中的每一个ChannelHandler
都是通过它的EventLoop
(IO线程)来处理它的事件的。所以不要阻塞这个线程,因为会对整体 IO产生负面影响。
但有时可能需要与那些使用阻塞API的遗留代码进行交互,对于这种情况,ChannelPipeline有一些接受一个EventExecutorGroup
的addFirst
,addLast
,addBefore
,addAfter
方法,如果一个事件被传递给一个自定义的EventExecutorGroup
,它将被包含在这个EventExecutorGroup
中EventExecutor
所处理 (类似新开一个线程),从而被该Channel
本身的EventLoop
中移除,对于这种用例,Netty
提供一个交DefaultEventExecutorGroup
默认实现
当然,在上述四个add*
方法也是有Synchronized
修饰的
ChannelHandlerContext
代表ChannelHandler
和ChannelPipline
之间的关联,每当有ChannelHandler
添加到ChannelPipline
中时,ChannelHandlerContext
,它的主要功能是管理它所关联的ChannelHandler
和它同一个ChannelPipline
中其他ChannelHandler
ChannelHandlerContext
有很多方法,其中一些方法也存在于Channel
和ChannelPipline
本身上,但有一点重要不同,如果调用ChannelChannelPipline
上的这些方法,它们将沿着整个ChannelPipline
进行传播。而调用位于ChannelHandlerContext
上相同方法,ChannelHandler
开始,并且只会传播给位于该ChannelPipline
中下一个能够处理的ChannelHandler
。ChannelHandlerContext
和ChannelHandler
之间的关联是永远不变的,所以缓存你对他的引用是安全的ChannelHandlerContext
的方法将产生更短的事件流,应该尽可能利用这个特性来获得最大的性能CHannel
或者ChannelPipline
上的write方法一直传播事件通过整个ChannelPipline
,但是在ChannelHandler
的级别上,ChannelHandler
到下一个ChannelHandler
的移动是由ChannelHandlerContext
上调用完成的。一个ChannelHandler
可以从属于多个ChannelPipline
,所以它也可以绑定到多个ChannelHandlerContext
实例,对于这种用法,
对应的ChannelHandler
必须要使用@Shareable
注解标注,否则试图将它添加多个ChannelPIpline
将会触发异常。显而易见,为了安全的
备用与多个并发的Channel
,这样的ChannelHandler
必须是线程安全的。
参考资料: