深入剖析 Netty 的核心组件

Posted GitChat精品课

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入剖析 Netty 的核心组件相关的知识,希望对你有一定的参考价值。

本篇文章对 Netty 中开发者最经常打交道的五个组件:ByteBuf,Channel,pipeline,ChannelHandler、EventLoop 做了详细的说明和使用讲解。掌握了这五个组件,就可以开始使用 Netty 编写网络应用了。

不过 Netty 带给我们的除了框架上的简化,也在于其异步化的编程模式。其异步编程模式与其背后的线程模型息息相关。本文我们将从以下几点,来深入剖析 Netty 的核心组件。


01 

ByteBuf

在 JDK 的 NIO 中,我们学习到了其原生的数据承载组件 ByteBufferByteBuffer 的体验着实不太好,读写状态的区别,还有 flip 这种乍看下不直观的操作。
Netty 设计了自己的数据存储组件ByteBuf ByteBuffer 一样, ByteBuf 也是代表了一段连续的二进制数据空间。同样的, ByteBuf 也按照数据存储的位置区分为:数据存储在堆上的 HeapByteBuf 和数据存储在直接内存的 DirectByteBuf
ByteBuf 的设计目标是简化用户的使用,所以不像 ByteBuffer 那样还有读写状态的区分, ByteBuf 当中只有 2 个指针:读指针和写指针。读指针指向的位置,意味着可以从这个位置开始读取;写指针指向的位置,意味着可以将数据写入指向位置。
用图表来表达更为形象,刚申请一个ByteBuf的时候其状态如下
此时读写指针均指向位置 0。此时没有数据可以读取,但是可以写入。
写入 4 个字节后,状态如下
深入剖析 Netty 的核心组件
写指针随着数据的写入增加。此时读写之间存在着一片区域,这片区域就可以读取的内容区域。
接着读取 2 个字节后,状态如下
深入剖析 Netty 的核心组件
读取数据时,读指针对应的增加读取的字节数。在读指针之前的数据则属于“不可读“的范畴。加了引号是因为读写指针可以在外部被修改,这也是将数据重复读取的原理。
从上面的三个示例来看,ByteBuf的使用十分简单,没有什么读写状态更加不需要翻转。 ByteBuf 带来的便利还不止如此, ByteBuf 还具备自动扩容的能力。 在 Netty 中申请一个ByteBuf都会指定一个初始容量,但是在写入的时候,如果剩余容量不足,则会自动扩容 。扩容规则为:
  1. 当写入后新的容量小于 512,则选择一个小于 512 但是大于容量且为 16 的倍数的值作为新容量。
  2. 当写入后新的容量大于 512,则选择一个大于容量且为 2 的次方幂的值作为新容量。
这个扩容规则可以不用关心,其容量确定本质上是因为其采用的内存管理办法。我们只需要知道其可以自动扩容满足我们的写入要求即可。
同时还有一点需要明确,扩容并不是我们想的直观的在原有的后续区域上继续增加新的写入区域,而是重新申请了一个满足写入大小要求的区域,将原先的数据复制到了新的区域。只不过在外部的用户无法感知到罢了。因此,为了减少因为扩容带来的数据复制引起的性能损耗,建议在初始化的时候选择一个相对合适的大小。
但是 ByteBuf 比上面说的要复杂的许多,我们来看看其相关的部分类图
深入剖析 Netty 的核心组件
这里面只是一部分, ByteBuf 是一个很庞大的继承体系。初略分的话大致是两个维度:
  • 数据存储在堆和数据存储在直接内存的区别

  • ByteBuf 持有的内存区域是一次性的依靠 JVM 进行 GC,还是池化的内存依靠 Netty 自行管理的区别
在 Netty3 的年代,由于池化内存只是刚刚开发的功能,处于观望状态,所以官方的建议是推荐使用非池化的ByteBuf
不过到了 Netty4,经过了验证和内存泄漏追踪功能的加入,池化内存也就成为了首选,也是官方所推荐的 。使用池化内存可以有效的降低 JVM 的 GC 压力,平稳系统的 GC 毛刺。在高并发的场景下,性能表现更加稳定。而不是像 Nettty3 那样因为频繁的申请内存和 GC 回收,造成 GC 的 CPU 占用成折线式的不停抖动。
考虑到堆外内存在进行 Socket 读取和写入的时候可以减少一次内核态和用户态之间的数据拷贝,一般而言都是推荐使用堆外内存。
两点相结合,可以总结出在实践中我们推荐使用的ByteBuf的具体类型,也就是 PoolDirectByteBuf不过在编写代码的时候我们并不会直接实例化这个类。而是通过 io.netty.buffer.ByteBufAllocator 这个接口的 buffer 方法来获得具体的实例。

可以通过io.netty.buffer.ByteBufAllocator#DEFAULT属性来获得系统默认的分配器。初始化的Netty会进行判断,如果当前是 android,则使用非池化的分配器;其余情况使用池化的分配器。对于服务器应用而言,池化分配器肯定是不二选择。

02 

CompositeByteBuf

Netty 的官网介绍自己的 ByteBuf 是一个具备零拷贝能力的富 ByteBuffer 实现。 ByteBuf 也的确提供了 ByteBuffer 更多的功能,但是零拷贝本身而言,对于 DirectByteBuf DirectByteBuffer 而言底层都是相同的,都是使用了堆外内存本身的零拷贝的特性。不过 Netty 还有额外提供了自己实现的零拷贝特性的 ByteBuf ,它是一个虚拟组合式的 ByteBuf 视图,也就是这个章节的主角: CompositeByteBuf

在应用程序的编写过程中,部分场景下存在着需要将多个ByteBuf合并的需求。此时简单的使用io.netty.buffer.ByteBuf#writeBytes(io.netty.buffer.ByteBuf)接口可以完成需求,不过就是需要额外的内存拷贝。

针对这种需要聚合多个ByteBuf的使用场景,Netty设计了CompositeByteBuf类。这个类代表着一个虚拟的ByteBuf,其内部是由多个ByteBuf实例组成的数组。每一个ByteBuf都代表着虚拟Buffer中的某一段数据。

举个例子,如下便是由三个ByteBuf实例组成的CompositeByteBuf

可以看到,三个不同的 ByteBuf 实例分别映射了虚拟Buffer不同区域的部分。 CompositeByteBuf 通过聚合的方式,对外提供了一个整体的Buffer的效果。
这种虚拟视图在某种情况下特别的好用。比如说Http协议的实现上都是按照协议头和内容体进行区分,而协议头和内容体往往会采用不同的 ByteBuf 进行存放,因此其解析方式不同的原因。

而当我们需要组装一个完整的Http报文的时候,如果将代表协议头和报文体的ByteBuf实例一起写到一个新的ByteBuf自然是可以满足需求,不过也带来了数据拷贝的消耗。此时使用CompositeByteBuf作为一个虚拟视图聚合2个ByteBuf,既能避免内存拷贝,又可以在对用户表现上呈现出一个完整单一的ByteBuf的效果。提升了开发效率和性能。

03 

Channel

Netty 也实现了自己对于通道的抽象,以便在接口的层面上添加更多能力,同时也与 NIO 的通道区分开 。对于 Channel 而言,我们通常不会接触到其接口,而是一般接触到它实际使用的实现类,比较常用的实现类主要有:
  • io.netty.channel.socket.nio.NiosocketChannel:这个实现类一般是在网络编程中,引导程序帮助我们实例化的,而且实例化的时候传递给我们也是接口io.netty.channel.socket.SocketChannel并不会让我们感知到这个具体的实现。这个类代表着一个具体的 TCP 通道。

  • io.netty.channel.socket.nio.NioServerSocketChannel:这个实现类提供的是 TCP 协议下服务端监听 Socket 通道的能力。这个实现类一般直接将类对象传递给引导程序用于启动一个基于 TCP 协议的服务端。
  • io.netty.channel.epoll.EpollServerSocketChannel:一般而言,Java 的服务端应用都是部署在 Linux 服务器上。在 Linux 环境上,Netty 提供了自己实现的,更为高效的基于 Epoll 实现方式的 IO 复用实现。在引导程序中将NioServerSocketChannel替换为EpollServerSocketChannel可以得到更高的性能。

一般而言,我们会在三个地方和 Channel 的不同实现打交道。
  1. 情况一:引导程序初始化完毕后,获得引导程序实际创建的 Channel 对象,等待其关闭;通过这个等待可以实现优雅退出以及在服务端关闭后执行一些业务逻辑。
  2. 情况二:在客户端链接通过io.netty.channel.ChannelInitializer#initChannel(C)方法被创建后,获得通道的管道对象pipeline,在其中添加处理器。

  3. 情况三:持有通道对象,通过通道对象来将数据写出。
扫码了解《深入浅出学 Netty》专栏详情


本文通过剖析 Netty 中的重点组件,加深对这些组件的理解和掌握,从底层熟悉和理解 Netty。点击阅读原文,订阅 Netty 专栏。

以上是关于深入剖析 Netty 的核心组件的主要内容,如果未能解决你的问题,请参考以下文章

Netty核心技术及源码剖析-Netty入站与出站机制

Netty框架之深入了解NIO核心组件

Netty框架之深入了解NIO核心组件

Netty系列・高级篇Netty核心源码解析

高性能异步事件驱动的NIO框架,结合英雄传说项目深入剖析Netty

剖析Tomcat核心思想和源码