网络数据的基本单位总是字节。Java NIO提供了ByteBuffer作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。
Netty的ByteBuffer替代品是ByteBuf,一个强大的实现,既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好地API。
Netty的数据处理API通过两个组件暴露——abstract class ByteBuf 和 interface ByteBufHolder。
ByteBuf API的优点:
(1)它可以被用户自定义的缓冲区类型扩展;
(2)通过内置的符合缓冲区类型实现了透明的零拷贝;
(3)容量可以按需增长(类似于JDK的StringBuilder);
(4)在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法;
(5)读和写使用了不同的索引;
(6)支持方法的链式调用;
(7)支持引用计数;
(8)支持池化。
其他类可用于管理ByteBuf实例的分配,以及执行各种针对于数据容器本身和它所持有的数据的操作。
因为所有的网络通信都设计字节序列的移动,所以高效易用的数据结构必不可少,Netty的ByteBuf实现满足并超越了这些需求。
ByteBuf维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf读取时,它的readIndex将会被递增已经被读取的字节数。同样地,当你写入ByteBuf时,它的writeIndex也会被递增。
如果打算读取字节直到readIndex达到和writeIndex同样的值时会发生什么?在那时,你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个IndexOutOfBoundsException。
名称以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get 开头的操作则不会。后面的这些方法将在作为一个参数传入的一个相对索引上执行操作。
可以指定ByteBuf的最大容量。试图移动写索引(即writerIndex)超过这个值将会触发一个异常。(默认的限制是Integer.MAX_VALUE。)
在使用Netty时,你将遇到几种常见的围绕ByteBuf而构建的使用模式。
(1)堆缓冲区
最常用的ByteBuf模式是将数据存储在JVM的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。这种方式,如下代码所示,非常适合于有遗留的数据需要处理的情况。
ByteBuf heapBuf = ...;
//检查ByteBuf是否有一个支撑数组
if(heapBuf.hasArray()){
//如果有,则获取对该数组的引用
byte[] array = heapBuf.array();
//计算第一个字节的偏移量
int offset = heapBuf.arrayOffset() + heapBuf.readIndex();
//获得可读字节数
int length = heapBuf.readableBytes();
//使用数组、偏移量和长度作为参数调用你的方法
handleArray(array, offset, length);
}
注意:当hasArray()方法返回false时,尝试访问支撑数组将触发一个UnsupportedOperationException。这个模式类似于JDK的ByteBuffer的用法。
(2)直接缓冲区
直接缓冲区是另外一种ByteBuf模式。我们期望用于对象创建的内存分配永远都来自于堆中,但这并不是必须的——NIO在JDK1.4中引入的ByteBuffer类允许JVM实现通过本地调用来分配内存。这主要为了避免在每次调用本地I/O操作之前(或者之后)将缓冲区的内容复制到一个中间缓冲区(或者从中间缓冲区把内容复制到缓冲区)。
ByteBuffer的Javadoc明确指出:“直接缓冲区的内容将驻留在常规的会被垃圾回收的堆之外。”这也就解释了为何直接缓冲区对于网络数据传输是理想的选择。如果你的数据包含在一个堆上分配的缓冲区中,那么事实上,在通过套接字发送它之前,JVM将会在内部把你的缓冲区复制到一个直接缓冲区中。
直接缓冲区的主要缺点就是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。如果你正在处理遗留代码,你也可能会遇到第二个缺点:因为数据不是在堆上,所以你不得不进行一次复制,如以下代码所示:
ByteBuf directBuf = ...;
//检查ByteBuf是否由数组支撑。如果不是,则这是一个直接缓冲区
if (!directBuf.hasArray()) {
//获取可读字节数
int length = directBuf.readableBytes();
//分配一个新的数组来保存具有该长度的字节数据
byte[] array = new byte[length];
//将字节复制到该数组
directBuf.getBytes(directBuf.readBuf.readerIndex(), array);
//使用数组、偏移量和长度作为参数调用你的方法
handleArray(array, 0, length);
}
第三种也是最后一种模式使用的是复合缓冲区,它为多个ByteBuf提供一个聚合视图。在这里你可以根据需要添加或者删除ByteBuf实例,这是一个JDK的ByteBuffer实现完全缺失的特性。
Netty通过一个ByteBuf子类——CompositeByteBuf——实现了这个模式,它提供了一个将多个缓冲区表示为单个合并缓冲区的虚拟表示。
警告 CompositeByteBuf中的ByteBuf实例可能同时包含直接内存分配和非直接内存分配。如果其中只有一个实例,那么对CompositionByteBuf上的hasArray()方法的调用将返回该组件上的hasArray()方法的值;否则它将返回false。
使用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的版本
CompositionByteBuf messageBuf = Unpooled.compositionBuffer();
ByteBuf = headerBuf = ...;//can be backing or direct
ByteBuf bodyBuf = ...;//can be backing or direct
//将ByteBuf实例追加到CompositeByteBuf
messageBuf.addComponents(headerBuf, bodyBuf);
......
//删除位于索引位置为0(第一个组件)的ByteBuf
messageBuf.removeComponent(0);//remove the header
//循环遍历所有的ByteBuf实例
for(ByteBuf buf: messageBuf){
System.out.println(buf.toString());
}
CompositeByteBuf可能不支持访问其支撑数据,因此访问CompositeByteBuf中的数组类似于(访问)直接缓冲区的模式,如下代码所示:
访问CompositeByteBuf中的数据
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
//获得可读字节数
int length = comBuf.readableBytes();
//分配一个具有可读字节数长度的新数组
byte[] array = new byte[length];
//将字节读到该数组中
compBuf.getBytes(compBuf.readerIndex(), array);
//使用偏移量和长度作为参数使用该数组
handleArray(array, 0, array.length);
需要注意的是,Netty使用了CompositeByteBuf来优化套接字的I/O操作,尽可能地消除了由JDK缓冲区实现所导致的性能以及内存使用率的惩罚。这种优化发生在Netty的核心代码中,因此不会被暴露出来。
CompositeByteBuf API 除了从ByteBuf继承的方法,CompositeByteBuf提供了大量的附加功能。
ByteBuf提供了许多超出基本读、写操作的方法用于修改它的数据。
ByteBuf的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是capacity()-1。以下代码表明,对存储机制的封装使得遍历ByteBuf的内容非常简单。
访问数据:
ByteBuf buffer = ...;
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.getByte(i);
System.out.println((char)b);
}
需要注意的是,使用那些需要一个索引值参数的方法(的其中)之一来访问数据既不会改变readerIndex也不会改变writeIndex。如果有需要,也可以通过调用readerIndex(index)或者writerIndex(index)来手动移动这两者。