javaNIO 学习笔记(三)

Java NIO Buffer

缓冲区,可以通过channel将数据写入缓冲区,也可以从通道中读取数据到缓冲区

缓冲区本质上是一个内存块,您可以将数据写入其中,然后再读取数据。这个内存块包装在一个NIO缓冲区对象中,该对象提供了一组方法,使使用内存块变得更容易。

使用缓冲区读取和写数据一般分为下面四个步骤:

  • 将数据写入缓冲区
  • 调用 flip()方法
  • 从缓冲区读取数据
  • 调用clear()或者compact()

当数据写入数据到缓冲区时,缓冲区会记录对应数据量的信息(这个需要看缓冲区大小和数据量大小)。

数据读取后需要使用flip()方法切换缓冲区的读写模式(换个角度可以理解为存取模式)。在读取模式下,缓可以通过相应的方法将数据从缓冲区中读取出来。读取完数据后需要将缓冲区清空。这样缓冲区就会再次进入写入模式。(这里调用的方式就是clearcompact, clear是清空, compact则是清理掉已读取的数据,未读取的数据则放入缓冲区头)

使用的例子上一篇学习笔记已经写了。

我们来看下buffer的工作模式。buffer有三个主要属性

  • capacity(buffer容量):这是buffer的大小即容量。在调用buffer.allocate()指定大小即是这个属性
  • position(读取模式的位置,在读写模式中表现不一样)
  • limit(读写模式中表现也不一样)

在写入模式下,position初始值为0.当一个字节,没写入一个字节数字都会将position+1.limit初始值为capacity。在读取模式下,会将limit位置设置在```position位置放置为0,此时读取数据是从position到limit。看下clearflip的源码大概就可以理解了

public final Buffer clear() {        
    position = 0; //设置当前下标为0        
    limit = capacity; //设置写越界位置与和Buffer容量相同         
    mark = -1; //取消标记         
    return this; 
} 


public final Buffer flip() { 
        limit = position; 
        position = 0; 
        mark = -1; 
        return this; 
}

buffer的主要类型如下:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
  • MappedByteBuffer

可以看到,这些缓冲区类型表示不同的数据类型。换句话说,它们允许您将缓冲区中的字节改为char、short、int、long、float或double。

学习下buffer的几个常见方法:

// 可以通过调用Buffer.mark()方法标记缓冲区中的给定位置。然后,您可以通过调用Buffer.reset()方法将位置重置回标记的位置。
public final Buffer mark() {
    mark = position;
    return this;
}

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}
  • equals()和compareTo ()
    可以使用equals()和compareTo()比较两个缓冲区。

equals ()

两个缓冲区是相等的,如果:

它们是相同类型的(字节、char、int等)。

它们在缓冲区中有相同数量的剩余字节、字符等。

所有剩余的字节、字符等都是相等的。

可以看到,equals只比较缓冲区的一部分,而不是其中的每个元素。实际上,它只是比较缓冲区中剩余的元素。

compareTo ()

方法比较两个缓冲区的剩余元素(字节,字符等),用于排序例程。一个缓冲区被认为比另一个缓冲区“小”,如果:

第一个元素等于另一个缓冲区中对应的元素,小于另一个缓冲区中的元素。

所有的元素都是相等的,但是第一个缓冲区比第二个缓冲区早耗尽元素(它的元素更少)。

Java NIO Scatter / Gather

Java NIO 提供了内置功能 Scatter/Gather。可以将数据从一个通道写入多个缓冲区。通道也可以从多个缓冲区收集数据。在需要分别处理传输数据的各个部分的情况下,Scatter/Gather非常有用。例如,如果消息由消息头和消息体组成,则可以将消息头和消息体保存在单独的缓冲区中。这样做可以更容易分别使用标题和主体。

package jniolearn;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @Author: jimmy
 * @Date: 2020/6/14 14:50
 * @Description:
 */
public class NioScatter {

    public static void main(String[] args) throws IOException {

        // 创建一个rw模式的随机文件
        RandomAccessFile randomAccessFile =new RandomAccessFile("D:\\nioFile.txt", "rw");
        // 获取fileChinnel
        FileChannel fileChannel = randomAccessFile.getChannel();

        // 1、分配缓冲区
        ByteBuffer header = ByteBuffer.allocate(10);
        ByteBuffer body  = ByteBuffer.allocate(128);

        // 缓冲数组
        ByteBuffer[] bufferArray = {header, body};
        // 2、将数组读入到数组
        fileChannel.read(bufferArray);

        // Buffer切换模式之前,即处于写模式下,打印Buffer,查看position, limit, capacity属性
        System.out.println(header.toString());
        System.out.println(body.toString());

        // 3、切换模式
        header.flip();
        body.flip();

        //  4、获取数组
        System.out.println("header:");
        while (header.hasRemaining()) {
            System.out.print((char) header.get());
        }

        System.out.println("\nbody:");
        while (body.hasRemaining()) {
            System.out.print((char) body.get());
        }

        header.clear();
        body.clear();

        fileChannel.close();

    }

}
// 返回结果
java.nio.HeapByteBuffer[pos=10 lim=10 cap=10]
java.nio.HeapByteBuffer[pos=24 lim=128 cap=128]
header:
Hi, I am j
body:
immy;
be happy everyday

read按照buffer数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息。这样若要是想在消息中使用那么就必须规定好每一段的字节长度,并且按照这个规定严格执行,不能存在偏差。不然就会出现部分信息少读或者多读的情况。

package jniolearn;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @Author: jimmy
 * @Date: 2020/6/14 15:07
 * @Description:
 */
public class NioGather {

    public static void main(String[] args) throws IOException {

        RandomAccessFile randomAccessFile =new RandomAccessFile("D:\\gaterFile.txt", "rw");
        // 获取fileChinnel
        FileChannel fileChannel = randomAccessFile.getChannel();

        // 1、分配缓冲区
        ByteBuffer header = ByteBuffer.allocate(32);
        ByteBuffer body  = ByteBuffer.allocate(128);

        // 2、放入数据
        header.put("hi, weather ?".getBytes());
        header.put("body:raining".getBytes());

        // 3、存入buffer数组
        ByteBuffer[] bufferArray = {header, body};

        System.out.println(header.toString());
        System.out.println(body.toString());

        header.flip();
        body.flip();

        fileChannel.write(bufferArray);

        fileChannel.close();
    }
}

查看文件信息:hi, weather ?body:raining

write方法同样是按照buffer数组的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含14byte的数据,那么这14byte的数据将被写入到channel中。和Scattering Reads相反,Gathering Writes能较好的处理动态消息。

Java NIO Channel to Channel Transfers

Java NIO还提供了一种通道间传输数据的方法,FileChannel类有一个transferTo()和一个transferFrom()方法

----  transferFrom   ----
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel      fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel      toChannel = toFile.getChannel();

long position = 0;
long count    = fromChannel.size();

toChannel.transferFrom(fromChannel, position, count);

----  transferTo   ----
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();

RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();

fromChannel.transferTo(position, count, toChannel);

小结下前面几篇学习的内容:

  • 如何使用buffer
  • 需要先分配一个缓冲区(我这里理解为初始化一个指定大小的缓冲区)使用ByteBuffer.allocate()
  • 读则是从channel read,写则是先要将数据放入缓冲区,然偶使用channel write把数据写入通道
  • 注意buffer的模式切换使用flip方法。
  • 另外还有 clear compact rewind等方法
  • buffer的几个主要属性
  • capacity 缓冲区大小(比如缓冲区长度48,则capacity也是48.但是这里要注意capacity下表是从0开始,所以这个可以理解为内存溢出位)
  • position 在读模式的时候初始值为0,读数据的时候会逐步增加最大能读到capacity -1。切换到写模式的时候position为0
  • limit 读模式的时候默认为capacity。写模式的时候,会将limit设置为读模式的position的位置。
  • scattergather
  • 可以理解为分散收集,一个是读,一个是写。这个是内置的。就是一个channel和多个buffer的读写。
  • channel如何获取
  • 可以直接使用open方法,fileChannel则还可以通过类RandomAccessFile的实例getChannel