此处将为大家介绍关于Netty的ByteBuf为什么无法读取字节?的详细内容,并且为您解答有关nettybytebuf的相关问题,此外,我们还将为您介绍关于7.netty内存管理-ByteBuf、BA
此处将为大家介绍关于Netty的ByteBuf为什么无法读取字节?的详细内容,并且为您解答有关netty bytebuf的相关问题,此外,我们还将为您介绍关于7.netty内存管理-ByteBuf、BAT面试必问细节:关于Netty中的ByteBuf详解、bytebuf池_Netty ByteBuf[通俗易懂]、io.netty.buffer.AbstractByteBuf的实例源码的有用信息。
本文目录一览:- Netty的ByteBuf为什么无法读取字节?(netty bytebuf)
- 7.netty内存管理-ByteBuf
- BAT面试必问细节:关于Netty中的ByteBuf详解
- bytebuf池_Netty ByteBuf[通俗易懂]
- io.netty.buffer.AbstractByteBuf的实例源码
Netty的ByteBuf为什么无法读取字节?(netty bytebuf)
int len = byteBuf.readableBytes();byte[] bytes = new byte[len];
byteBuf.getBytes(0, bytes);
7.netty内存管理-ByteBuf
<!-- TOC -->
- ByteBuf
- ByteBuf是什么
- ByteBuf重要API
- read、write、set、skipBytes
- mark和reset
- duplicate、slice、copy
- retain、release
- ByteBuf扩容
- ByteBuf种类
- ByteBufAllocate
- UnPooledByteBufAllocate
- newHeapBuffer
- newDirectBuffer
- UnPooledByteBufAllocate
<!-- /TOC -->
ByteBuf
ByteBuf是什么
为了平衡数据传输时CPU与各种IO设备速度的差异性,计算机设计者引入了缓冲区这一重要抽象。jdkNIO库提供了java.nio.Buffer接口,并且提供了7种默认实现,常见的实现类为ByteBuffer。不过netty并没有直接使用nio的ByteBuffer,这主要是由于jdk的Buffer有以下几个缺点:
- 当调用allocate方法分配内存时,Buffer的长度就固定了,不能动态扩展和收缩,当写入数据大于缓冲区的capacity时会发生数组越界错误
- Buffer只有一个位置标志位属性position,读写切换时,必须先调用flip或rewind方法。不仅如此,因为flip的切换
- Buffer只提供了存取、翻转、释放、标志、比较、批量移动等缓冲区的基本操作,想使用高级的功能(比如池化),就得自己手动进行封装及维护,使用非常不方便。 也因此,netty实现了自己的缓冲区——ByteBuf,连名字都如此相似。那么ByteBuf是如何规避ByteBuffer的缺点的? 第一点显然是很好解决的,由于ByteBuf底层也是数组,那么它就可以像ArrayList一样,在写入操作时进行容量检查,当容量不足时进行扩容。 第二点,ByteBuf通过2个索引readerIndex,writerIndex将数组分为3部分,如下图所示
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
| | (CONTENT) | |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity
初始化时,readerIndex和writerIndex都是0,随着数据的写入writerIndex会增加,此时readable byte部分增加,writable bytes减少。当读取时,discardable bytes增加,readable bytes减少。由于读操作只修改readerIndex,写操作只修改writerIndex,让ByteBuf的使用更加容易理解,避免了由于遗漏flip导致的功能异常。 此外,当调用discardReadBytes方法时,可以把discardable bytes这部分的内存释放。总体想法是通过将readerIndex移动到0,writerIndex移动到writerIndex-readerIndex下标,具体移动下标的方式依据ByteBuf实现类有所不同。这个方法可以显著提高缓冲区的空间复用率,避免无限度的扩容,但会发生字节数组的内存复制,属于以时间换空间的做法。
ByteBuf重要API
read、write、set、skipBytes
前3个系列的方法及最后一个skipBytes都属于改变指针的方法。举例来说,readByte会移动readerIndex1个下标位,而int是4个byte的大小,所以readInt会移动readerIndex4个下标位,相应的,writeByte会移动writerIndex1个下标位,writeInt会移动writerIndex4个下标位。set系列方法比较特殊,它的参数为index和value,意即将value写入指定的index位置,但这个操作不会改变readerIndex和writerIndex。skipBytes比较简单粗暴,直接将readerIndex移动指定长度。
mark和reset
markReaderIndex和markWriterIndex可以将对应的指针做一个标记,当需要重新操作这部分数据时,再使用resetReaderIndex或resetWriterIndex,将对应指针复位到mark的位置。
duplicate、slice、copy
这3种方法都可以复制一份字节数组,不同之处在于duplicate和slice两个方法返回的新ByteBuf和原有的老ByteBuf之间的内容会互相影响,而copy则不会。duplicate和slice的区别在于前者复制整个ByteBuf的字节数组,而后者默认仅复制可读部分,但可以通过slice(index, length)分割指定的区间。
retain、release
这是ByteBuf接口继承自ReferenceCounted接口的方法,用于引用计数,以便在不使用对象时及时释放。实现思路是当需要使用一个对象时,计数加1;不再使用时,计数减1。考虑到多线程场景,一般也多采用AtomicInteger实现。netty却另辟蹊径,选择了volatile + AtomicIntegerFieldUpdater这样一种更节省内存的方式。
ByteBuf扩容
在ByteBuf写入数据时会检查可写入的容量,若容量不足会进行扩容。
final void ensureWritable0(int minWritableBytes) {
if (minWritableBytes <= writableBytes()) {
return;
}
int minNewCapacity = writerIndex + minWritableBytes;
int newCapacity = alloc().calculateNewCapacity(minNewCapacity, maxCapacity);
int fastCapacity = writerIndex + maxFastWritableBytes();
if (newCapacity > fastCapacity && minNewCapacity <= fastCapacity) {
newCapacity = fastCapacity;
}
capacity(newCapacity);
}
忽略一些检验性质的代码后,可以看到扩容时先尝试将现有写索引加上需要写入的容量大小作为最小新容量,并调用ByteBufAllocate的calculateNewCapacity方法进行计算。跟入这个方法:
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
if (minNewCapacity == threshold) {
return threshold;
}
if (minNewCapacity > threshold) {
int newCapacity = minNewCapacity / threshold * threshold;
if (newCapacity > maxCapacity - threshold) {
newCapacity = maxCapacity;
} else {
newCapacity += threshold;
}
return newCapacity;
}
int newCapacity = 64;
while (newCapacity < minNewCapacity) {
newCapacity <<= 1;
}
return Math.min(newCapacity, maxCapacity);
}
可以看到这个方法的目的则是计算比可写容量稍大的2的幂次方。minNewCapacity由上一个方法传入,而maxCapacity则为Integer.MAX_VALUE。具体步骤是首先判断新容量minNewCapacity是否超过了计算限制CALCULATE_THRESHOLD,默认为4M,如果没有超过4MB,那么从64B开始不断以2的幂次方形式扩容,直到newCapacity超过minNewCapacity。而若一开始新容量就超过了4M,则调整新容量到4M的倍数+1。比如newCapacity为6M,因为6/4 = 1,所以调整为(1+1)*4M=8M。
在计算完容量之后会调用capacity方法。这是一个抽象方法,这里以UnpooledHeapByteBuf为例。
public ByteBuf capacity(int newCapacity) {
checkNewCapacity(newCapacity);
byte[] oldArray = array;
int oldCapacity = oldArray.length;
if (newCapacity == oldCapacity) {
return this;
}
int bytesToCopy;
if (newCapacity > oldCapacity) {
bytesToCopy = oldCapacity;
} else {
trimIndicesToCapacity(newCapacity);
bytesToCopy = newCapacity;
}
byte[] newArray = allocateArray(newCapacity);
System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy);
setArray(newArray);
freeArray(oldArray);
return this;
}
首先检查newCapacity是否大于0且小于最大容量。之后准备好老数组要复制的长度。trimIndicesToCapacity(newCapacity)是缩容时调用的,它将readerIndex和newCapacity的较小值设置为新的readerIndex,将newCapacity设置为新的writerIndex。 之后便分配一个新数组,并开始复制旧数组的元素。复制成功后,将新数组保存为成员变量,将老数组释放掉。
ByteBuf种类
出于性能和空间的多方考虑,netty从3个维度定义了各种不同的ByteBuf实现类,主要是池化、堆内堆外、可否使用Unsafe类这3个维度,从而演化出8种不同的ByteBuf,它们分别是PooledUnsafeHeapBytebuf、PooledHeapByteBuf、PooledUnsafeDirectByteBuf、PooledDirectBytebuf、UnpooledUnsafeHeapByteBuf、UnpooledHeapByteBuf、UnpooledUnsafeDirectByteBuf、UnpooledDirectByteBuf。 ByteBuf接口之下有一个抽象类AbstractByteBuf,实现了接口定义的read、write、set相关的方法,但在实现时只做了检查,而具体逻辑则定义一系列以_开头的proteced方法,留待子类实现。
ByteBufAllocate
不同于一般形式的创建对象,ByteBuf需要通过内存分配器ByteBufAllocate分配,对应于不同的ByteBuf也会有不同的BtteBufferAllocate。netty将之抽象为ByteBufAllocate接口。我们看一下有哪些方法:
- buffer()、buffer(initialCapacity)、buffer(initialCapacity、maxCapacity),分配ByteBuf的方法,具体分配的Buffer是堆内还是堆外则由实现类决定。2个重载方法分别以给定初始容量、最大容量的方式分配内存
- ioBuffer()、ioBuffer(initialCapacity)、ioBuffer(initialCapacity、maxCapacity)更倾向于分配堆外内存的方法,因为堆外内存更适合用于IO操作。重载方法同上
- heapBuffer()、heapBuffer(initialCapacity)、heapBuffer(initialCapacity、maxCapacity)分配堆内内存的方法。
- directBuffer()、directBuffer(initialCapacity)、directBuffer(initialCapacity、maxCapacity)分配堆外内存的方法。
- compositeBuffer()。可以将多个ByteBuf合并为一个ByteBuf,多个ByteBuf可以部分是堆内内存,部分是堆外内存。 ByteBufAllocate接口定义了heap和direct这一个维度,其他维度则交由子类来定义。
UnPooledByteBufAllocate
ByteBufAllocate有一个直接实现类AbstractByteBufAllocate,它实现了大部分方法,只留下2个抽象方法newHeapBuffer和newDirectBuffer交由子类实现。AbstractByteBufAllocate有2个子类PooledByteBufAllocate和UnpooledByteBufAllocate,在这里定义了pooled池化维度的分配方式。 看看UnpooledByteBufAllocate如何实现2个抽象方法:
newHeapBuffer
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
return PlatformDependent.hasUnsafe() ?
new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}
可以看到实现类根据PlatformDependent.hasUnsafe()方法自动判定是否使用unsafe维度,这个方法通过在静态代码块中尝试初始化sun.misc.Unsafe来判断Unsafe类是否在当前平台可用,在juc中,这个类使用颇多,作为与高并发打交道的netty,出现这个类不令人意外。UnpooledUnsafeHeapByteBuf与UnpooledHeapByteBuf并不是平级关系,事实上前者继承了后者,在构造方法上也直接调用UnpooledHeapByteBuf的构造方法。构造方法比较简单,初始化byte数组、初始容量、最大容量,将读写指针的设置为0,并将子类传入的this指针保存到alloc变量中。 两种Bytebuf的区别在于unsafe会尝试通过反射的方式创建byte数组,并将数组的地址保存起来,之后再获取数据时也会调用Unsafe的getByte方法,通过数组在内存中的地址+偏移量的形式直接获取,而普通的SafeByteBuf则是保存byte数组,通过数组索引即array[index]访问。
// UnsafeHeapByteBuf初始化数组
protected byte[] allocateArray(int initialCapacity) {
return PlatformDependent.allocateUninitializedArray(initialCapacity);
}
// HeapByteBuf初始化数组
protected byte[] allocateArray(int initialCapacity) {
return new byte[initialCapacity];
}
// UnsafeHeapByteBuf通过UnsafeByteBufUtil获取字节
static byte getByte(byte[] data, int index) {
return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index);
}
// HeapByteBuf获取字节
static byte getByte(byte[] memory, int index) {
return memory[index];
}
newDirectBuffer
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
return PlatformDependent.hasUnsafe() ?
new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
DirectByteBuf构造方法大致与heap的类似,只是保存数据的容器由字节数组变为了jdk的ByteBuffer。相应的,分配与释放内存的方法也变成调用jdk的ByteBuffer方法。而UnsafeByteBuf更是直接用long类型记录内存地址。
// DirectByteBuf获取字节
protected byte _getByte(int index) {
return buffer.get(index);
}
// UnsafeDirectByteBuf获取字节
protected byte _getByte(int index) {
return UnsafeByteBufUtil.getByte(addr(index));
}
// 获取内存地址
final long addr(int index) {
return memoryAddress + index;
}
// UnsafeByteBufUtil获取字节
static byte getByte(long address) {
return UNSAFE.getByte(address);
}
由于PooledByteBufAllocate内容较为庞大,放入下一节讲述。 未完待续···
原文出处:https://www.cnblogs.com/spiritsx/p/12158853.html
BAT面试必问细节:关于Netty中的ByteBuf详解
在Netty中,还有另外一个比较常见的对象ByteBuf,它其实等同于Java Nio中的ByteBuffer,但是ByteBuf对Nio中的ByteBuffer的功能做了很作增强,下面我们来简单了解一下ByteBuf。
下面这段代码演示了ByteBuf的创建以及内容的打印,这里显示出了和普通ByteBuffer最大的区别之一,就是ByteBuf可以自动扩容,默认长度是256,如果内容长度超过阈值时,会自动触发扩容
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
log(buf);
StringBuilder sb=new StringBuilder();
for (int i = 0; i < 32; i++) { //演示的时候,可以把循环的值扩大,就能看到扩容效果
sb.append(" - "+i);
}
buf.writeBytes(sb.toString().getBytes());
log(buf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex()) //获取读索引
.append(" write index:").append(buf.writerIndex()) //获取写索引
.append(" capacity:").append(buf.capacity()) //获取容量
.append(StringUtil.NEWLINE);
//把ByteBuf中的内容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
ByteBuf创建的方法有两种
-
第一种,创建基于堆内存的ByteBuf
ByteBuf buffer=ByteBufAllocator.DEFAULT.heapBuffer(10);
-
第二种,创建基于直接内存(堆外内存)的ByteBuf(默认情况下用的是这种)
Java中的内存分为两个部分,一部分是不需要jvm管理的直接内存,也被称为堆外内存。堆外内存就是把内存对象分配在JVM堆意外的内存区域,这部分内存不是虚拟机管理,而是由操作系统来管理,这样可以减少垃圾回收对应用程序的影响
ByteBufAllocator.DEFAULT.directBuffer(10);
直接内存的好处是读写性能会高一些,如果数据存放在堆中,此时需要把Java堆空间的数据发送到远程服务器,首先需要把堆内部的数据拷贝到直接内存(堆外内存),然后再发送。如果是把数据直接存储到堆外内存中,发送的时候就少了一个复制步骤。
但是它也有缺点,由于缺少了JVM的内存管理,所以需要我们自己来维护堆外内存,防止内存溢出。
另外,需要注意的是,ByteBuf默认采用了池化技术来创建。关于池化技术在前面的课程中已经重复讲过,它的核心思想是实现对象的复用,从而减少对象频繁创建销毁带来的性能开销。
池化功能是否开启,可以通过下面的环境变量来控制,其中unpooled表示不开启。
-Dio.netty.allocator.type={unpooled|pooled}
public class NettyByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();
System.out.println(buf);
}
}
ByteBuf的存储结构
ByteBuf的存储结构如图3-1所示,从这个图中可以看到ByteBuf其实是一个字节容器,该容器中包含三个部分
- 已经丢弃的字节,这部分数据是无效的
- 可读字节,这部分数据是ByteBuf的主体数据,从ByteBuf里面读取的数据都来自这部分; 可写字节,所有写到ByteBuf的数据都会存储到这一段
- 可扩容字节,表示ByteBuf最多还能扩容多少容量。
<center>图3-1</center>
在ByteBuf中,有两个指针
- readerIndex: 读指针,每读取一个字节,readerIndex自增加1。ByteBuf里面总共有witeIndex-readerIndex个字节可读,当readerIndex和writeIndex相等的时候,ByteBuf不可读
- writeIndex: 写指针,每写入一个字节,writeIndex自增加1,直到增加到capacity后,可以触发扩容后继续写入。
- ByteBuf中还有一个maxCapacity最大容量,默认的值是
Integer.MAX_VALUE
,当ByteBuf写入数据时,如果容量不足时,会触发扩容,直到capacity扩容到maxCapacity。
ByteBuf中常用的方法
对于ByteBuf来说,常见的方法就是写入和读取
Write相关方法
对于write方法来说,ByteBuf提供了针对各种不同数据类型的写入,比如
- writeChar,写入char类型
- writeInt,写入int类型
- writeFloat,写入float类型
- writeBytes, 写入nio的ByteBuffer
- writeCharSequence, 写入字符串
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
buf.writeBytes(new byte[]{1,2,3,4}); //写入四个字节
log(buf);
buf.writeInt(5); //写入一个int类型,也是4个字节
log(buf);
}
private static void log(ByteBuf buf){
System.out.println(buf);
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的内容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
扩容
当向ByteBuf写入数据时,发现容量不足时,会触发扩容,而具体的扩容规则是
假设ByteBuf初始容量是10。
- 如果写入后数据大小未超过512个字节,则选择下一个16的整数倍进行库容。 比如写入数据后大小为12,则扩容后的capacity是16。
- 如果写入后数据大小超过512个字节,则选择下一个2^n^。 比如写入后大小是512字节,则扩容后的capacity是2^10^=1024 。(因为2^9^=512,长度已经不够了)
- 扩容不能超过max capacity,否则会报错。
Reader相关方法
reader方法也同样针对不同数据类型提供了不同的操作方法,
- readByte ,读取单个字节
- readInt , 读取一个int类型
- readFloat ,读取一个float类型
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
buf.writeBytes(new byte[]{1,2,3,4});
log(buf);
System.out.println(buf.readByte());
log(buf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的内容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
从下面结果中可以看到,读完一个字节后,这个字节就变成了废弃部分,再次读取的时候只能读取 未读取的部分数据。
read index:0 write index:7 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 |....... |
+--------+-------------------------------------------------+----------------+
1
read index:1 write index:7 capacity:256
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 02 03 04 05 06 07 |...... |
+--------+-------------------------------------------------+----------------+
Process finished with exit code 0
另外,如果想重复读取哪些已经读完的数据,这里提供了两个方法来实现标记和重置
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
buf.writeBytes(new byte[]{1,2,3,4,5,6,7});
log(buf);
buf.markReaderIndex(); //标记读取的索引位置
System.out.println(buf.readInt());
log(buf);
buf.resetReaderIndex();//重置到标记位
System.out.println(buf.readInt());
log(buf);
}
另外,如果想不改变读指针位置来获得数据,在ByteBuf中提供了get
开头的方法,这个方法基于索引位置读取,并且允许重复读取的功能。
ByteBuf的零拷贝机制
需要说明一下,ByteBuf的零拷贝机制和我们之前提到的操作系统层面的零拷贝不同,操作系统层面的零拷贝,是我们要把一个文件发送到远程服务器时,需要从内核空间拷贝到用户空间,再从用户空间拷贝到内核空间的网卡缓冲区发送,导致拷贝次数增加。
而ByteBuf中的零拷贝思想也是相同,都是减少数据复制提升性能。如图3-2所示,假设有一个原始ByteBuf,我们想对这个ByteBuf其中的两个部分的数据进行操作。按照正常的思路,我们会创建两个新的ByteBuf,然后把原始ByteBuf中的部分数据拷贝到两个新的ByteBuf中,但是这种会涉及到数据拷贝,在并发量较大的情况下,会影响到性能。
<center>图3-2</center>
ByteBuf中提供了一个slice方法,这个方法可以在不做数据拷贝的情况下对原始ByteBuf进行拆分,使用方法如下
public static void main(String[] args) {
ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
buf.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,10});
log(buf);
ByteBuf bb1=buf.slice(0,5);
ByteBuf bb2=buf.slice(5,5);
log(bb1);
log(bb2);
System.out.println("修改原始数据");
buf.setByte(2, 5); //修改原始buf数据
log(bb1);//再打印bb1的结果,发现数据发生了变化
}
在上面的代码中,通过slice对原始buf进行切片,每个分片是5个字节。
为了证明slice是没有数据拷贝,我们通过修改原始buf的索引2所在的值,然后再打印第一个分片bb1,可以发现bb1的结果发生了变化。说明两个分片和原始buf指向的数据是同一个。
Unpooled
在前面的案例中我们经常用到Unpooled工具类,它是同了非池化的ByteBuf的创建、组合、复制等操作。
假设有一个协议数据,它有头部和消息体组成,这两个部分分别放在两个ByteBuf中
ByteBuf header=...
ByteBuf body= ...
我们希望把header和body合并成一个ByteBuf,通常的做法是
ByteBuf allBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
在这个过程中,我们把header和body拷贝到了新的allBuf中,这个过程在无形中增加了两次数据拷贝操作。那有没有更高效的方法减少拷贝次数来达到相同目的呢?
在Netty中,提供了一个CompositeByteBuf组件,它提供了这个功能。
public class ByteBufExample {
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();
//其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex.
//默认是false,也就是writeIndex=0,这样的话我们不可能从compositeByteBuf中读取到数据。
compositeByteBuf.addComponents(true,header,body);
log(compositeByteBuf);
}
private static void log(ByteBuf buf){
StringBuilder builder=new StringBuilder()
.append(" read index:").append(buf.readerIndex())
.append(" write index:").append(buf.writerIndex())
.append(" capacity:").append(buf.capacity())
.append(StringUtil.NEWLINE);
//把ByteBuf中的内容,dump到StringBuilder中
ByteBufUtil.appendPrettyHexDump(builder,buf);
System.out.println(builder.toString());
}
}
之所以CompositeByteBuf能够实现零拷贝,是因为在组合header和body时,并没有对这两个数据进行复制,而是通过CompositeByteBuf构建了一个逻辑整体,里面仍然是两个真实对象,也就是有一个指针指向了同一个对象,所以这里类似于浅拷贝的实现。
wrappedBuffer
在Unpooled工具类中,提供了一个wrappedBuffer方法,来实现CompositeByteBuf零拷贝功能。使用方法如下。
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
ByteBuf allBb=Unpooled.wrappedBuffer(header,body);
log(allBb);
//对于零拷贝机制,修改原始ByteBuf中的值,会影响到allBb
header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8);
log(allBb);
}
copiedBuffer
copiedBuffer,和wrappedBuffer最大的区别是,该方法会实现数据复制,下面代码演示了copiedBuffer和wrappedbuffer的区别,可以看到在case
标注的位置中,修改了原始ByteBuf的值,并没有影响到allBb。
public static void main(String[] args) {
ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可自动扩容
header.writeCharSequence("header", CharsetUtil.UTF_8);
ByteBuf body=ByteBufAllocator.DEFAULT.buffer();
body.writeCharSequence("body", CharsetUtil.UTF_8);
ByteBuf allBb=Unpooled.copiedBuffer(header,body);
log(allBb);
header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8); //case
log(allBb);
}
内存释放
针对不同的ByteBuf创建,内存释放的方法不同。
- UnpooledHeapByteBuf,使用JVM内存,只需要等待GC回收即可
- UnpooledDirectByteBuf,使用对外内存,需要特殊方法来回收内存
- PooledByteBuf和它的之类使用了池化机制,需要更复杂的规则来回收内存
如果ByteBuf是使用堆外内存来创建,那么尽量手动释放内存,那怎么释放呢?
Netty采用了引用计数方法来控制内存回收,每个ByteBuf都实现了ReferenceCounted接口。
- 每个ByteBuf对象的初始计数为1
- 调用release方法时,计数器减一,如果计数器为0,ByteBuf被回收
- 调用retain方法时,计数器加一,表示调用者没用完之前,其他handler即时调用了release也不会造成回收。
- 当计数器为0时,底层内存会被回收,这时即使ByteBuf对象还存在,但是它的各个方法都无法正常使用
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自
Mic带你学架构
! 如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!
bytebuf池_Netty ByteBuf[通俗易懂]
大家好,又见面了,我是你们的朋友全栈君。
ByteBuf
ByteBuf需要提供JDK ByteBuffer的功能(包含且不限于),主要有以下几类基本功能:
7种Java基础类型、byte[]、ByteBuffer(ByteBuf)的等的读写
缓冲区自身的copy和slice
设置网络字节序
构造缓冲区实例
操作位置指针
扩容原理
首先确认ByteBuf是否已经被释放,如果被释放,则抛出IllegalReferenceCountException异常
判断写入需要的最小空间,如果该空间小于ByteBuf的可写入空间,直接返回,不进行扩容
判断写入需要的最小空间,如果该空间大于ByteBuf的(最大容量-当前的写索引),不进行扩容,抛出indexoutofboundsexception异常
计算新容量,动态扩容的规则,当新容量大于4MB时,以4MB的方式递增扩容,在小于4MB时,从64字节开始倍增(Double)扩容
读写索引
Netty提供readindex和writeIndex用来支持读取和写入操作,两个索引将缓冲区划分为三个区域
0 ~ readindex:已读区域(可丢弃区域)
readindex ~ writeIndex:未读取区域
writeIndex ~ capacity:待写入区域
已读区域(discardable Bytes)
位于已读区域的内容表明该内容已被Netty处理完成,我们可以重用这块缓冲区,尽量减少缓冲区的动态扩容(复制,耗时操作)。
调用discardBytes()方法可以清除已读区域内容,但同时会导致未读区域的左移,也是将未读区域的内容复制到原来的已读区域(耗时),
因此频繁的调用discardBytes也是不可取的,可以根据实际情况进行调用。
Readable Bytes和Writable Bytes
Readable Bytes(可读空间)存储的是未被读取处理的内容,以read或者skip开头的方法都会从readindex开始读取或者跳过指定的数据,同时readindex会增加读取或跳过
的字节数长度。如果读取的字节数长度大于实际可读取的字节数,抛出indexoutofboundsexception异常。
Writable Bytes(可写入空间)是未被数据填充的缓冲区块,以write开头的操作都会从writeIndex开始向缓冲区写入数据,同时writeIndex会增加写入的数据的字节数长度。
如果写入的字节数大于可写入的字节数,会抛出indexoutofboundsexception异常。
Clear
Clear操作并不会清除缓冲区的内容,只是将readindex和writeIndex还原为初始分配值。
Mark和Reset
markReadindex
resetReadindex
markWriteIndex
resetWriteIndex
查找操作
indexOf(int fromIndex, int toIndex, byte value):fromIndex<=toIndex时,从头开始查找首次出现value的位置(查找范围fromIndex ~ toIndex),当fromIndex > toIndex时,倒着查找首次出现value的位置(查找的范围toIndex ~ fromIndex – 1),查不到返回-1
bytesBefore(byte value):从ByteBuf的可读区域中首次定位出现value的位置,没有找到返回-1。该方法不会修改readindex和writeIndex
bytesBefore(int length, byte value):从ByteBuf的可读区域中定位首次出现value的位置,结束索引是readindex+length。如果length大于可读字节数,抛出indexoutofboundsexception异常
bytesBefore(int index, int length, byte value):从ByteBuf中定位首次出现value的位置,起始索引为index,结束索引为index+length,如果index+length大于当前缓冲区的容量,抛出indexoutofboundsexception异常
forEachByte(int index, int length, ByteProcessor processor):从index开始,到index + length结束,与ByteProcessor设置的查找条件进行对比,满足条件,返回位置索引,否则返回-1
forEachByteDesc(ByteProcessor processor):倒序遍历ByteBuf的可读字节数组,与ByteProcessor设置的查找条件进行对比,满足条件,返回位置索引,否则返回-1
forEachByteDesc(int index, int length, ByteProcessor processor):以index + length – 1开始,直到index结束,倒序遍历ByteBuf字节数组,与ByteProcessor设置的查找条件进行对比,满足条件,返回位置索引,否则返回-1
Netty提供了大量的默认的ByteProcessor,来对常用的查找自己进行查找,具体可见ByteProcessor接口。
Derived buffers(派生缓冲区)
duplicate():返回当前ByteBuf的复制对象,复制后返回的ByteBuf与操作的ByteBuf共享缓冲区内容,但是维护自己独立的读写索引。当修改复制后的ByteBuf内容后,原ByteBuf的内容也随之改变,因为双方持有的是同一个内容的指针引用。
copy():复制一个新的ByteBuf对象,内容和索引都与原ByteBuf独立,复制操作本身并不修改原ByteBuf的读写索引
copy(int index, int length):复制一个新的ByteBuf对象,复制开始的索引为index,复制的长度为length
slice():返回与当前ByteBuf的可读子缓冲区,范围是readindex ~ writeIndex,返回后的ByteBuf与原ByteBuf内容共享,读写索引独立维护,maxCapacity是当前ByteBuf的可读字节数(换句话说就是这个新返回的缓冲区不能再进行写入)
slice(int index, int length):返回index开始,length长度的当前ByteBuf的子缓冲区,返回后的ByteBuf与原ByteBuf内容共享,读写索引独立维护,maxCapacity是length(换句话说就是这个新返回的缓冲区不能再进行写入)
转换成标准的ByteBuffer
ByteBuffer nioBuffer():将当前ByteBuf可读的缓冲区转换成ByteBuffer,两者共享同一个缓冲区内容引用,对ByteBuffer的读写操作并不会修改原ByteBuf的读写索引。返回后的ByteBuffer无法感知ByteBuf的动态扩展。
ByteBuffer nioBuffer(int index, int length):从ByteBuf的index位置开始长度为length的缓冲区转换成ByteBuffer,两者共享同一个缓冲区内容引用,对ByteBuffer的读写操作并不会修改原ByteBuf的读写索引。返回后的ByteBuffer无法感知ByteBuf的动态扩展。
随机读写
主要通过set和get开头的方法,这两个方法可以指定索引位置。
ByteBuf源码
从内存分配的角度来看,ByteBuf主要分为以下两类:
堆内存(HeapByteBuf)字节缓冲区:内存分配和回收速度快,可以被JVM自动回收;缺点是如果Socket进行I/O读写,需要进行一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有所下降
直接内存(DirectByteBuf)字节缓冲区:堆外内存直接分配,相比于堆内存,分配和回收速度比较慢,但是在Socket Channel中进行读写比较快(少一次内存复制)
ByteBuf的最佳时间是在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf。
从内存回收的角度进行分类:
基于对象池的ByteBuf:自己维护了一个内存池,可以重复利用ByteBuf对象,提升内存使用率,降低GC频率
普通的ByteBuf
AbstractByteBuf
AbstractByteBuf继承ByteBuf,ByteBuf中的一些公共属性和方法会在AbstractByteBuf中实现。
主要变量
ResourceLeakDetector leakDetector对象:被定义为static,所有的ByteBuf实例共享一个ResourceLeakDetector leakDetector对象。ResourceLeakDetector主要用来检测对象是否泄漏。
索引设置:读写索引、重置读写索引、最大容量
读操作
读操作的公共功能由父类实现,差异化由具体的子类实现。
选取readBytes(byte[] dst, int dstIndex, int length)分析:
首先对缓冲区可读空间进行校验:如果读取的长度(length) < 0,会抛出IllegalArgumentException异常;如果可读的字节数小于需要读取的长度(length),会抛出indexoutofboundsexception异常
校验通过之后,调用getBytes方法从当前的读索引开始进行读取(这一块就需要由真正的子类来各自实现),复制length个字节到目标byte数组,数组开始的位置是dstIndex
读取成功后,对读索引进行递增,增加的长度为length
写操作
写操作的公共功能由父类实现,差异化由具体的子类实现。
选取writeBytes(byte[] src, int srcIndex, int length)分析:
首先对缓冲区的可写空间进行校验:如果要写入的长度(length) < 0,会抛出IllegalArgumentException异常;如果要写入的长度小于缓冲区可写入的字节数,表明可写;如果要写入的长度 > 最大容量 – writeIndex,会抛出indexoutofboundsexception;否则进行扩容操作(扩容操作的原理前面已经讲过)。
操作索引
与索引相关的操作主要涉及设置读写索引、mark、和reset等。
选取readerIndex(int readerIndex)进行分析:
首先对索引合法性进行判断:如果readerIndex小于0或者readerIndex > writeIndex,则抛出indexoutofboundsexception异常
校验通过之后,将读索引设置为readerIndex
重用缓冲区
选取discardReadBytes()进行分析:
如果readindex等于0,直接返回
如果readindex和writeIndex不相等,首先调用setBytes(int index, ByteBuf src, int srcIndex, int length)方法进行字节数组的复制,
然后重新设置markReadindex、markWriteIndex、readindex和writeIndex
如果readindex等于writeIndex,调整markReadindex和markWriteIndex,不进行字节数组复制,设置readindex=writeIndex=0
skipBytes
校验跳过的字节长度:如果跳过的字节长度小于0,则抛出IllegalArgumentException异常,如果跳过的字节数大于可读取的字节数,则抛出indexoutofboundsexception异常
校验通过之后,readindex增加跳过的字节长度
AbstractReferenceCountedByteBuf
该类主要是对引用进行计数,类似于JVM内存回收的对象引用计数器,用于跟踪对象的分配和销毁,做自动内存回收。
成员变量
AtomicIntegerFieldUpdater refCntUpdater对象:通过原子的方式对成员变量进行更新操作,实现线程安全,消除锁。
volatile int refCnt:用于跟踪对象的引用次数,使用volatile是为了解决多线程并发访问的可见性问题。
对象引用计数器
每调用retain()方法一次,引用计数器就会加1,但加完之后会对数据进行校验,具体的校验内容如下:
如果加1之前的引用次数小于等于0或者原来的引用次数 + 增加的次数 < 原来的引用次数,则需要还原这次引用计数器增加操作,并且抛出IllegalReferenceCountException异常
UnpooledHeapByteBuf
UnpooledHeapByteBuf是基于堆内存分配的字节缓冲区,每次I/O读写都会创建一个新的UnpooledHeapByteBuf。
成员变量
ByteBufAllocator alloc:用于UnpooledHeapByteBuf的内存分配
byte[] array:缓冲区数组,此处也可用ByteBuffer,但是用byte数组的原因是提升性能和更加便捷的进行位操作
ByteBuffer tmpNioBuf:用于实现Netty的ByteBuf到JDK NIO ByteBuffer的转换
动态扩展缓冲区
校验新容量:如果新容量小于0或者新容量大于最大容量,抛出IllegalArgumentException异常,否则校验通过
如果新容量大于旧容量,使用new byte[newCapacity]创建新的缓冲数组,然后通过System.arraycopy进行复制,将旧的缓冲区内容拷贝到新的缓冲区中,最后在ByteBuf中替换旧的数组,并且将原来的ByteBuffer tmpNioBuf置为空
如果新容量小于旧容量,使用new byte[newCapacity]创建新的缓冲数组,如果读索引小于新容量(如果写索引大于新容量,将写索引直接置为新容量),然后通过System.arraycopy将当前可读的缓冲区内容复制到新的byte数组,如果读索引大于新容量,说明没有可以拷贝的缓冲区,直接将读写索引置为新容量,并且使用新的byte数组替换原来的字节数组
字节数组复制
setBytes(int index, byte[] src, int srcIndex, int length)
首先是合法性校验,先是校验index,length,如果这两个值有小于0,或者相加小于0,或者两个相加大于ByteBuf的容量,则抛出indexoutofboundsexception异常,接着校验被复制的数组的长度和索引问题(srcIndex、length),如果srcIndex、length小于0,或者两个相加小于0,或者两个相加超过了src字节数组的容量,也抛出indexoutofboundsexception异常
校验通过之后,使用System.arraycopy方法进行字节数组的拷贝
ByteBuf以get和set开头读写缓冲区的方法不会修改读写索引
转换成JDK ByteBuffer
由于UnpooledHeapByteBuf缓冲区采用了byte数组实现,同样的ByteBuffer底层也是用了byte数组实现,同时ByteBuffer还提供了wrap方法,
直接将字节数组转换成ByteBuffer,最后调用slice方法。由于每次调用都会创建一个新的ByteBuffer,因此起不到重用缓冲区内容的效果。
子类实现相关的方法
hasArray():是否支持数组,判断缓冲区的实现是否基于字节数组
array():如果缓冲区的实现基于字节数组,返回字节数组
PooledByteBuf
PoolArena
Arena是指一块区域,在内存管理中,Memory Arena指内存中的一大块连续的区域,PoolArena是Netty的内存池实现类。
为了集中管理内存的分配和释放,同时提高分配和释放内存的性能,框架会预先申请一大块内存,然后通过提供相应的分配和释放接口来使用内存。由于不再使用系统调用来申请和释放内存,
应用或者系统的性能大大提高。预先申请的那一大块内存称之为Memory Arena。
Netty的PoolArena是由多个Chunk组成的大块内存区域,每个Chunk由一个或者多个Page组成。因此,对内存的组织管理主要集中在如何组织管理Chunk和Page。
PoolChunk
Chunk主要用来组织和管理多个Page的内存分配。Netty中,Chunk中的Page被构造成一棵二叉树。
每一个Page可以成为一个节点,第一层的Page节点用来分配所有Page需要的内存。每个节点记录了自己在Memory Arena中的偏移地址,当一个节点代表的内存区域被分配出去以后,
该节点会被标记为已分配,从这个节点往下的所有节点在后面的内存分配请求中都会被忽略。
在内存分配查找节点时,对树的遍历采用深度优先的算法,但在选择在哪个子节点继续遍历时则是随机的,并不总是访问左边的子节点。
PoolSubpage
对于小于一个Page的内存,Netty在Page中完成分配。每个Page会被切分成大小相等的多个存储块,存储块的大小由第一次申请的内存块大小决定。
一个Page只能用于分配与第一次申请时大小相同的内存。
Page中存储区域的使用状态通过一个long数组来维护,数组中每个long的每一位表示一个块存储区域的占用情况:0表示未占用,1表示已占用。
内存回收策略
Chunk和Page都通过状态位来标识内存是否可用,不同的是Chunk通过在二叉树上对节点进行标识实现,Page是通过维护块的使用状态标识来实现。
PooledDirectByteBuf
PooledDirectByteBuf基于内存池实现。
创建字节缓冲区实例
新创建PooledDirectByteBuf对象不能直接new,而是从内存池Recycler中获取,然后设置引用计数器的值为1,设置缓冲区的最大空间,
设置读写索引、标记读写索引为0。
复制新的字节缓冲区实例
copy(int index, int length)方法可以复制一个ByteBuf实例,并且与原来的ByteBuf相互独立。
首先校验索引和长度的合法性
校验通过之后,调用PooledByteBufAllocator分配一个新的ByteBuf,最终会调用PooledByteBufAllocator中的newDirectBuffer(int initialCapacity, int maxCapacity)方法进行内存的分配
在newDirectBuffer中,直接从缓存中获取ByteBuf而不是创建一个新的对象
ByteBuf辅助类
ByteBufHolder
ByteBufHolder是BytBuf容器。比如,Http协议的请求消息和应答消息都可以携带消息体,这个消息体在Netty中就是ByteBuf对象。由于不同的协议消息体可以包含不同的
协议字段和功能,因此需要对ByteBuf进行包装和抽象,为了满足这些定制化的需求,Netty抽象出了ByteBufHolder对象。
ByteBufAllocator
ByteBufAllocator是字节缓冲区分配器,按照Netty缓冲区的实现不同可以分为:基于内存池的字节缓冲区分配器和普通的字节缓冲区分配器。
方法名称
返回值说明
功能说明
buffer()
ByteBuf
分配一个字节缓冲区,缓冲区的类型由ByteBufAllocator的实现类决定
buffer(int initialCapacity)
ByteBuf
分配一个初始容量为initialCapacity的字节缓冲区,缓冲区的类型由ByteBufAllocator的实现类决定
buffer(int initialCapacity, int maxCapacity)
ByteBuf
分配一个初始容量为initialCapacity,最大容量为maxCapacity的字节缓冲区,缓冲区的类型由ByteBufAllocator的实现类决定
ioBuffer(int initialCapacity, int maxCapacity)
ByteBuf
分配一个初始容量为initialCapacity,最大容量为maxCapacity的Direct Buffer,Direct Buffer I/O性能高
heapBuffer(int initialCapacity, int maxCapacity)
ByteBuf
分配一个初始容量为initialCapacity,最大容量为maxCapacity的Heap Buffer
directBuffer(int initialCapacity, int maxCapacity)
ByteBuf
分配一个初始容量为initialCapacity,最大容量为maxCapacity的Direct Buffer
compositeBuffer(int maxnumComponents)
CompositeByteBuf
分配一个最多包含maxnumComponents个缓冲区的复合缓冲区,缓冲区的类型由ByteBufAllocator的实现类决定
isDirectBufferPooled()
boolean
是否使用了直接内存池
calculateNewCapacity(int minNewCapacity, int maxCapacity)
int
动态扩容时计算新容量
CompositeByteBuf
CompositeByteBuf允许将多个ByteBuf的实例组装到一起。
CompositeByteBuf定义了一个Component类型的集合,Component实际上是ByteBuf的包装实现类,它聚合了ByteBuf对象,维护ByteBuf在集合中的位置偏移量等信息。
CompositeByteBuf支持动态增加(addComponent(ByteBuf buffer))和删除(removeComponent(int cIndex))ByteBuf,增加或删除ByteBuf之后,
需要更新各个ByteBuf的索引偏移量。
ByteBufUtil
ByteBufUtil提供了大量的静态方法来操作ByteBuf。列举三个:
ByteBuf encodeString(ByteBufAllocator alloc, CharBuffer src, Charset charset):对需要编码的字符串src按照指定的字符集charset进行编码,利用指定的ByteBufAllocator生成一个ByteBuf
decodeString(ByteBuf src, int readerIndex, int len, Charset charset):从指定索引readindex开始往后len个字节长度,对ByteBuf对象src按照指定的字符集charset进行解码
hexDump(ByteBuf buffer):将ByteBuf对象的参数内容以十六进制的格式输出
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/191670.html原文链接:https://javaforall.cn
io.netty.buffer.AbstractByteBuf的实例源码
public static List<Class> getPipeGenDependencies() { return new ArrayList<Class>() {{ add(InterceptedFileOutputStream.class); add(InterceptedFileInputStream.class); add(InterceptedOutputStreamWriter.class); add(InterceptedBufferedWriter.class); add(InterceptedInputStreamReader.class); add(InterceptedBufferedReader.class); add(Interceptedbufferedoutputstream.class); add(InterceptedFileChannel.class); add(InterceptedTextInputFormat.class); add(InterceptedFSDataInputStream.class); add(OptimizedInterceptedFileOutputStream.class); add(OptimizedInterceptedFileInputStream.class); add(OptimizedInterceptedOutputStreamWriter.class); add(OptimizedInterceptedBufferedWriter.class); add(OptimizedInterceptedInputStreamReader.class); add(OptimizedInterceptedBufferedReader.class); add(OptimizedInterceptedbufferedoutputstream.class); add(InterceptUtilities.class); add(InterceptMetadata.class); add(RuntimeConfiguration.class); add(WorkerDirectoryClient.class); add(WorkerDirectoryEntry.class); add(Direction.class); add(org.brandonhaynes.pipegen.instrumentation.injected.java.String.class); add(org.brandonhaynes.pipegen.instrumentation.injected.java.StringBuffer.class); add(org.brandonhaynes.pipegen.instrumentation.injected.java.StringBuilder.class); add(AugmentedString.class); add(AugmentedStringBuilder.class); add(AugmentedStringBuffer.class); add(AugmentedResultSet.class); add(ArrayUtilities.class); add(ColumnUtilities.class); add(StreamUtilities.class); add(StringUtilities.class); add(PathUtilities.class); add(ThreadUtilities.class); add(CompositeVector.class); add(CompositeVector.Accessor.class); add(CompositeVector.Mutator.class); add(CompositeVector.Reader.class); JarUtilities.getClasses(getJarPath(Metric.class),false).forEach(this::add); JarUtilities.getClasses(getJarPath(ArrowBuf.class),false).forEach(this::add); JarUtilities.getClasses(getJarPath(ZeroVector.class),false).forEach(this::add); JarUtilities.getClasses(getJarPath(AbstractByteBuf.class),false).forEach(this::add); }}; }
关于Netty的ByteBuf为什么无法读取字节?和netty bytebuf的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于7.netty内存管理-ByteBuf、BAT面试必问细节:关于Netty中的ByteBuf详解、bytebuf池_Netty ByteBuf[通俗易懂]、io.netty.buffer.AbstractByteBuf的实例源码等相关知识的信息别忘了在本站进行查找喔。
本文标签: