【Java】NIO ByteBuffer类使用

6 篇文章 0 订阅

最近在看zookeeper的实现,所以必须先学会java nio,记录下。

nio中有三个重要的概念,buffer,channel和selector。channel是连接对应于传统io的流,而buffer就是channel交换数据的地方。

buffer有很多类型,每一个基本类型都有一个buffer,但是网络中用的最多的是ByteBuffer。我们通过channel向程序内读数据或者向程序外写数据都是通过bytebuffer来进行的,这样可以在一次io中尽可能多地传递数据,这也是buffer的作用。传统io中也有buffer的api,但是不强制,在nio中,buffer是唯一的方式。下面是ByteBuffer的一些常用方法。

1.创建:

有四种的方法:

ByteBuffer buffer = ByteBuffer.allocate(capacity);
会在java堆中分配一个空数组,大小是capacity。

ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);
直接方式会在操作系统的内存中直接分配空间,io更有效,但是是系统调用,调用一次的开销要大于前者。所以一般在缓冲区长期要被使用的情况下才会使用第二种构建方法。

ByteBuffer buffer = ByteBuffer.wrap(array);
使用一个数组来构建一个缓冲区,其实每一种buffer底层就是一个数组,采用这种方式可以直接操作底层的数组。缓冲区的大小是数组的大小,缓冲区中会有数组原本的内容。

ByteBuffer buffer = ByteBuffer.wrap(array, offset, length);
仍然使用一个数组,但是可以通过offset和length两个参数指定buffer的参数。buffer的参数后面会说,offset就是buffer的position,offset+length就是buffer的limit。


2.Buffer参数:

Buffer是一个封装的数组,关于这个数组会有一些重要的参数:

position:相当于一个内置的游标,一些buffer的api会通过这个游标修改或者读取数据。

limit:这个其实在读写时的含义不同,但是position只能在0-limit之间。

capacity:这个是固定的,也就是底层数组的大小。

mark:用于记录一个position,之后可以回到这个position。
我们通过buffer的allocate方法构建的buffer,会处于一个初始化的状态,limit=capacity,position游标=0。mark=-1(未使用)。

无论在任何情况下,下面的关系是存在的:

mark<=position<=limit<=capacity。
所有这些参数其实都是封装好的,我们使用buffer时并不会直接操作这些,但是为了更好地使用buffer,还是必须明白这些参数的作用和运作,这就要结合buffer的api了。

3.写入Buffer操作:

主要分为两种,一个是从channel写入,那么就是调用channel的read方法,也就是把数据读入程序中。

另外一种是从程序中往buffer中写入,最后一般是再把buffer中的数据写入channel中。这种写入buffer的方法有三个,两个是相对的,一个是绝对的。

相对的:意思就是位置由buffer自己来定,插入以后position会自动增加。

put(byte a):会在position位置写入byte,同时position++。

put(byte[] a):会在position位置写入byte数组的全部内容,同时position+=a.length。

绝对的:意思是人为指定插入位置,position不会改变。

put(int index, byte a):把byte插入到index位置,position不变。一般情况下使用相对。


4.从Buffer写出操作:

也分为两种,一个是向channel写入,那么就是调用channel的write方法,也就是把数据从程序中写出。

另外一种是从buffer中把数据读到程序中,一般是先从channel中读到buffer,再从buffer中读出。这种也分为了相对和绝对两种。

相对的:会改变position。

get():返回一个position处的byte,同时position++。

get(byte[] dst):把buffer中数据读入到dst数组中,position+=dst.length。这里的dst数组的大小必须小于等于buffer的大小,否则报错。

绝对的:

get(int index):返回指定位置的byte。


5.其他操作:

flip():这个函数会将limit设置为position,position设置为0,cap不变。

clear():会将limit设置为cap,position设置为0。

compact():会拷贝position到limit之间的数据到0位置,然后position设置为剩余limit-postion,limit设置为cap。

flip用于buffer的模式转换,把写入模式转为读取模式,clear和compact用于把读模式转换为写模式。区别在于clear相当于清空,compact保留未读的数据。


6.模式:

一般会说buffer有读模式和写模式,其实单从buffer的设计上并没有,只是我们可以总结出一些buffer的使用方式,也就成为了读模式和写模式。上面的api没有限定所谓的模式,但是设计的时候其实是用于特定的模式或者场景的。

buffer的写入模式,一般是刚allocate以后或者调用了compact和clear方法以后,特征是position是下一个可写的位置,然后limit与cap相等。

buffer的读模式,一般是调用了flip方法以后,此时limit是数据的最后一位,position是0,我们可以通过position游标来读取。当我们要往channel写入时,必须先转变buffer为读取模式,这样channel的write操作就会把0-limit的数据写入。

或者说,只要我们要从buffer读取,就要先调用flip,只要向buffer写入,就要先clear活着compact。

盗一张图:http://ifeve.com/buffers/(原图地址)。


buffer的模式在使用中要与channel结合。

对于channel的使用,我们无非就两种,一个是从程序外读取,一个是向程序外写入。

从外界读取,一般的用法是:

新建一个buffer,此时buffer处于初始化状态,是一个可以写入的状态,position=0,limit=cap。

然后调用channel的read方法把数据读到buffer中,那么此时position正是我们读到的数据的最后位置。read方法通常是放在一个while中,while检测读取的byte的数目,只要数目大于0,就认为读到了数据,就处理;否则就跳出循环。

调用buffer的flip方法,会把limit设置为position,position设置为0,这样buffer就进入了一个读的状态,

调用buffer的get方法从buffer中读取数据,最后调用clear,以便于buffer后续被使用。


向外界写入:

新建一个buffer,调用buffer的put方法写入数据,然后调用flip函数,这时就会把limit设置为position,把position设置为0,那么写入的数据就会在0到limit之间,也就是以后要向channel写入的内容。最后调用channel的write方法即可。


总结来说,buffer有两种模式,channel有两种操作。channel的每一种操作都会对应buffer的两种模式。

channel读入数据,那么buffer先是写模式,然后flip,然后再从buffer读。

channel写入数据,那么buffer先是写模式,然后flip,然后调用channel的write,相当于是从buffer中读。

所以buffer的操作肯定是先写再读,中间用一个flip转换。如果要写入多次(空间不够),那么每一次读完需要clear或者compact来继续下一次地写入。

所以个人感觉,对于buffer,必须搞清楚buffer的变量,一些读写的api,模式切换,和channel配合使用这些内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值