Netty 源码解析(二):对 Netty 中一些重要接口和类的介绍

1. 开篇

这部分是我想唠叨的一些话,想直接了解 Netty 的同学可以跳过这一节

上文我们说到了介绍 Netty 的原因和重要性以及 Netty 的基础 —— Java NIO 的一些知识。接下来本文将对 Netty 的重要的类做一个介绍。

在正式开始之前先多唠叨几句。我写 Netty 这一系列(目前只有两篇,斗胆叫系列吧)文章的主要目的还是为了自我学习,为了更深入地了解 Netty。

学一个技术,看一篇文章,读一本书,并不是看完了,自认为读懂了就结束了。看完读懂只是第一步,对于需要熟练掌握的技术,深入理解的知识,一个善于学习的人接下来要做的是尝试用自己的语言去表达自己所学到的东西。通过表达可以检验自己是否真的掌握技术、理解知识。如果有条件,最好能去通过演讲的形式去讲给别人。通过与他人交流的方式进一步检验自己的掌握程度。同时还能锻炼口才和交流能力,这是很多技术人员,包括一些顶尖技术人员所缺乏的。

我对于 Netty 来说只是一个初学者。因为产品的特点,我们需要为遗留系统提供更多协议的支持、负载均衡组件需要支持一些老的协议,安全方面也要有全面的掌控,这使得我们要在 TCP 层面开发很多东西,最终 Netty 便成为了我们的选择。

在使用 Netty 的过程中发现,想要用好它并不是很容易,遇到一些奇怪的问题是家常便饭。再加上 Netty 不像 Spring 那样文档详细到可以当书看,所以便开始考虑去看 Netty 的源代码。

另一方面,作为一个在 Java 服务器端开发领域工作了8年的,收入平平的,但尚有理想的一个”老“程序员,我是不甘把完成一些简单的业务功能代码作为一生的工作内容。我想去能够去实现技术复杂的系统是多数有理想的程序员的目标。所以,去理解一个复杂的框架的源代码便成为我的学习计划中的一项。

好了,唠叨了这么多该开始正式内容了。

2. Netty 简介

Netty 是一个 Java NIO 库,基于 Reactor 模式。Reactor 直译为反应器、反应堆。这个翻译容易误导人。其实将 Reactor 意译为分发器更为恰当。Reactor 模式本质上是一个事件机制,通过一个或一组线程检查事件,发现事件之后交由另一组事件处理线程执行该事件所对应的事件处理器(回调),从而实现高响应的程序。

Reactor 模式和 Java NIO 的结合,变为 Java 世界带来了一批高性能 IO 实现,Netty 便是其中一个。当使用 Netty 实现服务器端,Netty 会使用两个线程池,一个线程池用于不断轮询 Selector 上的事件;另一个线程池用于处理 IO 事件。

接下来的内容是基于 Netty 4.0.30。

3. 对学习 Netty 源代码重要性的补充

在 Netty 中,异步调用被大量使用。虽然 Java(包括其它很多语言)写成的程序大部分还是同步调用,但现在为了提高系统响应能力,越来越多的系统开始引入了 Reactor 设计思想和异步风格的代码调用。在语言层面,从 Java 5 开始引入线程池,Java 7 引入 ForkJoin,Java 8 引入了 CompletableFuture 和在 Stream 中加入了异步并行的特性。专注并发的 Scala 和 Go 语言出现并流行。在框架领域,Spring 5 计划引入 spring-reactiveJuergen Hoeller在SpringOne2GX大会上宣布Spring 4.3与5.0的总体规划) 以支持响应式的编程。Akka 的出现。这些都说明了异步和 Reactive 编程越来越重要。通过了解 Netty 的源代码,了解 Netty 的实现原理,我们也可以通过一个被广泛使用的真实技术去了解在 Java 编程领域,异步编程如何被实现和使用。进一步,在我们自己的日常工作中,在服务器端开发上,要经常思考是同步调用还是异步。

3.1 异步编程

可能有人会问异步编程有什么好处?我们先来看同步编程。在同步编程中,一个个方法的执行必须按照先来后到的顺序,后面的方法必须等前面的方法执行完之后才能执行(这里我们就不考虑指令重排)。不管后面的方法是否真的需要等前面方法执行完毕后再执行。同时这些方法也都是在一个线程里执行的,如果前一个方法出问题,抛出异常,后面的方法便无法执行。或者前面的方法耗时过程或者因为某些原因挂起线程,后面的方法也不会执行。

另外,各个方法之间也有轻重缓急之分,有些方法需要被优先执行,有些方法的优先级就比较低。这种情况,不同优先级的方法最好被不同调度逻辑的线程执行。

异步编程虽然会写起来复杂一些,但是可以解决上述这些问题。这对构建一个高可用的服务器端应用是很重要的。

上述原因也导致了各种 Reactive 框架和 Go 等新语言的出现。

总结一下,同步编程对方法的调度产生限制,而异步编程可以打破这些限制,帮助构建可用性和响应度更好的系统。

4. Netty 源码中重要的接口和类

Netty 主要由这么几个主要的组件组成:Channel、ChannelPipeline、ChannelHandler、EventLoopGroup、EventLoop、Bootstrap

4.1 Channel

Channel 是 Netty 中最主要的概念之一。在 Netty 中,有一系列的定义和实现 Channel 概念的接口和类。

如同 JDK 中对 Socket Channel 的定义一样,Netty 中的 Socket Channel 也被分为两大类:SocketChannelServerSocketChannel。其中,对于服务器端开发最为常用的 Socket Channel 实现类是 NioSocketChannelNioServerSocketChannel。下面我们来看这两个类的类图:

NioSocketChannel

NioSocketChannel 类图

NioSocketChannel

NioServerSocketChannel 类图

NioSocketChannelNioServerSocketChannel 的底层实现基于 JDK 的 SocketChannelServerSocketChannel 。这两个类(其实主要是其父类),通过其 Unsafe 内部类,实现了大量的底层实际操作。例如,AbstractNioChannel 通过其内部类 AbstractNioUnsafe 实现了很多底层的操作。在后面的文章中,我们会经常看到 Unsafe 内部类的源码。

Channel 接口

在 Netty 的 Channel 接口定义了很多的方法。除了大量和 IO 操作相关的方法外,这里重点介绍两个方法:

EventLoop eventLoop();
ChannelPipeline pipeline();
EventLoop eventLoop()

在 Netty 中,大多数 Channel 都有一个 EventLoop 处理它(少数没有 EventLoop 是尚未完成注册工作)。在 NIO 场景中,Channel 通过注册到 Selector 而与一个 NioEventLoop 关联上。这时,当调用到这个 ChannelEventLoop eventLoop() 方法时,这个 NioEventLoop 便会被返回。

ChannelPipeline pipeline()

一个 Channel 包含一个 ChannelPipeline 的引用,进而引用了一组 ChannelHandler

4.2 ChannelPipeline

channelpipeline 类图

ChannelPipeline 类图

在 Netty 中,Channel 是通讯的载体,而 ChannelPipeline 则是数据处理器的载体。虽然 Netty 是基于事件机制实现的,但在 4.0 中并没有名字包含 Event 的接口(在 3.0 版本中存在一个 ChannelEvent)。ChannelPipeline 上各种事件的传递是直接通过传递数据进行的,或者不包含数据(例如 Channel Active 事件)。

从上图中可以看到,ChannelPipeline 是一个接口,其有一个默认的实现类 DefaultChannelPipeline 。在 DefaultChannelPipeline 中有两个属性:head 和 tail。这两者都扩展了 AbstractChannelHandlerContext,所以他们都是 ChannelHandlerContext。同时,它们两个还是 ChannelHandler。这两者就是 ChannelPipelineChannelHandler 链的头和尾。后续文章会对此详细介绍。

ChannelPipeline 的 一个重要作用就是承载 ChannelHandler。向 ChannelPipeline 中添加 ChannelHandler 非常简单,可以使用下列方法:

ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(aChannelHandler);
pipeline.addFirst(aChannelHandler);
pipeline.addLast(anEventExecutorGroup, aChannelHandler);
pipeline.addLast("theNameOftheChannelHandler", aChannelHandler);

ChannelPipeline 所提供的添加 ChannelHandler 的方法还有很多,我就不一一列举了,它们的形式和功能大体类似。

ChannelPipeline 可以在运行时动态进行动态修改其中的 ChannelHandler

4.3 ChannelHandler

ChannelHandler 是承载业务处理逻辑的地方,是开发人员使用最多的 Netty 接口。ChannelHandler 可分为两大类:ChannelInboundHandlerChannelOutboundHandler。故名思维,这两接口分别对应入站和出站消息的处理。(Netty 5 不再区分 ChannelInboundHandlerChannelOutboundHandler

开发人员通常不直接实现上面提到的三个接口,而通常是继承 ChannelInboundHandlerAdapterChannelOutboundHandlerAdapter

需要注意的是,不建议在 ChannelHandler 中直接实现耗时或阻塞的操作,因为这可能会阻塞 Netty 工作线程,导致 Netty 无法及时响应 IO 处理。

4.4 ChannelHandlerContext

ChannelHandlerContext 是一个接口,被定义用于在 ChannelHandler 的 Inbound 和 Outbound 业务处理方法中传递上下文信息和传播事件。所谓传播事件指的是调用某个方法会导致相应的事件在 ChannelPipelineChannelHandler 链中按照规定的顺序传播。规定顺序指的是 inbound 事件是从 head -> tail 的顺序传播,outbound 事件是按照 tail -> head 的顺序传播,这在后面会有详细介绍。(Netty 5 还是会区分 inbound 和 outbound 事件)

上下文信息包括 ChannelEventExecutor 等属性。可触发的事件包括 Channel 的注册、激活、读、写等,这些事件被分为了 inbound 和 outbound 两大类。因为事件比较多,所以不一一列举相关的事件触发方法。简单来说,fire 开头的事件触发方法都是用来触发 inbound 事件,例如 ChannelHandlerContext fireChannelRegistered()ChannelHandlerContext fireChannelActive()ChannelHandlerContext fireChannelRead() 等。其它的事件触发方法都是触发 outbound 事件的,例如 ChannelFuture bind(SocketAddress, ChannelPromise)ChannelFuture connect(SocketAddress, SocketAddress, ChannelPromise)ChannelFuture write(Object, ChannelPromise)。关于这些事件传播方法的完整介绍可以在 ChannelHandlerContextChannelPipeline 的 Javadoc 中找到。

需要注意的一点是在 inbound 和 outbound 事件中都有 read,这是容易让初学者困惑的地方(至少我困惑了)。Inbound 事件中的 read 事件所对应的方法为 fireChannelRead(Object),用于读到的消息继续在 ChannelHandler 链中传播。Outbound 事件中的 read 所对应的方法为 read(),用于将当前 Channel 设置为可读消息的状态。如果这个 ChannelAbstractNioChannel 时,即调用 selectionKey.interestOps(interestOps | readInterestOp)

ChannelHandlerContext 是 Netty 中一个很重要的概念,正确理解这个接口所定义的方法的使用,深入理解该接口实现类的原理对于正确使用 Netty 至关重要。

4.5 ChannelFuture & Promise

Netty 源码中大量使用了异步编程,从代码实现角度看就是大量使用了线程池和 Future。对于 Netty 的线程池部分我们稍后介绍,这里先介绍 Future。熟悉 Java 5 的同学一定对 Future 不陌生。简单来说就是其代表了一个异步任务,任务将在未来某个时刻完成,而 Future 这个接口就是用来提供例如获取接口、查看任务状态等功能。

Netty 扩展了 Java 5 引入的 Future 机制。从下面的类图我们可以看到相关类的关系:

DefaultChannelPromise

DefaultChannelPromise 类图

4.5.1 Netty 的 Future 接口

需要注意的是,上面类图中有两个 Future,最上面的是 java.util.concurrent.Future,而其下面的则是 io.netty.util.concurrent.Future。后面如果提到 Future,不加说明,指的就是 Netty 中的 Future。Netty 的 Future 对 JDK Future 的一个主要改变是增加了用于表示任务是否成功的方法 —— boolean isSuccess()。因为 JDK Futureboolean isDone() 方法不区分任务是正常结束,还是因为异常或被取消。ChannelFuture 的 Javadoc 详细说明了任务未完成、正常完成、异常完成和被取消四种状态下相关方法的返回值,这里就不重复介绍了。

除了 boolean isSuccess() 方法外,Future 还定义了下列方法:

  • Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener)
  • Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners)
  • Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener)
  • Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners)
  • Future<V> await() throws InterruptedException
  • Future<V> awaitUninterruptibly()
  • Future<V> sync() throws InterruptedException
  • Future<V> syncUninterruptibly()

前四个方法是顾名思义,是用来添加删除 GenericFutureListener,这里就不过多做介绍了。本系列后续文章会提到某些实现了对这几个方法的实现。

这里稍微说明一下 await()sync() 这两个用于线程同步的方法。简单来说 sync() 就是会抛出异常的 await()

4.5.2 Promise

什么是 Promise?

用 Javascript 的同学可能对 Promise 这个词不陌生,但是 Java 程序员刚看到这个词可能会丈二和尚摸不着头脑。所以这里我先简单介绍一下我对 Promise 的理解。

从尚在制定中的 ECMAScript 6 引入 Promise 机制,Promise 是一个用来定义异步调用链的东东。举个栗子:

Promise.all([
    fetchPromised("http://backend/foo.txt", 500),
    fetchPromised("http://backend/bar.txt", 500),
    fetchPromised("http://backend/baz.txt", 500)
]).then((data) => {
    let [ foo, bar, baz ] = data
    console.log(`success: foo=${foo} bar=${bar} baz=${baz}`)
}, (err) => {
    console.log(`error: ${err}`)
})

在 Java 8 中被引入的 CompletableFuture 也有类似的 API,这在我博客里的一篇阅读总结中有提到。在这篇阅读总结里面,我所读到的关于 CompletableFuture 文章里有一句对 Promise 的总结我觉得很恰当 —— “future without underlying task or thread pool”。但这句话说的还不是太明白。我的理解是 Promise 不想 Future 那样代表一个已经提交的任务。Promise 反映的是对未来的“允诺”,即它反映了将来会被提交的异步任务的行为。虽然现在这个任务尚未被提交,刚未开始执行。从代码的形式上看,Promise 机制提供了一些 API 用来编排组织异步任务。

但是 Netty 的 Promise 和上面提到的 Javascript 和 Java 8 的 Promise 不太一样。Netty 中的 Promise,如同它的 Javadoc 里的解释一样,是一个可写的 Future。这个还不是太好理解,但是看完我对 Netty 回调机制便会明白。

回调

在介绍 Future 接口的时候我们提到了其定义了添加和删除 GenericFutureListener 的方法,那这些监听器在添加之后如何被调用呢。这就涉及到了 Promise 所定义的方法了:

/**
* Marks this future as a success and notifies all
* listeners.
*
* If it is success or failed already it will throw an {@link IllegalStateException}.
*/
Promise<V> setSuccess(V result);
 
/**
* Marks this future as a success and notifies all
* listeners.
*
* @return {@code true} if and only if successfully marked this future as
 *         a success. Otherwise {@code false} because this future is
 *         already marked as either a success or a failure.
*/
boolean trySuccess(V result);

从其 Javadoc 就可以看出,这两个方法不仅会标记任务成功完成,同时还会通知所有的监听器。在本系列后续文章会介绍其实现类是具体如何实现通知功能的。

4.5.3 ChannelFuture

除了 Promise,Netty 中另一个继承自 Future 的接口是 ChannelFuture。这个接口增加的内容不多,和 Future 相比只是增加了 Channel channel() 方法,用来返回与之对应的 Channel

4.6 EventLoop & EventLoopGroup

NioEventLoopNioEventLoopGroup 是常用的 EventLoopEventLoopGroup 的实现类。当我们使用 ServerBootstrap 去构建 Netty 服务端组件的时候,我们通常会这么写:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);

NioEventLoop 没有在上面的代码中体现出来,但实际也是被使用了的。所以我们从这两个实现类来简单了解一下相关的接口和类。

4.6.1 NioEventLoopGroup

我们先从 NioEventLoopGroup 开始,先来看看类图:

NioEventLoopGroup

NioEventLoopGroup 类图

从上图中我们可以看出,Netty 的 EventLoopGroup 是通过继承 EventExecutorGroup 接口继承了 JDK 的 IterableScheduledExecutorService 接口,即它是一个可以被迭代的可调度线程池服务。EventExecutorGroup 所增加的方法主要用于任务调度,这里不做介绍。EventLoopGroup 中定义了三个新方法:

EventLoop next();
ChannelFuture register(Channel channel);
ChannelFuture register(Channel channel, ChannelPromise promise);

接下来分别对它们做一下解释

EventLoop next()

其实这个方法是覆写了其父接口中的方法,但是我们在这里介绍一下。

EventLoopGroup 顾名思义是 EventLoop 的 group,即包含了一组 EventGroup 。在实际的业务处理中,EventLoopGroup 会通过 EventLoop next() 方法选择一个 EventLoop,然后将实际的业务处理交给这个被选出的 EventLoop 去做。

对于 NioEventLoopGroup 来说,其真实功能都会交给 EventLoopGroup 去实现。

ChannelFuture register(Channel channel)

此方法的功能是将一个 Channel 注册到这个 EventLoopGroup 上。这样,当和这个 Channel 相关的事件发生时,EventLoopGroup 就能进行处理。从这个方法的返回值可以看出,这是一个异步方法。

ChannelFuture register(Channel channel, ChannelPromise promise)

此方法同上面的方法类似,只是增加了对回调功能的支持。

4.6.2 NioEventLoop

我们依旧先来看类图:

NioEventLoop

NioEventLoop 类图

从上图可以看出,EventLoop 继承自 EventLoopGroup,即 EventLoop 也是一种 EventLoopGroup。这样可以形成多层级的 EventLoopGroup。同样,EventLoopEventLoopGroup 彼此的父接口 EventExecutorEventExecutorGroup 也是这样的关系。

因为同 EventExecutor 相比,EventLoop 几乎没有新加方法(只是覆写了一个方法),所以我们这里就只看 EventExecutor 里新定义的方法

EventExecutor
EventExecutorGroup parent();
boolean inEventLoop();
boolean inEventLoop(Thread thread);
<V> Promise<V> newPromise();
<V> ProgressivePromise<V> newProgressivePromise();
<V> Future<V> newSucceededFuture(V result);
<V> Future<V> newFailedFuture(Throwable cause);

这里说一下 boolean inEventLoop()。其它的方法 EventExecutorGroup parent() 顾名思义就好了,不用怎么解释。几个 newXXX() 方法通常的实现就是 new 一个相应的对象,所以这里也不做介绍了。

boolean inEventLoop()boolean inEventLoop(Thread thread) 方法在 Netty 源码中的被经常使用。当 Netty 准备执行一个异步任务,即准备向 EventLoop 提交一个新任务的时候,会先调用 boolean inEventLoop() 方法判断当前线程(提交任务的线程,而非执行任务的线程)是否属于 EventLoop 的执行线程。如果是,通常的做法就是直接调用,否则才是调用 EventLoopexecute 之类的方法提交任务。

NioEventLoop

这个实现类实现了很多具体的工作。我们前面提到 EventLoopGroupChannelFuture register(Channel channel) 方法,在 NioEventLoop 的实现中便是将 Channel 注册到 NioEventLoop 中的 Selector 上(实际工程不是这么简单直接,在后续文章会详细介绍)

4.6.3 EventLoopGroup 的其它一些事儿

NioEventLoopGroup 为例,其中的每个线程都有自己的任务队列。和 JDK 的 ThreadPoolExecutorScheduledThreadPoolExecutor 线程池实现不同,Netty 的 NioEventLoopGroup 每一个执行线程都有自己的任务队列,而不是像 JDK 的线程池那样,执行线程共享一个任务队列(ForkJoinPool 除外,其每个执行线程都有自己的任务队列,并可以从其它执行线程的任务队列获取任务,即工作盗取 Work stealing 算法)。这种方式避免了线程之间为获取任务而产生的竞争和线程同步带来的开销。

在 Netty 中,一个 ChannelHandler 实例从始至终都是被一个线程调用。所以一个 ChannelHandler 实例肯定也不会被多个线程同时调用。这避免了内存可见性等的问题,简化了线程安全方面的设计。(Netty 5 目前不保证 ChannelHandler 实例会始终被一个线程调用,但是保证不会被多个线程同时调用)

在 Netty 4 和 5 中,Inbound 和 outbound 事件处理都是在工作线程中执行的。(Netty 3 中 outbound 会在用户发起的线程中执行)

5. 调用关系

在了解完 Netty 中主要接口和类的功能和作用之后,我们来看一下它们直接是如何协调配合的。下面是一个简化过的,当有 IO 事件发生时的时序图,反映了 Netty 接口和类之间大致的调用关系。

Netty 简化调用关系图

Netty 简化调用关系图

简单来说,当有 IO 事件发生时,NioEventLoop 会通过不断轮询 Selector 获得相应的 SelectionKey。进而通过,SelectionKeyObject attachment() 方法获得底层连接对应的对象,通常是一个 AbstractNioChannel。进而调用 Channel 上相应的方法。然后是调用 ChannelPipeline 上的事件触发方法,最终调用到用户自定义的 ChannelHandler 上。

6. 小结

写到这里,啰啰嗦嗦地把 Netty 简单介绍了一下。介绍这么多代码层面的东西是为了后续正是开始介绍 Netty 的源码做铺垫。

其实 OSChina、ITeye 等国内的技术社区已经有很多篇介绍 Netty 源码的文章。我在这里继续造车轮的原因主要是为了自己更好的学习 Netty,同时也为了能和大家交流。同时,我希望我的文章能够补全一些别的文章没涉及的内容,包含一些我的理解和想法。

软件和其它技术一样,最核心、最有价值的部分是思想。通过阅读源码,在帮助更好地使用软件、设计软件的同时,理解、吸收好的思想,并产生自己的有价值的思想,这才是最重要的。

转载于:https://my.oschina.net/lifany/blog/517275

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值