Netty学习5——ByteBuf

ByteBuf

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

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

1.ByteBuf的API

    Netty的数据处理API通过两个组件暴露——abstract class ByteBuf 和 interface ByteBufHolder。

    ByteBuf API的优点:

(1)它可以被用户自定义的缓冲区类型扩展;

(2)通过内置的符合缓冲区类型实现了透明的零拷贝;

(3)容量可以按需增长(类似于JDK的StringBuilder);

(4)在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法;

(5)读和写使用了不同的索引;

(6)支持方法的链式调用;

(7)支持引用计数;

(8)支持池化。

    其他类可用于管理ByteBuf实例的分配,以及执行各种针对于数据容器本身和它所持有的数据的操作。

2.ByteBuf类——Netty的数据容器

    因为所有的网络通信都设计字节序列的移动,所以高效易用的数据结构必不可少,Netty的ByteBuf实现满足并超越了这些需求。

2.1它是如何工作的

    ByteBuf维护了两个不同的索引:一个用于读取,一个用于写入。当你从ByteBuf读取时,它的readIndex将会被递增已经被读取的字节数。同样地,当你写入ByteBuf时,它的writeIndex也会被递增。

    如果打算读取字节直到readIndex达到和writeIndex同样的值时会发生什么?在那时,你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个IndexOutOfBoundsException。

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

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

2.2ByteBuf的使用模式

    在使用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);
}
2.3复合缓冲区

    第三种也是最后一种模式使用的是复合缓冲区,它为多个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提供了大量的附加功能。

3.字节级操作

    ByteBuf提供了许多超出基本读、写操作的方法用于修改它的数据。

3.1随机访问索引

    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)来手动移动这两者。

你可能感兴趣的:(Netty)