Netty之ByteBuf综合剖析
2016-11-09 21:35
417 查看
概述:Netty缓存区名为ByteBuf,在netty出来之前,主要有Java NIO提供的ByteBuffer来操作字节,然而因其作用太有限,也没有做相关性能优化,因此诞生了这个新的缓冲类。该类提供了强大的缓冲区用来表示字节序列以及帮助你操作字节和自定义POJO。
其主要实现了缓冲区的池化,使得内存区域可以重复使用;此外增加了直接内存,该内存区域不在JVM的堆区域,从而提高了IO读写操作的效率,即所谓高大上的零拷贝;而针对直接内存的分配和销毁,java的自动回收策略GC并不能很轻松的管控直接内存,因此Netty自带了轻量级的引用计数策略来定时清除缓存。
[1]、ByteBuffer使用起来非常蛋疼
最大的蛋疼就在于要每次读写完数据调用flip方法,而ByteBuf在底层设计上就做了改进,使用上非常简单,先通过ByteBufAllocator进行Buffer分配,当然要指明创建什么类型的Buf,然后读取数据时直接调用readBytes(int length),获取读取情况则调用readableBytes(),重置读取游标则调用resetReaderIndex(),操作上不容易产出问题。
[2]、ByteBuffer的家族成员比较简单,对于多数缓存使用场景都需要开发者自己扩展
HeapByteBuffer是基于堆上字节数组为存储结构的缓冲区。
DirectByteBuffer是基于直接内存上的内存区域为存储结构的缓冲区,底层是通过了Unsafe进行字节操作。
MappedByteBuffer主要是文件操作相关的,它提供了一种基于虚拟内存映射的机制,使得我们可以像操作文件一样来操作文件,而不需要每次将内容更新到文件之中,同时读写效率非常高。
[3]、ByteBuf的家族成员就非常庞大
围绕着Direct、Heap、Pooled、UnPooled、Unsafe等五个关键字进行组合扩展,如下是比较典型的Buffer
PS:当然还有CompositeByteBuf、ReadOnlyByteBuf、ThreadLocalDirectBuf等。
PooledHeapByteBuf:池化的基于堆内存的缓冲区。
PooledDirectByteBuf:池化的基于直接内存的缓冲区。
PooledUnsafeDirectByteBuf:池化的基于Unsafe和直接内存实现的缓冲区。
UnPooledHeapByteBuf:非池化的基于堆内存的缓冲区。
UnPooledDirectByteBuf:非池化的基于直接内存的缓冲区。
针对如上丰富的关键字组合模式,逐一介绍下大致的内部机理。
A、第一个是Pool和Unpool系列:
如上代码是Pool方式,代码通过一个Recycler类来进行对象池管理,其内部是通过thread-local+queue来实现的。而Unpool,则只是简单的进行buf的用完即销毁策略。此处具体的原理及分析细节不加以展开…
B、第二个是Heap系列
该系列都是基于byte数组,举例UnpooledHeapByteBuf,底层的实现都是对array数组进行各种字节操作
C、第三个是Unsafe系列
抽取来自PooledUnsafeDirectoryByteBuf类的部分代码,该系列的核心实现都依赖于UnsafeByteBufUtil,其实现经过了PlatformDependent的包装,实际底层走到了sun.misc.Unsafe的逻辑,针对getByte具体有两种实现方式,对于heapByteBuf,则是如下方法:
而针对DirectByteBuf,因其不依赖于array数组,其底层则是直接操作内存地址来做字节操作
[4]、对内存的管理策略不一样,Bytebuf更为灵活,也更为人性化
ByteBuffer采用的是在内存中预留指定大小的存储空间来存放临时数据,因此在创建buffer时,需要先评估好空间大小。而ByteBuf,是能够基于使用情况进行动态扩充,但为防止内存的高效率重复使用,ByteBuf还提供了对象池化的方案,极大提高缓存的利用率。
通过ByteBufAllocator类,我们可以分配一块池化的内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer();
ByteBuf buffer = ByteBufAllocator.DEFAULT.ioBuffer();
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer();
ByteBuf buffer = ByteBufAllocator.DEFAULT.compositeBuffer();
[5]、Netty为很好的兼容JDK的ByteBuffer,还支持从ByteBuffer到ByteBuf的转换
写入数据到 ByteBuf 后,writerIndex(写入索引)增加写入的字节数。读取字节后,readerIndex(读取索引)也增加读取出的字节数。你可以读取字节,直到写入索引和读取索引处在相同的位置。此时ByteBuf不可读,所以下一次读操作将会抛出 IndexOutOfBoundsException,就像读取数组时越位一样。
调用 ByteBuf 的以 “read” 或 “write” 开头的任何方法都将自动增加相应的索引。另一方面,”set” 、 “get”操作字节将不会移动索引位置,它们只会在指定的相对位置上操作字节。
可以给ByteBuf指定一个最大容量值,这个值限制着ByteBuf的容量。任何尝试将写入超过这个值的数据的行为都将导致抛出异常。ByteBuf 的默认最大容量限制是 Integer.MAX_VALUE。
ByteBuf 类似于一个字节数组,最大的区别是读和写的索引可以用来控制对缓冲区数据的访问。需要注意的是,调用discardReadBytes会发生字节数组的内存复制,所以频繁调用将会导致性能下降。
1、频繁分配、释放buffer时减少了GC压力
2、在初始化新buffer时减少内存带宽消耗(从池中获取即可)
3、及时的释放了direct buffer
池化的过程是基于ThreadLocal来实现的轻量级对象池,具体类为Recycler,有兴趣可研读
netty默认的内存分配器是采用Unpooled,若对底层逻辑实现感兴趣可深入“Memory Arena”
可通过”-Dio.netty.allocator.type=pooled”(默认false)—指定默认使用“Pooled”模型。
“-Dio.netty.noPreferDirect=false”(默认false)和”-Dio.netty.noUnsafe=true”(默认false)来指定默认使用direct内存
值得注意的是,该区域内存,GC是无能为力的,启动时通过-XX:MaxDirectMemorySize=xxM做限制,有利于规避在高负载下频繁GC对应用线程产生中断的影响。
PS:http://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html.
然而也有缺点,其内存空间的分配和释放更为复杂,数据传递给其他模块,因不在堆上导致必须做一个副本
1.检查 ByteBuf 是不是由数组支持。如果不是,这是一个直接缓冲区。
2.获取可读的字节数
3.分配一个新的数组来保存字节
4.字节复制到数组
5.将数组,偏移量和长度作为参数调用某些处理方法
显然,这比使用数组要多做一些工作。因此,如果你事前就知道容器里的数据将作为一个数组被访问,你可能更愿意使用堆内存。
在引用计数接口中,有几个关键的概念,refCnt()、retain()、release(),如下一一进行说明
1、release()将会递减对象引用的数目。当这个引用计数达到0时,对象已被释放,并且该方法返回 true。如果尝试访问已经释放的对象,将会抛出 IllegalReferenceCountException 异常。
需要注意的是一个特定的类可以定义自己独特的方式其释放计数的“规则”。 例如,release() 可以将引用计数器直接计为 0 而不管当前引用的对象数目。
PS:那随负责释放呢,在netty中,一般由最后一个Handler访问对象负责释放
2、retain()将会递增该对象的引用数目,而refCnt()则是用来判断该对象的引用数目是多少
PS:官方有一篇关于引用计数对象的文章,很值得研读,有兴趣同学读下,戳我
一般情况下,Netty默认会从分配的ByteBuf里抽样出大约1%来进行跟踪,若有泄露,则日志会打印如下:
LEAK: ByteBuf.release() was not called before it’s garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option ‘-Dio.netty.leakDetectionLevel=advanced’ or call ResourceLeakDetector.setLevel()
这句话报告有泄漏的发生,提示你用-D参数,把防漏等级从默认的simple升到advanced,就能具体看到被泄漏的ByteBuf被创建和访问的地方。
禁用(DISABLED) - 完全禁止泄露检测,省点消耗。
简单(SIMPLE) - 默认等级,告诉我们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了。
高级(ADVANCED) - 告诉我们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(创建的地方与访问路径一致)只打印一次。对性能有影响。
偏执(PARANOID) - 跟高级选项类似,但此选项检测所有ByteBuf,而不仅仅是取样的那1%。对性能有绝大的影响。
如上不同级别,实现的方式还有所区别,每当各种ByteBufAllocator 创建ByteBuf时,都会问问是否需要采样,Simple和Advanced级别下,就是以113这个素数来取模(害我看文档的时候还在瞎担心,1%,万一泄漏的地方有所规律,刚好躲过了100这个数字呢,比如都是3倍数的),命中了就创建一个PhantomReference。然后创建一个Wrapper,包住ByteBuf和Reference。
simple级别下,wrapper只在执行release()时调用Reference.clear(),Advanced级别下则会记录每一个创建和访问的动作。
当GC发生,还没有被clear()的Reference就会被JVM放入到之前设定的ReferenceQueue里。
在每次创建PhantomReference时,都会顺便看看有没有因为忘记执行release()把Reference给clear掉,在GC时被放进了ReferenceQueue的对象,有则以 “io.netty.util.ResourceLeakDetector”为logger name,写出前面例子里的Error级别的日日志。顺便说一句,Netty能自动匹配日志框架,先找Slf4j,再找Log4j,最后找JDK logger。
PS:因此在进行netty编程时,要特别留意日志有没“LEAK”字样而且最好开发及测试时打开“-Dio.netty.leakDetectionLevel=paranoid”
其主要实现了缓冲区的池化,使得内存区域可以重复使用;此外增加了直接内存,该内存区域不在JVM的堆区域,从而提高了IO读写操作的效率,即所谓高大上的零拷贝;而针对直接内存的分配和销毁,java的自动回收策略GC并不能很轻松的管控直接内存,因此Netty自带了轻量级的引用计数策略来定时清除缓存。
1. ByteBuf和ByteBuffer
Buffer,用维基百科的话是:”在数据传输时,在内存里开辟的一块临时保存数据的区域”。它其实是一种化同步为异步的机制,可以解决数据传输的速率不对等以及不稳定的问题。而JDK官方本身就有关于缓冲区的包装,即ByteBuffer,但使用时槽点较多,因此就诞生了高大上的ByteBuf,来自Netty。下面重点讲述下两者的主要区别:[1]、ByteBuffer使用起来非常蛋疼
最大的蛋疼就在于要每次读写完数据调用flip方法,而ByteBuf在底层设计上就做了改进,使用上非常简单,先通过ByteBufAllocator进行Buffer分配,当然要指明创建什么类型的Buf,然后读取数据时直接调用readBytes(int length),获取读取情况则调用readableBytes(),重置读取游标则调用resetReaderIndex(),操作上不容易产出问题。
[2]、ByteBuffer的家族成员比较简单,对于多数缓存使用场景都需要开发者自己扩展
HeapByteBuffer是基于堆上字节数组为存储结构的缓冲区。
DirectByteBuffer是基于直接内存上的内存区域为存储结构的缓冲区,底层是通过了Unsafe进行字节操作。
MappedByteBuffer主要是文件操作相关的,它提供了一种基于虚拟内存映射的机制,使得我们可以像操作文件一样来操作文件,而不需要每次将内容更新到文件之中,同时读写效率非常高。
[3]、ByteBuf的家族成员就非常庞大
围绕着Direct、Heap、Pooled、UnPooled、Unsafe等五个关键字进行组合扩展,如下是比较典型的Buffer
PS:当然还有CompositeByteBuf、ReadOnlyByteBuf、ThreadLocalDirectBuf等。
PooledHeapByteBuf:池化的基于堆内存的缓冲区。
PooledDirectByteBuf:池化的基于直接内存的缓冲区。
PooledUnsafeDirectByteBuf:池化的基于Unsafe和直接内存实现的缓冲区。
UnPooledHeapByteBuf:非池化的基于堆内存的缓冲区。
UnPooledDirectByteBuf:非池化的基于直接内存的缓冲区。
针对如上丰富的关键字组合模式,逐一介绍下大致的内部机理。
A、第一个是Pool和Unpool系列:
final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> { private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() { @Override protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) { return new PooledDirectByteBuf(handle, 0); } }; static PooledDirectByteBuf newInstance(int maxCapacity) { PooledDirectByteBuf buf = RECYCLER.get(); buf.reuse(maxCapacity); return buf; }
如上代码是Pool方式,代码通过一个Recycler类来进行对象池管理,其内部是通过thread-local+queue来实现的。而Unpool,则只是简单的进行buf的用完即销毁策略。此处具体的原理及分析细节不加以展开…
B、第二个是Heap系列
public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf { private final ByteBufAllocator alloc; byte[] array; private ByteBuffer tmpNioBuf; /** * Creates a new heap buffer with a newly allocated byte array. * * @param initialCapacity the initial capacity of the underlying byte array * @param maxCapacity the max capacity of the underlying byte array */ protected UnpooledHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) { this(alloc, new byte[initialCapacity], 0, 0, maxCapacity); }
该系列都是基于byte数组,举例UnpooledHeapByteBuf,底层的实现都是对array数组进行各种字节操作
C、第三个是Unsafe系列
@Override protected byte _getByte(int index) { return UnsafeByteBufUtil.getByte(addr(index)); } @Override protected short _getShort(int index) { return UnsafeByteBufUtil.getShort(addr(index)); } @Override protected short _getShortLE(int index) { return UnsafeByteBufUtil.getShortLE(addr(index)); }
抽取来自PooledUnsafeDirectoryByteBuf类的部分代码,该系列的核心实现都依赖于UnsafeByteBufUtil,其实现经过了PlatformDependent的包装,实际底层走到了sun.misc.Unsafe的逻辑,针对getByte具体有两种实现方式,对于heapByteBuf,则是如下方法:
static byte getByte(byte[] data, int index) { return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index); }
而针对DirectByteBuf,因其不依赖于array数组,其底层则是直接操作内存地址来做字节操作
static byte getByte(long address) { return UNSAFE.getByte(address); }
[4]、对内存的管理策略不一样,Bytebuf更为灵活,也更为人性化
ByteBuffer采用的是在内存中预留指定大小的存储空间来存放临时数据,因此在创建buffer时,需要先评估好空间大小。而ByteBuf,是能够基于使用情况进行动态扩充,但为防止内存的高效率重复使用,ByteBuf还提供了对象池化的方案,极大提高缓存的利用率。
通过ByteBufAllocator类,我们可以分配一块池化的内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer();
ByteBuf buffer = ByteBufAllocator.DEFAULT.ioBuffer();
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer();
ByteBuf buffer = ByteBufAllocator.DEFAULT.compositeBuffer();
[5]、Netty为很好的兼容JDK的ByteBuffer,还支持从ByteBuffer到ByteBuf的转换
2. ByteBuf的读写指针
在ByteBuffer中,读写指针都是position,而在ByteBuf中,读写指针分别为readerIndex和writerIndex,直观看上去ByteBuffer仅用了一个指针就实现了两个指针的功能,节省了变量,但是当对于ByteBuffer的读写状态切换的时候必须要调用flip方法,而当下一次写之前,必须要将Buffe中的内容读完,再调用clear方法。每次读之前调用flip,写之前调用clear,这样无疑给开发带来了繁琐的步骤,而且内容没有读完是不能写的,这样非常不灵活。相比之下我们看看ByteBuf,读的时候仅仅依赖readerIndex指针,写的时候仅仅依赖writerIndex指针,不需每次读写之前调用对应的方法,而且没有必须一次读完的限制。写入数据到 ByteBuf 后,writerIndex(写入索引)增加写入的字节数。读取字节后,readerIndex(读取索引)也增加读取出的字节数。你可以读取字节,直到写入索引和读取索引处在相同的位置。此时ByteBuf不可读,所以下一次读操作将会抛出 IndexOutOfBoundsException,就像读取数组时越位一样。
调用 ByteBuf 的以 “read” 或 “write” 开头的任何方法都将自动增加相应的索引。另一方面,”set” 、 “get”操作字节将不会移动索引位置,它们只会在指定的相对位置上操作字节。
可以给ByteBuf指定一个最大容量值,这个值限制着ByteBuf的容量。任何尝试将写入超过这个值的数据的行为都将导致抛出异常。ByteBuf 的默认最大容量限制是 Integer.MAX_VALUE。
ByteBuf 类似于一个字节数组,最大的区别是读和写的索引可以用来控制对缓冲区数据的访问。需要注意的是,调用discardReadBytes会发生字节数组的内存复制,所以频繁调用将会导致性能下降。
3. ByteBuf的资源池化
主要功能类是PooledByteBuf,其内存分配策略是结合了buddy allocation和slb allocation的jemalloc变种,代码逻辑在io.netty.buffer.PoolArena。具体此处不展开研读,优势主要如下:1、频繁分配、释放buffer时减少了GC压力
2、在初始化新buffer时减少内存带宽消耗(从池中获取即可)
3、及时的释放了direct buffer
池化的过程是基于ThreadLocal来实现的轻量级对象池,具体类为Recycler,有兴趣可研读
netty默认的内存分配器是采用Unpooled,若对底层逻辑实现感兴趣可深入“Memory Arena”
可通过”-Dio.netty.allocator.type=pooled”(默认false)—指定默认使用“Pooled”模型。
“-Dio.netty.noPreferDirect=false”(默认false)和”-Dio.netty.noUnsafe=true”(默认false)来指定默认使用direct内存
4. 堆外直接内存
顾名思义,内存分配不在堆内,目的是通过免去中间交换的内存拷贝,提升IO处理速度;直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。值得注意的是,该区域内存,GC是无能为力的,启动时通过-XX:MaxDirectMemorySize=xxM做限制,有利于规避在高负载下频繁GC对应用线程产生中断的影响。
PS:http://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html.
然而也有缺点,其内存空间的分配和释放更为复杂,数据传递给其他模块,因不在堆上导致必须做一个副本
ByteBuf directBuf = ... if (!directBuf.hasArray()) { //1 int length = directBuf.readableBytes();//2 byte[] array = new byte[length]; //3 directBuf.getBytes(directBuf.readerIndex(), array); //4 handleArray(array, 0, length); //5 }
1.检查 ByteBuf 是不是由数组支持。如果不是,这是一个直接缓冲区。
2.获取可读的字节数
3.分配一个新的数组来保存字节
4.字节复制到数组
5.将数组,偏移量和长度作为参数调用某些处理方法
显然,这比使用数组要多做一些工作。因此,如果你事前就知道容器里的数据将作为一个数组被访问,你可能更愿意使用堆内存。
5. 引用计数回收机制
引用计数主要用于解决对象跟踪管理回收的一种手段。实现方式为继承ReferenceCounted接口。实现了接口的实例通常会开始于一个活动的引用计数器为1,当活动的引用计数大于0的对象会被保证不被销毁释放。当引用数为0时,该实例将被释放,因此在read或者write时,会进行如下的检测判断。/** * Should be called by every method that tries to access the buffers content to check * if the buffer was released before. */ protected final void ensureAccessible() { if (checkAccessible && refCnt() == 0) { throw new IllegalReferenceCountException(0); } }
在引用计数接口中,有几个关键的概念,refCnt()、retain()、release(),如下一一进行说明
1、release()将会递减对象引用的数目。当这个引用计数达到0时,对象已被释放,并且该方法返回 true。如果尝试访问已经释放的对象,将会抛出 IllegalReferenceCountException 异常。
需要注意的是一个特定的类可以定义自己独特的方式其释放计数的“规则”。 例如,release() 可以将引用计数器直接计为 0 而不管当前引用的对象数目。
PS:那随负责释放呢,在netty中,一般由最后一个Handler访问对象负责释放
2、retain()将会递增该对象的引用数目,而refCnt()则是用来判断该对象的引用数目是多少
PS:官方有一篇关于引用计数对象的文章,很值得研读,有兴趣同学读下,戳我
6. 有效规避内存泄露
从如上的内容,大家可以知道,对于UnpooledByteBuf,其没有通过池化技术,因此对象的管理是“用完即销毁”,通常无需多次复用,实现简单,维护成本低,不易出错。而对于PooledByteBuf,在被JVM GC掉之前,没有调用release()把底下的DirectByteBuffer或byte[]归还到池里,会导致池越来越大。则需要考虑内存泄露及对应的一些检测机制。好在netty有提供用于内存泄露检测的服务ResourceLeakDetector。一般情况下,Netty默认会从分配的ByteBuf里抽样出大约1%来进行跟踪,若有泄露,则日志会打印如下:
LEAK: ByteBuf.release() was not called before it’s garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option ‘-Dio.netty.leakDetectionLevel=advanced’ or call ResourceLeakDetector.setLevel()
这句话报告有泄漏的发生,提示你用-D参数,把防漏等级从默认的simple升到advanced,就能具体看到被泄漏的ByteBuf被创建和访问的地方。
禁用(DISABLED) - 完全禁止泄露检测,省点消耗。
简单(SIMPLE) - 默认等级,告诉我们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了。
高级(ADVANCED) - 告诉我们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(创建的地方与访问路径一致)只打印一次。对性能有影响。
偏执(PARANOID) - 跟高级选项类似,但此选项检测所有ByteBuf,而不仅仅是取样的那1%。对性能有绝大的影响。
如上不同级别,实现的方式还有所区别,每当各种ByteBufAllocator 创建ByteBuf时,都会问问是否需要采样,Simple和Advanced级别下,就是以113这个素数来取模(害我看文档的时候还在瞎担心,1%,万一泄漏的地方有所规律,刚好躲过了100这个数字呢,比如都是3倍数的),命中了就创建一个PhantomReference。然后创建一个Wrapper,包住ByteBuf和Reference。
simple级别下,wrapper只在执行release()时调用Reference.clear(),Advanced级别下则会记录每一个创建和访问的动作。
当GC发生,还没有被clear()的Reference就会被JVM放入到之前设定的ReferenceQueue里。
在每次创建PhantomReference时,都会顺便看看有没有因为忘记执行release()把Reference给clear掉,在GC时被放进了ReferenceQueue的对象,有则以 “io.netty.util.ResourceLeakDetector”为logger name,写出前面例子里的Error级别的日日志。顺便说一句,Netty能自动匹配日志框架,先找Slf4j,再找Log4j,最后找JDK logger。
PS:因此在进行netty编程时,要特别留意日志有没“LEAK”字样而且最好开发及测试时打开“-Dio.netty.leakDetectionLevel=paranoid”
相关文章推荐
- Netty之ByteBuf综合剖析
- 【Netty源码】ByteBuf源码剖析
- netty源码分析(二十一)Netty数据容器ByteBuf底层数据结构深度剖析与ReferenceCounted初探
- Netty in action—Netty中的ByteBuf
- Netty的ByteBuf介绍
- netty源码分析之-ByteBuf详解(8)
- 自顶向下深入分析Netty(九)--ByteBuf
- 【Netty4.X】Netty源码分析之ByteBuf(七)
- Netty 权威指南笔记(五):ByteBuf 源码解读
- 自顶向下深入分析Netty(九)--ByteBuf源码分析
- Netty ByteBuf
- Netty中ByteBuf对象的创建方式对内存的影响
- netty(十一)源码分析之ByteBuf 三
- netty 入门二 (传输bytebuf 或者pojo)
- Netty源码分析:AbstractByteBuf
- Netty学习之旅----ByteBuf内部结构与API学习
- Netty入门(四)ByteBuf 字节级别的操作
- Netty中的ByteBuf
- <Netty>(二十二)(高级篇)Netty中bytebuf的用法API
- netty(十一)源码分析之ByteBuf 二