天天看点

高并发编程系列之Netty Pipeline设计原理

作者:Civen

1 Pipeline设计原理

1.1 Channel与ChannelPipeline

大家已经知道,在Netty中每个Channel都有且仅有一个ChannelPipeline与之对应,它们的组成关系如下图所示。

高并发编程系列之Netty Pipeline设计原理

通过上图可以看到,一个Channel包含了一个ChannelPipeline,而ChannelPipeline中又维护了一个由ChannelHandlerContext组成的双向链表。这个链表的头是HeadContext,链表的尾是TailContext,并且每个ChannelHandlerContext中又关联着一个ChannelHandler。

上图给了我们一个对ChannelPipeline的直观认识,但是实际上Netty实现的Channel是否真的这样呢?我们继续通过代码进行分析。我们已经知道了一个Channel初始化的基本过程,下面再回顾一下。AbstractChannel构造器的代码如下。

高并发编程系列之Netty Pipeline设计原理

AbstractChannel有一个pipeline属性,在构造器中会把它初始化为DefaultChannelPipeline的实例。这里的代码就印证了这一点:每个Channel都有一个ChannelPipeline。我们来跟踪一下DefaultChannelPipeline的初始化过程,DefaultChannelPipeline构造器的代码如下。

高并发编程系列之Netty Pipeline设计原理

在DefaultChannelPipeline构造器中,首先将与之关联的Channel保存到属性channel中。然后实例化两个ChannelHandlerContext:一个是HeadContext实例Head,另一个是TailContext实例Tail。接着将Head和Tail互相指向,构成一个双向链表。

特别注意的是:在开始的示意图中,Head和Tail并没有包含ChannelHandler,这是因为HeadContext和TailContext继承于AbstractChannelHandlerContext的同时,也实现了ChannelHandler接口,所以它们有Context和Handler的双重属性。

1.2 再谈ChannelPipeline的初始化

在第7章中我们已经对ChannelPipeline的初始化有了一个大致的了解,不过当时没有重点关注ChannelPipeline,因此没有深入分析它的初始化过程。下面就来看一下ChannelPipeline的初始化具体都做了哪些工作。先回顾一下,在实例化一个Channel时,会伴随着一个ChannelPipeline的实例化,并且此Channel会与这个ChannelPipeline相互关联,这一点可以通过NioSocketChannel的父类AbstractChannel的构造器予以佐证,代码如下。

高并发编程系列之Netty Pipeline设计原理

当实例化一个NioSocketChannel时,其pipeline属性就是我们新创建的DefaultChannelPipeline对象,再来回顾一下DefaultChannelPipeline的构造方法,代码如下。

高并发编程系列之Netty Pipeline设计原理

上面代码中的Head实现了ChannelInboundHandler接口,而Tail实现了ChannelOutboundHandler接口,因此可以说Head和Tail既是ChannelHandler,又是ChannelHandlerContext。

1.3 ChannelInitializer的添加

我们在第7章已经分析过Channel的组成,我们了解到,最开始的时候ChannelPipeline中含有两个ChannelHandlerContext(同时也是ChannelHandler),但是此时的Pipeline并不能实现特定的功能,因为还没有添加自定义的ChannelHandler。通常来说,在初始化Bootstrap时,会添加自定义的ChannelHandler,下面就以具体的客户端启动代码片段来举例。

高并发编程系列之Netty Pipeline设计原理
高并发编程系列之Netty Pipeline设计原理

上面代码的初始化过程,相信大家都不陌生。在调用Handler时,传入ChannelInitializer对象,它提供了一个initChannel()方法来初始化ChannelHandler。那么这个初始化过程是怎样的呢?下面我们来进行分析。

通过代码跟踪,我们发现ChannelInitializer是在Bootstrap的init()方法中添加到ChannelPipeline中的,代码如下。

高并发编程系列之Netty Pipeline设计原理

由上面的代码可见,将handler()方法返回的ChannelHandler添加到Pipeline中,而handler()方法返回的其实就是我们在初始化Bootstrap时通过handler()方法设置的ChannelInitializer实例,因此这里就将ChannelInitializer插到了Pipeline的末端。此时Pipeline的结构如下图所示。

高并发编程系列之Netty Pipeline设计原理

这时候,可能就有小伙伴疑惑了,明明插入的是一个ChannelInitializer实例,为什么在ChannelPipeline的双向链表中的元素却是一个ChannelHandlerContext呢?我们继续去代码中寻找答案。

在上面提到的Bootstrap的init()方法中会调用p.addLast()方法,将ChannelInitializer插入链表的末端,代码如下。

高并发编程系列之Netty Pipeline设计原理
高并发编程系列之Netty Pipeline设计原理

addLast()方法有很多重载的方法,我们只需关注这个比较重要的方法就可以。上面的addLast()方法中,首先检查ChannelHandler的名字是否重复,如果不重复,则调用newContex()方法为这个Handler创建一个对应的DefaultChannelHandlerContext实例,并与之关联起来(Context中有一个Handler属性保存着对应的Handler实例)。

为了添加一个Handler到Pipeline中,必须把此Handler包装成ChannelHandlerContext。因此在上面的代码中,我们新实例化了一个newCtx对象,并将Handler作为参数传递到构造方法中。那么我们来看一下实例化的DefaultChannelHandlerContext到底有什么玄机。首先看它的构造器。

高并发编程系列之Netty Pipeline设计原理

在DefaultChannelHandlerContext的构造器中,调用了两个很有意思的方法:isInbound()方法与isOutbound()方法,这两个方法是做什么的呢?来看代码。

从上面代码中可以看到,当一个Handler实现了ChannelInboundHandler接口,则isInbound返回true;类似地,当一个Handler实现了ChannelOutboundHandler接口,则isOutbound也返回true。而这两个boolean变量会传递到父类AbstractChannelHandlerContext中,并初始化父类的两个属性:inbound与outbound。

这里的ChannelInitializer所对应的DefaultChannelHandlerContext的inbound与outbound属性分别是什么呢?先来看ChannelInitializer的类层次结构图,如下图所示。

高并发编程系列之Netty Pipeline设计原理

从上图可以清楚地看到,ChannelInitializer仅仅实现了ChannelInboundHandler接口,因此这里实例化的DefaultChannelHandlerContext的inbound=true,outbound=false。

为什么要这么大费周折地分析一番inbound和outbound两个属性呢?其实这两个属性关系到Pipeline事件的流向与分类,因此是十分关键的,但暂时先不分析这两个属性所起的作用。至此,我们先记住一个结论:ChannelInitializer所对应的DefaultChannelHandlerContext的inbound=true,outbound=false。

当创建好Context之后,就将这个Context插入Pipeline的双向链表中。

高并发编程系列之Netty Pipeline设计原理

添加完ChannelInitializer的Pipeline内部如下图所示。

高并发编程系列之Netty Pipeline设计原理

1.4 自定义ChannelHandler的添加过程

上一节我们已经分析了ChannelInitializer是如何插入Pipeline中的,接下来探讨ChannelInitializer在哪里被调用、ChannelInitializer的作用及自定义的ChannelHandler是如何插入Pipeline中的。

我们自定义ChannelHandler的添加过程,发生在AbstractUnsafe的register0()方法中,在这个方法中调用了pipeline.fireChannelRegistered()方法,其代码实现如下。

高并发编程系列之Netty Pipeline设计原理

再看AbstractChannelHandlerContext的invokeChannelRegistered()方法。

高并发编程系列之Netty Pipeline设计原理

很显然,这个代码将从Head开始遍历Pipeline的双向链表,然后找到第一个属性inbound为true的ChannelHandlerContext实例。我们在分析ChannelInitializer时,花了大量的篇幅来分析inbound和outbound属性,现在这里就用上了。回想一下,ChannelInitializer实现了ChannelInboudHandler,因此它所对应的ChannelHandlerContext的inbound属性就是true,因此这里返回的就是ChannelInitializer实例所对应的ChannelHandlerContext对象,如下图所示。

高并发编程系列之Netty Pipeline设计原理

当获取inbound的Context后,就调用它的invokeChannelRegistered()方法。

高并发编程系列之Netty Pipeline设计原理

我们已经知道,每个ChannelHandler都和一个ChannelHandlerContext关联,可以通过ChannelHandlerContext获取对应的ChannelHandler。很明显,这里handler()方法返回的对象其实就是一开始实例化的ChannelInitializer对象,接着调用了ChannelInitializer的channelRegistered()方法。ChannelInitializer的channelRegistered()方法我们在第7章已经接触了,但是并没有深入分析其调用过程。下面来看这个方法到底有什么玄机。继续看代码。

高并发编程系列之Netty Pipeline设计原理

initChannel((C)ctx.channel())方法我们也很熟悉,它就是在初始化Bootstrap时,调用handler()方法传入的匿名内部类所实现的方法,代码如下。

高并发编程系列之Netty Pipeline设计原理
高并发编程系列之Netty Pipeline设计原理

因此,在调用这个方法之后,我们自定义的ChannelHandler就插入Pipeline中了,此时Pipeline的状态如下图所示。

高并发编程系列之Netty Pipeline设计原理

当添加完自定义的ChannelHandler后,在finally代码块会删除自定义的ChannelInitializer,也就是remove(ctx),最终调用ctx.pipeline().remove(this),因此最后Pipeline的状态如下图所示。

高并发编程系列之Netty Pipeline设计原理

至此,自定义ChannelHandler的添加过程也分析完成了。

1.5 给ChannelHandler命名

不知道大家注意到没有,pipeline.addXXX()都有一个重载的方法,例如addLast()有一个重载的版本,代码如下。

高并发编程系列之Netty Pipeline设计原理

第一个参数指定添加的是Handler的名字(更准确地说是ChannelHandlerContext的名字,说成Handler的名字更便于理解)。那么Handler的名字有什么用呢?如果我们不设置name,那么Handler默认的名字是怎样的呢?带着这些疑问,我们依旧去代码中寻找答案。还是以addLast()方法为例,代码如下。

高并发编程系列之Netty Pipeline设计原理

这个方法会调用重载的addLast()方法,代码如下。

高并发编程系列之Netty Pipeline设计原理
高并发编程系列之Netty Pipeline设计原理

第一个参数设置为null,我们不用关心它。第二个参数就是Handler的名字。由代码可知,在添加一个Handler之前,需要调用checkMultiplicity()方法来确定新添加的Handler名字是否与已添加的Handler名字重复。

1.6 ChannelHandler的默认命名规则

如果我们调用如下的addLast()方法:

高并发编程系列之Netty Pipeline设计原理

那么Netty就会调用generateName()方法为新添加的Handler自动生成一个默认的名字。

高并发编程系列之Netty Pipeline设计原理

而generateName()方法会接着调用generateName0()方法来实际生成一个新的Handler名字。

高并发编程系列之Netty Pipeline设计原理

默认命名的规则很简单,就是用反射获取Handler的simpleName加上“#0”,因此我们自定义ChatClientHandler的名字就是“ChatClientHandler#0”。

继续阅读