JDK ByteBuffer
当我们进行数据传输的时候,往往需要使用缓冲区,常用的缓存区就是JDK NIO类库提供的java.nio.Buffer
;
实际上,7种基础类型(Boolean除外)都有自己的缓冲区实现,对于NIO编程而言,我们主要使用的是ByteBuffer
;从功能角度而言,ByteBuffer
完全可以满足NIO编程的需要,但是对于NIO编程的复杂性,ByteBuffer也具有局限性,它的缺点如下:
-
ByteBuffer
长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界; -
ByteBuffer
只有一个标识位置的指针position
,读写的时候需要手工调用flip()和rewind()等; -
ByteBuffer
的API功能有限,一些高级和实用的特性它不支持,需要使用者自己实现;
JDK ByteBuffer由于只有一个位置指针用于处理读写操作,因此每次读写的时候都需要额外调用flip()和clear()等方法,否则功能将会出错;
capacity:缓冲区中的最大数据容量,它代表这个缓冲区的容量,一旦设定就不可以更改;比如 capacity 为 1024 的 IntBuffer,代表其一次可以存放 1024 个 int 类型的值
position: 被写入或者读取的元素索引,值由get()/put()自动更新,被初始为0;
limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)
mark: 标记着当前position可读或可写的索引值
从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了;
写操作模式下,limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity;
写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了;
java.nio.Buffer#flip 用于状态翻转
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
java.nio.Buffer#mark
public final Buffer mark() { mark = position; return this; }
关系:mark <= position <= limit <= capacity
测试demo如下:
public class ByteBufferTest { public static void main(String[] args) { CharBuffer buffer = CharBuffer.allocate(10); System.out.println("---读取数据前——-"); printBuffer(buffer); for(int i= 0 ; i < 5 ; i++) { buffer.put(String.valueOf(i).charAt(0)); } System.out.println("---读取数据后——-"); printBuffer(buffer); System.out.println("当前buffer值:" + buffer); buffer.flip(); System.out.println("---调用flip方法后——-"); printBuffer(buffer); System.out.println("当前buffer值:" + buffer); System.out.println("---设置position(3).mark()——-"); buffer.position(3).mark(); printBuffer(buffer); System.out.println("---设置position(3).limit(6).mark().position(5)——-"); buffer.position(3).limit(6).mark().position(5); printBuffer(buffer); System.out.println("---调用clear方法后——-"); buffer.clear(); printBuffer(buffer); System.out.println("---调用duplicate方法后——-"); CharBuffer dupeBuffer = buffer.duplicate(); printBuffer(dupeBuffer); System.out.println("---dupeBuffer调用clear方法后——-"); dupeBuffer.clear(); printBuffer(dupeBuffer); } private static void printBuffer(Buffer buffer) { System.out.println("[limit=" + buffer.limit() +", position = " + buffer.position() +", capacity = " + buffer.capacity() + "]"); } }
执行结果如下:
---读取数据前——- [limit=10, position = 0, capacity = 10] ---读取数据后——- [limit=10, position = 5, capacity = 10] 当前buffer值: ---调用flip方法后——- [limit=5, position = 0, capacity = 10] 当前buffer值:01234 ---设置position(3).mark()——- [limit=5, position = 3, capacity = 10] ---设置position(3).limit(6).mark().position(5)——- [limit=6, position = 5, capacity = 10] ---调用clear方法后——- [limit=10, position = 0, capacity = 10] ---调用duplicate方法后——- [limit=10, position = 0, capacity = 10] ---dupeBuffer调用clear方法后——- [limit=10, position = 0, capacity = 10]
当执行flip()操作后,它的limit被设置为position,position设置为0,capacity不变,读取的内容是从position到limit之间;
上图如果不执行flip操作,buffer读取到是position到limit之间的内容,因此测试代码中第一次打印buffer为空,第二次即执行flip操作后,buffer打印为01234;
ByteBuffer有其局限性,缺点如下:
-
ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常;
-
ByteBuffer 只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API;
-
ByteBuffer的API功能有限,需要使用者自己实现;
Netty ByteBuf
从结构上来说,ByteBuf 由Byte数组构成的缓冲区;数组中每个字节用来存放信息;
ByteBuf 通过两个位置指针来协助缓冲区的读写操作,一个用于读取数据,一个用于写入数据;这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置;读操作使用readerIndex,写操作使用writerIndex;
测试demo如下:
public class NettyByteBuf { public static void main(String[] args) { // 创建byteBuf对象,该对象内部包含一个字节数组byte[10] // 通过readerindex和writerIndex和capacity,将buffer分成三个区域 // 已经读取的区域:[0,readerindex) // 可读取的区域:[readerindex,writerIndex) // 可写的区域: [writerIndex,capacity) ByteBuf byteBuf = Unpooled.buffer(10); System.out.println("写入前 byteBuf -> " + byteBuf); for (int i = 0; i < 8; i++) { byteBuf.writeByte(i); } System.out.println("写入后 byteBuf ->" + byteBuf); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("执行getByte方法:"); for (int i = 0; i < 5; i++) { stringBuilder.append(byteBuf.getByte(i)).append(" "); } System.out.println(stringBuilder.toString()); System.out.println("调用getByte方法后 byteBuf ->" + byteBuf); stringBuilder.setLength(0); stringBuilder.append("执行readByte方法:"); for (int i = 0; i < 5; i++) { stringBuilder.append(byteBuf.readByte()).append(" "); } System.out.println(stringBuilder.toString()); System.out.println("调用readByte方法后 byteBuf ->" + byteBuf); } }
执行结果如下:
写入前 byteBuf -> UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 10) 写入后 byteBuf ->UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 8, cap: 10) 执行getByte方法:0 1 2 3 4 调用getByte方法后 byteBuf ->UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 8, cap: 10) 执行readByte方法:0 1 2 3 4 调用readByte方法后 byteBuf ->UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 5, widx: 8, cap: 10)
rederIndex和writerIndex的取值一开始都是0,随着数据的写入writerIndex会增加,读取数据会使rederIndex增加,但它不会超过writerIndex;
在读取之后,[0,readerindex)之间的数据被视为discard,调用discardReadBytes方法,可以释放这部分空间,它的作用类似ByteBuffer的compact方法;
[readerindex,writerIndex)之间的数据是可读取的,等价于ByteBuffer position到limit之间的数据;
[writerIndex,capacity)之间的空间是可写的,等价于ByteBuffer limit到capacity之间的可用空间;
由于写操作不修改rederIndex指针,读操作不修改writerIndex指针,因此读写间不再需要调整位置指针,简化了缓存区的读写操作,避免了由于遗漏flip()操作导致功能的异常;