ByteBuffer的概述
Buffer是java NIO的通道与I/O通信的入口,Buffer是通道向I/O发送数据的来源或者接受I/O数据的目的,一个Buffer对象是固定数量的一个容器,本质是一个基本类型的数组+属性信息。
ByteBuffer是Buffer的一个子类,由于I/O通信底层都是字节通信,因此ByteBuffer是学习的重点。
ByteBuffer的使用
1.ByteBuffer创建
ByteBuffer的创建是通过调用ByteBuffer的静态方法创建,不是通过new的方法,具体如下:
ByteBuffer bb = ByteBuffer.allocate(1024);
2.ByteBuffer属性
capacity: ByteBuffer的大小,创建ByteBuffer的时候指定,比如上面的1024,一旦创建好的ByteBuffer大小是不变的。
limit:ByteBuffer中第一个不能被读或者被写的字节的位置,新创建的ByteBuffer中limit的值等于capacity
position:ByteBuffer中下一个被读或者写的位置。ByteBuffer执行put/get方法的时候,会改变position的值,初始值为0。
mark:一个备用标记,用于临时记录position,当ByteBuffer执行mark()的时候,mark的值就被设置为position的值。执行reset方法的时候,会把mark的值赋值给position,mark与reset是一对。mark在创建ByteBuffer的时候值为-1。
3.Bytebuffer API用法
创建一个Buffer 对象bf = ByteBuffer.allocate(8);
执行bf.put((byte)’a’), bf.put((byte)’b’), 执行get 方法后,position 的值会加1
注意:bf.put()方法有一个重载的方法:put(int index,byte b),该方法是把数据放到index 代
表的位置上,次方法操作后不会修改position 的值,同理get 方法跟put 一样。
执行bf.put(new byte[]{'c','d'}) 后:
以上的方法都是用来向bf 填充数据,加入需要填充的数据只有这4 个字母,那么下一步就
是想bf 的内容发给socket 上。下面就需要执通道channel 的write 方法:
channel.write(bf)
这个方法会把bf 中数据写入channel 的发送缓存区,channel 的write 的方法有个特点,他
只会发送position 与limit 之间的数据。如果此时调用write 方法,肯定会不能将abcd 发送
出去,因为此时bf 的position=4, limit = 8。因此需要将position 设置为a 的位置即0,将
limit 设置为d 的下一个位置即4,这样channel 才会把abcd 写出去。由于手动执行这些操
作太麻烦,NIO 提供了一个api 方法:flip(),这个方法可以完成上面的逻辑,flip 具体实现:
执行完flip()方法后,bf 如下图:
再创建一个容量为8 的ByteBuffer bf:用来接收从通道传来的数据。
执行channel.read(bf),把通道缓存中的数据读入到bf 中,加入读入3 个字符:k ,n, m
此时bf 中存储了k,n,m 三个字符,如果想获取k,n,m,需要使用bf.get(byte[] b)方法,把数
据一次性读入byte[]数组中,bf.get 的方法是从bf 的positon 位置开始一直读到limit 为止,
而此时position 为3,因此需要执行bf.flip()方法。修改position 和limit 的值:
bf 有一个方法叫remaining(),该方法的作用就是获取limit 与position 之间的大小,也就是获取了本次要读取数据长度。因此在执行bf.get(byte[] b)之前需要创建一个数组,该数组的大小通常使用:
b = new byte[ bf. remaining()]
compact()方法:该方法的作用是把poistion 和limit 直接的数据移动到bf 的初始位置,具体实现:
public ByteBuffer compact() {
//移动数组
System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
//数组移动后把设置position 的值为下一个可写入bf 的位置
position(remaining());
//设置limit 的值为capacity
limit(capacity());
//销毁标记,mark=-1
discardMark();
return this;
}
假如bf 的状态入下:
执行bf.get()操作后,变成:
在上图的基础上执行bf.compact():
这里特别要注意的是索引2 里面的值仍然是m,因为compact 操作只是把1 和2 的值移动到了0 和1 的位置上,2 的位置上的值并没有清除,但是2 的位置已经成为下一个可以写入值得位置了。
mark()方法与reset()方法使用:
这两个方法,通过一个具体的例子来讲解:就是当一个ByteBuffer 的大小超过了channel 的缓存,那么channel 就不能一次性把byteBuffer 中的值写出去。sc 是channel,当执行完sc.write(bf),之后,bf 中的数据,还有剩余,比如bf 有8 个字节,sc.write()一次写出5,还剩余3 个,此时bf 的内存图如下:
因为没有写完,需要下次写事件发生时,需要接着从position=5 的地方输出后面的数据。bf 前5 个字节已经使用,那么bf 就有空闲的容量,可以接收新的数据存入bf,然后执行bf.compact()操作bf 的内存图如下:
此时position=3 那么,bf 可以执行put()操作继续往bf 写入新数据,而不会担心覆盖老数据。当新的写事件到的时候,channel 又可以往出去写数据,需要把上次没有写完的数据写出去,如果直接执行write()方法,因为执行了compact()操作,position=3,而正确的数据是从position=0 开始,到position=3 为止,因为position 位置错误,导致write 的时候,老有数据写出,并且永远写不完。正确的做法是,在write 之前,把position 设置为0,limit 设置为3。limit=3 这个值如何设置呢?由于在上一次写操作后,执行完compact 后的position的值就是下一次写操作的limit 值。因此需要把position 备份一下,这里就用到了mark()操作,执行mark,把position 赋值给mark。如下图:
等到下一次写事件发生时,首先把mark 值取出来,赋值给limit,然后把position 设置为0。把mark 不能直接赋值给limit,首先要执行reset 操作,如下图:
把mark 值赋值position,然后调用position()方法,把值赋给limit,
最后执行rewind 方法把position 赋0,把mark 设置为-1。
最后在执行write 方法,f,g,h 被正确写到通道中。