2、Netty源码解读之-项目结构&核心组件

       Netty提供异步的、事件驱动的网络高并发的通信框架,构建在操作系统的底层IO模型(基于select/epolle的IO多路复用模型)、Java NIO、Reactor响应器模式、异步回调模式这些基础之上的;

当我们聊netty的核心组件的时候我们应该想到那些问题?

  • netty的各核心组件的职责是什么?netty是对java nio、reactor反应器模式、异步回调模式做了一个整合
  • netty如何利用操作系统内核的select/epolle调用和reactor响应模式提升自己的事件处理能力?(EventLoopGroup)
  • netty如何做堆外内存的管理?如何进行GC ? Direct Buffer 零拷贝、引用计数
  • netty如何进行通道和事件处理器的绑定的?(ChannelInitializer #initChannel()方法在ChannlePipline中创建一组ChannelHandler,ChannelPipLine的责任链保存ChannelHandler处理器)

netty源码解读后续补充,项目结构可以参考 netty源码分析系列——概述 杨武兵

Netty包含以下几个核心构建:

1、Bootstrap & ServerBootstrap

       这 2 个类都继承了AbstractBootstrap,因此它们有很多相同的方法和职责,它们都是启动器。他们将netty的其他组件进行组装和配置;

Bootstrap 用于启动一个 Netty TCP 客户端,或者 UDP 的一端。

  • 通常使用 #connet(...) 方法连接到远程的主机和端口,作为一个 Netty TCP 客户端。
  • 也可以通过 #bind(...) 方法绑定本地的一个端口,作为 UDP 的一端。
  • 仅仅需要使用一个 EventLoopGroup 。

ServerBootstrap 往往是用于启动一个 Netty 服务端。

  • 通常使用 #bind(...) 方法绑定本地的端口上,然后等待客户端的连接。
  • 使用两个 EventLoopGroup 对象( 当然这个对象可以引用同一个对象 ):第一个用于处理它本地 Socket 连接的 IO 事件处理,而第二个责负责处理远程客户端的 IO 事件处理。

2、Channel

Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channel 的 EventLoop 。

在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而 Netty 的 Channel 则提供的一系列的 API ,它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势( 摘自《Netty权威指南( 第二版 )》) :

  • 在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。
  • Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
  • 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,由 Channel 统一负责和调度,功能实现更加灵活。

3、ChannelFuture

Netty 为异步非阻塞,即所有的 I/O 操作都为异步的,因此,我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口,通过该接口的 #addListener(...) 方法,注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

4、EventLoop & EventLoopGroup

Netty 基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。

Channel 为Netty 网络操作抽象类,EventLoop 负责处理注册到其上的 Channel 处理 I/O 操作,两者配合参与 I/O 操作。

EventLoopGroup 是一个 EventLoop 的分组,它可以获取到一个或者多个 EventLoop 对象,因此它提供了迭代出 EventLoop 对象的方法。

下图是 Channel、EventLoop、Thread、EventLoopGroup 之间的关系( 摘自《Netty In Action》) :

Channel、EventLoop、Thread、EventLoopGroup

Channel、EventLoop(对应响应器模式中的一个reactor,java nio的一个selector,可以注册多个channel到eventLoop中)、Thread、EventLoopGroup

  • 一个 EventLoopGroup 包含一个或多个 EventLoop ,即 EventLoopGroup : EventLoop = 1 : n 。
  • 一个 EventLoop 在它的生命周期内,只能与一个 Thread 绑定,即 EventLoop : Thread = 1 : 1 。
  • 所有有 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,从而保证线程安全,即 Thread : EventLoop = 1 : 1
  • 一个 Channel 在它的生命周期内只能注册到一个 EventLoop 上,即 Channel : EventLoop = n : 1 。
  • 一个 EventLoop 可被分配至一个或多个 Channel ,即 EventLoop : Channel = 1 : n 。

当一个连接到达时,Netty 就会创建一个 Channel,然后从 EventLoopGroup 中分配一个 EventLoop 来给这个 Channel 绑定上,在该 Channel 的整个生命周期中都是有这个绑定的 EventLoop 来服务的。EventLoop会负责分配事件到指定的handler处理器;

5、ChannelHandler

ChannelHandler ,连接通道处理器,我们使用 Netty 中最常用的组件。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。

ChannelHandler 有两个核心子类 ChannelInboundHandlerChannelOutboundHandler,其中 ChannelInboundHandler 用于接收、处理入站( Inbound )的数据和事件(系统read调用复制数据到用户缓冲区,进行业务处理),而 ChannelOutboundHandler 则相反,用于接收、处理出站( Outbound )的数据和事件(系统write调用复制数据到内核缓冲区,通过网卡返回给请求方)。

  • ChannelInboundHandler 的实现类还包括一系列的 Decoder 类,对输入字节流进行解码。
  • ChannelOutboundHandler 的实现类还包括一系列的 Encoder 类,对输入字节流进行编码。
  • ChannelDuplexHandler 可以同时用于接收、处理入站和出站的数据和时间。
  • ChannelHandler 还有其它的一系列的抽象实现 Adapter ,以及一些用于编解码具体协议的 ChannelHandler 实现类。

6、ChannelPipeline

ChannelPipeline 为 ChannelHandler 的,提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。一个数据或者事件可能会被多个 Handler 处理,在这个过程中,数据或者事件经流 ChannelPipeline ,由 ChannelHandler 处理。在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler。

ChannelPipeline

  • 当一个数据流进入 ChannelPipeline 时,它会从 ChannelPipeline 头部开始,传给第一个 ChannelInboundHandler 。当第一个处理完后再传给下一个,一直传递到管道的尾部。
  • 与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部的“最后”一个ChannelOutboundHandler ,当它处理完成后会传递给前一个 ChannelOutboundHandler 。

上图更详细的,可以是如下过程:

当 ChannelHandler 被添加到 ChannelPipeline 时,它将会被分配一个 ChannelHandlerContext ,它代表了 ChannelHandler 和 ChannelPipeline 之间的绑定。其中 ChannelHandler 添加到 ChannelPipeline 中,通过 ChannelInitializer 来实现,过程如下:

  • 一个 ChannelInitializer 的实现对象,被设置到了 BootStrap 或 ServerBootStrap 中。
  • 当 ChannelInitializer#initChannel() 方法被调用时,ChannelInitializer 将在 ChannelPipeline 中创建一组自定义的 ChannelHandler 对象。
  • ChannelInitializer 将它自己从 ChannelPipeline 中移除。
ChannelInitializer 是一个特殊的 ChannelInboundHandlerAdapter 抽象类。

使用场景:

  • RocketMQ ,分布式消息队列。
  • Dubbo ,服务调用框架。
  • Spring WebFlux ,基于响应式的 Web 框架。
  • HDFS ,分布式文件系统。

Netty 的零拷贝实现

Netty 的零拷贝实现,是体现在多方面的,主要如下:

1、【重点】Netty 的接收和发送 ByteBuffer 采用堆外直接内存 Direct Buffer 。

  • 使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝;使用堆内内存会多了一次内存拷贝,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。
  • Netty 创建的 ByteBuffer 类型,由 ChannelConfig 配置。而 ChannelConfig 配置的 ByteBufAllocator 默认创建 Direct Buffer 类型。

2、CompositeByteBuf 类,可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf ,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer 。

  • #addComponents(...) 方法,可将 header 与 body 合并为一个逻辑上的 ByteBuf 。这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的,即 CompositeByteBuf 只是逻辑上是一个整体。

3、通过 FileRegion 包装的 FileChannel 。

  • #tranferTo(...) 方法,实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel ,避免了传统通过循环 write 方式,导致的内存拷贝问题。

4、通过 wrap 方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。

TCP 粘包 / 拆包

概念

TCP 是以流的方式来处理数据,所以会导致粘包 / 拆包。

  • 拆包:一个完整的包可能会被 TCP 拆分成多个包进行发送。
  • 粘包:也可能把小的封装成一个大的数据包发送。

原因

  • 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象。而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象。
  • 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包
  • 以太网帧的 payload(净荷)大于 MTU(默认为 1500 字节)进行 IP 分片拆包
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包

解决

在 Netty 中,提供了多个 Decoder 解析类,如下:

  • ① FixedLengthFrameDecoder ,基于固定长度消息进行粘包拆包处理的。
  • ② LengthFieldBasedFrameDecoder ,基于消息头指定消息长度进行粘包拆包处理的。
  • ③ LineBasedFrameDecoder ,基于换行来进行消息粘包拆包处理的。
  • ④ DelimiterBasedFrameDecoder ,基于指定消息边界方式进行粘包拆包处理的。

实际上,上述四个 FrameDecoder 实现可以进行规整:

  • ① 是 ② 的特例,固定长度消息头指定消息长度的一种形式。
  • ③ 是 ④ 的特例,换行是于指定消息边界方式的一种形式。

Netty的内存管理

在 Netty 中,IO 读写必定是非常频繁的操作,而考虑到更高效的网络传输性能,Direct ByteBuffer 必然是最合适的选择。但是 Direct ByteBuffer 的申请和释放是高成本的操作,那么进行池化管理,多次重用是比较有效的方式。但是,不同于一般于我们常见的对象池、连接池等池化的案例,ByteBuffer 是有大小一说。申请多大的 Direct ByteBuffer 进行池化又会是一个大问题,太大会浪费内存,太小又会出现频繁的扩容和内存复制!!!所以呢,就需要有一个合适的内存管理算法,解决高效分配内存的同时又解决内存碎片化的问题。

Netty 内存管理机制,基于 Jemalloc 算法。

  • 首先会预申请一大块内存 Arena ,Arena 由许多 Chunk 组成,而每个 Chunk 默认由2048个page组成。
  • Chunk 通过 AVL 树的形式组织 Page ,每个叶子节点表示一个 Page ,而中间节点表示内存区域,节点自己记录它在整个 Arena 中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。大于 8k 的内存分配在 PoolChunkList 中,而 PoolSubpage 用于分配小于 8k 的内存,它会把一个 page 分割成多段,进行内存分配。

附参考文档:

发布了42 篇原创文章 · 获赞 6 · 访问量 2629

猜你喜欢

转载自blog.csdn.net/a1290123825/article/details/104741362