八张图理清 Netty 的体系结构

44 阅读7分钟

1. 背景

大家都说,Netty 相比于原生 JAVA NIO 的更易使用,就整了本书看。

我翻开一看,这咋就更易使用了,歪歪斜斜的每页上都写着 Handler、EventLoop。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着两个字是:吃人!

sorry,串台了,拉回来。Netty 的抽象做的十分优秀,一套 API 就能支持多种IO模型、传输类型的网络开发。但这也带来了问题,就是 API 太多太杂,上手难度大。

这篇博文的目的是想通过几张 UML 类图,梳理清楚 Netty 的体系结构,让大家对 Netty 有个全貌,降低上手难度。

关键字:Netty,Netty API,UML

2. ByteBuf

ByteBuf 对标的是原生 ByteBuffer,是 Netty 的数据容器,各组件之间传输的基本单位都是 ByteBuf。

ByteBuf 的类图如下。(为了简洁,去掉了部分边缘的继承/实现关系,之后的类图也一样,就不赘述了,需要完整的可留言/私信邮箱) image.png

ByteBuf 实现了 ReferenceCounted 接口

通过类图,我们可以看到 ByteBuf 实现了 ReferenceCounted 接口。

ReferenceCounted 是一个引用计数的接口。因为 ByteBuf 需要手动释放,不然会有内存泄露的风险。目前 Netty 所有的ByteBuf 都实现了 ReferenceCounted 接口,所有的 ByteBuf 都要考虑使用完后释放的问题。

池化 ByteBuf/非池化 ByteBuf

左下方是四个池化的 ByteBuf,右下方四个是非池化的 ByteBuf。

ByteBuf 是 Netty 中数据传输的基本单位,申请 ByteBuf 是一个十分频繁的操作,所以有一个ByteBuf 池,减少申请 ByteBuf 的性能开销。支持池化的 ByteBuf 都实现了 PooledByteBuf 接口。

除了池化/非池化,ByteBuf 又细分堆内内存/堆外内存,Unsafe/非Unsafe。

堆内内存 ByteBuf/堆外内存 ByteBuf

名字中有“Heap”的基于堆内内存的ByteBuf,名字中有“Direct”的是基于堆外内存的ByteBuf。

我们平时申请 Java 对象,都是分配在堆内内存的,受 JVM 虚拟机管理,有 GC 线程自动清理。而为了支持零拷贝,又有了堆外内存,堆外内存不受 JVM 虚拟管理,需要手动清理。

Unsafe ByteBuf/非Unsafe ByteBuf

名字中有“Unsafe”的是基于 Unsafe 实现的 ByteBuf,名字中没有“Unsafe”的则是基于普通的数组操作实现的 ByteBuf。

Unsafe 是 Java 提供的可以直接操纵内存写数据的 API,底层都是 native 的。所以,性能相比于普通的 ByteBuf 要快很多。 image.png

Netty 是申请 Unsafe ByteBuf 还是申请非 Unsafe ByteBuf 的逻辑如下。大体上是通过 JVM 参数来判定的。目前大部分使用的都是 Unsafe ByteBuf。 image.png

ByteBufUtil

右边还有一个 ByteBufUtil,是一个 ByteBuf 工具类,有两个比较有用的方法。

一个是 equals() 方法,可以用来比较两个 ByteBuf。

一个是 hexdump() 方法,十六进制打印 ByteBuf,调试用。

3. ByteBufAllocator

image.png ByteBufAllocator 是分配 ByteBuf 的工具类。PooledByteBufAllocator 分配池化的 ByteBuf。UnpooledByteBufAllocator 分配的是非池化的 ByteBuf。Unpooled 则是比 UnpooledByteBufAllocator 更近一步的工具类。

至于更细分的堆内内存、堆外内存、Unsafe、非Unsafe 类型的 ByteBuf,就要调用各分配器对应的方法了。 image.png

PreferHeapByteBufAllocator 是内置的类,访问权限是包范围的,外部应用程序用不了的,可以忽略。 image.png

4. Bootstrap

image.png 这个简单,服务端启动器是 ServerBootstrap,客户端启动器是 Bootstrap。他们俩都继承自 AbstractBootstrap。

5. Future & Promise

image.png 通过类图,我们发现 Netty Future 总体是继承自 JDK 原生的 java.util.concurrent.Future。蓝色的是对Future 的定义,红色的是对 Promise 的定义。

平时开发过程中接触到的 Netty Future 对象基本上都是最下面 DefaultChannelPromise 的实例或其子类

Future

我们先看看Future。Netty 的 Future 本身继承自 java.util.concurrent.Future。主要在两方便做了增强。

一个是依赖了 ChannelFutureListener(上图右边,黄色框),实现了监听器模式,让编码更加灵活。 如下图,添加端口绑定事件,可以依赖这个事件的触发做日志记录、失败重连等操作。 image.png

第二个是完善了 Future 的状态体系。Netty Future有四个状态:Uncompleted、Completed successfully、Completed with failure、Completed by cancellation。

如下是 ChannelFuture 上的注释。

java
复制代码
 *                                      +---------------------------+
 *                                      | Completed successfully    |
 *                                      +---------------------------+
 *                                 +---->      isDone() = true      |
 * +--------------------------+    |    |   isSuccess() = true      |
 * |        Uncompleted       |    |    +===========================+
 * +--------------------------+    |    | Completed with failure    |
 * |      isDone() = false    |    |    +---------------------------+
 * |   isSuccess() = false    |----+---->      isDone() = true      |
 * | isCancelled() = false    |    |    |       cause() = non-null  |
 * |       cause() = null     |    |    +===========================+
 * +--------------------------+    |    | Completed by cancellation |
 *                                 |    +---------------------------+
 *                                 +---->      isDone() = true      |
 *                                      | isCancelled() = true      |
 *                                      +---------------------------+

Promise

Promise 则是对 Future 的增强。新增了如下方法:

java
复制代码
    Promise<V> setSuccess(V result);

    boolean trySuccess(V result);

    Promise<V> setFailure(Throwable cause);

    boolean tryFailure(Throwable cause);

    boolean setUncancellable();

它的设计思路主要是为了方便异步操作的编写、管理和响应回调。Promise 定义了一种异步操作完成的协议,即 Promise 成功完成或者失败,并且成功后有对应的结果值,失败时有对应的异常信息。 使用示例如下,在线程执行完后,把成功结果设置到 promise 变量上。

image.png

总之,Netty 增强了 Futrue,又在 Future 的基础上引入 Promise 的概念,目的是为了简化异步代码的编写。这个可能需要多写相关的代码才能理解,暂时知道就好。

6. Channel

image.png Channel 是对连接的抽象,服务启动后,一个Channel 对象就对应一个网络连接。 同时,Netty 支持多种 网络IO 模型,如果是阻塞IO,Channel 对象就是 AbstractOioChannel,如果是多路复用IO,Channel对象就是AbstractNioChannel 或者 AbstractEpollChannel(二者一个是水平触发,一个是边缘触发)

最后,Channel 本身还支持灵活的配置(分配器、超时时间等),是依赖ChannelConfig 类来实现的。

7. NioSocketChannel & NioServerSocketChannel

image.png 我们平时用的较多的是 AbstractNioChannel 实现的多路复用 IO 模型。他的两个实现类在就是 NioSocketChannel 和 NioServerSocketChannel(在类图的最下面左右两个)。

NioSocketChannel 和 NioServerSocketChannel 都继承自 AbstractNioChannel。而 AbstractNioChannel 本身又实现了 Channel 接口。

AbstractOioChannel 和 AbstractEpollChannel 也类似,都有一套自己的 Channel 体系。

我们启动客户端、服务端的时候,会往channel 里面传入一个 class,这里就是指定 Netty 所用IO模型的地方,如下如所示,分别是服务端的启动代码,和客户端的启动代码。

image.pngimage.png

8. ChannelPipeline & ChannelContext & ChannelHandler

image.png 基于 Netty 实现的各种业务逻辑,都是在这套体系内完成的。本质上就是一个责任链模式,业务数据流经责任链上的各个处理节点,得到最终的数据,再返回。

ChannelPipeline

ChannelPipeline 顾名思义就是责任链本身了,本质是一个双向链表,用来组织各种处理节点。依赖 ChannelHandlerContext 和 ChannelHandler。

ChannelHandlerContext

处理节点有多个,每个节点怎么的上下文信息就存储在ChannelHandlerContext 上。

ChannelHander

对处理节点的抽象。分为入站(inbound)和出站(inbound)两种类型,如图,分列在 ChannelHander 的右下边和左下边。

Netty 内置了心跳检查、编解码、沾包拆包等 Handler,可以直接使用。

9. EventLoop & EventLoopGroup

image.png 最后一个了,坚持住!!!

EventLoop 是对事件循环的抽象, EventLoopGroup 则相当于事件循环组,是 EventLoop 实例的容器。他们两个都由 JDK 的线程池 Executor 派生而来。

这里重点关注关注 NioEventLoop 和 NioEventLoopGroup 两个实现类。

事件循环

什么是事件循环?Netty 把所有要处理的业务抽象成事件,通过异步线程死循环的方式去执行,称为事件循环。 下图是 NioEventLoop 事件循环的代码,开始就是一行死循环的代码:for(;;),然后就是调两行核心代码:

  • processSelectedKeys:阻塞 selector,等待 IO 事件
  • runAllTasks:执行 IO 事件

image.png 此即事件循环,然后再看看具体的继承关系。

NioEventLoop

左边蓝色部分就是 NioEventLoop 的继承关系,NioEventLoop 直接继承自SingleThreadEventLoop,本质上就是一个执行事件循环的线程。

NioEventLoopGroup

右边黄色部分就是 NioEventLoopGroup 的继承关系。NioEventLoopGroup的父类 MultithreadEventExecutorGroup 持有一个children 属性,充当EventLoop的容器,用来组织 EventLoop。

EventLoop 和 EventLoopGroup 是 has-a 的关系,为什么要用继承来实现呢?

初接触 Netty 的时候,从名字就能看出 NioEventLoopGroup 是NioEventLoop 的容器。他们两个之间是 has-a 的关系,同时面向对象打工这么多年【组合优先于继承】早已是铁律,能不用继承就不用继承,为什么NioEventLoop 继承自 EventLoopGroup 呢? image.png

是因为这是一种设计模式:组合模式。设计上把 EventLoop 看做是只有一个事件循环的 EventLoopGroup,这样就能行为统一。 image.png

都看到这了,给个赞吧

10. 总结

综上所述,本文通过八张 UML 类图梳理了 Netty 的结构体系,希望对大家有帮助。

作者:小黑233
链接:juejin.cn/post/724324…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。