vlambda博客
学习文章列表

【Netty专题】源码剖析netty核心基础ByteBuf

在面试的时候,当面试问到netty的时候问到:你知道jdk nio中的ByteBuffernetty 中的ByteBuf有什么区别吗?来看看面试者的基础掌握的如何!你能准确回到出来个所以然吗?

说到jdk我先说说我身边使用jdk nio的情况;

我现在公司就有个游戏项目是jdk nio2一行一行实现的通讯架构,一直在线上运营,目前该架构单服承载最高的时候达到3000多人,没发现有什么性能瓶颈,当然人数可能还会继续增加,只要提高服务器配置,或者选择增加新的服务器去负载均衡;

我们都知道jdk nio臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题;当然我没有遇到这个bug,希望后面也不要遇到(祈祷),而netty的设计就很自然的避免了这个问题;不用担心这个bug出现;

这篇文章就是由浅入深解读ByteBuf家族,从而彻底掌握ByteBuf各知识点。

 

一、先来比较下jdk nio中的ByteBuffernetty 中的ByteBuf

JDK NIO 劣势:

1、ByteBuffer给开发者的第一感觉就是他的API太少了,程序员的很多需求都不能满足,只能自己去实现或者做一下封装;

2、ByteBuffer 长度固定,不能动态收缩拓展,如果存很长的数据时候,就容易越界报错,例如:

  

3、ByteBuffer 读写操作的时候只有一个索引,操作起来flip(),rewind(),让人蛋疼,小白操作很容易混乱,这一点Mina IoBufferByteBuffer同病相怜;

 

相反netty 的ByteBuf解决上面的所有的痛点;丰富的API,支持动态收缩拓展,并且读写索引分开,不用操作字节的时候,那么蛋疼;

 

二、ByteBuf实现原理以及如何使用

Netty ByteBuf提供了2个指针变量分别用于顺序读取,和顺序写入,readerIndex是读索引,writerIndex是写索引,二者划分ByteBuf缓冲区关系:

 

【Netty专题】源码剖析netty核心基础ByteBuf


capacity:

 缓冲区的容量。

readerIndex:

【Netty专题】源码剖析netty核心基础ByteBuf

读取字节,改变readerIndex位置,或者下面的方法去初始化readerIndex


【Netty专题】源码剖析netty核心基础ByteBuf


  设置当前读的位置。可以使用readerIndex()和readerIndex(int)方法获取、设置readerIndex值。每次调用readXXX方法都会导致readerIndexwriterIndex移动,直到等于writerIndex为止。

writerIndex:

【Netty专题】源码剖析netty核心基础ByteBuf

  设置写的当前位置。可以使用writerIndex()和writerIndex(int)方法获取、设置writeIndex的值。每次调用writeXXX方法都会导致writeIndexcapacity移动,直到等于capacity为止。

discardable bytes:

【Netty专题】源码剖析netty核心基础ByteBuf

  表示读取之后丢弃,节约空间资源。0--readerIndex之间的数据长度是readerIndex - 0,调用discardReadBytes会丢弃这部分数据,把readerIndex--writerIndex之间的数据移动到ByteBuf的开始位置(0)


使用案例展示:

 

【Netty专题】源码剖析netty核心基础ByteBuf


三、ByteBuf缓冲分类

1、Heap buffer(堆缓冲区):

就是将数据存在JVM堆空间中,在没有被池化的情况可以快速分配和释放。

优点:由于数据是存储在JVM堆中,因此可以快速的创建与快速的释放,并且它提供了直接访问内部字节数组的方法。

缺点:每次读写数据时,都需要先将数据复制到直接缓冲区中再进行网路传输。

2、Direct buffer(直接缓冲区):

直接缓冲区,在堆外直接分配内存空间,直接缓冲区并不会占用堆的容量空间,因为它是由操作系统在本地内存进行的数据分配。

优点:在使用Socket进行数据传递时,性能非常好,因为数据直接位于操作系统的本地内存中,所以不需要从JVM将数据复制到直接缓冲区中 。

缺点:因为Direct Buffer是直接在操作系统内存中的,所以内存空间的分配与释放要比堆空间更加复杂,而且速度要慢一些。

注意:

如果你的数据包含在一个在堆上的分配的缓冲区中,那么事实上,在通过套接字发送他之前,jvm将会在内部把你的缓冲区复制到一个直接缓冲区中;这样分配释放就比较浪费资源;

建议:

直接缓冲区并不支持通过字节数组的方式来访问数据。对于后端业务的消息编解码来说,推荐使用HeapByteBuf;对于I/O通信线程在读写缓冲区时,推荐使用DirectByteBuf

3、Composite Buffer 复合缓冲区:

可以拥有以上两种的缓冲区,通过一种聚合视图来操作底层持有的多种类型Buffer。这种缓冲,jdk nio是没有这种特性的。

 

四、源码解读ByteBuf家族

 

【Netty专题】源码剖析netty核心基础ByteBuf


 

先看下ByteBuf的接口和他的三个不同方向的实现抽象类,下面还有具体实现类后面再具体列出来讲解,先介绍下这几个ByteBuf的顶级接口和抽象父类:

(Deprecated 的SwappedByteBuf官方已经不赞成去使用了,是不安全缓冲接口)

1、ReferenceCounted引用计数器接口。

  Netty 4开始,对象的生命周期由它们的引用计数(reference counts)管理,而不是由垃圾收集器(garbage collector)管理了。ByteBuf是最值得注意的,它使用了引用计数来改进分配内存和释放内存的性能。ByteBuf利用引用计数来改进分配和回收性能;

      只要引用计数大于 0,就能保证对象不会被释放。当活动引用的数量减少到时,该实例就会被释放;是由最后访问(引用计数)对象的那一方来负责将它释放。


【Netty专题】源码剖析netty核心基础ByteBuf

(1)、如果一个对象实现了ReferenceCounted接口,被初始化的时候,计数为1。

(2)、retain()方法能够增加计数,release() 方法能够减少计数,如果计数被减少到0则对象会被显示回收,再次访问被回收的这些对象将会抛出异常。

(3)、如果一个对象实现了ReferenceCounted,并且包含有其他对象也实现来ReferenceCounted,当这个对象计数为0被回收的时候,所包含的对象同样会通过release()释放掉。

 

2、Comparable:排序接口。

ByteBuf:定义了一下是否可读写的属性以及定义了一些读写操作的抽象接口,供子类继承实现。

 

【Netty专题】源码剖析netty核心基础ByteBuf


3、WrappedByteBuf:用于装饰ByteBuf对象,主要有AdvancedLeakAwareByteBufSimpleLeakAwareByteBufUnreleasableByteBuf三个子类。

(1)、WrappedByteBuf使用装饰者模式装饰ByteBuf对象

(2)、AdvancedLeakAwareByteBuf用于对所有操作记录堆栈信息,方便监控内存泄漏;

(3)、SimpleLeakAwareByteBuf只记录order(ByteOrder endianness)的堆栈信息;

(4)、UnreleasableByteBuf用于阻止修改对象引用计数器refCnt的值。

 

 

4、AbstractByteBuf: 抽象继承ByteBuf, ByteBuf缓冲的默认实现接口.

 

【Netty专题】源码剖析netty核心基础ByteBuf


 

子类很多,我们上面说的三种缓冲策略的类实现都是最终继承实现AbstracByteBuf,而AbstractByteBuf本身并没有具体去对不同ByteBuf缓冲区去做具体实现,而是由子类去实现,原因很简单父类不知道子类去实现堆内存还是直接内存,还是复合缓冲区;只是提供一个抽象接口而已;

再看看AbstractByteBuf源码中属性:

 

【Netty专题】源码剖析netty核心基础ByteBuf


 

定义了读,写索引和读写索引的标记,以及最大容量,leakDetectro:是监测内存是否泄漏,是static类型,说明是共享公共的对象;

 

AbstractByteBuf直接子类 AbstractDerivedByteBuf:提供派生ByteBuf的默认实现,主要有DuplicatedByteBufReadOnlyByteBufSlicedByteBuf

(1)、DuplicatedByteBuf:使用装饰者模式创建ByteBuf的复制对象,使得复制后的对象与原对象共享缓冲区的内容,但是独立维护自己的readerIndexwriterIndex

(2)、ReadOnlyByteBuf:使用装饰者模式创建ByteBuf的只读对象,该只读对象与原对象共享缓冲区的内容,但是独立维护自己的readerIndexwriterIndex,之后所有的写操作都被限制;

(3)、SlicedByteBuf:使用装饰者模式创建ByteBuf的一个子区域ByteBuf对象,返回的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndexwriterIndex,允许写操作。

(4)、其实还可以按照另外一个维度去理解,一个是池化的ByteBuf一个是非池化的ByteBuf(用完就销毁);即PooledByteBuf_ 和UnpooledByteBuf_;

 

以上三种缓冲官方已经Deprecated不推介使用;

 

这也不推介使用那也不推介使用,那我们用哪些类去操作缓冲呢?

我们用AbstractByteBuf直接子类AbstractReferenceCountedByteBuf,该抽象类的子类实现的缓冲类;

 


 

上面我们看到很多实现类,而且没有一个被Deprecated的;这里只重点介绍几个常用的;

(1)、UnpooledDirectByteBuf

 在堆外进行内存分配的非内存池ByteBuf,内部持有ByteBuffer对象,相关操作委托给ByteBuffer实现。


(2)、UnpooledHeapByteBuf

  基于堆内存分配非内存池ByteBuf,即内部持有byte数组。


(3)、UnpooledUnsafeDirectByteBuf

  和另外一个类UnpooledDirectByteBuf差不多相同,区别在于UnpooledUnsafeDirectByteBuf内部使用基于PlatformDependent相关操作实现ByteBuf,依赖平台。


(4)、ReadOnlyByteBufferBuf

  只读ByteBuf,内部持有ByteBuffer对象,相关操作委托给ByteBuffer实现,该ByteBuf限内部使用;


(5)、FixedCompositeByteBuf

 用于将多个ByteBuf组合在一起,形成一个虚拟的只读ByteBuf对象,不允许写入和动态扩展。内部使用Object[]将多个ByteBuf组合在一起,一旦FixedCompositeByteBuf对象构建完成,则不会被更改。


(6)、CompositeByteBuf

 用于将多个ByteBuf组合在一起,形成一个虚拟的ByteBuf对象,支持读写和动态扩展。内部使用List<Component>组合多个ByteBuf一般使用使用ByteBufAllocatorcompositeBuffer()方法,Unpooled的工厂方法compositeBuffer()wrappedBuffer(ByteBuf... buffers)创建CompositeByteBuf对象。


(7)、PooledByteBuf<T>

  基于内存池的ByteBuf,主要为了重用ByteBuf对象,提升内存的使用效率;适用于高负载,高并发的应用中。主要有PooledDirectByteBufPooledHeapByteBufPooledUnsafeDirectByteBuf三个子类,PooledDirectByteBuf是在堆外进行内存分配的内存池ByteBufPooledHeapByteBuf是基于堆内存分配内存池ByteBufPooledUnsafeDirectByteBuf也是在堆外进行内存分配的内存池ByteBuf,区别在于PooledUnsafeDirectByteBuf内部使用基于PlatformDependent相关操作实现ByteBuf,具有平台相关性。

 



  就到这里了,感谢读者认真读完;差不多就netty的ByteBuf家族有了初步的了解了,后面博客我会对三种缓冲(直接缓冲,复合缓冲,堆缓冲)进行具体源码剖析,和大家一起学习一起探讨;如有些的不对的地方,请积极指出,以便及时纠正。