上一篇文章学习了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
ChannelPipline可以理解为管家,对ChannelHandler进行拦截和调度。
当一个消息被ChannelPipeline的Handler链拦截和处理过程是怎样的呢?
在上文中的HelloClientHandler
的channelActive
打一个端点,分析其执行流程
先来分析下:
- 启动
Server.java
,随后启动Client.java
- 当Client尝试连接到Server时,初始化了Channel信息,可看:Netty的Channel
- 随后,由于HelloClientHandler也是一个Handler,它的调用必定经过ChannelPipeline。
- 随后,底层的SocketChannel read()方法读取ByteBuf,触发ChannelRead事件
- 由IO线程
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());
}
}
- 此时由于NIO的Keys变化,执行了
unsafe
的read()
方法,而在read
方法里面,将调用pipeline
的fireChannelRead
方法,将事件流传递,如read
中下面代码:
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
- 此时消息已经传递到ChannelPipeline,通过
fireChannel*
方法将消息向后传递向后传递,利用ChannelHandlerContext
来依次传递给channelHandler1,channelHandler2,channelHandler3…
整个read事件就如上。
那么write事件呢?可以理解为和read相反,消息从tailHandler开始,途经channelHandlerN……channelHandler1, 最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回等。
事件
事件主要分为inbound和outbound,即入站和出站事件
如fireChannelActive
,fireChannelRead
,fireChannelReadComplete
等则为入站事件,而
write
,flush
,disconnect
则为出战事件。
看ChannelInbound和ChannelOutbound结构图:
即一般实现ChannelHandler,继承相应的装饰器类Adapter即可,然后重写需要的方法。
构建ChannelPipeline
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中有四个方法:
- addFirst
- addBefore
- addAfter
- addLast
通过查DefaultChannelPipeline
的源码不难发现,这四个方法都使用Synchronized(this)
来加锁,将当前整个ChannelPipeline给锁起来,这样依赖就很好的避免了更改Pipeline内部链表结构时候出现的并发问题。
每当添加时候,ChannelPipeline都会调用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
用于能够分别给不同类型的事件发送通知。
ChannelPipeline中的耗时操作
ChannelPipline
中的每一个ChannelHandler
都是通过它的EventLoop
(IO线程)来处理它的事件的。所以不要阻塞这个线程,因为会对整体 IO产生负面影响。
但有时可能需要与那些使用阻塞API的遗留代码进行交互,对于这种情况,ChannelPipeline有一些接受一个EventExecutorGroup
的addFirst
,addLast
,addBefore
,addAfter
方法,如果一个事件被传递给一个自定义的EventExecutorGroup
,它将被包含在这个EventExecutorGroup
中EventExecutor
所处理 (类似新开一个线程),从而被该Channel
本身的EventLoop
中移除,对于这种用例,Netty
提供一个交DefaultEventExecutorGroup
默认实现
当然,在上述四个add*
方法也是有Synchronized
修饰的
ChannelHandlerContext接口
ChannelHandlerContext
代表ChannelHandler
和ChannelPipline
之间的关联,每当有ChannelHandler
添加到ChannelPipline
中时,
都会创建ChannelHandlerContext
,它的主要功能是管理它所关联的ChannelHandler
和它同一个ChannelPipline
中其他ChannelHandler
之间的交互。ChannelHandlerContext
有很多方法,其中一些方法也存在于Channel
和ChannelPipline
本身上,但有一点重要不同,如果调用Channel
或者ChannelPipline
上的这些方法,它们将沿着整个ChannelPipline
进行传播。而调用位于ChannelHandlerContext
上相同方法,
则讲从当前所关联的ChannelHandler
开始,并且只会传播给位于该ChannelPipline
中下一个能够处理的ChannelHandler
。ChannelHandlerContext
和ChannelHandler
之间的关联是永远不变的,所以缓存你对他的引用是安全的- 相对于其他类的同名方法,
ChannelHandlerContext
的方法将产生更短的事件流,应该尽可能利用这个特性来获得最大的性能
虽然被调用的CHannel
或者ChannelPipline
上的write方法一直传播事件通过整个ChannelPipline
,但是在ChannelHandler
的级别上,
事件从一个ChannelHandler
到下一个ChannelHandler
的移动是由ChannelHandlerContext
上调用完成的。
一个ChannelHandler
可以从属于多个ChannelPipline
,所以它也可以绑定到多个ChannelHandlerContext
实例,对于这种用法,
对应的ChannelHandler
必须要使用@Shareable
注解标注,否则试图将它添加多个ChannelPIpline
将会触发异常。显而易见,为了安全的
备用与多个并发的Channel
,这样的ChannelHandler
必须是线程安全的。
参考资料:
- Netty In Action
- Netty 权威指南
- Netty 源码 4.1.12 Final