近日学习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执行顺序已经介绍完毕了,总结为:

  1. 对于channelInboundHandler,总是会从传递事件的开始,向链表末尾方向遍历执行可用的inboundHandler。

  2. 对于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

  1. 所以实际使用中,如果添加的顺序不好,很可能会意外跳过某些inbount或者outbound。建议实际使用上,先通过addFirst插入所有outBound再通过addLast插入所有inBound这样inBound与outBound的插入顺序与执行顺序完全一致,且不会出现跳过的情况。

    很多源码中的习惯都是只使用addLast或者addFirst插入,然后顺序在心中,具体方法见仁见智,保证顺序不错就行
  2. 所以一些统一编码解码的handler,例如ssl,httpcodec,最好是按照顺序放在链表头!这样才会保证进出都会执行到并且业务逻辑可以正常插入

Netty学习:ChannelHandler执行顺序详解,附源码分析的更多相关文章

  1. Android应用AsyncTask处理机制详解及源码分析

    1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个知识点.前面我们分析了Handler异步机制原理(不了解的可以阅读我的<Android异步消息处理机 ...

  2. Spring Boot启动命令参数详解及源码分析

    使用过Spring Boot,我们都知道通过java -jar可以快速启动Spring Boot项目.同时,也可以通过在执行jar -jar时传递参数来进行配置.本文带大家系统的了解一下Spring ...

  3. 【转载】Android应用AsyncTask处理机制详解及源码分析

    [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 1 背景 Android异步处理机制一直都是Android的一个核心,也是应用工程师面试的一个 ...

  4. 线程池底层原理详解与源码分析(补充部分---ScheduledThreadPoolExecutor类分析)

    [1]前言 本篇幅是对 线程池底层原理详解与源码分析  的补充,默认你已经看完了上一篇对ThreadPoolExecutor类有了足够的了解. [2]ScheduledThreadPoolExecut ...

  5. Java SPI机制实战详解及源码分析

    背景介绍 提起SPI机制,可能很多人不太熟悉,它是由JDK直接提供的,全称为:Service Provider Interface.而在平时的使用过程中也很少遇到,但如果你阅读一些框架的源码时,会发现 ...

  6. SpringMVC异常处理机制详解[附带源码分析]

    目录 前言 重要接口和类介绍 HandlerExceptionResolver接口 AbstractHandlerExceptionResolver抽象类 AbstractHandlerMethodE ...

  7. ArrayList用法详解与源码分析

    说明 此文章分两部分,1.ArrayList用法.2.源码分析.先用法后分析是为了以后忘了查阅起来方便-- ArrayList 基本用法 1.创建ArrayList对象 //创建默认容量的数组列表(默 ...

  8. [转]SpringMVC拦截器详解[附带源码分析]

      目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:ht ...

  9. SpringMVC拦截器详解[附带源码分析]

    目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:h ...

  10. Servlet容器Tomcat中web.xml中url-pattern的配置详解[附带源码分析]

    目录 前言 现象 源码分析 实战例子 总结 参考资料 前言 今天研究了一下tomcat上web.xml配置文件中url-pattern的问题. 这个问题其实毕业前就困扰着我,当时忙于找工作. 找到工作 ...

随机推荐

  1. css 01-CSS属性:字体属性和文本属性

    01-CSS属性:字体属性和文本属性 #本文重要内容 CSS的单位 字体属性 文本属性 定位属性:position.float.overflow等 #CSS的单位 html中的单位只有一种,那就是像素 ...

  2. Ecshop V2.7代码执行漏洞分析

    0x01 此漏洞形成是由于未对Referer的值进行过滤,首先导致SQL注入,其次导致任意代码执行. 0x02 payload: 554fcae493e564ee0dc75bdf2ebf94caads ...

  3. 彻底理解Spring如何解决循环依赖

    Spring bean生命周期 可以简化为以下5步. 1.构建BeanDefinition 2.实例化 Instantiation 3.属性赋值 Populate 4.初始化 Initializati ...

  4. 使用CDN后如何配置Apache使其记录访客真实IP

    今天想看看哪些地区的人访问过我的网站,于是打开Apache网站响应日志,把访客IP复制到百度,发现搜到的全部都是我是用的CDN的节点IP,真实的访客IP并没有被记录. 如图所示,上面的103.45.7 ...

  5. 使用mybatis-generator插件结合tk.mybatis自动生成mapper

    本篇文章将介绍使用spring boot框架,引入mybatis-generator插件,结合tk.mybatis自动生成Mapper和Entity的一整套流程,其中包括最重要的踩坑与填坑.     ...

  6. nginx反向代理部署springboot项目报404无法加载静态资源

    问题:nginx反向代理部署springboot项目报404无法加载静态资源(css,js,jpg,png...) springboot默认启动端口为8080,如果需要通过域名(不加端口号)直接访问s ...

  7. [leetcode]48RotateImage二维数组翻转

    import java.util.Arrays; /** * You are given an n x n 2D matrix representing an image. Rotate the im ...

  8. 使用JWT+RSA完成SSO单点登录

    无状态登录原理 1.1.什么是有状态? 有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session. 例如登录:用户登 ...

  9. 分析http协议和高并发网站架构

    案例任务名称 分析http协议和高并发网站架构 案例训练目标 深入理解http协议的工作原理 掌握http协议的分析方法 包含技能点 搭建web服务器 编辑简单的html页面并上传到服务器 使用wir ...

  10. Linux 时间同步 01 简介

    Linux 时间同步 01 简介 目录 Linux 时间同步 01 简介 时间同步 公共NTP服务器地址及IP 系统时间相关文件 时间同步 大数据产生与处理系统是各种计算设备集群的,计算设备将统一.同 ...