自顶向下深入分析Netty(七)--ChannelPipeline和ChannelHandler总述

时间:2023-03-08 18:27:50

自顶向下深入分析Netty(七)--ChannelPipeline和ChannelHandler总述

自顶向下深入分析Netty(七)--ChannelPipeline源码实现

自顶向下深入分析Netty(七)--ChannelHandlerContext源码实现

像以往一样,继续回顾这幅图。目前为止,我们学习了Netty的EventLoop、Channel以及ChannelFuture,还差最后两个部分:ByteBuf和ChannelHandler。ByteBuf作为通道读写数据的缓冲区,Channel底层数据的读写细节正是由ByteBuf完成。ChannelHandler作为处理各种事件的处理器,为用户提供实际的业务逻辑处理功能。在本章中,我们将介绍ChannelHandler以及存储它的容器ChannelPipeline。使用自顶向下的方法,首先介绍整体ChannePipeline,然后介绍ChannelHandler。

7.1 总述

7.1.1 ChannelPipeline

提到pipeline,我们首先想到的是*nix中的管道,可实现将一个程序的输出作为另一个程序的输入。ChannelPipeline也实现类似的功能,不同的是:ChannelPipeline将一个ChannelHandler的处理后的数据作为下一个ChannelHandler处理的数据源。Netty的ChannelPipeline示意图如下:

自顶向下深入分析Netty(七)--ChannelPipeline和ChannelHandler总述

Xnix的管道中流动的是数据,ChnanelPipeline中流动的是事件(事件中可能附加数据)。Netty定义了两种事件类型:入站(inbound)事件和出站(outbound)事件。ChannelPipeline使用拦截过滤器模式使用户可以掌控ChannelHandler处理事件的流程。注意:事件在ChannelPipeline中不自动流动而需要调用ChannelHandlerContext中诸如fileXXX()或者read()类似的方法将事件从一个ChannelHandler传播到下一个ChannelHandler。
事实上,ChannelHandler不处理具体的事件,处理具体的事件由相应的子类完成:ChannelInboundHandler处理和拦截入站事件,ChannelOutboundHandler处理和拦截出站事件。那么事件是怎么在ChannelPipeline中流动的呢?我们使用代码注释中的例子:

    ChannelPipeline p = ...;
p.addLast("1", new InboundHandlerA());
p.addLast("2", new InboundHandlerB());
p.addLast("3", new OutboundHandlerA());
p.addLast("4", new OutboundHandlerB());
p.addLast("5", new InboundOutboundHandlerX());

对于入站事件,处理序列为:1-->2-->5;对于出站事件,处理序列为:5-->4-->3。可见,入站事件与出站事件处理顺序正好相反。事件不会在ChannelPipeline中自动流动,而完全由用户控制,所以ChannelHandler处理的代码可能如下:

    public class InboundHandlerA implements ChannelInboundHandler {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("Connected!"); // 用户自定义处理逻辑
ctx.fireChannelActive(); // 将channelActive事件传播到InboundHandlerB
}
} public class OutboundHandlerB extends ChannelOutboundHandler{
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
System.out.println("Closing .."); // 用户自定义处理逻辑
ctx.close(promise); // 将close事件传播到OutboundHandlerA
}
}

入站事件一般由I/O线程触发,以下事件为入站事件:

    ChannelRegistered() // Channel注册到EventLoop
ChannelActive() // Channel激活
ChannelRead(Object) // Channel读取到数据
ChannelReadComplete() // Channel读取数据完毕
ExceptionCaught(Throwable) // 捕获到异常
UserEventTriggered(Object) // 用户自定义事件
ChannelWritabilityChanged() // Channnel可写性改变,由写高低水位控制
ChannelInactive() // Channel不再激活
ChannelUnregistered() // Channel从EventLoop中注销

出站事件一般由用户触发,以下事件为出站事件:

    bind(SocketAddress, ChannelPromise) // 绑定到本地地址
connect(SocketAddress, SocketAddress, ChannelPromise) // 连接一个远端机器
write(Object, ChannelPromise) // 写数据,实际只加到Netty出站缓冲区
flush() // flush数据,实际执行底层写
read() // 读数据,实际设置关心OP_READ事件,当数据到来时触发ChannelRead入站事件
disconnect(ChannelPromise) // 断开连接,NIO Server和Client不支持,实际调用close
close(ChannelPromise) // 关闭Channel
deregister(ChannelPromise) // 从EventLoop注销Channel

入站事件一般由I/O线程触发,用户程序员也可根据实际情况触发。考虑这样一种情况:一个协议由头部和数据部分组成,其中头部含有数据长度,由于数据量较大,客户端分多次发送该协议的数据,服务端接收到数据后需要收集足够的数据,组装为更有意义的数据传给下一个ChannelInboudHandler。也许你已经知道,这个收集数据的ChannelInboundHandler正是Netty中基本的Encoder,Encoder中会处理多次ChannelRead()事件,只触发一次对下一个ChannelInboundHandler更有意义的ChannelRead()事件。
出站事件一般由用户触发,而I/O线程也可能会触发。比如,当用户已配置ChannelOption.AutoRead选项,则I/O在执行完ChannelReadComplete()事件,会调用read()方法继续关心OP_READ事件,保证数据到达时自动触发ChannelRead()事件。
如果你初次接触Netty,会对下面的方法感到疑惑,所以列出区别:

    channelHandlerContext.close()   // close事件传播到下一个Handler
channel.close() // ==channelPipeline.close()
channelPipeline.close() // 事件沿整个ChannelPipeline传播,注意in/outboud的传播起点

回忆AbstractChannel的构造方法:

    protected AbstractChannel(Channel parent) {
this.parent = parent;
unsafe = newUnsafe();
pipeline = newChannelPipeline();
} protected DefaultChannelPipeline newChannelPipeline() {
return new DefaultChannelPipeline(this);
}

可见,新建一个Channel时会自动新建一个ChannelPipeline,也就是说他们之间是一对一的关系。另外需要注意的是:ChannelPipeline是线程安全的,也就是说,我们可以动态的添加、删除其中的ChannelHandler。考虑这样的场景:服务器需要对用户登录信息进行加密,而其他信息不加密,则可以首先将加密Handler添加到ChannelPipeline,验证完用户信息后,主动从ChnanelPipeline中删除,从而实现该需求。

7.1.2 ChannelHandler

ChannelHandler并没有方法处理事件,而需要由子类处理:ChannelInboundHandler拦截和处理入站事件,ChannelOutboundHandler拦截和处理出站事件。我们已经明白,ChannelPipeline中的事件不会自动流动,而我们一般需求事件自动流动,Netty提供了两个Adapter:ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter来满足这种需求。其中的实现类似如下:

    // inboud事件默认处理过程
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelRegistered(); // 事件传播到下一个Handler
} // outboud事件默认处理过程
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
ctx.bind(localAddress, promise); // 事件传播到下一个Handler
}

在Adapter中,事件默认自动传播到下一个Handler,这样带来的另一个好处是:用户的Handler类可以继承Adapter且覆盖自己感兴趣的事件实现,其他事件使用默认实现,不用再实现ChannelIn/outboudHandler接口中所有方法,提高效率。
我们常常遇到这样的需求:在一个业务逻辑处理器中,需要写数据库、进行网络连接等耗时业务。Netty的原则是不阻塞I/O线程,所以需指定Handler执行的线程池,可使用如下代码:

    static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
...
ChannelPipeline pipeline = ch.pipeline();
// 简单非阻塞业务,可以使用I/O线程执行
pipeline.addLast("decoder", new MyProtocolDecoder());
pipeline.addLast("encoder", new MyProtocolEncoder());
// 复杂耗时业务,使用新的线程池
pipeline.addLast(group, "handler", new MyBusinessLogicHandler());

ChannelHandler中有一个Sharable注解,使用该注解后多个ChannelPipeline中的Handler对象实例只有一个,从而减少Handler对象实例的创建。代码示例如下:

    public class DataServerInitializer extends ChannelInitializer<Channel> {
private static final DataServerHandler SHARED = new DataServerHandler(); @Override
public void initChannel(Channel channel) {
channel.pipeline().addLast("handler", SHARED);
}
}

Sharable注解的使用是有限制的,多个ChannelPipeline只有一个实例,所以该Handler要求无状态。上述示例中,DataServerHandler的事件处理方法中,不能使用或改变本身的私有变量,因为ChannelHandler是非线程安全的,使用私有变量会造成线程竞争而产生错误结果。

7.1.3 ChannelHandlerContext

Context指上下文关系,ChannelHandler的Context指的是ChannleHandler之间的关系以及ChannelHandler与ChannelPipeline之间的关系。ChannelPipeline中的事件传播主要依赖于ChannelHandlerContext实现,由于ChannelHandlerContext中有ChannelHandler之间的关系,所以能得到ChannelHandler的后继节点,从而将事件传播到下一个ChannelHandler。
ChannelHandlerContext继承自AttributeMap,所以提供了attr()方法设置和删除一些状态属性值,用户可将业务逻辑中所需使用的状态属性值存入到Context中。此外,Channel也继承自AttributeMap,也有attr()方法,在Netty4.0中,这两个attr()方法并不等效,这会给用户程序员带来困惑并且增加内存开销,所以Netty4.1中将channel.attr()==ctx.attr()。在使用Netty4.0时,建议只使用channel.attr()防止引起不必要的困惑
一个Channel对应一个ChannelPipeline,一个ChannelHandlerContext对应一个ChannelHandler,但一个ChannelHandler可以对应多个ChannelHandlerContext。当一个ChannelHandler使用Sharable注解修饰且添加同一个实例对象到不用的Channel时,只有一个ChannelHandler实例对象,但每个Channel中都有一个ChannelHandlerContext对象实例与之对应。

Channel和ChannelPipeline是一对一的关系,ChannelPipeline会有多个ChannelHandler,而一般情况,ChannelHandlerContext和ChannelHandler也是一一对应,除了Sharable场景。

作者:Hypercube
链接:https://www.jianshu.com/p/7dc5da98694c
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。