首页 » Netty实战 » Netty实战全文在线阅读

《Netty实战》第5章 ByteBuf

关灯直达底部

本章主要内容

  • ByteBuf——Netty的数据容器
  • API的详细信息
  • 用例
  • 内存分配

正如前面所提到的,网络数据的基本单位总是字节。Java NIO提供了ByteBuffer作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。

Netty的ByteBuffer替代品是ByteBuf,一个强大的实现,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的API。

在本章中我们将会说明和JDK的ByteBuffer相比,ByteBuf的卓越功能性和灵活性。这也将有助于更好地理解Netty数据处理的一般方式,并为将在第6章中针对ChannelPipelineChannelHandler的讨论做好准备。

5.1 ByteBuf的API

Netty的数据处理API通过两个组件暴露——abstract class ByteBufinterface ByteBufHolder

下面是一些ByteBuf API的优点:

  • 它可以被用户自定义的缓冲区类型扩展;
  • 通过内置的复合缓冲区类型实现了透明的零拷贝;
  • 容量可以按需增长(类似于JDK的StringBuilder);
  • 在读和写这两种模式之间切换不需要调用ByteBufferflip方法;
  • 读和写使用了不同的索引;
  • 支持方法的链式调用;
  • 支持引用计数;
  • 支持池化。

其他类可用于管理ByteBuf实例的分配,以及执行各种针对于数据容器本身和它所持有的数据的操作。我们将在仔细研究ByteBufByteBufHolder时探讨这些特性。

5.2 ByteBuf类——Netty的数据容器

因为所有的网络通信都涉及字节序列的移动,所以高效易用的数据结构明显是必不可少的。Netty的ByteBuf实现满足并超越了这些需求。让我们首先来看看它是如何通过使用不同的索引来简化对它所包含的数据的访问的吧。

5.2.1 它是如何工作的

ByteBuf维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf读取时,它的readerIndex将会被递增已经被读取的字节数。同样地,当你写入ByteBuf时,它的writerIndex也会被递增。图5-1展示了一个空ByteBuf的布局结构和状态。

图5-1 一个读索引和写索引都设置为0的16字节ByteBuf

要了解这些索引两两之间的关系,请考虑一下,如果打算读取字节直到readerIndex达到和writerIndex同样的值时会发生什么。在那时,你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个IndexOutOf-BoundsException

名称以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get开头的操作则不会。后面的这些方法将在作为一个参数传入的一个相对索引上执行操作。

可以指定ByteBuf的最大容量。试图移动写索引(即readerIndex)超过这个值将会触发一个异常[1]。(默认的限制是Integer.MAX_VALUE。)

5.2.2 ByteBuf的使用模式

在使用Netty时,你将遇到几种常见的围绕ByteBuf而构建的使用模式。在研究它们时,我们心里想着图5-1会有所裨益—— 一个由不同的索引分别控制读访问和写访问的字节数组。

1.堆缓冲区

最常用的ByteBuf模式是将数据存储在JVM的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式,如代码清单5-1所示,非常适合于有遗留的数据需要处理的情况。

代码清单5-1 支撑数组

ByteBuf heapBuf = ...;if (heapBuf.hasArray) {  ← --  检查ByteBuf 是否有一个支撑数组  byte array = heapBuf.array;  ← --  如果有,则获取对该数组的引用   int offset = heapBuf.arrayOffset + heapBuf.readerIndex;  ← --  计算第一个字节的偏移量。  int length = heapBuf.readableBytes;  ← --  获得可读字节数  handleArray(array, offset, length);  ← --  使用数组、偏移量和长度作为参数调用你的方法}  

注意 当hasArray方法返回false时,尝试访问支撑数组将触发一个UnsupportedOperationException。这个模式类似于JDK的ByteBuffer的用法。

2.直接缓冲区

直接缓冲区是另外一种ByteBuf模式。我们期望用于对象创建的内存分配永远都来自于堆中,但这并不是必须的——NIO在JDK 1.4中引入的ByteBuffer类允许JVM实现通过本地调用来分配内存。这主要是为了避免在每次调用本地I/O操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。

ByteBuffer的Javadoc[2]明确指出:“直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外。”这也就解释了为何直接缓冲区对于网络数据传输是理想的选择。如果你的数据包含在一个在堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲区复制到一个直接缓冲区中。

直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你正在处理遗留代码,你也可能会遇到另外一个缺点:因为数据不是在堆上,所以你不得不进行一次复制,如代码清单5-2所示。

显然,与使用支撑数组相比,这涉及的工作更多。因此,如果事先知道容器中的数据将会被作为数组来访问,你可能更愿意使用堆内存。

代码清单5-2 访问直接缓冲区的数据

ByteBuf directBuf = ...; if (!directBuf.hasArray) {  ← --  检查ByteBuf 是否由数组支撑。如果不是,则这是一个直接缓冲区  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

为了举例说明,让我们考虑一下一个由两部分——头部和主体——组成的将通过HTTP协议传输的消息。这两部分由应用程序的不同模块产生,将会在消息被发送的时候组装。该应用程序可以选择为多个消息重用相同的消息主体。当这种情况发生时,对于每个消息都将会创建一个新的头部。

因为我们不想为每个消息都重新分配这两个缓冲区,所以使用CompositeByteBuf是一个完美的选择。它在消除了没必要的复制的同时,暴露了通用的ByteBuf API。图5-2展示了生成的消息布局。

图5-2 持有一个头部和主体的CompositeByteBuf

代码清单5-3展示了如何通过使用JDK的ByteBuffer来实现这一需求。创建了一个包含两个ByteBuffer的数组用来保存这些消息组件,同时创建了第三个ByteBuffer用来保存所有这些数据的副本。

代码清单5-3 使用ByteBuffer的复合缓冲区模式

// Use an array to hold the message partsByteBuffer message = new ByteBuffer { header, body };// Create a new ByteBuffer and use copy to merge the header and bodyByteBuffer message2 =  ByteBuffer.allocate(header.remaining + body.remaining);message2.put(header);message2.put(body);message2.flip;  

分配和复制操作,以及伴随着对数组管理的需要,使得这个版本的实现效率低下而且笨拙。代码清单5-4展示了一个使用了CompositeByteBuf的版本。

代码清单5-4 使用CompositeByteBuf的复合缓冲区模式

CompositeByteBuf messageBuf = Unpooled.compositeBuffer;ByteBuf headerBuf = ...; // can be backing or directByteBuf bodyBuf = ...;  // can be backing or directmessageBuf.addComponents(headerBuf, bodyBuf);  ← --  将ByteBuf 实例追加到CompositeByteBuf.....messageBuf.removeComponent(0); // remove the header  ← -- 删除位于索引位置为 0(第一个组件)的ByteBuffor (ByteBuf buf : messageBuf) {  ← -- 循环遍历所有的ByteBuf 实例  System.out.println(buf.toString);}  

CompositeByteBuf可能不支持访问其支撑数组,因此访问CompositeByteBuf中的数据类似于(访问)直接缓冲区的模式,如代码清单5-5所示。

代码清单5-5 访问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的缓冲区实现所导致的性能以及内存使用率的惩罚。[3]这种优化发生在Netty的核心代码中,因此不会被暴露出来,但是你应该知道它所带来的影响。

CompositeByteBuf API 除了从ByteBuf继承的方法,CompositeByteBuf提供了大量的附加功能。请参考Netty的Javadoc以获得该API的完整列表。

5.3 字节级操作

ByteBuf提供了许多超出基本读、写操作的方法用于修改它的数据。在接下来的章节中,我们将会讨论这些中最重要的部分。

5.3.1 随机访问索引

如同在普通的Java字节数组中一样,ByteBuf的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是capacity - 1。代码清单5-6表明,对存储机制的封装使得遍历ByteBuf的内容非常简单。

代码清单5-6 访问数据

ByteBuf buffer = ...;for (int i = 0; i < buffer.capacity; i++) {  byte b = buffer.getByte(i);  System.out.println((char)b);}  

需要注意的是,使用那些需要一个索引值参数的方法(的其中)之一来访问数据既不会改变readerIndex也不会改变writerIndex。如果有需要,也可以通过调用readerIndex(index)或者writerIndex(index)来手动移动这两者。

5.3.2 顺序访问索引

虽然ByteBuf同时具有读索引和写索引,但是JDK的ByteBuffer却只有一个索引,这也就是为什么必须调用flip方法来在读模式和写模式之间进行切换的原因。图5-3展示了ByteBuf是如何被它的两个索引划分成3个区域的。

图5-3 ByteBuf的内部分段

5.3.3 可丢弃字节

在图5-3中标记为可丢弃字节的分段包含了已经被读过的字节。通过调用discardRead-Bytes方法,可以丢弃它们并回收空间。这个分段的初始大小为0,存储在readerIndex中,会随着read操作的执行而增加(get*操作不会移动readerIndex)。

图5-4展示了图5-3中所展示的缓冲区上调用discardReadBytes方法后的结果。可以看到,可丢弃字节分段中的空间已经变为可写的了。注意,在调用discardReadBytes之后,对可写分段的内容并没有任何的保证[4]。

图5-4 丢弃已读字节之后的ByteBuf

虽然你可能会倾向于频繁地调用discardReadBytes方法以确保可写分段的最大化,但是请注意,这将极有可能会导致内存复制,因为可读字节(图中标记为CONTENT的部分)必须被移动到缓冲区的开始位置。我们建议只在有真正需要的时候才这样做,例如,当内存非常宝贵的时候。

5.3.4 可读字节

ByteBuf的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的readerIndex值为0。任何名称以read或者skip开头的操作都将检索或者跳过位于当前readerIndex的数据,并且将它增加已读字节数。

如果被调用的方法需要一个ByteBuf参数作为写入的目标,并且没有指定目标索引参数,那么该目标缓冲区的writerIndex也将被增加,例如:

readBytes(ByteBuf dest);  

如果尝试在缓冲区的可读字节数已经耗尽时从中读取数据,那么将会引发一个IndexOutOf-BoundsException

代码清单5-7展示了如何读取所有可以读的字节。

代码清单5-7 读取所有数据

ByteBuf buffer = ...;while (buffer.isReadable) {  System.out.println(buffer.readByte);}  

5.3.5 可写字节

可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writerIndex的默认值为0。任何名称以write开头的操作都将从当前的writerIndex处开始写数据,并将它增加已经写入的字节数。如果写操作的目标也是ByteBuf,并且没有指定源索引的值,则源缓冲区的readerIndex也同样会被增加相同的大小。这个调用如下所示:

writeBytes(ByteBuf dest);  

如果尝试往目标写入超过目标容量的数据,将会引发一个IndexOutOfBoundException[5]

代码清单5-8是一个用随机整数值填充缓冲区,直到它空间不足为止的例子。writeableBytes方法在这里被用来确定该缓冲区中是否还有足够的空间。

代码清单5-8 写数据

// Fills the writable bytes of a buffer with random integers.ByteBuf buffer = ...;while (buffer.writableBytes >= 4) {  buffer.writeInt(random.nextInt);}  

5.3.6 索引管理

JDK的InputStream定义了mark(int readlimit)reset方法,这些方法分别被用来将流中的当前位置标记为指定的值,以及将流重置到该位置。

同样,可以通过调用markReaderIndexmarkWriterIndexresetWriterIndexresetReaderIndex来标记和重置ByteBufreaderIndexwriterIndex。这些和InputStream上的调用类似,只是没有readlimit参数来指定标记什么时候失效。

也可以通过调用readerIndex(int)或者writerIndex(int)来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException

可以通过调用clear方法来将readerIndexwriterIndex都设置为0。注意,这并不会清除内存中的内容。图5-5(重复上面的图5-3)展示了它是如何工作的。

图5-5 clear方法被调用之前

和之前一样,ByteBuf包含3个分段。图5-6展示了在clear方法被调用之后ByteBuf的状态。

图5-6 在clear方法被调用之后

调用clear比调用discardReadBytes轻量得多,因为它将只是重置索引而不会复制任何的内存。

5.3.7 查找操作

在ByteBuf中有多种可以用来确定指定值的索引的方法。最简单的是使用indexOf方法。较复杂的查找可以通过那些需要一个ByteBufProcessor[6]作为参数的方法达成。这个接口只定义了一个方法:

boolean process(byte value)  

它将检查输入值是否是正在查找的值。

ByteBufProcessor针对一些常见的值定义了许多便利的方法。假设你的应用程序需要和所谓的包含有以NULL结尾的内容的Flash套接字[7]集成。调用

forEachByte(ByteBufProcessor.FIND_NUL)  

将简单高效地消费该Flash数据,因为在处理期间只会执行较少的边界检查。

代码清单5-9展示了一个查找回车符(/r)的例子。

代码清单5-9 使用ByteBufProcessor来寻找/r

ByteBuf buffer = ...;int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);  

5.3.8 派生缓冲区

派生缓冲区为ByteBuf提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:

  • duplicate;
  • slice;
  • slice(int, int);
  • Unpooled.unmodifiableBuffer(…);
  • order(ByteOrder);
  • readSlice(int)。

每个这些方法都将返回一个新的ByteBuf实例,它具有自己的读索引、写索引和标记索引。其内部存储和JDK的ByteBuffer一样也是共享的。这使得派生缓冲区的创建成本是很低廉的,但是这也意味着,如果你修改了它的内容,也同时修改了其对应的源实例,所以要小心。

ByteBuf复制 如果需要一个现有缓冲区的真实副本,请使用copy或者copy(int, int)方法。不同于派生缓冲区,由这个调用所返回的ByteBuf拥有独立的数据副本。

代码清单5-10展示了如何使用slice(int,int)方法来操作ByteBuf的一个分段。

代码清单5-10 对ByteBuf进行切片

Charset utf8 = Charset.forName("UTF-8");ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);  ← --  创建一个用于保存给定字符串的字节的ByteBufByteBuf sliced = buf.slice(0, 15);  ← --  创建该ByteBuf 从索引0 开始到索引15结束的一个新切片System.out.println(sliced.toString(utf8));   ← --  将打印“Netty in Action”buf.setByte(0, (byte)'J');   ← --  更新索引0 处的字节assert buf.getByte(0) == sliced.getByte(0);  ← --  将会成功,因为数据是共享的,对其中一个所做的更改对另外一个也是可见的  

现在,让我们看看ByteBuf的分段的副本和切片有何区别,如代码清单5-11所示。

代码清单5-11 复制一个ByteBuf

Charset utf8 = Charset.forName("UTF-8"); ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);  ← --  创建ByteBuf 以保存所提供的字符串的字节 ByteBuf copy = buf.copy(0, 15);  ← --  创建该ByteBuf 从索引0 开始到索引15结束的分段的副本System.out.println(copy.toString(utf8));  ← --   将打印“Netty in Action”buf.setByte(0, (byte) 'J');  ← --  更新索引0 处的字节 assert buf.getByte(0) != copy.getByte(0);  ← --  将会成功,因为数据不是共享的  

除了修改原始ByteBuf的切片或者副本的效果以外,这两种场景是相同的。只要有可能,使用slice方法来避免复制内存的开销。

5.3.9 读/写操作

正如我们所提到过的,有两种类别的读/写操作:

  • getset操作,从给定的索引开始,并且保持索引不变;
  • readwrite操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。

表5-1列举了最常用的get方法。完整列表请参考对应的API文档。

表5-1 get操作

名  称

描  述

getBoolean(int)

返回给定索引处的Boolean

getByte(int)

返回给定索引处的字节

getUnsignedByte(int)

将给定索引处的无符号字节值作为short返回

getMedium(int)

返回给定索引处的24位的中等int

getUnsignedMedium(int)

返回给定索引处的无符号的24位的中等int

getInt(int)

返回给定索引处的int

getUnsignedInt(int)

将给定索引处的无符号int值作为long返回

getLong(int)

返回给定索引处的long

getShort(int)

返回给定索引处的short

getUnsignedShort(int)

将给定索引处的无符号short值作为int返回

getBytes(int, ...)

将该缓冲区中从给定索引开始的数据传送到指定的目的地

大多数的这些操作都有一个对应的set方法。这些方法在表5-2中列出。

表5-2 set操作

名  称

描  述

setBoolean(int, boolean)

设定给定索引处的Boolean

setByte(int index, int value)

设定给定索引处的字节值

setMedium(int index, int value)

设定给定索引处的24位的中等int

setInt(int index, int value)

设定给定索引处的int

setLong(int index, long value)

设定给定索引处的long

setShort(int index, int value)

设定给定索引处的short

代码清单5-12说明了getset方法的用法,表明了它们不会改变读索引和写索引。

代码清单5-12 getset方法的用法

Charset utf8 = Charset.forName("UTF-8");ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);   ← --  创建一个新的ByteBuf以保存给定字符串的字节System.out.println((char)buf.getByte(0));  ← --  打印第一个字符'N'int readerIndex = buf.readerIndex;   ← --  存储当前的readerIndex 和writerIndexint writerIndex = buf.writerIndex;buf.setByte(0, (byte)'B');   ← --  将索引0 处的字节更新为字符'B'System.out.println((char)buf.getByte(0));   ← --  打印第一个字符,现在是'B' assert readerIndex == buf.readerIndex;  ← --  将会成功,因为这些操作并不会修改相应的索引 assert writerIndex == buf.writerIndex;  

现在,让我们研究一下read操作,其作用于当前的readerIndexwriterIndex。这些方法将用于从ByteBuf中读取数据,如同它是一个流。表5-3展示了最常用的方法。

表5-3 read操作

名  称

描  述

readBoolean

返回当前readerIndex处的Boolean,并将readerIndex增加1

readByte

返回当前readerIndex处的字节,并将readerIndex增加1

readUnsignedByte

将当前readerIndex处的无符号字节值作为short返回,并将readerIndex增加1

readMedium

返回当前readerIndex处的24位的中等int值,并将readerIndex增加3

readUnsignedMedium

返回当前readerIndex处的24位的无符号的中等int值,并将readerIndex增加3

readInt

返回当前readerIndexint值,并将readerIndex增加4

readUnsignedInt

将当前readerIndex处的无符号的int值作为long值返回,并将readerIndex增加4

readLong

返回当前readerIndex处的long值,并将readerIndex增加8

readShort

返回当前readerIndex处的short值,并将readerIndex增加2

readUnsignedShort

将当前readerIndex处的无符号short值作为int值返回,并将readerIndex增加2

readBytes(ByteBuf | byte destination, int dstIndex [,int length])

将当前ByteBuf中从当前readerIndex处开始的(如果设置了,length长度的字节)数据传送到一个目标ByteBuf或者byte,从目标的dstIndex开始的位置。本地的readerIndex将被增加已经传输的字节数

几乎每个read方法都有对应的write方法,用于将数据追加到ByteBuf中。注意,表5-4中所列出的这些方法的参数是需要写入的值,而不是索引值。

表5-4 写操作

名  称

描  述

writeBoolean(boolean)

在当前writerIndex处写入一个Boolean,并将writerIndex增加1

writeByte(int)

在当前writerIndex处写入一个字节值,并将writerIndex增加1

writeMedium(int)

在当前writerIndex处写入一个中等的int值,并将writerIndex增加3

writeInt(int)

在当前writerIndex处写入一个int值,并将writerIndex增加4

writeLong(long)

在当前writerIndex处写入一个long值,并将writerIndex增加8

writeShort(int)

在当前writerIndex处写入一个short值,并将writerIndex增加2

writeBytes(source ByteBuf |byte [,int srcIndex ,int length])

从当前writerIndex开始,传输来自于指定源(ByteBuf或者byte)的数据。如果提供了srcIndexlength,则从srcIndex开始读取,并且处理长度为length的字节。当前writerIndex将会被增加所写入的字节数

代码清单5-13展示了这些方法的用法。

代码清单5-13 ByteBuf上的readwrite操作

Charset utf8 = Charset.forName("UTF-8");ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);   ← --  创建一个新的ByteBuf 以保存给定字符串的字节System.out.println((char)buf.readByte);  ← --  打印第一个字符'N'int readerIndex = buf.readerIndex;  ← --   存储当前的readerIndexint writerIndex = buf.writerIndex;  ← --   存储当前的writerIndexbuf.writeByte((byte)'?');  ← --  将字符'?'追加到缓冲区  assert readerIndex == buf.readerIndex;assert writerIndex != buf.writerIndex;  ← --  将会成功,因为writeByte方法移动了writerIndex  

5.3.10 更多的操作

表5-5 列举了由ByteBuf提供的其他有用操作。

表5-5 其他有用的操作

名  称

描  述

isReadable

如果至少有一个字节可供读取,则返回true

isWritable

如果至少有一个字节可被写入,则返回true

readableBytes

返回可被读取的字节数

writableBytes

返回可被写入的字节数

capacity

返回ByteBuf可容纳的字节数。在此之后,它会尝试再次扩展直 到达到maxCapacity

maxCapacity

返回ByteBuf可以容纳的最大字节数

hasArray

如果ByteBuf由一个字节数组支撑,则返回true

array

如果 ByteBuf由一个字节数组支撑则返回该数组;否则,它将抛出一个UnsupportedOperationException异常

5.4 ByteBufHolder接口

我们经常发现,除了实际的数据负载之外,我们还需要存储各种属性值。HTTP响应便是一个很好的例子,除了表示为字节的内容,还包括状态码、cookie等。

为了处理这种常见的用例,Netty提供了ByteBufHolderByteBufHolder也为Netty的高级特性提供了支持,如缓冲区池化,其中可以从池中借用ByteBuf,并且在需要时自动释放。

ByteBufHolder只有几种用于访问底层数据和引用计数的方法。表5-6列出了它们(这里不包括它继承自ReferenceCounted的那些方法)。

表5-6 ByteBufHolder的操作

名  称

描  述

content

返回由这个ByteBufHolder所持有的ByteBuf

copy

返回这个ByteBufHolder的一个深拷贝,包括一个其所包含的ByteBuf的非共享拷贝

duplicate

返回这个ByteBufHolder的一个浅拷贝,包括一个其所包含的ByteBuf的共享拷贝

如果想要实现一个将其有效负载存储在ByteBuf中的消息对象,那么ByteBufHolder将是个不错的选择。

5.5 ByteBuf分配

在这一节中,我们将描述管理ByteBuf实例的不同方式。

5.5.1 按需分配:ByteBufAllocator接口

为了降低分配和释放内存的开销,Netty通过interface ByteBufAllocator实现了(ByteBuf的)池化,它可以用来分配我们所描述过的任意类型的ByteBuf实例。使用池化是特定于应用程序的决定,其并不会以任何方式改变ByteBuf API(的语义)。

表5-7 列出了ByteBufAllocator提供的一些操作。

表5-7 ByteBufAllocator的方法

名  称

描  述

buffer
buffer(int initialCapacity);
buffer(int initialCapacity, int maxCapacity);

返回一个基于堆或者直接内存存储的ByteBuf

heapBuffer
heapBuffer(int initialCapacity)
heapBuffer(int initialCapacity, int maxCapacity)

返回一个基于堆内存存储的ByteBuf

directBuffer
directBuffer(int initialCapacity)
directBuffer(int initialCapacity, int maxCapacity)

返回一个基于直接内存存储的ByteBuf

compositeBuffer
compositeBuffer(int maxNumComponents)
compositeDirectBuffer
compositeDirectBuffer(int maxNumComponents);
compositeHeapBuffer
compositeHeapBuffer(int maxNumComponents);

返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf

ioBuffer [8]

返回一个用于套接字的I/O操作的ByteBuf

可以通过Channel(每个都可以有一个不同的ByteBufAllocator实例)或者绑定到ChannelHandlerChannelHandlerContext获取一个到ByteBufAllocator的引用。代码清单5-14说明了这两种方法。

代码清单5-14 获取一个到ByteBufAllocator的引用

Channel channel = ...;ByteBufAllocator allocator = channel.alloc;  ← --  从Channel 获取一个到ByteBufAllocator 的引用....ChannelHandlerContext ctx = ...;ByteBufAllocator allocator2 = ctx.alloc;  ← --  从ChannelHandlerContext 获取一个到ByteBufAllocator 的引用...  

Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocatorUnpooled-ByteBufAllocator。前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片。此实现使用了一种称为jemalloc[9]的已被大量现代操作系统所采用的高效方法来分配内存。后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。

虽然Netty默认[10]使用了PooledByteBufAllocator,但这可以很容易地通过Channel-Config API或者在引导你的应用程序时指定一个不同的分配器来更改。更多的细节可在第8章中找到。

5.5.2 Unpooled缓冲区

可能某些情况下,你未能获取一个到ByteBufAllocator的引用。对于这种情况,Netty提供了一个简单的称为Unpooled的工具类,它提供了静态的辅助方法来创建未池化的 ByteBuf实例。表5-8列举了这些中最重要的方法。

表5-8 Unpooled的方法

名  称

描  述

buffer
buffer(int initialCapacity)
buffer(int initialCapacity, int maxCapacity)

返回一个未池化的基于堆内存存储的ByteBuf

directBuffer
directBuffer(int initialCapacity)
directBuffer(int initialCapacity, int maxCapacity)

返回一个未池化的基于直接内存存储的ByteBuf

wrappedBuffer

返回一个包装了给定数据的ByteBuf

copiedBuffer

返回一个复制了给定数据的ByteBuf

Unpooled类还使得ByteBuf同样可用于那些并不需要Netty的其他组件的非网络项目,使得其能得益于高性能的可扩展的缓冲区API。

5.5.3 ByteBufUtil类

ByteBufUtil提供了用于操作ByteBuf的静态的辅助方法。因为这个API是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现。

这些静态方法中最有价值的可能就是hexdump方法,它以十六进制的表示形式打印ByteBuf的内容。这在各种情况下都很有用,例如,出于调试的目的记录ByteBuf的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。

另一个有用的方法是boolean equals(ByteBuf, ByteBuf),它被用来判断两个ByteBuf实例的相等性。如果你实现自己的ByteBuf子类,你可能会发现ByteBufUtil的其他有用方法。

5.6 引用计数

引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty在第4版中为ByteBufByteBufHolder引入了引用计数技术,它们都实现了interface ReferenceCounted

引用计数背后的想法并不是特别的复杂;它主要涉及跟踪到某个特定对象的活动引用的数量。一个ReferenceCounted实现的实例将通常以活动的引用计数为1作为开始。只要引用计数大于0,就能保证对象不会被释放。当活动引用的数量减少到0时,该实例就会被释放。注意,虽然释放的确切语义可能是特定于实现的,但是至少已经释放的对象应该不可再用了。

引用计数对于池化实现(如PooledByteBufAllocator)来说是至关重要的,它降低了内存分配的开销。代码清单5-15和代码清单5-16展示了相关的示例。

代码清单5-15 引用计数

Channel channel = ...;ByteBufAllocator allocator = channel.alloc;  ← --  从Channel 获取ByteBufAllocator....ByteBuf buffer = allocator.directBuffer;  ← --  从ByteBufAllocator分配一个ByteBuf    assert buffer.refCnt == 1; ← --  检查引用计数是否为预期的1...  

代码清单5-16 释放引用计数的对象

ByteBuf buffer = ...;boolean released = buffer.release;  ← --  减少到该对象的活动引用。当减少到0 时,该对象被释放,并且该方法返回true...  

试图访问一个已经被释放的引用计数的对象,将会导致一个IllegalReferenceCount- Exception

注意,一个特定的(ReferenceCounted的实现)类,可以用它自己的独特方式来定义它的引用计数规则。例如,我们可以设想一个类,其release方法的实现总是将引用计数设为零,而不用关心它的当前值,从而一次性地使所有的活动引用都失效。

谁负责释放 一般来说,是由最后访问(引用计数)对象的那一方来负责将它释放。在第6章中,我们将会解释这个概念和ChannelHandler以及ChannelPipeline的相关性。

5.7 小结

本章专门探讨了Netty的基于ByteBuf的数据容器。我们首先解释了ByteBuf相对于JDK所提供的实现的优势。我们还强调了该API的其他可用变体,并且指出了它们各自最佳适用的特定用例。

我们讨论过的要点有:

  • 使用不同的读索引和写索引来控制数据访问;
  • 使用内存的不同方式——基于字节数组和直接缓冲区;
  • 通过CompositeByteBuf生成多个ByteBuf的聚合视图;
  • 数据访问方法——搜索、切片以及复制;
  • 读、写、获取和设置API;
  • ByteBufAllocator池化和引用计数。

在下一章中,我们将专注于ChannelHandler,它为你的数据处理逻辑提供了载体。因为ChannelHandler大量地使用了ByteBuf,你将开始看到Netty的整体架构的各个重要部分最终走到了一起。


[1] 也就是说用户直接或者间接使capacity(int)或者ensureWritable(int)方法来增加超过该最大容量时抛出异常。——译者注

[2] Java平台,标准版第8版API规范,java.nio,class ByteBuffer:http://docs.oracle. com/javase/8/docs/api/ java/nio/ByteBuffer.html。

[3] 这尤其适用于JDK所使用的一种称为分散/收集I/O(Scatter/Gather I/O)的技术,定义为“一种输入和输出的方法,其中,单个系统调用从单个数据流写到一组缓冲区中,或者,从单个数据源读到一组缓冲区中”。《Linux System Programming》,作者Robert Love(O’Reilly, 2007)。

[4] 因为只是移动了可以读取的字节以及writerIndex,而没有对所有可写入的字节进行擦除写。——译者注

[5] 在往ByteBuf中写入数据时,其将首先确保目标ByteBuf具有足够的可写入空间来容纳当前要写入的数据,如果没有,则将检查当前的写索引以及最大容量是否可以在扩展后容纳该数据,可以则会分配并调整容量,否则就会抛出该异常。——译者注

[6] 在Netty 4.1.x中,该类已经废弃,请使用io.netty.util.ByteProcessor。——译者注

[7] 有关Flash套接字的讨论可参考Flash ActionScript 3.0 Developer’s Guide中Networking and Communication部分里的Sockets页面:http://help.adobe.com/en_US/as3/dev/WSb2ba3b1aad8a27b0-181c51321220efd9d1c-8000.html。

[8] 默认地,当所运行的环境具有sun.misc.Unsafe支持时,返回基于直接内存存储的ByteBuf,否则返回基于堆内存存储的ByteBuf;当指定使用PreferHeapByteBufAllocator时,则只会返回基于堆内存存储的ByteBuf。——译者注

[9] Jason Evans的“A Scalable Concurrent malloc(3) Implementation for FreeBSD”(2006):http://people.freebsd. org/~jasone/jemalloc/bsdcan2006/jemalloc.pdf。

[10] 这里指Netty4.1.x,Netty4.0.x默认使用的是UnpooledByteBufAllocator。——译者注