天天看点

Netty源码分析——ChannelPipeline

基于Netty源代码版本:netty-all-4.1.33.Final

前言

netty在服务端端口绑定和新连接建立的过程中会建立相应的channel,而与channel的动作密切相关的是pipeline这个概念,pipeline像是可以看作是一条流水线,原始的原料(字节流)进来,经过加工,最后输出。

ChannelPipeline不是单独存在,它肯定会和Channel、ChannelHandler、ChannelHandlerContext关联在一起,所以有关概念这里一起讲。

一、ChannelHandler

1、概念,先看图

Netty源码分析——ChannelPipeline

ChannelHandler下主要是两个子接口:

  • ChannelInboundHandler(入站):处理输入数据和Channel状态类型改变。
    • 适配器: ChannelInboundHandlerAdapter(适配器设计模式)
    • 常用的: SimpleChannelInboundHandler
  • ChannelOutboundHandler(出站):处理输出数据
    • 适配器: ChannelOutboundHandlerAdapter

每一个Handler都一定会处理出站或者入站(可能两者都处理数据),例如对于入站的Handler可能会继承SimpleChannelInboundHandler或者ChannelInboundHandlerAdapter,而SimpleChannelInboundHandler又是继承于ChannelInboundHandlerAdapter,最大的区别在于SimpleChannelInboundHandler会对没有外界引用的资源进行一定的清理,并且入站的消息可以通过泛型来规定。

这里为什么有设配器模式呢?

我们在写自定义Handel时候,很少会直接实现上面两个接口,因为接口中有很多默认方法需要实现,所以这里就采用了设配器模式ChannelInboundHandlerAdapter和ChannelInboundHandlerAdapter就是设配器模式的产物,让它去实现上面接口,实现它所有方法。那么你自己写自定义Handel时,只要继承它,就无须重写上面接口的所有方法了。

2、Channel 生命周期(执行顺序也是从上倒下)

  • (1)channelRegistered: channel注册到一个EventLoop。
  • (2)channelActive: 变为活跃状态(连接到了远程主机),可以接受和发送数据
  • (3)channelInactive: channel处于非活跃状态,没有连接到远程主机
  • (4)channelUnregistered: channel已经创建,但是未注册到一个EventLoop里面,也就是没有和Selector绑定

3、ChannelHandler 生命周期

handlerAdded:当 ChannelHandler 添加到 ChannelPipeline 调用

handlerRemoved:当 ChannelHandler 从 ChannelPipeline 移除时调用

exceptionCaught:当 ChannelPipeline 执行抛出异常时调用

二、ChannelPipeline

1、概念,先看图

Netty源码分析——ChannelPipeline

ChannelPipeline类是ChannelHandler实例对象的链表,用于处理或截获通道的接收和发送数据。它提供了一种高级的截取过滤模式(类似serverlet中的filter功能),让用户可以在ChannelPipeline中完全控制一个事件以及如何处理ChannelHandler与ChannelPipeline的交互。

对于每个新的通道Channel,都会创建一个新的ChannelPipeline,并将器pipeline附加到channel中。

下图描述ChannelHandler与pipeline中的关系,一个io操作可以由一个ChannelInboundHandler或ChannelOutboundHandle进行处理,并通过调用ChannelInboundHandler处理入站io或通过ChannelOutboundHandler处理出站IO。

Netty源码分析——ChannelPipeline

2、ChannelPipeline的初始化

public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {

    private final DefaultChannelPipeline pipeline;

    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }

    protected AbstractChannel(Channel parent, ChannelId id) {
        this.parent = parent;
        this.id = id;
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }

    protected DefaultChannelPipeline newChannelPipeline() {
        return new DefaultChannelPipeline(this);
    }
}
           

NioServerSocketChannel继承了AbstractChannel,所以NioServerSocketChannel中保存了pipeline的引用

DefaultChannelPipeline

public class DefaultChannelPipeline implements ChannelPipeline {

    final AbstractChannelHandlerContext head;
    final AbstractChannelHandlerContext tail;

    private final Channel channel;

    protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }
}
           

pipeline中保存了channel的引用,创建完pipeline之后,整个pipeline是这个样子的

Netty源码分析——ChannelPipeline

pipeline中的每个节点是一个ChannelHandlerContext对象,每个context节点保存了它包裹的执行器 ChannelHandler 执行操作所需要的上下文,其实就是pipeline,因为pipeline包含了channel的引用,可以拿到所有的context信息。

3、常用方法

addFirst(...)	//添加ChannelHandler在ChannelPipeline的第一个位置
addBefore(...)	//在ChannelPipeline中指定的ChannelHandler名称之前添加ChannelHandler
addAfter(...)	//在ChannelPipeline中指定的ChannelHandler名称之后添加ChannelHandler
addLast(...)	//在ChannelPipeline的末尾添加ChannelHandler
remove(...)		//删除ChannelPipeline中指定的ChannelHandler
replace(...)	//替换ChannelPipeline中指定的ChannelHandler
           

ChannelPipeline可以动态添加、删除、替换其中的ChannelHandler,因为ChannelPipeline本身是线程安全的,这样的机制可以提高灵活性。示例:

ChannelPipeline pipeline = ch.pipeline(); 
FirstHandler firstHandler = new FirstHandler(); 
pipeline.addLast("handler1", firstHandler); 
pipeline.addFirst("handler2", new SecondHandler()); 
pipeline.addLast("handler3", new ThirdHandler()); 
pipeline.remove("“handler3“"); 
pipeline.remove(firstHandler); 
pipeline.replace("handler2", "handler4", new FourthHandler());
           

4、入站出站Handler执行顺序

一般的项目中,inboundHandler和outboundHandler有多个,在Pipeline中的执行顺序?

重点记住: InboundHandler顺序执行,OutboundHandler逆序执行。

问题:下面的handel的执行顺序?

ch.pipeline().addLast(new InboundHandler1());
ch.pipeline().addLast(new OutboundHandler1());
ch.pipeline().addLast(new OutboundHandler2());
ch.pipeline().addLast(new InboundHandler2());
或者:
ch.pipeline().addLast(new OutboundHandler1());
ch.pipeline().addLast(new OutboundHandler2());
ch.pipeline().addLast(new InboundHandler1());
ch.pipeline().addLast(new InboundHandler2());
           

其实上面的执行顺序都是一样的:

InboundHandler1--> InboundHandler2 -->OutboundHandler2 -->OutboundHandler1
           

结论:

  • 1)InboundHandler顺序执行,OutboundHandler逆序执行
  • 2)InboundHandler之间传递数据,通过ctx.fireChannelRead(msg)
  • 3)InboundHandler通过ctx.write(msg),则会传递到outboundHandler
    1. 使用ctx.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然outboundhandler会不执行;

      但是使用channel.write(msg)、pipline.write(msg)情况会不一致,都会执行,那是因为channel和pipline会贯穿整个流。

    1. outBound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接受数据,先outbound再inbound,服务端则相反。

三、ChannelHandlerContext

ChannelPipeline并不是直接管理ChannelHandler,而是通过ChannelHandlerContext来间接管理,这一点通过ChannelPipeline的默认实现DefaultChannelPipeline可以看出来。

DefaultChannelHandlerContext和DefaultChannelPipeline是ChannelHandlerContext和ChannelPipeline的默认实现在DefaultPipeline内部

DefaultChannelHandlerContext组成了一个双向链表。 我们看下DefaultChannelPipeline的构造函数:

protected DefaultChannelPipeline(Channel channel) {
	this.channel = ObjectUtil.checkNotNull(channel, "channel");
	succeededFuture = new SucceededChannelFuture(channel, null);
	voidPromise =  new VoidChannelPromise(channel, true);

	tail = new TailContext(this);
	head = new HeadContext(this);

	head.next = tail;
	tail.prev = head;
}
           

所以对于DefaultChinnelPipeline它的Handel头部和尾部的Handel是固定的,我们所添加的Handel是添加在这个头和尾之前的Handel。(下面这个图更加清晰)

Netty源码分析——ChannelPipeline

四、几者关系

先大致说下什么是Channel

通常来说, 所有的 NIO 的 I/O 操作都是从 Channel 开始的. 一个 channel 类似于一个 stream。在Netty中,Channel是客户端和服务端建立的一个连接通道。

虽然java Stream 和 NIO Channel都是负责I/O操作,但他们还是有许多区别的:

  • 1)我们可以在同一个 Channel 中执行读和写操作, 然而同一个 Stream 仅仅支持读或写。
  • 2)Channel 可以异步地读写, 而 Stream 是阻塞的同步读写。
  • 3)Channel 总是从 Buffer 中读取数据, 或将数据写入到 Buffer 中。

几者的关系图如下:

Netty源码分析——ChannelPipeline

总结:

一个Channel包含一个ChannelPipeline,创建Channel时会自动创建一个ChannelPipeline,每个Channel都有一个管理它的pipeline,这关联是永久性的。

这点从源码中就可以看出,我之前写的博客里有说到:【Netty】5 源码 Bootstrap。每一个ChannelPipeline中可以包含多个ChannelHandler。所有ChannelHandler都会顺序加入到ChannelPipeline中,ChannelHandler实例与ChannelPipeline之间的桥梁是ChannelHandlerContext实例。

可见,Pipeline的事件传播,是靠Pipeline,Context和Handler共同协作完成的。

五、详细分析下ChannelPipeline的addLast()和remove()

1、ChannelPipeline.addLast()

这里关注一下ChannelPipeline接口中的这个重载方法

ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler);
           

我们常常遇到这样的需求:在一个业务逻辑处理器中,需要写数据库、进行网络连接等耗时业务。Netty的原则是不阻塞I/O线程,所以需指定Handler执行的线程池。

如果MyBusinessLogicHandler是一个耗时的处理逻辑,应该制定group,避免I/O线程被阻塞,如果业务逻辑是异步处理或处理时间很快那么可以不用指定group。

#指定group的用法
static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);

// 复杂耗时业务,使用新的线程池
pipline.addLast(group, "handler", new MyBusinessLogicHandler());
           

ChannelPipeline.addLast()内部实现

public class DefaultChannelPipeline implements ChannelPipeline {
    @Override
    public final ChannelPipeline addLast(String name, ChannelHandler handler) {
        return addLast(null, name, handler);
    }

    @Override
    public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
			// 1.检查是否有重复handler
            checkMultiplicity(handler);
			// 2.创建节点
            newCtx = newContext(group, filterName(name, handler), handler);
			// 3.添加节点
            addLast0(newCtx);

            // If the registered is false it means that the channel was not registered on an eventLoop yet.
            // In this case we add the context to the pipeline and add a task that will call
            // ChannelHandler.handlerAdded(...) once the channel is registered.
            if (!registered) {
                newCtx.setAddPending();
                callHandlerCallbackLater(newCtx, true);
                return this;
            }

            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                callHandlerAddedInEventLoop(newCtx, executor);
                return this;
            }
        }
		// 4.回调用户方法
        callHandlerAdded0(newCtx);
        return this;
    }
}
           

这里简单地用synchronized方法是为了防止多线程并发操作pipeline底层的双向链表

我们还是逐步分析上面这段代码

  • 检查是否有重复handler

    在用户代码添加一条handler的时候,首先会查看该handler有没有添加过

private static void checkMultiplicity(ChannelHandler handler) {
	if (handler instanceof ChannelHandlerAdapter) {
		ChannelHandlerAdapter h = (ChannelHandlerAdapter) handler;
		if (!h.isSharable() && h.added) {
			throw new ChannelPipelineException(
					h.getClass().getName() +
					" is not a @Sharable handler, so can't be added or removed multiple times.");
		}
		h.added = true;
	}
}
           

netty使用一个成员变量added标识一个channel是否已经添加,上面这段代码很简单,如果当前要添加的Handler是非共享的,并且已经添加过,那就抛出异常,否则,标识该handler已经添加

由此可见,一个Handler如果是sharable的,就可以无限次被添加到pipeline中,我们客户端代码如果要让一个Handler被共用,只需要加一个@Sharable标注即可,如下:

@Sharable
public class BusinessHandler {
    
}
           

而如果Handler是sharable的,一般就通过spring的注入的方式使用,不需要每次都new 一个

isSharable() 方法正是通过该Handler对应的类是否标注@Sharable来实现的

public abstract class ChannelHandlerAdapter implements ChannelHandler {
    public boolean isSharable() {
        
        Class<?> clazz = getClass();
        Map<Class<?>, Boolean> cache = InternalThreadLocalMap.get().handlerSharableCache();
        Boolean sharable = cache.get(clazz);
        if (sharable == null) {
            sharable = clazz.isAnnotationPresent(Sharable.class);
            cache.put(clazz, sharable);
        }
        return sharable;
    }
}
           

通过反射判断是否有Sharable.class注解

  • 创建节点

    回到主流程,看创建上下文这段代码

newCtx = newContext(group, filterName(name, handler), handler);
           

这里我们需要先分析 filterName(name, handler) 这段代码,这个函数用于给handler创建一个唯一性的名字

private String filterName(String name, ChannelHandler handler) {
	if (name == null) {
		return generateName(handler);
	}
	checkDuplicateName(name);
	return name;
}
           

显然,我们传入的name为null,netty就给我们生成一个默认的name,否则,检查是否有重名,检查通过的话就返回

netty创建默认name的规则为 简单类名#0,下面我们来看些具体是怎么实现的

private static final FastThreadLocal<Map<Class<?>, String>> nameCaches =
            new FastThreadLocal<Map<Class<?>, String>>() {
        @Override
        protected Map<Class<?>, String> initialValue() {
            return new WeakHashMap<Class<?>, String>();
        }
};

private String generateName(ChannelHandler handler) {
	// 先查看缓存中是否有生成过默认name
	Map<Class<?>, String> cache = nameCaches.get();
	Class<?> handlerType = handler.getClass();
	String name = cache.get(handlerType);
	if (name == null) {
		// 没有生成过,就生成一个默认name,加入缓存 
		name = generateName0(handlerType);
		cache.put(handlerType, name);
	}

	// 生成完了,还要看默认name有没有冲突
	if (context0(name) != null) {
		String baseName = name.substring(0, name.length() - 1); // Strip the trailing '0'.
		for (int i = 1;; i ++) {
			String newName = baseName + i;
			if (context0(newName) == null) {
				name = newName;
				break;
			}
		}
	}
	return name;
}
           

netty使用一个 FastThreadLocal变量来缓存Handler的类和默认名称的映射关系,在生成name的时候,首先查看缓存中有没有生成过默认name(简单类名#0),如果没有生成,就调用generateName0()生成默认name,然后加入缓存

接下来还需要检查name是否和已有的name有冲突,调用context0(),查找pipeline里面有没有对应的context

private AbstractChannelHandlerContext context0(String name) {
	AbstractChannelHandlerContext context = head.next;
	while (context != tail) {
		if (context.name().equals(name)) {
			return context;
		}
		context = context.next;
	}
	return null;
}
           

context0()方法链表遍历每一个 ChannelHandlerContext,只要发现某个context的名字与待添加的name相同,就返回该context,最后抛出异常,可以看到,这个其实是一个线性搜索的过程

如果context0(name) != null 成立,说明现有的context里面已经有了一个默认name,那么就从 简单类名#1 往上一直找,直到找到一个唯一的name,比如简单类名#3

如果用户代码在添加Handler的时候指定了一个name,那么要做到事仅仅为检查一下是否有重复

private void checkDuplicateName(String name) {
	if (context0(name) != null) {
		throw new IllegalArgumentException("Duplicate handler name: " + name);
	}
}
           

处理完name之后,就进入到创建context的过程

private AbstractChannelHandlerContext newContext(EventExecutorGroup group, String name, ChannelHandler handler) {
	return new DefaultChannelHandlerContext(this, childExecutor(group), name, handler);
}

DefaultChannelHandlerContext(
		DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
	super(pipeline, executor, name, isInbound(handler), isOutbound(handler));
	if (handler == null) {
		throw new NullPointerException("handler");
	}
	this.handler = handler;
}
           

构造函数中,DefaultChannelHandlerContext将参数回传到父类,保存Handler的引用,进入到其父类

AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
							  boolean inbound, boolean outbound) {
	this.name = ObjectUtil.checkNotNull(name, "name");
	this.pipeline = pipeline;
	this.executor = executor;
	this.inbound = inbound;
	this.outbound = outbound;
	// Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
	ordered = executor == null || executor instanceof OrderedEventExecutor;
}
           

netty中用两个字段来表示这个channelHandlerContext属于inBound还是outBound,或者两者都是,两个boolean是通过下面两个小函数来判断(见上面一段代码)

final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
	private static boolean isInbound(ChannelHandler handler) {
        return handler instanceof ChannelInboundHandler;
    }

    private static boolean isOutbound(ChannelHandler handler) {
        return handler instanceof ChannelOutboundHandler;
    }
}
           

通过instanceof关键字根据接口类型来判断,因此,如果一个Handler实现了两类接口,那么他既是一个inBound类型的Handler,又是一个outBound类型的Handler,比如下面这个类

Netty源码分析——ChannelPipeline

常用的,将decode操作和encode操作合并到一起的codec,一般会继承 MessageToMessageCodec,而MessageToMessageCodec就是继承ChannelDuplexHandler

public abstract class MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN> extends ChannelDuplexHandler {

    protected abstract void encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, List<Object> out)
            throws Exception;

    protected abstract void decode(ChannelHandlerContext ctx, INBOUND_IN msg, List<Object> out)
            throws Exception;
}
           

添加节点

用下面这幅图可见简单的表示这段过程,说白了,其实就是一个双向链表的插入操作

Netty源码分析——ChannelPipeline

操作完毕,该context就加入到pipeline中

Netty源码分析——ChannelPipeline

到这里,pipeline添加节点的操作就完成了,你可以根据此思路掌握所有的addxxx()系列方法

回调用户方法

abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
        implements ChannelHandlerContext, ResourceLeakHint {
    final void callHandlerAdded() throws Exception {
        //查看该节点的状态
        if (setAddComplete()) {
            handler().handlerAdded(this);
        }
    }

	//设置该节点的状态
    final boolean setAddComplete() {
        for (;;) {
            int oldState = handlerState;
            if (oldState == REMOVE_COMPLETE) {
                return false;
            }
            // Ensure we never update when the handlerState is REMOVE_COMPLETE already.
            // oldState is usually ADD_PENDING but can also be REMOVE_COMPLETE when an EventExecutor is used that is not
            // exposing ordering guarantees.
            if (HANDLER_STATE_UPDATER.compareAndSet(this, oldState, ADD_COMPLETE)) {
                return true;
            }
        }
    }
}
           

2、ChannelPipeline.remove()

pipeline删除节点

netty 有个最大的特性之一就是Handler可插拔,做到动态编织pipeline,比如在首次建立连接的时候,需要通过进行权限认证,在认证通过之后,就可以将此context移除,下次pipeline在传播事件的时候就就不会调用到权限认证处理器

下面是权限认证Handler最简单的实现,第一个数据包传来的是认证信息,如果校验通过,就删除此Handler,否则,直接关闭连接

public class AuthHandler extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf data) throws Exception {
        if (verify(authDataPacket)) {
            ctx.pipeline().remove(this);
        } else {
            ctx.close();
        }
    }

    private boolean verify(ByteBuf byteBuf) {
        //...
    }
}
           

重点就在 ctx.pipeline().remove(this) 这段代码

@Override
public final ChannelPipeline remove(ChannelHandler handler) {
	remove(getContextOrDie(handler));
	return this;
}
           

找到待删除的节点

public class DefaultChannelPipeline implements ChannelPipeline {	
	private AbstractChannelHandlerContext getContextOrDie(ChannelHandler handler) {
		AbstractChannelHandlerContext ctx = (AbstractChannelHandlerContext) context(handler);
		if (ctx == null) {
			throw new NoSuchElementException(handler.getClass().getName());
		} else {
			return ctx;
		}
	}
    @Override
    public final ChannelHandlerContext context(Class<? extends ChannelHandler> handlerType) {
        if (handlerType == null) {
            throw new NullPointerException("handlerType");
        }

        AbstractChannelHandlerContext ctx = head.next;
        for (;;) {
            if (ctx == null) {
                return null;
            }
            if (handlerType.isAssignableFrom(ctx.handler().getClass())) {
                return ctx;
            }
            ctx = ctx.next;
        }
    }
}
           

这里为了找到Handler对应的context,照样是通过依次遍历双向链表的方式,直到某一个context的Handler和当前Handler相同,便找到了该节点

调整双向链表指针删除

public class DefaultChannelPipeline implements ChannelPipeline {	
    private AbstractChannelHandlerContext remove(final AbstractChannelHandlerContext ctx) {
        assert ctx != head && ctx != tail;

        synchronized (this) {
            remove0(ctx);

            // If the registered is false it means that the channel was not registered on an eventloop yet.
            // In this case we remove the context from the pipeline and add a task that will call
            // ChannelHandler.handlerRemoved(...) once the channel is registered.
            if (!registered) {
                callHandlerCallbackLater(ctx, false);
                return ctx;
            }

            EventExecutor executor = ctx.executor();
            if (!executor.inEventLoop()) {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callHandlerRemoved0(ctx);
                    }
                });
                return ctx;
            }
        }
        callHandlerRemoved0(ctx);
        return ctx;
    }

    private static void remove0(AbstractChannelHandlerContext ctx) {
        AbstractChannelHandlerContext prev = ctx.prev;
        AbstractChannelHandlerContext next = ctx.next;
        prev.next = next;
        next.prev = prev;
    }
}
           

经历的过程要比添加节点要简单,可以用下面一幅图来表示

Netty源码分析——ChannelPipeline

删除后的结果为:

Netty源码分析——ChannelPipeline

结合这两幅图,可以很清晰地了解权限验证Handler的工作原理,另外,被删除的节点因为没有对象引用到,过段时间就会被gc自动回收

public class DefaultChannelPipeline implements ChannelPipeline {	
    private void callHandlerRemoved0(final AbstractChannelHandlerContext ctx) {
        // Notify the complete removal.
        try {
            ctx.callHandlerRemoved();
        } catch (Throwable t) {
            fireExceptionCaught(new ChannelPipelineException(
                    ctx.handler().getClass().getName() + ".handlerRemoved() has thrown an exception.", t));
        }
    }
}
           

pipeline中的节点删除完成,于是便开始回调用户代码 ctx.handler().handlerRemoved(ctx);,常见的代码如下:

public class DemoHandler extends SimpleChannelInboundHandler<...> {
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        // 节点被删除完毕之后回调到此,可做一些资源清理
        // do something
    }
}
           

最后,将该节点的状态设置为removed

final void setRemoved() {
    handlerState = REMOVE_COMPLETE;
}
           
  • 1、在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应。
  • 2、ChannelPipeline是一个维护了一个以 AbstractChannelHandlerContext 为节点的双向链表,其中此链表是 以head(HeadContext)作为头,以tail(TailContext)作为尾的双向链表.
  • 3、pipeline中的每个节点包着具体的处理器ChannelHandler,节点根据ChannelHandler的类型是ChannelInboundHandler还是ChannelOutboundHandler来判断该节点属于in还是out或者两者都是