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

Netty架构模式

像以往一样,继续回顾这幅图。目前为止,我们学习了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示意图如下:

ChanelPipeline

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对象实例与之对应。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容