1、NIOEventLoop
NioEventLoop有以下有个核心功能。
- 开启Selector并初始化。
- 把ServerSocketChannel注册到Selector上。
- 处理各种I/O事件,如OP_ACCEPT、OP_CONNECT、OP_READ、
OP_WRITE事件。
- 执行定时调度任务。
- 解决JDK空轮询bug。
NioEventLoop这些功能的具体实现大部分都是委托其他类来完成 的,其本身只完成数据流的接入工作
2、AbstractChannel
AbstractChannel抽象类包含以下几个重要属性。
- EventLoop:每个Channel对应一条EventLoop线程。
- DefaultChannelPipeline:一个Handler的容器,也可以将其理解为一个Handler链。Handler主要处理数据的编/解码和业务逻辑。
- Unsafe:实现具体的连接与读/写数据,如网络的读/写、链路 关闭、发起连接等。命名为Unsafe表示不对外提供使用,并非不安全。
3、ByteBuf
在网络传输中,字节是基本单位,NIO使用ByteBuffer作为Byte字 节容器,但是其使用过于复杂。因此Netty写了一套Channel,代替了NIO 的 Channel 。 Netty 缓 冲 区 又 采 用 了 一 套 ByteBuf 代 替 了 NIO 的ByteBuffer。Netty的ByteBuf子类非常多,这里只对核心的ByteBuf进 行详细的剖析
NIO ByteBuffer只有一个位置指针position,在切换读/写状态 时,需要手动调用flip()方法或rewind()方法,以改变position的 值,而且ByteBuffer的长度是固定的,一旦分配完成就不能再进行扩 容和收缩,当需要放入或存储的对象大于ByteBuffer的容量时会发生异常。每次编码时都要进行可写空间校验。
Netty的AbstractByteBuf将读/写指针分离,同时在写操作时进行 了自动扩容。对其使用而言,无须关心底层实现,且操作简便、代码无冗余
3.1、AbstractByteBuf
AbstractByteBuf是ByteBuf的子类,它定义了一些公共属性,如 读索引、写索引、mark、最大容量等。AbstractByteBuf实现了一套 读/写操作的模板方法,其缓冲区真正的数据读/写由其子类完成
3.2、AbstractReferenceCountedByteBuf
Netty在进行I/O的读/写时使用了堆外直接内存,实现了零拷贝, 堆外直接内存Direct Buffer的分配与回收效率要远远低于JVM堆内存 上对象的创建与回收速率。Netty使用引用计数法来管理Buffer的引用 与释放。
Netty采用了内存池设计,先分配一块大内存,然后不断地重 复利用这块内存。例如,当从SocketChannel中读取数据时,先在大内 存块中切一小部分来使用,由于与大内存共享缓存区,所以需要增加 大内存的引用值,当用完小内存后,再将其放回大内存块中,同时减少其引用值
由于ByteBuf的操作可能存在多线程并发使用的情况,其refCnt属 性的操作必须是线程安全的,因此采用了volatile来修饰,以保证其 多线程可见。在Netty中,ByteBuf会被大量地创建,为了节省内存开 销,通过AtomicIntegerFieldUpdater来更新refCnt的值,
而**没有采用AtomicInteger类型。因为AtomicInteger类型创建的对象比int类型多 占用16B的对象头,当有几十万或几百万ByteBuf对象时,节约的内存
可能就是几十MB或几百MB。**
3.3、ReferenceCountUpdater
ReferenceCountUpdater 是 AbstractReferenceCountedByteBuf 的 辅助类,用于完成对引用计数值的具体操作
3.4、CompositeByteBuf
CompositeByteBuf的主要功能是组合多个ByteBuf,对外提供统一 readerIndex和writerIndex。由于它只是将多个ByteBuf的实例组装 到一起形成了一个统一的视图,并没有对ByteBuf中的数据进行拷贝,因此也属于Netty零拷贝的一种,主要应用于编码和解码,注意:组合并不一定真的要将数据全部放入一个集合,我可以仅仅保留指向单个的引用指针
应用场景
编码时,将消息头和消息体两个ByteBuf组合到一块进行编码,可能 会觉得Netty有写缓冲区,其本身就会存储多个ByteBuf,此时只需把 两个ByteBuf分别写入缓冲区ChannelOutboundBuffer即可,没必要使 用组合ByteBuf。但是在将ByteBuf写入缓冲区之前,需要对整个消息进行编码,如长度编码,此时需要把两个ByteBuf合并成一个,无须额 外处理就可以知道其整体长度。因此使CompositeByteBuf是非常适合的。
在解码时,由于Socket通信传输数据会产生粘包和半包问题,因 此需要一个读半包字节容器,这个容器采用CompositeByteBuf比较合 适,将每次从Socket中读到的数据直接放入此容器中,少了一次数据 的拷贝。Netty的解码类ByteToMessageDecoder默认的读半包字节容器Cumulator 未 采 CompositeByteBuf , 此 时 可 在 其 子 类 中 调 用setCumulator进行修改。但需要注意的是,CompositeByteBuf需要依 赖具体的使用场景。因为CompositeByteBuf使用了复杂的算法逻辑,所以其效率有可能比使用内存拷贝的低
3.5、PooledByteBuf
这 个类继承于AbstractReference CountedByteBuf,其对象主要由内存 池分配器PooledByteBufAllocator创建。
比较常用的实现类有两种: 一种是基于堆外直接内存池构建的PooledDirectByteBuf,是Netty在 进行I/O的读/写时的内存分配的默认方式,堆外直接内存可以减少内 存 数 据 拷 贝 的 次 数 ; 另 一 种 是 基 于 堆 内 内 存 池 构 建 的PooledHeapByteBuf。
除 了 上 述 两 种 实 现 类 **, Netty 还 使 用 Java 的 后 门 类
sun.misc.Unsafe实现了两个缓冲区,即PooledUnsafeDirectByteBuf和PooledUnsafeHeapByteBuf**。这个强大的后门类会暴露对象的底层地址,一般不建议使用,Netty为了优化性能引入了Unsafe。PooledUnsafeDirectByteBuf会暴露底层DirectByteBuffer的地址memoryAddress,而Unsafe则可通过memoryAddress+Index的方式取得对应的字节。
PooledUnsafeHeapByteBuf也会暴露字节数组在Java堆中的地址, 因此不再使用字节数组的索引,即array[index]。同样,Unsafe可通过BYTE_ARRAY_BASE_OFFSET+Index字节的地址获取字节。
由于创建PooledByteBuf对象的开销大,而且在高并发情况下,当 网络I/O进行读/写时会创建大量的实例。因此,为了降低系统开销,Netty对Buffer对象进行了池化,缓存了Buffer对象,使对此类型的Buffer可进行重复利用。PooledByteBuf是从内存池中分配出来的Buffer,因此它需要包含内存池的相关信息,如内存块Chunk、PooledByteBuf在内存块中的位置及其本身所占空间的大小等