NIO 缓冲区

摘要

        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需要先写入到缓冲区,内核才能把缓冲区数据传输给客户端或文件。

API        

        首先看看Buffer的属性:

NIO 缓冲区_第1张图片

容量(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);
    }

 打印结果如下:

NIO 缓冲区_第2张图片

可以看到:

put():可以将内容添加到buffer中,并将更新position=position+内容长度。

get():是直接读取索引为position的元素,并更新position=position+1。

因此,直接put()添加完内容后用get()读取内容,是读取不到数据的!需要put()完,再调用flip()才能用get()读取到数据,具体如下:

NIO 缓冲区_第3张图片

 可以看到:

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()方法。

NIO 缓冲区_第4张图片

clear()方法会将limit改成capacity,即允许可写,且是整个buffer都可写。将position=0,即后写入的数据将历史数据覆盖。所以调用clear()方法前一定要读完所有数据。

所以一般的buffer读写操作是:

先clear(),再写入()。确保buffer的数据都是新写入的内容,避免使用flip()后读取会读取到历史数据。

当然,有时我们需要保留历史数据,然后再写入,不调用flip()直接读取新写入的数据,这也是可以的,这就需要用到mark了。具体方法如下:

 NIO 缓冲区_第5张图片

 缓冲区内有历史数据,先mark(),让mark=position,然后直接put写入新内容,写完后,调用reset(),使postion回到新内容的起点,再直接调用get()读取数据。这样就只会读新内容,也无需清除历史内容了。

总结

        buffer若不需要追加读,则只需要 get()读取前调用flip()方法从头读取,put()方法前调用clear()重置position从头写入覆盖历史数据。

        buffer需要追加读,则put()前需要调用mark()记录下写入的起始点,写完后直接调用reset()将position调整为新内容的起始点mark。接着再调用get()方法进行读取。

你可能感兴趣的:(Java,nio,java,ByteBuffer,buffer,缓冲区)