Java提供了NIO操作的API,但真正处理NIO流,经常会出现如下代码:
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (channel.read(buffer)!=-1){
//复位,转化为读模式
buffer.flip();
while (buffer.hasRemaining()){
System.out.println("收到客户端"+channel.socket().getPort()+"的信息:"+ StandardCharsets.UTF_8.decode(buffer).toString());
}
//清空缓存区,转化为写模式
buffer.clear();
}
可以看出读写经常会出现flip()和clear()等方法,这究极有什么作用?为什么要这么写?
所有IO都有缓冲区,需要将客户端数据或文件数据读取到内存缓冲区,Java程序才能读取到内存里的客户端或文件内容数据。写入也是如此,Java需要先写入到缓冲区,内核才能把缓冲区数据传输给客户端或文件。
首先看看Buffer的属性:
容量(Capacity):缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。
上界(Limit):缓冲区的能被写入或读取的最大个数(索引值为该值-1),比如limit为5,capacity为10,则put(5,6)会报错,因为5的代表第6个值,而limit最大只有5,即索引0~4内可写。limit限制了buffer内可写的范围。而capacity限制了buffer的范围。
位置(Position):下一个要被读或写的元素的索引。位置会自动由相应的put( )、get()、flip()、clear()函数更新。
标记(Mark):一个备忘位置。调用 mark( )来设定 mark = postion。调用 reset( )设定 position = mark。mark默认值为-1(JDK8下)。
这些属性在通过实例化或get()和put()等函数操作Buffer时会更新,无论上面怎么变,会向源码里解释的那样:mark <= position <= limit <= capacity。
具体测试代码如下:
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(100);
System.out.println(buffer);
byte[] content = "123".getBytes(Charset.forName("UTF-8"));
System.out.println("添加的byte数组长度为:"+content.length);
buffer.put(content);
System.out.println(buffer);
// buffer.flip();
// System.out.println(buffer);
byte data = buffer.get();
System.out.println((char)data);
// System.out.println("buffer的数据为:"+StandardCharsets.UTF_8.decode(buffer).toString());
System.out.println(buffer);
}
打印结果如下:
可以看到:
put():可以将内容添加到buffer中,并将更新position=position+内容长度。
get():是直接读取索引为position的元素,并更新position=position+1。
因此,直接put()添加完内容后用get()读取内容,是读取不到数据的!需要put()完,再调用flip()才能用get()读取到数据,具体如下:
可以看到:
flip():将limit=position,position=0,mark=-1。这样由于limit=position,buffer是不能添加数据的,因此限制了buffer的写入,而position=0,可以让get()从第一个元素读取。mark=-1,也是给标记重置,虽然mark没啥作用,一般的读写我们也用不上这个mark。
所以,读取buffer数据前一定要调用flip(),否则读取不到数据!
那么读取数据怎么判断position是否超过了limit,可以用remaining()和hasRemaining()判断,具体如下:
所以读取数据需要用 flip()限制buffer写入并将position和mark复位,然后调用hasRemaining()对position的位置和limit进行判断,看是否还有数据读取。最后调用get()读取buffer的内容。当然,用其它API读取buffer可以可以的。
写入数据上面将了,直接调用put()即可。但问题来了,读取数据需要调用flip(),position会置为0,因此若buffer有历史数据的话,调用flip()再读,会造成历史数据重复读取!所以,Java的API给我们提供了clear()和mark()、reset()方法。
clear()方法会将limit改成capacity,即允许可写,且是整个buffer都可写。将position=0,即后写入的数据将历史数据覆盖。所以调用clear()方法前一定要读完所有数据。
所以一般的buffer读写操作是:
先clear(),再写入()。确保buffer的数据都是新写入的内容,避免使用flip()后读取会读取到历史数据。
当然,有时我们需要保留历史数据,然后再写入,不调用flip()直接读取新写入的数据,这也是可以的,这就需要用到mark了。具体方法如下:
缓冲区内有历史数据,先mark(),让mark=position,然后直接put写入新内容,写完后,调用reset(),使postion回到新内容的起点,再直接调用get()读取数据。这样就只会读新内容,也无需清除历史内容了。
buffer若不需要追加读,则只需要 get()读取前调用flip()方法从头读取,put()方法前调用clear()重置position从头写入覆盖历史数据。
buffer需要追加读,则put()前需要调用mark()记录下写入的起始点,写完后直接调用reset()将position调整为新内容的起始点mark。接着再调用get()方法进行读取。