Netty学习:ChannelHandler执行顺序详解,附源码分析
近日学习Netty,在看书和实践的时候对于书上只言片语的那些话不是十分懂,导致尝试写例子的时候遭遇各种不顺,比如decoder和encoder还有HttpObjectAggregator的添加顺序,研究了一番之后和大家分享一下自己的理解,希望后来人可以少走弯路。
模型浅析
简单描述下ChannelHandler的存储模型,ChannelHandler在ChannelPipeline中主要以AbstractChannelHandlerContext为基类存储,存储的数据结构为链表,传进去的ChannelHandler都会转化为DefaultChannelHandlerContext来存储在ChannelPipeline里,ChannelPipeline主要的实现为DefaultChannelPipeline。
DefaultChannelPipeline
DefaultChannelPipeline使用双向链表储存所有AbstractChannelHandlerContext,定义如下:
public class DefaultChannelPipeline implements ChannelPipeline {
    static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultChannelPipeline.class);
    private static final String HEAD_NAME = generateName0(DefaultChannelPipeline.HeadContext.class);
    private static final String TAIL_NAME = generateName0(DefaultChannelPipeline.TailContext.class);
    private static final FastThreadLocal<Map<Class<?>, String>> nameCaches = new FastThreadLocal<Map<Class<?>, String>>() {
        protected Map<Class<?>, String> initialValue() throws Exception {
            return new WeakHashMap();
        }
    };
    final AbstractChannelHandlerContext head;//双向链表,头指针
    final AbstractChannelHandlerContext tail;//双向链表,尾指针
    private final Channel channel;
    private final ChannelFuture succeededFuture;
    private final VoidChannelPromise voidPromise;
    private final boolean touch = ResourceLeakDetector.isEnabled();
    private Map<EventExecutorGroup, EventExecutor> childExecutors;
    private Handle estimatorHandle;
    private boolean firstRegistration = true;
    private DefaultChannelPipeline.PendingHandlerCallback pendingHandlerCallbackHead;
    private boolean registered;
    ……
}
在调用addLast(First,Before)等方法添加ChannelHandler到ChannelPipeline时,实际上是new了一个DefaultChannelHandlerContext对象插入到链表中:
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized(this) {
            checkMultiplicity(handler);
            newCtx = this.newContext(group, this.filterName(name, handler), handler);//第一个参数为eventgroup,第二个参数为通过方法获取的channelhandler名称,第三个为channelhandler
            this.addLast0(newCtx);//构造完成后,last使用前插法插入链表尾部,first使用后插法插入链表头部
            if(!this.registered) {
                newCtx.setAddPending();
                this.callHandlerCallbackLater(newCtx, true);
                return this;
            }
            EventExecutor executor = newCtx.executor();
            if(!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    public void run() {
                        DefaultChannelPipeline.this.callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        this.callHandlerAdded0(newCtx);
        return this;
    }
DefaultChannelHandlerContext
final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
    private final ChannelHandler handler;
    DefaultChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
        super(pipeline, executor, name, isInbound(handler), isOutbound(handler));//调用父类构造方法创建context
        if(handler == null) {
            throw new NullPointerException("handler");
        } else {
            this.handler = handler;//保存handler引用
        }
    }
    public ChannelHandler handler() {
        return this.handler;
    }
    private static boolean isInbound(ChannelHandler handler) {
        return handler instanceof ChannelInboundHandler;
    }
    private static boolean isOutbound(ChannelHandler handler) {
        return handler instanceof ChannelOutboundHandler;
    }
}
Handler传递顺序
现在我们知道Pipeline里实际是一个context的链表,现在我们来看看fireChannelRead和write的传递顺序
fireChannelRead
调用fireChannelRead方法时,调用该方法的context会从自己开始在链表中根据自己的next指针来寻找下一个注册(invoke)的handler去处理事件,代码如下:
public ChannelHandlerContext fireChannelRead(Object msg) {
        invokeChannelRead(this.findContextInbound(), msg);//将查找到的context传递进入执行channelRead方法,里面还有一些eventLoop的判断
        return this;
    }
private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.next;
        } while(!ctx.inbound);//从当前context开始,查找到下一个为inbound的handler,所以说outbound和inbound的插入顺序与执行顺序或执行成功与否没有任何关系,只与最后链表的结果有关,并且当handler过多时会影响遍历速度
        return ctx;
    }
write
调用write方法时,调用该方法的context会从自己开始在链表中根据自己的pre指针来寻找上一个注册(invoke)的handler去处理事件,顺序与fireChannelRead相反,代码如下:
private void write(Object msg, boolean flush, ChannelPromise promise) {
        AbstractChannelHandlerContext next = this.findContextOutbound();//查找到上一个outBoundContext
        Object m = this.pipeline.touch(msg, next);
        EventExecutor executor = next.executor();
        if(executor.inEventLoop()) {
            if(flush) {
                next.invokeWriteAndFlush(m, promise);
            } else {
                next.invokeWrite(m, promise);
            }
        } else {
            Object task;
            if(flush) {
                task = AbstractChannelHandlerContext.WriteAndFlushTask.newInstance(next, m, promise);
            } else {
                task = AbstractChannelHandlerContext.WriteTask.newInstance(next, m, promise);
            }
            safeExecute(executor, (Runnable)task, promise, m);
        }
    }
private AbstractChannelHandlerContext findContextOutbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.prev;
        } while(!ctx.outbound);
        return ctx;
    }
补充
如果write不调用context的write方法,而是调用context.channel().write(),则会直接调用使用pipeline的tail指针开始向前遍历outboundhandler执行,如果有特殊执行需求时可以考虑使用这种调用方法。相关源码就不贴了,有兴趣的小伙伴可以自己去看。
总结
到这里,Handler执行顺序已经介绍完毕了,总结为:
- 对于channelInboundHandler,总是会从传递事件的开始,向链表末尾方向遍历执行可用的inboundHandler。 
- 对于channelOutboundHandler,总是会从write事件执行的开始,向链表头部方向遍历执行可用的outboundHandler。 
举例说明如下代码:
ch.pipeline().addLast(new OutboundHandler1());
ch.pipeline().addLast(new OutboundHandler2());
ch.pipeline().addLast(new InboundHandler1());
ch.pipeline().addLast(new InboundHandler2());
链表中的顺序为head->out1->out2->in1->in2->tail
那么Inbound的执行顺序为read->in1->in2
在Inbound执行write后,outbound执行顺序为out1<-out2<-write
- 所以实际使用中,如果添加的顺序不好,很可能会意外跳过某些inbount或者outbound。建议实际使用上,先通过addFirst插入所有outBound再通过addLast插入所有inBound这样inBound与outBound的插入顺序与执行顺序完全一致,且不会出现跳过的情况。
 很多源码中的习惯都是只使用addLast或者addFirst插入,然后顺序在心中,具体方法见仁见智,保证顺序不错就行
- 所以一些统一编码解码的handler,例如ssl,httpcodec,最好是按照顺序放在链表头!这样才会保证进出都会执行到并且业务逻辑可以正常插入
Netty学习:ChannelHandler执行顺序详解,附源码分析的更多相关文章
- Android应用AsyncTask处理机制详解及源码分析
		1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点.前面我们分析了Handler异步机制原理(不了解的可以阅读我的<Android异步消息处理机 ... 
- Spring Boot启动命令参数详解及源码分析
		使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring ... 
- 【转载】Android应用AsyncTask处理机制详解及源码分析
		[工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个 ... 
- 线程池底层原理详解与源码分析(补充部分---ScheduledThreadPoolExecutor类分析)
		[1]前言 本篇幅是对 线程池底层原理详解与源码分析 的补充,默认你已经看完了上一篇对ThreadPoolExecutor类有了足够的了解. [2]ScheduledThreadPoolExecut ... 
- Java SPI机制实战详解及源码分析
		背景介绍 提起SPI机制,可能很多人不太熟悉,它是由JDK直接提供的,全称为:Service Provider Interface.而在平时的使用过程中也很少遇到,但如果你阅读一些框架的源码时,会发现 ... 
- SpringMVC异常处理机制详解[附带源码分析]
		目录 前言 重要接口和类介绍 HandlerExceptionResolver接口 AbstractHandlerExceptionResolver抽象类 AbstractHandlerMethodE ... 
- ArrayList用法详解与源码分析
		说明 此文章分两部分,1.ArrayList用法.2.源码分析.先用法后分析是为了以后忘了查阅起来方便-- ArrayList 基本用法 1.创建ArrayList对象 //创建默认容量的数组列表(默 ... 
- [转]SpringMVC拦截器详解[附带源码分析]
		目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:ht ... 
- SpringMVC拦截器详解[附带源码分析]
		目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:h ... 
- Servlet容器Tomcat中web.xml中url-pattern的配置详解[附带源码分析]
		目录 前言 现象 源码分析 实战例子 总结 参考资料 前言 今天研究了一下tomcat上web.xml配置文件中url-pattern的问题. 这个问题其实毕业前就困扰着我,当时忙于找工作. 找到工作 ... 
随机推荐
- DRF框架笔记
			序列化器类的定义格式? 继承serializers.Serializer:字段 = serializers.字段类型(选项参数) 序列化器类的基本使用? 序列化器类(instance=None, da ... 
- hive中的虚拟列
			hive为用户提供了三个虚拟列:用户可以通过这三个虚拟列确定记录是来自哪个文件以及这条记录的具体位置信息 INPUT__FILE__NAME 返回记录所在的具体hdfs文件全路径 hive> s ... 
- 初始Node
			node是什么?  一句话: 服务器 什么是服务器:  一句话: 客户端访问 并且能够响应 为什么:  一句话: 执行效率高 #安装 #控制台 切换磁盘: e: 改变目录: cd 目录 cd.. ... 
- [日常摸鱼]Luogu1801 黑匣子(NOI导刊)
			题意:写一个数据结构,要求滋兹两种操作,ADD:插入一个数,GET:令$i++$然后输出第$i$小的数 这个数据结构当然是平衡树啦!(雾) 写个Treap直接过掉啦- #include<cstd ... 
- python 实现数值积分与画图
			import numpy as np from scipy import integrate def half_circle(x): return (1 - x ** 2) ** 0.5 N = 10 ... 
- php学习之sqlite查询语句之多条件查询
			一.PHP+Mysql多条件-多值查询示例代码: index.html代码:<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitio ... 
- 从零实现Linux一键自动化部署.netCore+Vue+Nginx项目到Docker中
			环境搭建 1.安装Linux,这里我用的阿里云服务器,CentOS7版本 2.进入Linux,安装Docker,执行以下命令 sudo yum update #更新一下yum包 sudo yum in ... 
- 基于LNMP架构搭建wordpress博客之安装架构说明
			架构情况 架构情况:基于LNMP架构搭建wordpress系统 软件包版本说明: 系统要求 : CentOS-6.9-x86_64-bin-DVD1.iso PHP版本 : php-7.2.29 ... 
- CentOS7 实战源码部署php服务与nginx 的整合
			简介:实战演练php服务的搭建 PHP是一种脚本语言,常用于做动态网站的. 源码编译安装: 安装依赖组件: yum -y install gcc gcc-c++ bzip2 bzip2-devel b ... 
- Angular实战之使用NG-ZORRO创建一个企业级中后台框架(进阶篇)
			前言: 上一篇文章我们讲了如何在创建的Angular项目中快速引入ng-zorro-antd企业中台组件库,并且快速构建后台管理页面框架模板.这一章主要介绍的是如何在创建好的后台管理页面框架的快速生成 ... 
