buffer
buffer类是nio的基础
Java buffer class
- buffer对象可以成为一个固定大小的容器.
- buffer和channel关系紧密, channel是io传输的入口, buffer是数据传输的源头或者是目标.
- 对于向外传输,数据(我们想要发送的)被放置在缓冲区中。缓冲区被传递到一个输出通道。
- 对于向内传输,通道将数据存储在我们提供的缓冲区中。然后数据从缓冲区复制到通道内。
- 像上面说的这种数据转换是nio api执行高效的关键
buffer类结构如下, 顶部是通用的Buffer类, 该类提供了通用的操作, 这些操作与存储的数据类型无关.
Buffer Attributes
原则上来说, buffer对象就是一个包含了原始数据的数组对象. buffer对象比一般数组有优势的地方是buffer封装了数据内容以及一些数据的信息.
下面是Buffer类的主要属性
- Capacity: 容量, buffer可以存储的最大数据量. capacity在buffer创建的时候指定, 后面不许修改.
- Limit: 长度, 当前buffer存储的数据量.
- Position: 位置, 下一个可以被读取或者写的位置. position会根据get()和put()方法更新值.
- Mark: 标记, 执行mark()方法会令mark = position, 执行reset()会令position = mark. 在没有设置mark前,mark的值为null.
Creating Buffers
一共有7种数据类型的Buffer(就是没有boolean). 每个类都是抽象的, 不可以直接创建. Buffer提供了两种创建方式
CharBuffer charBuffer = CharBuffer.allocate (100);
这种会在堆中创建一个可以容纳100个char的数组.
char [] myArray = new char [100];
CharBuffer charbuffer = CharBuffer.wrap (myArray);
这种和上面的区别是使用自己创建的数组, 那么后面对于该数组的修改myArray和charbuffer都可以感知到.
Working With Buffers
Accessing the Buffer - get() and put() Methods
这里的方法描述使用完全可以类比list. 区别是出现越界抛出的异常不同, 另一个是Buffer的大小是固定的, 不会像list自动扩容.
Filling the Buffer
现在我们尝试将"Hello"放到buffer中
CharBuffer charBuffer = CharBuffer.allocate (10);
buffer.put('H').put('e').put('l').put('l').put('o');
下面对数据进行修改
buffer.put(0, 'M').put('w');
可以看到位置0的数据已经修改, 但是并没有影响position, 还是可以继续put数据.
Flipping the Buffer
填充好数据后就要准备获取数据了. 如果在填充好数据马上执行get()方法, 会返回一个null数据, 因为此时的position指向的位置为null. 所以如果想要从头获取数据则需要将position设置为0. 那么一共要读取多少数据呢? 这就是limit需要做的. 在设置position之前需要设置limit的值. 可以使用flip()方法完成上述内容.
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
聪明如你, 肯定马上意识到一个问题, 如果连续调用两次filp()方法会怎么样? 根据代码, 两次调用后position = 0, limit = 0. 之后执行get()和put()方法都会抛出异常.
Draining the Buffer
有了上面的基础, 我们就可以循环读取所有的buffer内容了. 思路就是一直读取, 直到position == limit, 当然我们可以自己实现这个逻辑, 不过还是使用Buffer自带的方便, Buffer提供两个方法hasRemaining() 和 remaining(), 我们只需要每次获取数据前调用进行校验.
public final int remaining() {
return limit - position;
}
public final boolean hasRemaining() {
return position < limit;
}
Compacting the Buffer
有时候, 我们可能会只读取buffer中的一部分数据, 读取之后再往buffer中填充数据. 为了实现这个, 需要将未读取的数据转移到位置0的地方. Buffer提供了compact()方法完成上述操作.
我们可以通过这种方式将buffer当作一个先入先出队列. 当然还存在很多性能比这个好的队列算法, 但是从socket中读取逻辑数据块, 压缩方式是一中方便的同步方式.
注: 原文:You can use a buffer in this way as a First In First Out (FIFO) queue. More efficient algorithms certainly exist (buffer shifting is not a very efficient way to do queuing), but compacting may be a convenient way to synchronize a buffer with logical blocks of data (packets) in a stream you are reading from a socket.
在执行完compact()方法后, 在读取之前记得执行flip().
Bulk Data Movement from Buffers
Buffer的设计目标就是提高数据传输的效率, 但是之前介绍的方法都是一个一个数据读取, 这显然不够高效. 下面来看下批量的读取方式.
以CharBuffer为例, 该类提供了两个批量获取方法, 如下
public CharBuffer get (char [] dst)
public CharBuffer get (char [] dst, int offset, int length)
两个方法都是将buffer中的内容复制到指定数组中, 区别是多参数方法会指定dst数组的开始offset和复制的长度length. 也就是说get(dst) 和 get(dst, 0, dst.length)是一样的.
当没有数据可以传输, 或者传输的数据不够数组的大小的话在执行批量操作后会抛出异常. 如果buffer的长度较小, 就需要在复制数据时指定长度, 代码如下:
char [] bigArray = new char [1000];
// 获取buffer可读取长度
int length = buffer.remaining( );
// 读取指定长度, 这里假设length < 1000
buffer.get (bigArrray, 0, length);
// 数据处理
processData (bigArray, length);
当buffer比较大时, 我们可以使用迭代的方式分批获取数据, 代码如下:
char [] smallArray = new char [10];
while (buffer.hasRemaining()) {
int length = Math.min (buffer.remaining( ), smallArray.length);
buffer.get (smallArray, 0, length);
processData (smallArray, length);
}
put的批量方法与get比较类似, 就不赘述了.
Duplicating Buffers
以下三种方式可以复制buffer对象
public abstract CharBuffer duplicate();
public abstract CharBuffer asReadOnlyBuffer();
public abstract CharBuffer slice();
duplicate()会创建一个跟原buffer一模一样的buffer, 二者的数据共享(position limit mark这些不共享), 修改一个的数据另一个也会有影响.
asReadOnlyBuffer()方法与duplicate()的不同在于, asReadOnlyBuffer()创建的buffer是只读的, 禁止访问put方法, 否则会抛出异常.
slice()方法与duplicate()方法的区别在于, slice()只保留原buffer position到limit之间的数据.如图所示
参考链接 https://howtodoinjava.com/java/nio/java-nio-2-0-working-with-buffers/