Netty学习系列(七)-总结

1,386 阅读22分钟

前言

       本篇文章是Netty的最后一篇文章,这篇文章中我会把学到的知识点进行总结概括,还有自己对Netty的一些见解。另外会对Netty中用到的几种设计模式进行分析,最后会对本系列学习中的一个心得总结。

一 初识Netty

       这节主要回顾一下什么是Netty,为什么会产生Netty这个库,还有Netty有什么优势等方面。

1.1 什么是Netty

       在《Netty实战》中的定义:"Netty是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端"。        在我这段时间的学习中了解到,Netty的内部实现是很复杂的,但是 Netty提供了简单易用的API从网络处理代码中解耦业务逻辑。正是因为他的优秀,使得它已经得到成百上千的商业、商用项目验证,许多框架和开源组件的底层rpc都是使用的Netty,如DubboElasticsearchZookeeperHadoopRocketMQ等等。        Netty是由一个叫Trustin Lee开始开发的,在4.0版本之前是归属于JBoss公司的,之后就由Netty社区进行管理。他的发展史大概如下:

  • 2004年6月Netty2发布,当时声称是Java社区中第一个基于事件驱动的易用网络框架
  • 2008年10月Netty3发布
  • 2013年7月Netty4发布
  • 2013年12月发布5.0.0.Alpha1
  • 2015年1月废弃5.0.0

       这里应该大家都很奇怪为什么会废弃5.0版本,这里可以看Netty社区中一个维护Netty的人发的一段话如下:

       这里主要的意思就是使用了ForkJoinPool使得代码变得复杂,但是却没有明显的性能提升,另外的就是维护太多的分支需要大量的工作,实际上很多都没必要,维护不过来。        其实Netty就是对原生的NiOOiOEpoll等基础网络编程的封装,那这里为什么我们不直接使用他们呢,而是选用了Netty,下面我们分析一下。

1.2 为什么会产生Netty

       Netty主要是对NIO的封装,这里就拿Netty与java NIO进行对比,看看是什么驱动Netty的产生。首先java.nio全称java non-blocking IO(实际上是new io),是指JDK 1.4及以上版本里提供的新api(New IO),为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。下面我们就分析为什么不用原生的NIO,而用Netty。        首先可以更好的规避JDK NIO的bug。这里例如经典的Epoll Bug:异常唤醒空转导致CPU 100%,如图:

       这里我们可以看到,他的解决结果是无法修复,而Netty中帮我们解决了这个bug,具体的我们可以看看源码:

/**
 * NioEventLoop.java 788行起
 */
  if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                        selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                    // The selector returned prematurely many times in a row.
                    // Rebuild the selector to work around the problem.
                    logger.warn(
                            "Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
                            selectCnt, selector);

                    rebuildSelector();
                    selector = this.selector;

                    // Select again to populate selectedKeys.
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

       这里的代码我在第三篇文章分析过,就是判断空轮询次数大于SELECTOR_AUTO_REBUILD_THRESHOLD这个值(默认为512),就会调用一个rebuildSelector() 方法,这个方法的作用就是把老的Selector上的所有Selector.Key,注册到一个新的Selector上去,这样新的Selector中的阻塞式操作就有可能不会发生空轮询bug。当然Netty中解决java NIO的bug不只是这一个,还有其他的这里就不过多的介绍。        紧接着,Netty做的更好的另一个理由就是,Netty的API更友好更强大。这里可以聚两个例子,其一: 在jdk中ByteBuffer的使用不够友好,而且功能薄弱,而Netty就封装了自己的ByteBuf,这个封装是的api的使用更加友好,而且功能更加强大。其二: Netty对Jdk的某些类进行了增强,如将TheadLocal增强,封装成了FastThreadLocal。        然后,Netty帮我们屏蔽了很多细节的实现,和隔离了很多变化。例如帮我们屏蔽了JDK 的NIO的实现。隔离了BIO(OIO)、NIO和Epoll的变化,想要切换他们,我们只需要该表几行代码就可以办到。        最后,我们如果自己要去用JDK NIO实现程序,我们需要解决多少问题,这里我们先看看Netty解决了多少问题,如图:

       这里可以看到Netty解决了4000多个bug,当然如果我们实现一个网络程序可能没有Netty功能那么全,不会遇到那么多bug,但是我们的水平没那么高可能还会产生更多的bug。另外我们来看看,JDK源码中有多少的NIO bug,如图:

       这里我们可以看到,JDK NIO的bug就有一千多条,我们如果用原生的JDK NIO去实现网络程序的话,那么我们就必须要去规避这些bug,这将是一个庞大的工程。通过上面这几点,这也决定Netty的发展壮大既是偶然也是必然。

1.3 Netty的优点

       有了上一节的分析,我们很容易就理解到了Netty的优点,这里还是总结一下,大概有如下几点:

  • API使用简单,开发门槛低。
  • 功能强大,预置了多种编解码功能,支持多种主流协议。
  • 定制能力强,通过channelHandler对通信框架进行灵活扩展。
  • 性能高。
  • 成熟,稳定,修复了大量的jdk nio bug。
  • 社区活跃。
  • 经历了大规模的商业应用考验,质量得到验证。

       这一节我主要讲了Netty的一些优点,发展历史等,下一章我将分析一下Netty的整体架构。

二 Netty的架构

       这一节我主要分析Netty的整体框架,具体细节部分前面已经分析了,这一节就不过多的分析,这里我们先来看看Netty的一个功能图:

       这里我们可以看到,值底部的core中他支持零拷贝的能力,还有更通用的api和可扩展的事件驱动。在上面,运输服务中支持NIO和OIO,对于协议支持包括Http、Protobuf,mqtt等,后面包括还有安全性,容器集成等都有相应实现。

2.1 任务处理模型。

       前面几篇文章我主要是通过源码的角度讲解了Netty中比较重要的几块内容,这一小节我将以宏观的角度看看,Netty中的线程模型,在此之前我们先看看几种常见的任务处理模型。

2.1.1 串行处理模型

       这个模型中用一个线程来处理网络请求连接和任务处理,具体的我们可以用一幅图来看看,如下:

       这里我们可以看到,当worker接受到一个任务之后,就立刻进行处理,也就是说任务接受和任务处理是在同一个worker线程中进行的,没有进行区分。这样做存在一个很大的问题是,必须要等待某个task处理完成之后,才能接受处理下一个task。        而通常情况下,任务的处理过程会比任务的接受流程慢得多。例如在处理任务的时候,我们可能会需要访问远程数据库,这属于一种网络IO。通常情况下IO操作是比较耗时的,这直接影响了下一个任务的接受,而且通常在IO操作的时候,CPU是比较空闲的,白白浪费了资源。有了这个弊端我们可以有以下改进。

       在这里,我们把接收任务和处理任务两个阶段分开处理,一个线程接收任务,放入任务队列,另外的线程异步处理任务队列中的任务,这样子就能提高了一些吞吐,但是还是不够完美,还有什么改进的呢,看下一节。

2.1.2 并行化处理模型

       有了上面一节的优化,那么我们可以将worker线程可以进一步的池化,具体的可以看下图:

       这里是由于任务处理一般比较缓慢,会导致任务队列中任务积压长时间得不到处理,这时可以使用多线程来处理。这里使用的是一个公共的任务队列,多线程环境中不免要通过加锁来保证线程安全,我们常用的线程池就是这种模式。可以通过为每个线程维护一个任务队列来改进这种模型。有了上面的铺垫我们下面就分析分析,线程处理模型。

2.2 Netty的线程处理模型

       其实在Netty中有很多中线程模型,大概有ThreadPerConnection模式Reactor模式Proactor模式。首先ThreadPerConnection模式对应的就是BIO的线程模型,也对应的上面的串行任务处理模式。然后Reactor模式对应的就是NIO的线程模型,这里对应的就是上面的并行处理模式。最后Proactor模式对应的就是AIO的线程模型,这里注意的是Netty已经废弃了AIO的实现,所以这里我们不过多介绍,本小节也主要是讲解Reactor模式。

2.2.1 Reactor模式

       reactor线程模型关注的是:任务接受之后,对处理过程继续进行切分,划分为多个不同的步骤,每个步骤用不同的线程来处理,也就是原本由一个线程处理的任务现在由多个线程来处理,每个线程在处理完自己的步骤之后,还需要将任务转发到下阶段线程继续进行处理。具体的我们可以看一幅图,如下:

2.2.2 Netty中的reactor线程模型

       在Netty中可能出现三种reactor线程模型,分别是单线程reactor多线程reactor主从多线程reactor。下面我们来分别分析,首先是单线程reactor,我们还是先看一幅图,如下:

       其实这里可以看出主要是acceptor来分发任务,那么在Netty中什么时候可能会发生这种情况呢,下面的使用方式就是这种线程模型,源码如下:

EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);

       有了上面的例子,那么我们看看什么是多线程reactor呢,具体的我还是用一幅图来开始说明,如下:

       这里我们可以看到多了一个ThreadPool线程池,来处理业务逻辑,其实类似于我们上面提到的第二种串行任务处理模式,那么在Netty中什么时候可能会出现这种情况呢?具体的例子程序源码如下:

EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);

       可以看到这个例子程序和前面的只有一个地方的区别就是上面在创建NioEventLoopGroup传了个1,其实这里如果不传参数则默认会构造一个CPU核数两倍的线程池,这一点我在前面的文章中也有将到,可以看前面文章的具体分析。下面还有最后一种线程模型主从多线程模型,这里我们也可以猜测到,应该就是我们上面说到的并行任务处理模型,具体的我们还是用一幅图来表示,如下:

       其中mainReacotor,subReactor,ThreadPool是三个线程池。mainReactor负责处理客户端的连接请求,并将accept的连接注册到subReactor的其中一个线程上;subReactor负责处理客户端通道上的数据读写;ThreadPool是具体的业务逻辑线程池,处理具体业务。这里具体在什么地方可能使用到呢,具体看例子程序源码,如下:

EventLoopGroup bossGroup = newNioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup(); 
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup); 

       相信上面的代码很多人都用过,这上面大概就是Netty中NIO的线程模型,那么Netty具体是怎样使用这些模型,下面我们分析分析它的实现。

2.3 Netty的运行流程

我们想具体看看Netty中是如何使用上面的线程模型,需要先看看一幅图,如下:

       这上面有几个注意的地方,这里我简单的进行分析。首先是关于NioEventLoop和NioEventLoopGroup的。NioEventLoop实际上就是工作线程,可以直接理解为一个线程。NioEventLoopGroup是一个线程池,线程池中的线程就是NioEventLoop。实际上bossGroup中有多个NioEventLoop线程,每个NioEventLoop绑定一个端口,也就是说,如果程序只需要监听1个端口的话,bossGroup里面只需要有一个NioEventLoop线程就行了。        每个NioEventLoop都绑定了一个Selector,所以在Netty的线程模型中,是由多个Selecotr在监听IO就绪事件。而Channel注册到Selector。        一个Channel绑定一个NioEventLoop,相当于一个连接绑定一个线程,这个连接所有的ChannelHandler都是在一个线程中执行的,避免了多线程干扰。更重要的是ChannelPipline链表必须严格按照顺序执行的。单线程的设计能够保证ChannelHandler的顺序执行。        一个NioEventLoop的selector可以被多个Channel注册,也就是说多个Channel共享一个EventLoop。EventLoop的Selecctor对这些Channel进行检查。至于其他的相关概念我在前面的时候有分析过这里就不过多的分析,下面我将走一遍Netty的流程。

2.3.1 启动服务

       从这一小节开始主要就是分析Netty的运行流程,这一节主要是启动服务的分析。其实在前面我们已经分析的很清楚了这里只是一个总结,主要是在以worker thread和boss thread这两个角度来分析。

worker Thread

  • 创建selector
  • 创建serverSocketChannel
  • 初始化serverSocketChannel
  • 给serverSocketChannel从bossGroup中选择一个NioEventLoop

boss Thread

  • 将 serverSocketChannel注册到选择的 NioEventLoop的selector
  • 绑定地址启动
  • 注册接受连接事件(OP_ACCEPT)到selector上

       这里面在前面详细分析源码的时候有讲过这几个步骤,这里面主要就是进行一些初始化的操作,然后调用一个bind()方法,这个具体的源码就不分析了,放先前文章的一张图,如下:

       上面有几点重要的是,Selector是在new NioEventLoopGroup()(创建一批NioEventLoop)时创建,最终监听OP_ACCEPT是通过bind完成后的fireChannelActive()来触发的,NioEventLoop是通过Register操作的执行来完成启动的,这一节差不多就这些是重要的知识点。

2.3.2 构建连接

       当服务启动起来就需要构建连接,等待连接的接入,这一小节的分析还是参照前面的模式,主要是分析worker thread和boss thread,如下:

boss thread

  • NioEventLoop中的selector轮询创建连接事件(OP_ACCEPT)
  • 创建socketChannel
  • 初始化socketChannel并从workerGroup 中选择一个NioEventLoop

worker thread

  • 将socketChannel注册到选择的NioEventLoop的selector
  • 注册读事件(OP_READ)到selector上

       这一节的很多步骤其实和上一节的知识点是类似的,只不过这一节的工作重心是在workerGroup中,这里我们可以把这两个过程合起来用一张图来表示,如下:

       在上面还有几点要注意的是Worker线程中的NioEventLoop是通过 Register操作执行来启动,这个注册操作在上一节的图中有明显的的表示。另外还有一点值得注意的就是接受连接的读操作,不会尝试读取更多次,这里默认最多是16次。

2.3.3 接收数据

       这里接收数据主要考虑的是初始的时候拿多大的容器去接收数据,Netty采用的是一个自适应数据大小的分配器AdaptiveRecvByteBufAllocator,这个容器会根据当前的数据量来预测下一次应该拿多大的容器来装数据。另外一个考虑的就是一个容器装满了,肯定又想继续拿个新的容器来装,那这样子是不是一直装下去呢,其实上面小节的最后有讲到最多默认16次。这些都是在worker thread中进行的,现在我梳理一下这条线,

  • 首先多路复用器(Selector)通过一定的select策略接收到OP_READ事件。
  • 然后就是处理这个事件,处理这个事件又有一定的流程。
  •        首先分配一个初始1024字节的byteBuf来接受数据。
  •        然后从Channel接受数据到byteBuf,在这里记录实际接受数据大小,调整下次分配byteBuf大小。
  •        紧接着会触发pipeline.fireChannelRead(byteBuf) 把读取到的数据传播出去。
  •        最后判断接受byteBuf是否满载而归:是,尝试继续读取直到没有数据或满16次;否,结束本轮读取,等待下次OP_READ事件。

       上面有几点注意的就是,读取数据本质:sun.nio.ch.SocketChannelImpl#read(java.nio.Byte Buffer);NioSocketChannel read()是读数据, NioServerSocketChannel read()是创建连接,pipeline.fireChannelReadComplete()表示一次读事件处理完成;pipeline.fireChannelRead(byte Buf)表示一次读数据完成,一次读事件处理可能会包含多次读数据操作。最后需要注意的是AdaptiveRecvByteBufAllocator对bytebuf的猜测,放大是很果断直接放大,但是在缩小的时候需要判断两次是否真的需要才会缩小。

2.3.4 业务处理

       这里处理业务本质:数据在pipeline中所有的handler的channelRead() 执行过程,这里我还是引用先前文章中的图,如下:

       这里业务逻辑的处理主要是InBound事件的处理,这里的的Handler要实现io.netty.channel.ChannelInboundHandler#channelRead(ChannelHandlerContext ctx,Object msg),且不能加注解@Skip才能被执行到。另外这里默认处理线程就是Channel绑定的NioEventLoop线程,也可以设置其他,如pipeline.addLast(new UnorderedThreadPoolEventExecutor(10), serverHandler)。

2.3.5 发送(写)数据

       在了解发送数据前先了解几个操作,如下:

  • write:写到一个buffer
  • flush: 把buffer里的数据发送出去
  • writeAndFlush:写到buffer,立马发送
  • Write和Flush之间有个ChannelOutboundBuffer

       这里Netty写数据,写不进去时会停写,然后注册一个OP_WRITE事件,来通知什么时候可以写进去了再写。另外Netty批量写数据时,如果想写的都写进去了,接下来可以调整maxBytesPerGatheringWrite来写的更多更快。Netty只要有数据要写,且能写的出去,则一直尝试,直到写不出去或者满16次,写 16 次还没有写完,就直接计划一个task来继续写,而不是用注册写事件来触发,更简洁有力。最后Netty待写数据太多,超过一定的水位线(writeBufferWaterMark.high()),会将可写的标志位改成false,让应用端自己做决定要不要发送数据了。下面我们看看,发送数据的具体步骤,如图:

  • Write:写数据到buffe也就是调用ChannelOutboundBuffer的addMessage
  • Flush:发送buffer里面的数据,也就是调用AbstractChannel.AbstractUnsafe的flush具体这里要两步,第一调用ChannelOutboundBuffer的addFlush准备数据,然后调用NioSocketChannel的doWrite发送。

       上面还有几个重要的点channelHandlerContext.channel().write() 是从TailContext开始执行,channelHandlerContext.write()是从当前的Context开始。

2.3.6 断开连接

       断开连接的操作其实很简单,主要还是根据接收到的OP_READ事件来区分,下面还是理一条线。

  • 多路复用器(Selector)接收到OP_READ事件
  • 处理OP_READ事件:NioSocketChannel.NioSocketChannelUnsafe.read():
  •        接受数据
  •        判断接受的数据大小是否<0 , 如果是,说明是关闭,开始执行关闭。
  •               关闭 channel(包含cancel多路复用器的key)
  •               清理消息:不接受新信息,舍弃掉所有queue中消息。
  •               触发fireChannelInactive和fireChannelUnregistered。

       这上面还是有几点需要注意,关闭连接,会触发OP_READ方法。读取字节数是-1代表关闭。数据读取进行时,强行关闭,触发IOException,进而执行关闭。

2.3.7 关闭服务

       关闭服务其实是一件很麻烦的事,主要是调用bossGroup和workerGroup的shutdownGracefully()方法,会关闭所有Group中的NioEventLoop,里面会有一系列的判断,具体的可以看下图:

       上面具体的分析这里就不进行了,关闭服务的本质就是关闭所有的连接及Selector,另外就是先不接货,尽量干完手头的活,最后关闭所有线程,退出事件循环。在这里我就把Netty的整个运行流程分析完了,下面一张我主要就介绍一下Netty中用到的一些设计模式。

三 Netty的设计模式

       Netty作为一个这么优秀的库,他里面用到的设计模式自然不少,当然本篇文章这里我只是挑选几个典型的例子分析。在此之前我先用一幅图来展示Netty中用到的设计模式,如下:

       其实可能Netty中远远不止这几设计模式,在本节中我对上面的设计模式也不全部分析,主要挑选几个来分析,在分析之前我们来看看设计模式的分类。

3.1 设计模式的分类

       其实将传统的设计模式分为三类,分别是创建型设计模式结构型设计模式行为型设计模式,他们每种设计模式解决问题的侧重点不同。        创建型设计模式:他主要就是好解决对象创建问题,封装复杂的创建过程,解耦对象创建和使用的代码。主要包括,单例模式工厂模式建造者模式原型模式(不常用)。        结构型模式:他主要是总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决待定应用场景的问题。主要包括,代理模式适配器模式装饰器模式桥接模式门面模式(不常用),组合模式(不常用)和享元模式(不常用)。        行为型设计模式:主要是解决类或对象之间的交互问题。这个类型的模式比较多,有11种。他们分别是:观察者模式责任链模式迭代器模式模板模式策略模式状态模式访问者模式(不常用)、备忘录模式(不常用)、命令模式(不常用)、解释器模式(不常用)和中介模式(不常用)。        上面差不多就是多数的设计模式,Netty差不多用了一大半吧,这里太多了,我也还没充分的吸收和学习到,所以我会分析一部分,后面又机会再写文章分析。这里我主要分析策略模式装饰模式模板模式

3.2 装饰模式

       充上面一小节的设计模式分类可以看到装饰模式属于结构型模式,给他的一句总结就是:动态地将责任附加到对象上,若要扩展功能,装饰者提供比继承更有弹性的替代方案。这里我们直接以Netty中的WrappedByteBuf源码来分析这个设计模式,源码如下:

class WrappedByteBuf extends ByteBuf {
    protected final ByteBuf buf;
    protected WrappedByteBuf(ByteBuf buf) {
        if (buf == null) {
            throw new NullPointerException("buf");
        }
        this.buf = buf;
    }
 
    @Override
    public final boolean hasMemoryAddress() {
        return buf.hasMemoryAddress();
    }
 
    @Override
    public final long memoryAddress() {
        return buf.memoryAddress();
    }
    ...
}

       这里先看构造函数,传入一个ByteBuf实例,这个传入的实例就是被装饰者,它的行为可以被当前这个类,也就是WrappedByteBuf(也就是装饰者)动态改变。因为这个WrappedByteBuf它只是装饰器的基类,所以他只对传入的被装饰者的行为做简单的返回,没做任何修改,后面是更多的方法,都是直接调用被装饰者的方法,下面我们看看他的子类,首先是UnreleasableByteBuf,源码如下:

final class UnreleasableByteBuf extends WrappedByteBuf {
 
    private SwappedByteBuf swappedBuf;
 
    UnreleasableByteBuf(ByteBuf buf) {
        super(buf);
    }
    ...
    @Override
    public boolean release() {
        return false;
    }
    ...
}

       首先调用父类WrappedByteBuf的构造方法,就是把被装饰者传进来,以供以后使用。然后看里面的release()方法,我们不管它release释放了什么,反正它release就是返回false,装饰或者说是改变了被装饰者buf的行为。这里我们再看另外一个WrappedByteBuf的子类SimpleLeakAwareByteBuf,这个类就是内存泄漏自动感知的一个ByteBuf,具体可以看源码,如下:

final class SimpleLeakAwareByteBuf extends WrappedByteBuf {
    private final ResourceLeak leak;
    SimpleLeakAwareByteBuf(ByteBuf buf, ResourceLeak leak) {
        super(buf);
        this.leak = leak;
    }
    ...
    @Override
    public boolean release() {
        boolean deallocated =  super.release();
        if (deallocated) {
            leak.close();
        }
        return deallocated;
    }
    ...
}

       构造器还是调用父类的方法,在release 这个方法里面,如果发现内存泄漏了,就执行leak.close()这个方法,然后在返回,其实也是修饰了被装饰者,动态改变了被装饰着的行为。下面我们在看另一个子类,AdvancedLeakAwareByteBuf,源码如下:

final class SimpleLeakAwareByteBuf extends WrappedByteBuf {
    private final ResourceLeak leak;
    AdvancedLeakAwareByteBuf(ByteBuf buf, ResourceLeak leak) {
        super(buf);
        this.leak = leak;
    }
    ...
    @Override
    public boolean release() {
        boolean deallocated = super.release();
        if (deallocated) {
            leak.close();
        } else {
            leak.record();
        }
        return deallocated;
    }
    ...
}

       可以看到这个类的套路和上面的一样,构造器还是调用父类的方法。然后就是release的方法实现不一样,首先如果发现内存泄漏了,就执行leak.close()这个方法,然后在返回。但是如果没有发生内存泄漏调用了release方法们这里会有一个记录,方便问题排查的堆栈跟踪。这里我们可以看看这上面的ByteBuf的具体用处,如下:

/**
 * AbstractByteBufAllocator.java 53行起
 */
 protected static CompositeByteBuf toLeakAwareBuffer(CompositeByteBuf buf) {
        ResourceLeak leak;
        switch (ResourceLeakDetector.getLevel()) {
            case SIMPLE:
                leak = AbstractByteBuf.leakDetector.open(buf);
                if (leak != null) {
                    buf = new SimpleLeakAwareCompositeByteBuf(buf, leak);
                }
                break;
            case ADVANCED:
            case PARANOID:
                leak = AbstractByteBuf.leakDetector.open(buf);
                if (leak != null) {
                    buf = new AdvancedLeakAwareCompositeByteBuf(buf, leak);
                }
                break;
            default:
                break;
        }
        return buf;
    }

       这里可以看到根据不同的策略,来对butebuf进行了不同的修饰。装饰模式最主要的就是这个装饰类修饰了还可以添加另一个修饰类再修饰一次。总结起来就是两个地方比较特殊,一个就是装饰类和原始类继承同样的父类,这样我们可以对原始类嵌套多个装饰器类,另一个就是装饰类就是对功能的增强,这也是装饰器模式应用场景的一个重要特点。上面的例子我们好像看到了根据不同的策略我们选择了不同的装饰器类,面我们就看看策略模式。

3.3 策略模式

       这节讲的策略模式下节讲的模板模式都是行为型模式,那么什么是策略模式呢?策略模式就是定义一组算法类,将每个算法分别封装起来,让他们可以相互替换。它包含一个策略接口和一组实现这个接口的策略类。因为所有的策略类都实现相同的接口,所以,客户端代码基于接口而非实现编程,这样可以灵活地替换不同的策略,具体的我们以Netty中的EventExecutorChooser来进行分析说明。        在此之前我这里先复习一下前面的知识点,先前我们分析过下面这个方法,如下:

public EventExecutorChooser newChooser(EventExecutor[] executors) {
    if (isPowerOfTwo(executors.length)) {
        return new PowerOfTwoEventExecutorChooser(executors);
    } else {
        return new GenericEventExecutorChooser(executors);
    }
}

       这个就是选择器EventExecurotChooser的创建,选择器就是为了当有新连接进入时, 通过这个选择器选择一个NioEventLoop进行服务。 虽然创建了不同的选择器, 但是它们选择的思路的一样的, 就是从数组中循环选择。        比如这里的NioEventLoop数组长度是8, 加入客户端连接到服务器之后, 服务器通过选择器EventExecutorChooser选择第一个NioEventLoop为其服务, 接着第二个客户端连接到服务器, 选择器EventExecutorChooser选择第二个NioEventLoop为其服务, 以此类推, 假如第八个客户端连接到服务器, 为其服务的是第八个NioEventLoop. 那么第九个客户端连接到服务器之后, 则又由第一个NioEventLoop为其服务了。 毕竟NioEventLoop封装的Selector是多路复用也就体现在此处。        上面会先调用一个isPowerOfTwo()的函数来判断数组的长度是不是2的倍数来选择不同的策略,这里为什么又不同的策略下面我们分析了具体的实现就很清楚了。首先我们看看策略接口,如下:

    /**
     * EventExecutorChooserFactory.java 34行起
     */
    @UnstableApi
    interface EventExecutorChooser {

        /**
         * Returns the new {@link EventExecutor} to use.
         */
        EventExecutor next();
    }

       这里上面就是一个很简单的next函数,下面我们看看他的具体实现函数,如下:

/**
 * DefaultEventExecutorChooserFactory.java 46行起
 */
 private static final class PowerOfTowEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        PowerOfTowEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next() {
            return executors[idx.getAndIncrement() & executors.length - 1];
        }
    }

    private static final class GenericEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        GenericEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next() {
            return executors[Math.abs(idx.getAndIncrement() % executors.length)];
        }
    }

       可以看到上面两个类都是EventExecutorChooser的实现接口,他们都对next进行了不同的实现。这里可以看到他们实现一个是取模“%”运算,一个是“&”运算,后面的一种运算效率要高于前面一种的,这也是Netty的强大之处,任何关于性能提升的机会都不放过。        其实上面的实现只是策略模式一个很简单实现,其实在大多数情况下策略模式是避免if-else分支判断逻辑。他主要是解耦策略的定义创建和使用,控制代码的复杂度,让每部分都不至于过于复杂,代码量过多。同时对于复杂的代码,策略模式再添加策略的时候,可以最小化,集中化代码的改定。

3.4 模板模式

       这一节我分析分析模板模式,对于模板模式就是在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法可以让子类在不改变算法整体结构的情况下,重新定义算法中的默写步骤。这里我们以Netty中的ByteToMessageDecoder作为例子,这个类经过前面的讲解大家都应该很清楚。这里我么可以继续回顾一下其实现,如下我们先看看他的channelRead()方法。

 @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof ByteBuf) {
            CodecOutputList out = CodecOutputList.newInstance();
            try {
                ByteBuf data = (ByteBuf) msg;
                first = cumulation == null;
                if (first) {
                    cumulation = data;
                } else {
                    cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
                }
                callDecode(ctx, cumulation, out);
	.....
    }

       这里面们跟进一下callDecode这个方法,如下:

  protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        ....
               int oldInputLength = in.readableBytes();
               decode(ctx, in, out);
	....
    }

       这里面主要就是一个decode方法,我们这里可以继续看看这个方法,如下:

 /**
     * Decode the from one {@link ByteBuf} to an other. This method will be called till either the input
     * {@link ByteBuf} has nothing to read when return from this method or till nothing was read from the input
     * {@link ByteBuf}.
     *
     * @param ctx           the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to
     * @param in            the {@link ByteBuf} from which to read data
     * @param out           the {@link List} to which decoded messages should be added
     * @throws Exception    is thrown if an error accour
     */
    protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;

       可以看到这个方法是一个抽象方法,其实前面的一大片就是算法骨架,然后这个具体的解码过程的步骤留给子类去实现。在我举这个例子的子类就很多了,例如最简单的固定长度解码器,还有Http的解码器,mqtt的解码器等等。        模板模式其实有两大作用,就是复用和扩展。复用指的是,所有的子类都可以复用父类中提供的模板方法的代码。扩展值得是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展带你定制化框架功能。        在Netty中上面我分析这三种模式的例子肯定不止这些,另外设计模式也不止这些。其实他的源码关于设计模式和设计思想的涉及太多了,可以留到后面慢慢的去体会和感悟。

四 总结

       这里算是对Netty的第一阶段学习的一个大总结了,这个阶段学习Netty的收获还是挺大的,同时也感觉到了Netty的强大。Netty真的是对性能要求到很高的水平,例如本系列没有分析的对象池,零拷贝这些。另外关于Netty的堆外内存和内存池的使用性能到底好多少,Netty的作者Trustin Lee发表了一篇Twitter使用Netty的性能分析文章,具体的可以看下图。

       另外Netty的代码风格也是相当的统一的,他里面的有些代码写的非常优雅,对设计模式的运用也是非常的好。其实这一阶段我学Netty学的很糙,还有很多的细节要去慢慢的分析,学习。另外想要用好Netty,可以参考一些开源项目是怎样使用Netty的如ZookeeperHadoopRocketMQ这些是咋使用Netty的。最后,希望后面能够好好利用Netty优秀的源码,学习他的设计思想,代码风格,设计模式等,当然也希望后面又机会能对Netty进行二次开发,写一些实用的中间件工具库。

参考资料

注:这里使用的是Netty4.1.6的源码进行分析