本章主要内容
ByteBuf
——Netty的数据容器- API的详细信息
- 用例
- 内存分配
正如前面所提到的,网络数据的基本单位总是字节。Java NIO提供了ByteBuffer
作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。
Netty的ByteBuffer
替代品是ByteBuf,
一个强大的实现,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的API。
在本章中我们将会说明和JDK的ByteBuffer
相比,ByteBuf
的卓越功能性和灵活性。这也将有助于更好地理解Netty数据处理的一般方式,并为将在第6章中针对ChannelPipeline
和ChannelHandler
的讨论做好准备。
5.1 ByteBuf的API
Netty的数据处理API通过两个组件暴露——abstract class ByteBuf
和interface ByteBufHolder
。
下面是一些ByteBuf
API的优点:
- 它可以被用户自定义的缓冲区类型扩展;
- 通过内置的复合缓冲区类型实现了透明的零拷贝;
- 容量可以按需增长(类似于JDK的
StringBuilder
); - 在读和写这两种模式之间切换不需要调用
ByteBuffer
的flip
方法; - 读和写使用了不同的索引;
- 支持方法的链式调用;
- 支持引用计数;
- 支持池化。
其他类可用于管理ByteBuf
实例的分配,以及执行各种针对于数据容器本身和它所持有的数据的操作。我们将在仔细研究ByteBuf
和ByteBufHolder
时探讨这些特性。
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)
或者writer
Index(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
方法,这些方法分别被用来将流中的当前位置标记为指定的值,以及将流重置到该位置。
同样,可以通过调用markReaderIndex
、markWriterIndex
、resetWriterIndex
和resetReaderIndex
来标记和重置ByteBuf
的readerIndex
和writerIndex
。这些和InputStream
上的调用类似,只是没有readlimit
参数来指定标记什么时候失效。
也可以通过调用readerIndex(int)
或者writerIndex(int)
来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException
。
可以通过调用clear
方法来将readerIndex
和writerIndex
都设置为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 读/写操作
正如我们所提到过的,有两种类别的读/写操作:
get
和set
操作,从给定的索引开始,并且保持索引不变;read
和write
操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。
表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说明了get
和set
方法的用法,表明了它们不会改变读索引和写索引。
代码清单5-12 get
和set
方法的用法
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
操作,其作用于当前的readerIndex
或writerIndex
。这些方法将用于从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
返回当前readerIndex
的int
值,并将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
)的数据。如果提供了srcIndex
和length
,则从srcIndex
开始读取,并且处理长度为length
的字节。当前writerIndex
将会被增加所写入的字节数
代码清单5-13展示了这些方法的用法。
代码清单5-13 ByteBuf
上的read
和write
操作
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提供了ByteBufHolder
。ByteBufHolder
也为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
实例)或者绑定到ChannelHandler
的ChannelHandlerContext
获取一个到ByteBufAllocator
的引用。代码清单5-14说明了这两种方法。
代码清单5-14 获取一个到ByteBufAllocator
的引用
Channel channel = ...;ByteBufAllocator allocator = channel.alloc; ← -- 从Channel 获取一个到ByteBufAllocator 的引用....ChannelHandlerContext ctx = ...;ByteBufAllocator allocator2 = ctx.alloc; ← -- 从ChannelHandlerContext 获取一个到ByteBufAllocator 的引用...
Netty提供了两种ByteBufAllocator
的实现:PooledByteBufAllocator
和Unpooled-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版中为ByteBuf
和ByteBufHolder
引入了引用计数技术,它们都实现了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
。——译者注