ByteBuf
Netty 的数据处理 API 通过两个组件暴露——abstract class ByteBuf 和 interface ByteBufHolder。
优点:
- 它可以被用户自定义的缓冲区类型扩展;
- 通过内置的复合缓冲区类型实现透明的零拷贝;
- 容量可以按需增长(类似于JDK的StringBuilder);
- 在读和写这两种模式之间切换不需要调用ByteBuffer的Flip()方法;
- 读和写使用了不同的索引;
- 支持方法的链式调用;
- 支持引用计数;
- 支持池化;
工作原理:
ByteBuf 维护了两个不同的索引:一个用于读取,一个用于写入。当你从 ByteBuf 读取时, 它的 readerIndex 将会被递增已经被读取的字节数。同样地,当你写入 ByteBuf 时,它的 writerIndex 也会被递增。

要了解这些索引两两之间的关系,请考虑一下,如果打算读取字节直到 readerIndex 达到 和 writerIndex 同样的值时会发生什么。在那时,你将会到达“可以读取的”数据的末尾。就 如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个 IndexOutOf- BoundsException。
名称以 read 或者 write 开头的 ByteBuf 方法,将会推进其对应的索引,而名称以 set 或 者 get 开头的操作则不会。后面的这些方法将在作为一个参数传入的一个相对索引上执行操作。
可以指定 ByteBuf 的最大容量。试图移动写索引(即 writerIndex)超过这个值将会触 发一个异常1。(默认的限制是 Integer.MAX_VALUE。)
ByteBuf的使用模式
1. 堆缓冲区:
最常用的 ByteBuf 模式是将数据存储在 JVM 的堆空间中。这种模式被称为支撑数组 (backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式,非常适合于有遗留的数据需要处理的情况。
ByteBuf heapBuf = ...; //检查ByteBuf是有一个支持数组 if (heapBuf.hasArray()) {
//如果有则获该数组的引用 byte[] array = heapBuf.array(); //计算第一个字节的偏移量 int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); //获得可读字节数 int length = heapBuf.readableBytes(); //使用数组、偏移量和长度作为参数调用方法 handleArray(array, offset, length); }
注意:当hasArray()方法返回false时,尝试访问支持数组将处罚一个UnsupportedOperationException。这个模式类似于JDK的ByteBuffer的用法。
2. 直接缓冲区:
ByteBuffer的Javadoc1明确指出:“直接缓冲区的内容将驻留在常规的会被垃圾回收的堆 之外。”这也就解释了为何直接缓冲区对于网络数据传输是理想的选择。如果你的数据包含在一 个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲 区复制到一个直接缓冲区中。
直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你 正在处理遗留代码,你也可能会遇到另外一个缺点:因为数据不是在堆上,所以你不得不进行一 次复制:
ByteBuf directBuf = ...; //检查ByteBuf是否由数组支撑。如果不是,则这是一个直接缓冲区 if (!directBuf.hasArray()) {
//获取可读字节数 int length = directBuf.readableBytes(); //分配一个新的数组来保存具有该长度的字节数据 byte[] array = new byte[length]; //将字节复制到该数组 directBuf.getBytes(directBuf.readerIndex(), array); //调用方法 handleArray(array, 0, length); }
3. 复合缓冲区:
第三种也是最后一种模式使用的是复合缓冲区,它为多个 ByteBuf 提供一个聚合视图。在 这里你可以根据需要添加或者删除 ByteBuf 实例,这是一个 JDK 的 ByteBuffer 实现完全缺失的特性。
Netty 通过一个 ByteBuf 子类CompositeByteBuf实现了这个模式,它提供了一 个将多个缓冲区表示为单个合并缓冲区的虚拟表示。
注意:CompositeByteBuf中的ByteBuf实例可能同时包含直接内存分配和非直接内存分配。 如果其中只有一个实例,那么对 CompositeByteBuf 上的 hasArray()方法的调用将返回该组 件上的 hasArray()方法的值;否则它将返回 false。
字节级操作
随机访问索引:
如同在普通的 Java 字节数组中一样,ByteBuf 的索引是从零开始的:第一个字节的索引是 0,最后一个字节的索引总是 capacity() - 1。
ByteBuf buffer = Unpooled.copiedBuffer("hello world!", Charset.forName("UTF-8")); for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i); System.out.println((char)b); }
需要注意的是,使用那些需要一个索引值参数的方法(的其中)之一来访问数据既不会改变 readerIndex 也不会改变 writerIndex。如果有需要,也可以通过调用 readerIndex(index) 或者 writerIndex(index)来手动移动这两者。
顺序访问索引:
标记为可丢弃字节的分段包含了已经被读过的字节。通过调用 discardReadBytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为 0,存储在 readerIndex 中, 会随着 read 操作的执行而增加(get*操作不会移动 readerIndex)。
下图所展示的缓冲区上调用discardReadBytes()方法后的结果。可以看到,可丢弃字节分段中的空间已经变为可写的了。注意,在调用discardReadBytes()之后,对可写分段的内容并没有任何的保证。

虽然你可能会倾向于频繁地调用 discardReadBytes()方法以确保可写分段的最大化,但是 请注意,这将极有可能会导致内存复制,因为可读字节(图中标记为 CONTENT 的部分)必须被移 动到缓冲区的开始位置。我们建议只在有真正需要的时候才这样做,例如,当内存非常宝贵的时候。
可读字节:
ByteBuf 的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的 readerIndex 值为 0。任何名称以 read 或者 skip 开头的操作都将检索或者跳过位于当前 readerIndex 的数据,并且将它增加已读字节数。
如果被调用的方法需要一个 ByteBuf 参数作为写入的目标,并且没有指定目标索引参数, 那么该目标缓冲区的 writerIndex 也将被增加,例如:
readBytes(ByteBuf dest);
如果尝试在缓冲区的可读字节数已经耗尽时从中读取数据,那么将会引发一个 IndexOutOfBoundsException。
可写字节:
可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的 writerIndex 的默认值为 0。任何名称以 write 开头的操作都将从当前的 writerIndex 处 开始写数据,并将它增加已经写入的字节数。如果写操作的目标也是 ByteBuf,并且没有指定 源索引的值,则源缓冲区的 readerIndex 也同样会被增加相同的大小。这个调用如下所示:
writeBytes(ByteBuf dest)
如果尝试往目标写入超过目标容量的数据,将会引发一个IndexOutOfBoundException。
索引管理:
JDK 的 InputStream 定义了 mark(int readlimit)和 reset()方法,这些方法分别 被用来将流中的当前位置标记为指定的值,以及将流重置到该位置。
同样,可以通过调用 markReaderIndex()、markWriterIndex()、resetWriterIndex() 和 resetReaderIndex()来标记和重置 ByteBuf 的 readerIndex 和 writerIndex。这些和 InputStream 上的调用类似,只是没有 readlimit 参数来指定标记什么时候失效。
查找操作:
在ByteBuf中有多种可以用来确定指定值的索引的方法。最简单的是使用indexOf()方法。 较复杂的查找可以通过那些需要一个ByteBufProcessor(4.1版本废弃,使用ByteProcessor)作为参数的方法达成。这个接口只定 义了一个方法:
//它将检查输入值是否是正在查找的值。 boolean prcess(byte value)
ByteBufProcessor针对一些常见的值定义了许多便利的方法。假设你的应用程序需要和所谓的包含有以NULL结尾的内容的Flash套接字集成。调用
forEachByte(ByteBufProcessor.FIND_NUL)
将简单高效地消费该 Flash 数据,因为在处理期间只会执行较少的边界检查。
//查找回车符\r ByteBuf buffer = ...; int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);
派生缓冲区:
派生缓冲区为 ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方 法被创建的:
- duplicate();
- slice();
- slice(int, int);
- Unpooled.unmodifiableBuffer(…);
- order(ByteOrder);
- readSlice(int)。
每个这些方法都将返回一个新的 ByteBuf 实例,它具有自己的读索引、写索引和标记索引。其内部存储和 JDK 的 ByteBuffer 一样也是共享的。这使得派生缓冲区的创建成本 是很低廉的,但是这也意味着,如果你修改了它的内容,也同时修改了其对应的源实例,所以要小心。
//对ByteBuf进行切片 Charset utf8 = Charset.forName("UTF-8"); ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); ByteBuf slice = buf.slice(0, 15); System.out.println(slice.toString(utf8)); //更新索引0处的字节 buf.setByte(0, (byte)'J'); //数据是共享的 assert buf.getByte(0) == slice.getByte(0);
复制:
如果需要一个现有缓冲区的真实副本,请使用 copy()或者 copy(int, int)方 法。不同于派生缓冲区,由这个调用所返回的 ByteBuf 拥有独立的数据副本。
//对ByteBuf进行复制 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)); //更新索引0处的字节 buf.setByte(0, (byte)'J'); //数据不是共享的 assert buf.getByte(0) != copy.getByte(0);
读写操作
有两种类别的读/写操作:
- get()和 set()操作,从给定的索引开始,并且保持索引不变;
- read()和 write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。
get操作:

set操作:

//get()和set()的用法 Charset utf8 = Charset.forName("UTF-8"); ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //打印第一个字符N System.out.println((char)buf.getByte(0)); int readerIndex = buf.readerIndex(); int writerIndex = buf.writerIndex(); buf.setByte(0, (byte)'B'); System.out.println((char)buf.getByte(0)); System.out.println(readerIndex); System.out.println(writerIndex);
read()操作:
其作用于当前的 readerIndex 或 writerIndex。 这些方法将用于从 ByteBuf 中读取数据,如同它是一个流。

write()操作:

Charset utf8 = Charset.forName("UTF-8"); ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8); //打印第一个字符N System.out.println((char)buf.readByte()); int readerIndex = buf.readerIndex(); int writerIndex = buf.writerIndex(); System.out.println(readerIndex); System.out.println(writerIndex); buf.writeByte((char)'?'); System.out.println(buf.readerIndex()); System.out.println(buf.writerIndex());
其他操作:


ByteBufHolder接口
我们经常发现,除了实际的数据负载之外,我们还需要存储各种属性值。HTTP 响应便是一 个很好的例子,除了表示为字节的内容,还包括状态码、cookie 等。
为了处理这种常见的用例,Netty 提供了 ByteBufHolder。ByteBufHolder 也为 Netty 的 高级特性提供了支持,如缓冲区池化,其中可以从池中借用 ByteBuf,并且在需要时自动释放。
ByteBuf分配
按需分配:ByteBufAllocator接口:
为了降低分配和释放内存的开销,Netty 通过 interface ByteBufAllocator 实现了 (ByteBuf 的)池化,它可以用来分配我们所描述过的任意类型的 ByteBuf 实例。使用池化是特定于应用程序的决定,其并不会以任何方式改变 ByteBuf API(的语义)。

可以通过 Channel(每个都可以有一个不同的 ByteBufAllocator 实例)或者绑定到 ChannelHandler 的 ChannelHandlerContext 获取一个到 ByteBufAllocator 的引用。
//从Channel获取一个到ByteBufAllocator的引用 Channel channnel = null; ByteBufAllocator allocator = channnel.alloc(); //从ChannelHandlerContext获取一个到ByteBufAllocator的引用 ChannelHandlerContext ctx = null; ByteBufAllocator allocator1 = ctx.alloc();
Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和UnpooledByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。此实 现 使 用 了 一 种 称 为 j e m a l l o c 的 已 被 大 量 现 代 操 作 系 统 所 采 用 的 高 效 方 法 来 分 配 内 存 。后 者 的 实 现 不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。
虽然Netty默认 1 使用了PooledByteBufAllocator,但这可以很容易地通过ChannelConfig API或者在引导你的应用程序时指定一个不同的分配器来更改。
Unpooled缓冲区
ByteBufUtil类
ByteBufUtil 提供了用于操作 ByteBuf 的静态的辅助方法。因为这个 API 是通用的,并 且和池化无关,所以这些方法已然在分配类的外部实现。
这些静态方法中最有价值的可能就是 hexdump()方法,它以十六进制的表示形式打印 ByteBuf 的内容。这在各种情况下都很有用,例如,出于调试的目的记录 ByteBuf 的内容。十 六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的 版本还可以很容易地转换回实际的字节表示。
另一个有用的方法是boolean equals(ByteBuf, ByteBuf),它被用来判断两个 ByteBuf 实例的相等性。如果你实现自己的 ByteBuf 子类,你可能会发现 ByteBufUtil 的其他有用方法。
引用计数
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty 在第 4 版中为 ByteBuf 和 ByteBufHolder 引入了 引用计数技术,它们都实现了 interface ReferenceCounted。
引用计数背后的想法并不是特别的复杂;它主要涉及跟踪到某个特定对象的活动引用的数 量。一个 ReferenceCounted 实现的实例将通常以活动的引用计数为 1 作为开始。只要引用计 数大于 0,就能保证对象不会被释放。当活动引用的数量减少到 0 时,该实例就会被释放。注意, 虽然释放的确切语义可能是特定于实现的,但是至少已经释放的对象应该不可再用了。
//从Channel获取ByteBufAllocator Channel channel = null; ByteBufAllocator allocator = channel.alloc(); //分配一个ByteBuf ByteBuf buffer = allocator.directBuffer(); //检查引用计数是否为预期的1 assert buffer.refCnt() == 1;
//减少到该对象的活动引用。当减少到0时,该对象被释放,并且该方法返回true ByteBuf buffer = ...; boolean released = buffer.release(); ...
试图访问一个已经被释放的引用计数的对象,将会导致一个IllegalReferenceCountException。
注意,一个特定的(ReferenceCounted 的实现)类,可以用它自己的独特方式来定义它 的引用计数规则。例如,我们可以设想一个类,其 release()方法的实现总是将引用计数设为 零,而不用关心它的当前值,从而一次性地使所有的活动引用都失效。
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/211445.html原文链接:https://javaforall.net
