5. ByteBuf
作者:互联网
第5章 ByteBuf
Netty提供的ByteBuf与JDK的ByteBuffer相比,前者具有卓越的功能性和灵活性。
5.1 ByteBuf的API
ByteBuf提供读访问索引(readerIndex)和写访问索引(writerIndex)来控制字节数组。ByteBuf API具有以下优点:
-
- 允许用户自定义缓冲区类型扩展
-
- 通过内置的复合缓冲区类型实现透明的零拷贝
-
- 容量可按需增长(类似于 JDK 的 StringBuilder)
-
- 读写这两种模式之间不需要调用类似于JDK的ByteBuffer的flip()方法进行切换
-
- 读和写使用不同的索引
-
- 支持方法的链式调用
-
- 支持引用计数
-
- 支持池化
5.2 ByteBuf类 ----- Netty的数据容器
5.2.1 ByteBuf如何工作的
ByteBuf维护两个不同的索引: 读索引(readerIndex)和写索引(writerIndex)。如下图:
-
- ByteBuf维护了readerIndex和writerIndex索引
-
- 当readerIndex > writerIndex时,则抛出IndexOutOfBoundsException
-
- ByteBuf容量 = writerIndex。
-
- ByteBuf可读容量 = writerIndex - readerIndex
-
- readXXX()和writeXXX()方法将会推进其对应的索引。自动推进
-
- getXXX()和setXXX()方法将对writerIndex和readerIndex无影响
5.2.2 ByteBuf的使用模式
ByteBuf本质是: 一个由不同的索引分别控制读访问和写访问的字节数组。
请记住这句话。ByteBuf共有三种模式: 堆缓冲区模式(Heap Buffer)、直接缓冲区模式(Direct Buffer)和复合缓冲区模式(Composite Buffer)
1. 堆缓冲区模式(Heap Buffer)
堆缓冲区模式又称为:支撑数组(backing array)。将数据存放在JVM的堆空间,通过将数据存储在数组中实现
能在没有使用池化的情况下提供快速的分配和释放。
-
- 堆缓冲的优点: 由于数据存储在Jvm堆中可以快速创建和快速释放,并且提供了数组直接快速访问的方法
-
- 堆缓冲的缺点: 每次数据与I/O进行传输时,都需要将数据拷贝到直接缓冲区
代码:
/**
* 代码清单 5-1 堆缓冲区
*/
public static void heapBuffer() {
// 创建Java堆缓冲区
ByteBuf heapBuf = Unpooled.buffer();
//检查ByteBuf是否有一个支撑数组
//当 hasArray()方法返回 false 时,尝试访问支撑数组将触发一个 Unsupported OperationException。这个模式类似于 JDK 的 ByteBuffer 的用法。
if (heapBuf.hasArray()) {
byte[] array = heapBuf.array();
//计算第一个字节的偏移量
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
int length = heapBuf.readableBytes();
//使用数组、偏移量和长度作为参数调用你的方法
handleArray(array, offset, length);
}
}
2. 直接缓冲区模式(Direct Buffer)
Direct Buffer属于堆外分配的直接内存,不会占用堆的容量。适用于套接字传输过程,避免了数据从内部缓冲区拷贝到直接缓冲区的过程,性能较好
-
- Direct Buffer的优点: 使用Socket传递数据时性能很好,避免了数据从Jvm堆内存拷贝到直接缓冲区的过程。提高了性能
-
- Direct Buffer的缺点: 相对于堆缓冲区而言,Direct Buffer分配内存空间和释放更为昂贵
-
- 对于涉及大量I/O的数据读写,建议使用Direct Buffer。而对于用于后端的业务消息编解码模块建议使用Heap Buffer
代码:
public static void directBuffer() {
ByteBuf directBuf = Unpooled.directBuffer();
if (!directBuf.hasArray()) {
int length = directBuf.readableBytes();
byte[] array = new byte[length];
directBuf.getBytes(directBuf.readerIndex(), array);
handleArray(array, 0, length);
}
}
3. 复合缓冲区模式(Composite Buffer)
Composite Buffer是Netty特有的缓冲区。本质上类似于提供一个或多个ByteBuf的组合视图,可以根据需要添加和删除不同类型的ByteBuf。
警告 CompositeByteBuf 中的 ByteBuf 实例可能同时包含直接内存分配和非直接内存分配。
如果其中只有一个实例,那么对 CompositeByteBuf 上的 hasArray()方法的调用将返回该组
件上的 hasArray()方法的值;否则它将返回 false。
-
- 想要理解Composite Buffer,请记住:它是一个组合视图。它提供一种访问方式让使用者自由的组合多个ByteBuf,避免了拷贝和分配新的缓冲区。
-
- Composite Buffer不支持访问其支撑数组。因此如果要访问,需要先将内容拷贝到堆内存中,再进行访问
-
- 下图是将两个ByteBuf:头部+Body组合在一起,没有进行任何复制过程。仅仅创建了一个视图
下面代码使用 JDK 的 ByteBuffer 来实现这一需求。
创建了一个包含两个 ByteBuffer 的数组用来保存这些消息组件,
同时创建了第三个 ByteBuffer 用来保存所有这些数据的副本。
// Use an array to hold the message parts
ByteBuffer[] message = new ByteBuffer[] { header, body };
// Create a new ByteBuffer and use copy to merge the header and body
ByteBuffer message2 =
ByteBuffer.allocate(header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip();
分配和复制操作,以及伴随着对数组管理的需要,使得这个版本的实现效率低下而且笨拙。
下面展示了一个使用了 CompositeByteBuf 的版本。
代码:
public static void byteBufComposite() {
// 复合缓冲区,只是提供一个视图
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = Unpooled.buffer(); // can be backing or direct
ByteBuf bodyBuf = Unpooled.directBuffer(); // can be backing or direct
messageBuf.addComponents(headerBuf, bodyBuf);
messageBuf.removeComponent(0); // remove the header
for (ByteBuf buf : messageBuf) {
System.out.println(buf.toString());
}
}
CompositeByteBuf 可能不支持访问其支撑数组,因此访问 CompositeByteBuf 中的数据类似于(访问)直接缓冲区的模式,如下
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
int length = compBuf.readableBytes();
byte[] array = new byte[length];
compBuf.getBytes(compBuf.readerIndex(), array);
handleArray(array, 0, array.length);
Netty使用了CompositeByteBuf来优化套接字的I/O操作,尽可能地消除了
由JDK的缓冲区实现所导致的性能以及内存使用率的惩罚
5.3 字节级操作
5.3.1 随机访问索引
ByteBuf的索引与普通的Java字节数组一样。第一个字节的索引是0,最后一个字节索引总是capacity()-1。请记住下列两条,非常有用:
-
- readXXX()和writeXXX()方法将会推进其对应的索引readerIndex和writerIndex。自动推进
-
- getXXX()和setXXX()方法用于访问数据,对writerIndex和readerIndex无影响
代码:
public static void byteBufRelativeAccess() {
ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i);// 不改变readerIndex值
System.out.println((char) b);
}
}
5.3.2 顺序访问索引
Netty的ByteBuf同时具有读索引和写索引,但JDK的ByteBuffer只有一个索引,所以JDK需要调用flip()方法在读模式和写模式之间切换。
-
- ByteBuf被读索引和写索引划分成3个区域:可丢弃字节区域,可读字节区域和可写字节区域
- ByteBuf被读索引和写索引划分成3个区域:可丢弃字节区域,可读字节区域和可写字节区域
5.3.3 可丢弃字节区域
可丢弃字节区域是指:[0,readerIndex)之间的区域。可调用discardReadBytes()方法丢弃已经读过的字节。
-
- discardReadBytes()效果 ----- 将可读字节区域(CONTENT)[readerIndex, writerIndex)往前移动readerIndex位,同时修改读索引和写索引。
-
- discardReadBytes()方法会移动可读字节区域内容(CONTENT)。如果频繁调用,会有多次数据复制开销,对性能有一定的影响
5.3.4 可读字节区域
可读字节区域是指:[readerIndex, writerIndex)之间的区域。任何名称以read和skip开头的操作方法,都会改变readerIndex索引。
5.3.5 可写字节区域
可写字节区域是指:[writerIndex, capacity)之间的区域。任何名称以write开头的操作方法都将改变writerIndex的值。
5.3.6 索引管理
-
- markReaderIndex()+resetReaderIndex() ----- markReaderIndex()是先备份当前的readerIndex,resetReaderIndex()则是将刚刚备份的readerIndex恢复回来。常用于dump ByteBuf的内容,又不想影响原来ByteBuf的readerIndex的值
-
- readerIndex(int) ----- 设置readerIndex为固定的值
-
- writerIndex(int) ----- 设置writerIndex为固定的值
-
- clear() ----- 效果是: readerIndex=0, writerIndex(0)。不会清除内存
-
- 调用clear()比调用discardReadBytes()轻量的多。仅仅重置readerIndex和writerIndex的值,不会拷贝任何内存,开销较小。
5.3.7 查找操作(indexOf)
查找ByteBuf指定的值。类似于,String.indexOf("str")操作
-
- 最简单的方法 ----- indexOf()
-
- 利用ByteProcessor作为参数来查找某个指定的值。
代码:
public static void byteProcessor() {
ByteBuf buffer = Unpooled.buffer(); //get reference form somewhere
// 使用indexOf()方法来查找
buffer.indexOf(buffer.readerIndex(), buffer.writerIndex(), (byte)8);
// 使用ByteProcessor查找给定的值
int index = buffer.forEachByte(ByteProcessor.FIND_CR);
}
5.3.8 派生缓冲区 ----- 视图
派生缓冲区为ByteBuf提供了一个访问的视图。视图仅仅提供一种访问操作,不做任何拷贝操作。下列方法,都会呈现给使用者一个视图,以供访问:
-
- duplicate()
-
- slice()
-
- slice(int, int)
-
- Unpooled.unmodifiableBuffer(...)
-
- Unpooled.wrappedBuffer(...)
-
- order(ByteOrder)
-
- readSlice(int)
理解
-
- 上面的6中方法,都会返回一个新的ByteBuf实例,具有自己的读索引和写索引。但是,其内部存储是与原对象是共享的。这就是视图的概念
-
- 请注意:如果你修改了这个新的ByteBuf实例的具体内容,那么对应的源实例也会被修改,因为其内部存储是共享的
-
- 如果需要拷贝现有缓冲区的真实副本,请使用copy()或copy(int, int)方法。
-
- 使用派生缓冲区,避免了复制内存的开销,有效提高程序的性能
代码:
public static void byteBufSlice() {
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
ByteBuf sliced = buf.slice(0, 15);
System.out.println(sliced.toString(utf8));
buf.setByte(0, (byte)'J');
assert buf.getByte(0) == sliced.getByte(0); // return true
}
public static void byteBufCopy() {
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
ByteBuf copy = buf.copy(0, 15);
System.out.println(copy.toString(utf8));
buf.setByte(0, (byte)'J');
assert buf.getByte(0) != copy.getByte(0); // return true
}
5.3.9 读/写操作
如上文所提到的,有两种类别的读/写操作:
-
- get()和set()操作 ----- 从给定的索引开始,并且保持索引不变
-
- read()和write()操作 ----- 从给定的索引开始,并且根据已经访问过的字节数对索引进行访问
-
- 下图给出get()操作API,对于set()操作、read()操作和write操作可参考书籍或API
5.3.10 更多的操作
下面的两个方法操作字面意思较难理解,给出解释:
-
- hasArray() ----- 如果ByteBuf由一个字节数组支撑,则返回true。通俗的讲:ByteBuf是堆缓冲区模式,则代表其内部存储是由字节数组支撑的。如果还没理解,可参考5.2.2章节
-
- array() ----- 如果ByteBuf是由一个字节数组支撑泽返回数组,否则抛出UnsupportedOperationException异常。也就是,ByteBuf是堆缓冲区模式
5.4 ByteBufHolder接口
ByteBufHolder为Netty的高级特性提供了支持,如缓冲区池化,可以从池中借用ByteBuf,并且在需要时自动释放。
-
- ByteBufHolder是ByteBuf的容器,可以通过子类实现ByteBufHolder接口,根据自身需要添加自己需要的数据字段。可以用于自定义缓冲区类型扩展字段。
-
- Netty提供了一个默认的实现DefaultByteBufHolder。
代码
public class CustomByteBufHolder extends DefaultByteBufHolder{
private String protocolName;
public CustomByteBufHolder(String protocolName, ByteBuf data) {
super(data);
this.protocolName = protocolName;
}
@Override
public CustomByteBufHolder replace(ByteBuf data) {
return new CustomByteBufHolder(protocolName, data);
}
@Override
public CustomByteBufHolder retain() {
super.retain();
return this;
}
@Override
public CustomByteBufHolder touch() {
super.touch();
return this;
}
@Override
public CustomByteBufHolder touch(Object hint) {
super.touch(hint);
return this;
}
...
}
5.5 ByteBuf分配
创建和管理ByteBuf实例的多种方式:按需分配(ByteBufAllocator)、Unpooled缓冲区和ByteBufUtil类
5.5.1 按序分配: ByteBufAllocator接口
Netty通过接口ByteBufAllocator实现了(ByteBuf的)池化。Netty提供池化和非池化的ButeBufAllocator:
-
- ctx.channel().alloc().buffer() ----- 本质就是: ByteBufAllocator.DEFAULT
-
- ByteBufAllocator.DEFAULT.buffer() ----- 返回一个基于堆或者直接内存存储的Bytebuf。默认是堆内存
-
- ByteBufAllocator.DEFAULT ----- 有两种类型: UnpooledByteBufAllocator.DEFAULT(非池化)和PooledByteBufAllocator.DEFAULT(池化)。对于Java程序,默认使用PooledByteBufAllocator(池化)。对于安卓,默认使用UnpooledByteBufAllocator(非池化)
-
- 可以通过BootStrap中的Config为每个Channel提供独立的ByteBufAllocator实例
解释:
-
- 上图中的buffer()方法,返回一个基于堆或者直接内存存储的Bytebuf ----- 缺省是堆内存。源码: AbstractByteBufAllocator() { this(false); }
-
- ByteBufAllocator.DEFAULT ----- 可能是池化,也可能是非池化。默认是池化(PooledByteBufAllocator.DEFAULT)
5.5.2 Unpooled缓冲区 ----- 非池化
Unpooled提供静态的辅助方法来创建未池化的ByteBuf。
注意:
-
- 上图的buffer()方法,返回一个未池化的基于堆内存存储的ByteBuf
-
- wrappedBuffer() ----- 创建一个视图,返回一个包装了给定数据的ByteBuf。非常实用
创建ByteBuf代码:
public void createByteBuf(ChannelHandlerContext ctx) {
// 1. 通过Channel创建ByteBuf
ByteBuf buf1 = ctx.channel().alloc().buffer();
// 2. 通过ByteBufAllocator.DEFAULT创建
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer();
// 3. 通过Unpooled创建
ByteBuf buf3 = Unpooled.buffer();
}
5.5.3 ByteBufUtil类
ByteBufUtil类提供了用于操作ByteBuf的静态的辅助方法: hexdump()和equals
-
- hexdump() ----- 以十六进制的表示形式打印ByteBuf的内容。非常有价值
-
- equals() ----- 判断两个ByteBuf实例的相等性
5.6 引用计数
Netty4.0版本中为ButeBuf和ButeBufHolder引入了引用计数技术。请区别引用计数和可达性分析算法(jvm垃圾回收)
-
- 谁负责释放: 一般来说,是由最后访问(引用计数)对象的那一方来负责将它释放
-
- buffer.release() ----- 引用计数减1
-
- buffer.retain() ----- 引用计数加1
-
- buffer.refCnt() ----- 返回当前对象引用计数值
-
- buffer.touch() ----- 记录当前对象的访问位置,主要用于调试。
-
- 引用计数并非仅对于直接缓冲区(direct Buffer)。ByteBuf的三种模式: 堆缓冲区(heap Buffer)、直接缓冲区(dirrect Buffer)和复合缓冲区(Composite Buffer)都使用了引用计数,某些时候需要程序员手动维护引用数值
代码:
public static void releaseReferenceCountedObject(){
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
// 引用计数加1
buffer.retain();
// 输出引用计数
buffer.refCnt();
// 引用计数减1
buffer.release();
}
5.7 建议
-
- 如果使用了Netty的ByteBuf,建议功能测试时,打开内存检测: -Dio.netty.leakDetectionLevel=paranoid
-
- ByteBuf的三种模式: 堆缓冲区(heap Buffer)、直接缓冲区(dirrect Buffer)和复合缓冲区(Composite Buffer)都使用了引用计数,某些时候需要程序员手动维护引用数值。
标签:索引,buffer,readerIndex,-----,ByteBuf,缓冲区 来源: https://www.cnblogs.com/qianingmeng/p/15911325.html