网络上数据的基本单位总是字节。java NIO提供了ByteBuffer作为它的字节容器,但是这个类使用起来过于复杂和繁琐。
netty的替代品ByteBuf,一个强大的实现。既解决了JDK API的局限性,又为网络应用程序的开发者提供了更好的api。
ByteBuf维护着两个索引,一个是读索引,一个是写索引。
* +-------------------+------------------+------------------+ * | discardable bytes | readable bytes | writable bytes | * | | (CONTENT) | | * +-------------------+------------------+------------------+ * | | | | * 0 <= readerIndex <= writerIndex <= capacity
可以看出
ByteBuf API的优点:
ByteBuf维护两个不同的索引,一个是读索引(readerIndex),一个是写索引(writerIndex)。当从ByteBuf中读取数据的时候readerIndex索引会递增已经被读取的字节数。同样,当写入ByteBuf的时候,它的writerIndex索引会被递增。下图展示了一个初始容量为16的空ByteBuf的布局和状态
readIndex和writeIndex均为0的16字节的ByteBuf
1、如果readerIndex和WriterIndex的值一样的时候,如果继续向前读取数据。将会抛出IndexOutOf-BoundsException
。
2、以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或get开头操作不会修改索引。
3、ByteBuf具有最大的容量值,试图移动写索引超过这个值,将会触发一个异常。其默认的限制为Integer.MAX_VALUE
最常用的ByteBuf
模式是将数据存储在 JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。
/**
* 测试堆缓冲区的使用 byteBuf分配的内存为Java 的堆内存
*/
@Test
public void testHeapByte(){
ByteBuf byteBuf = Unpooled.buffer();
//向byteBuf中写入数据
for(int i= 65;i<65+26;i++){
byteBuf.writeByte(i);
}
//从byteBuf中读取数据
byte[] bytes = new byte[26];
byteBuf.readBytes(bytes);
System.out.println(new String(bytes));
}
直接缓冲区是另外一种ByteBuf的模式。我们期望用于对象创建的内存分配永远都来自于堆中,NIO 在JDK1.4中引入的ByteBuffer类允许JVM实现通过本地调用来分配内存。这主要是避免在每次调用本地I/O操作之前将缓冲区的数据复制到一个中间缓冲区。使用直接缓冲区的示例代码如下:
/**
* 测试直接缓冲区的使用
*/
@Test
public void testDirectByte(){
ByteBuf byteBuf = Unpooled.directBuffer();
//向byteBuf中写入数据
for(int i= 65;i<65+26;i++){
byteBuf.writeByte(i);
}
//从byteBuf中读取数据
byte[] bytes = new byte[26];
byteBuf.readBytes(bytes);
System.out.println(new String(bytes));
}
第三种 也是最后一种模式使用的是符合缓冲区,它为多个ByteBuffer提供一个聚合视图。可以根据实际的需要添加或者删除一个ByteBuf的实例。这个功能是JDK的ByteBuffer实现完全缺失的一个特性。
ByteBuf的子类 – compositeByteBuf实现了这个模式,它提供了一个将多个缓冲区一个统一的视图。
我们考虑一下一个又两部分 — 头部和主题 — 组成的将通过http协议发送的消息。
示例代码如下:
/**
* 测试符合缓冲区的使用
* 符合缓冲区的如果包含多个缓冲区,那么就会直接返回false
*/
@Test
public void testCompositeBuffer() throws IOException {
CompositeByteBuf byteBufs = Unpooled.compositeBuffer();
ByteBuf heapBuf = Unpooled.buffer();
ByteBuf directBuf = Unpooled.directBuffer();
heapBuf.readableBytes();
heapBuf.writeBytes("我是头部信息".getBytes());
directBuf.writeBytes("我是尾部信息".getBytes());
//将 buf添加到符合缓冲区中
byteBufs.addComponents(true,heapBuf);
byteBufs.addComponents(true,directBuf);
//从符合缓冲区中读取数据
byte[] bytes = new byte[byteBufs.readableBytes()];
byteBufs.readBytes(bytes);
System.out.println(new String(bytes));
}
跟java的普通数组一样,ByteBuf的索引位置也是从0开始的,第一个字节的索引是0,最后一个字节的索引是capacity()-1。如下面的代码所示:
@Test
public void randomAccessByte(){
ByteBuf byteBuf = Unpooled.buffer(16);
byteBuf.writeBytes("abcdefg".getBytes());
System.out.println((char)byteBuf.getByte(2));
}
使用那些需要一个索引值参数的方法时不会改变readerIndex和writerIndex这两个索引。如果有需要,可以调用readerIndex(index)和writerIndex(index)手动调整。
由于JDK的ByteBuffer只有一个索引,所以其需要通过调用flip()方法来在读写模式之间进行切换,下图展示了ByteBuf如何被两个索引划分为3个区域。
可丢弃字节的分段包含了已经被读过的字节。通过调用 方法,可以丢弃他们并回收空间。这个分段的初始大小为0,存储在readIndex中,随着readerIndex的值不断增加而增大。
* BEFORE discardReadBytes() * * +-------------------+------------------+------------------+ * | discardable bytes | readable bytes | writable bytes | * +-------------------+------------------+------------------+ * | | | | * 0 <= readerIndex <= writerIndex <= capacity * * * AFTER discardReadBytes() * * +------------------+--------------------------------------+ * | readable bytes | writable bytes (got more space) | * +------------------+--------------------------------------+ * | | | * readerIndex (0) <= writerIndex (decreased) <= capacity
上述是摘录自ByteBuf的注释片段。我们可以清楚的看到,执行完discardBytes()方法后,readIndex的值变为0,且可写区域增大 。
调用discardReadBytes()可以确保可写分段的最大化,但是这里面伴随着内存的复制。建议还是在只有真正需要的时候才那么做。