Netty学习笔记(五)ByteBuf
网络传输的基本单位是字节,而ByteBuf则是Netty提供的一个字节容器,通过它我们可以很方便的操作字节。
一、ByteBuf特点
- 它可以被用户自定义的缓冲区类型扩展
- 通过内置的复合缓冲区类型实现了透明的零拷贝
- 量可以按需增长(类似于 JDK 的 StringBuilder)
- 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法
- 读和写使用了不同的索引
- 支持方法的链式调用
- 支持引用计数
- 支持池化
二、ByteBuf类介绍
1、ByteBuf工作原理
ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当从ByteBuf读取时,它的readerIndex将会被递增已经被读取的字节数。同样地,当写入ByteBuf时,它的writerIndex也会被递增,如下图所示:

2、ByteBuf的使用模式
1)堆缓冲区
最常用的ByteBuf模式是将数据存储在JVM的堆空间中。这种模式被称为支撑数组 (backing array),它能在没有使用池化的情况下提供快速的分配和释放。
ByteBuf heapBuf = ...; if (heapBuf.hasArray()) { byte[] array = heapBuf.array(); int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); int length = heapBuf.readableBytes(); handleArray(array, offset, length); }
2)直接缓冲区
直接缓冲区是另外一种ByteBuf模式,它可以避免在每次调用本地 I/O 操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区。
ByteBuf directBuf = ...; if (!directBuf.hasArray()) { int length = directBuf.readableBytes(); byte[] array = new byte[length]; directBuf.getBytes(directBuf.readerIndex(), array); handleArray(array, 0, length); }
3)复合缓冲区
复合缓冲区可以为为多个 ByteBuf 提供一个聚合视图。在这里你可以根据需要添加或者删除ByteBuf实例,即它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示。Netty通过CompositeByteBuf类实现这一模式。
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = ...; ByteBuf bodyBuf = ...; 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);
三、字节级操作
ByteBuf提供了许多方法方便我们对字节数据进行操作,接下来让我们逐一学习。
1、随机访问索引
如同在普通的Java字节数组中一样,ByteBuf的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是capacity() - 1。
ByteBuf buffer = ...; for (int i = 0; i < buffer.capacity(); i++) { byte b = buffer.getByte(i); System.out.println((char)b); }
2、顺序访问索引
ByteBuf具有读索引和写索引,下图展示的是ByteBuf如何被这两个索引分割的,从图中可以看出从一个ByteBuf顺序读取字节时需要考虑读索引和写索引的位置。

3、可丢弃字节
可丢弃字节的分段包含了已经被读过的字节。通过调用 discardReadBytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为0,存储在readerIndex 中,会随着 read 操作的执行而增加。下图展示当调用discardReadyBytes()方法后的效果:

从上图可以看出,虽然discardReadyBytes()可以回收空间,但是会移动可读空间中字节的位置,因而会发生内存复制,因而频繁吊用此方法会带来效率问题。
4、可读字节
ByteBuf 的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的readerIndex值为 0。任何名称以read或者skip开头的操作都将检索或者跳过位于当前readerIndex的数据,并且将它增加已读字节数。
ByteBuf buffer = ...; while (buffer.isReadable()) { System.out.println(buffer.readByte()); }
5、可写字节
可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writerIndex的默认值为0。任何名称以write开头的操作都将从当前的writerIndex处开始写数据,并将它增加已经写入的字节数。如果写操作的目标也是ByteBuf,并且没有指定源索引的值,则源缓冲区的readerIndex也同样会被增加相同的大小。
ByteBuf buffer = ...; while (buffer.writableBytes() >= 4) { buffer.writeInt(random.nextInt()); }
6、索引管理
我们可以通过调用markReaderIndex()、markWriterIndex()、resetWriterIndex() 和 resetReaderIndex()来标记和重置ByteBuf的readerIndex和writerIndex。 也可以通过调用readerIndex(int)或者 writerIndex(int)来将索引移动到指定位置。可以通过调用clear()方法来将readerIndex和writerIndex都设置为0(这里并不会清除数据,只是移动了索引位置,所以此操作效率极高)。
7、查找操作
在ByteBuf中有多种可以用来确定指定值的索引的方法。最简单的是使用indexOf()方法。 较复杂的查找可以通过那些需要一个ByteProcessor作为参数的方法达成。这个接口只定义了一个方法:boolean process(byte value)它将检查输入值是否是正在查找的值。
8、派生缓冲区
派生缓冲区为 ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:
- duplicate()
- slice()
- slice(int, int)
- Unpooled.unmodifiableBuffer(...)
- order(ByteOrder)
- readSlice(int)
这些方法都将返回一个新的 ByteBuf 实例,它具有自己的读索引、写索引和标记索引,其内部的存储与源实例是共享的,这就意味着你对派生实例的修改有可能会影响到源数据。
ByteBuf 复制:如果需要一个现有缓冲区的真实副本,请使用copy()或者 copy(int, int)方法。不同于派生缓冲区,由这个调用所返回的ByteBuf拥有独立的数据副本。
9、读/写操作
ByteBuf提供两种类别的读/写操作:
- get()和 set()操作,从给定的索引开始,并且保持索引不变;
- read()和 write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。
10、其他操作
下表提供了列举了由ByteBuf提供的其他有用操作:
| 名称 | 描述 |
|
isReadable() |
如果至少有一个字节可供读取,则返回 true |
|
isWritable() |
如果至少有一个字节可被写入,则返回 true |
|
readableBytes() |
返回可被读取的字节数 |
|
writableBytes() |
返回可被写入的字节数 |
|
capacity() |
返回 ByteBuf 可容纳的字节数。在此之后,它会尝试再次扩展直 到达到 maxCapacity() |
|
maxCapacity() |
返回 ByteBuf 可以容纳的最大字节数 |
|
hasArray() |
如果 ByteBuf 由一个字节数组支撑,则返回 true |
|
array() |
如果 ByteBuf 由一个字节数组支撑则返回该数组;否则,它将抛出一个 UnsupportedOperationException 异常 |
四、ByteBufHolder接口
在实际使用中我们发现在除了实际的数据负载之外,我们还需要存储各种属性值。 为了处理这种常见的用例,Netty 提供了ByteBufHolder。ByteBufHolder也为Netty的高级特性提供了支持,如缓冲区池化,其中可以从池中借用 ByteBuf,并且在需要时自动释放。
五、ByteBuf的分配
1、按需分配
为了降低分配和释放内存的开销,Netty通过ByteBufAllocator实现了(ByteBuf 的)池化,它可以用来分配我们所描述过的任意类型的 ByteBuf 实例。使用池化是特定于应用程序的决定,其并不会以任何方式改变ByteBuf。
可以通过Channel(每个都可以有一个不同的ByteBufAllocator实例)或者绑定到ChannelHandler的ChannelHandlerContext获取一个到ByteBufAllocator的引用。如下图代码所示:
Channel channel = ...; ByteBufAllocator allocator = channel.alloc(); .... ChannelHandlerContext ctx = ...; ByteBufAllocator allocator2 = ctx.alloc();
Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。此实现使用了一种称为jemalloc的已被大量现代操作系统所采用的高效方法来分配内存。后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。
2、Unpooled缓冲区
可能某些情况下,你未能获取一个到ByteBufAllocator的引用。对于这种情况,Netty提供了一个简单的称为Unpooled的工具类,它提供了静态的辅助方法来创建未池化的ByteBuf实例。Unpooled 类还使得 ByteBuf 同样可用于那些并不需要 Netty 的其他组件的非网络项目, 使得其能得益于高性能的可扩展的缓冲区 API。
3、ByteBufUtil类
ByteBufUtil 提供了用于操作 ByteBuf 的静态的辅助方法。因为这个 API 是通用的,并 且和池化无关,所以这些方法已然在分配类的外部实现。这些静态方法中最有价值的可能就是 hexdump()方法,它以十六进制的表示形式打印 ByteBuf 的内容。这在各种情况下都很有用,例如,出于调试的目的记录 ByteBuf 的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外十六进制的版本还可以很容易地转换回实际的字节表示。另一个有用的方法是 boolean equals(ByteBuf, ByteBuf),它被用来判断两个 ByteBuf 实例的相等性。如果你实现自己的 ByteBuf 子类,你可能会发现 ByteBufUtil 的其他有用方法。
六、引用计数
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。引用计数背后的想法并不是特别的复杂;它主要涉及跟踪到某个特定对象的活动引用的数量。一个ReferenceCounted实现的实例将通常以活动的引用计数为1作为开始。只要引用计数大于0,就能保证对象不会被释放。当活动引用的数量减少到0时,该实例就会被释放。注意,虽然释放的确切语义可能是特定于实现的,但是至少已经释放的对象应该不可再用了。引用计数对于池化实现(如 PooledByteBufAllocator)来说是至关重要的,它降低了内存分配的开销。
谁负责释放:一般来说,是由最后访问(引用计数)对象的那一方来负责将它释放。

浙公网安备 33010602011771号