Java NIO

46 阅读5分钟

Java NIO

简介

Java NIO是java 1.4之后新出的一套IO接口,这里的的新是相对于原有标准的Java IO和Java Networking接口。NIO提供了一种完全不同的操作方式。

标准的IO编程接口是面向字节流和字符流的。而NIO是面向通道和缓冲区的,数据总是从通道中读到buffer缓冲区内,或者从buffer写入到通道中。

Java NIO使我们可以进行非阻塞IO操作。比如说,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。

NIO中有一个“slectors”的概念。selector可以检测多个通道的事件状态(例如:链接打开,数据到达)这样单线程就可以操作多个通道的数据。 所有这些都会在后续章节中更详细的介绍。

概念

NIO包含下面几个核心的组件:

Channels
Buffers
Selectors

通道和缓冲区(Channels and Buffers)

通道(Channel)

通道(Channel) :由java.nio.channels包定义 的。Channel 表示I0源与目标打开的连接。 Channel类似于传统的“流”。只不过Channel 本身不能直接访问数据,Channel 只能与Buffer进行交互。

通道 Channel 用于源节点与目标的连接。在Java NIO 中负责缓冲区中数据传输。Channel本身不存储数据,因此配合缓冲区进行传输。

通道主要实现类
      java.nio.channels.Channel 接口
         FileChannel
         SocketChannel
         ServerSocketChannel
         DatagramChannel
获取通道

1 java 针对支持通道的类提供了getChannel()方法

        本地IO
             FileInputStrem/FileOutputStream
             RandomAccessFile
         网络IO
             Socket
             ServerSocket
             DatagramSocket

2 在JDK 1.7 中的NIO .2 针对各个通道提供了静态方法open();

3 在JDK 1.7 中的NIO.2 的Files 工具类的 new ByteChannel();

缓冲区

缓存区 (Buffer) 在NIO 中负责数据的存取。缓冲区就是数组,用于不同类型的数据的存储。

根据数据类型不同 Boolean 除外 ,提供了相应的类型的缓冲区:
ByteBuffer 最常用
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer

以上缓冲区管理方式几乎一致,通过 allocate()获取缓冲区。

2 缓冲区存取数据的2个核心方法

put() 存入数据到缓冲区
get() 获取缓冲区的数据

3 缓冲区四个核心属性

private int mark = -1; 用于标记 记录当前 position 位置,可以通过reset 恢复到mark位置。
private int position = 0; 位置,表示缓冲区正在操作的数据,position <= limit <= capacity
private int limit; 界限,表示缓冲区可以操作数据的大小。limit 后面的数据不能进行读写。
private int capacity; 容量,表示缓冲区最大存储数据的容量,一旦声明,不能改变。

NIO缓冲区写数据
第一行是初始化缓冲区三个参数
第二行表示写数据模式
第三行表示读数据模式
在这里插入图片描述

初始化缓冲区,给缓冲区写入数据,读取缓冲区数据,可重复读数据,清空缓冲区数据后三个参数的变化。

 //1 分配一个指定大小的缓冲器
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        System.out.println("-------------------- allocate 初始化分配  ---------------------- ");
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
        System.out.println(byteBuffer.capacity());

        // 2 利用Put() 存入数据到缓冲区
        String str = "12345";
        byteBuffer.put(str.getBytes());
        System.out.println("-------------------- put 写数据  ---------------------- ");
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
        System.out.println(byteBuffer.capacity());


        // 3 切换到读数据模式
        byteBuffer.flip();
        System.out.println("-------------------- flip 切换读模式  ---------------------- ");
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
        System.out.println(byteBuffer.capacity());

        //4 利用get() 读取缓冲区中的数据
        byte dst[] = new byte[byteBuffer.limit()];
        byteBuffer.get(dst);
        System.out.println("读取到数据  : " + new String(dst, 0, dst.length));

        System.out.println("-------------------- get 读取数据  ---------------------- ");
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
        System.out.println(byteBuffer.capacity());


        // 5 rewind 可重复读数据 (可以重新从读模式开始重新读取数据)
        byteBuffer.rewind();
        System.out.println("-------------------- rewind 可重复读数据  ---------------------- ");
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
        System.out.println(byteBuffer.capacity());


        // 6 clear 清空缓冲区  但是缓冲区数据依然存在,但是出于“被遗忘状态”。
        byteBuffer.clear();
        System.out.println("-------------------- clear 清空缓冲区  ---------------------- ");
        System.out.println(byteBuffer.position());
        System.out.println(byteBuffer.limit());
        System.out.println(byteBuffer.capacity());

        System.out.println(" 获取被遗忘数据  " + (char) byteBuffer.get());

结果

-------------------- allocate 初始化分配  ---------------------- 
0
1024
1024
-------------------- put 写数据  ---------------------- 
5
1024
1024
-------------------- flip 切换读模式  ---------------------- 
0
5
1024
读取到数据   12345
-------------------- get 读取数据  ---------------------- 
5
5
1024
-------------------- rewind 可重复读数据  ---------------------- 
0
5
1024
-------------------- clear 清空缓冲区  ---------------------- 
0
1024
1024

规律

0 <= mark <= position <= limit <= capacity

直接缓冲区与非直接缓冲区

非直接缓冲区 : 通过allocate() 方法分配缓冲区,将缓冲区建立在JVM内存中

在这里插入图片描述

直接缓冲区:通过allocateDirect() 方法分配直接缓冲区,将缓冲区建立在操作系统物理内存中。

在这里插入图片描述

直接与非直接缓冲区区别

在这里插入图片描述

直接缓冲区获取

  ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
        //是否是直接缓冲区
        System.out.println(byteBuffer.isDirect());

选择器(Selectors)

Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

选择器 Selector : 是 SelectableChannel 的多路复用。用于监控 SelectableChannel 的IO状况。

mark()标记和reset()恢复标记位置方法

 ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byte dst[] = new byte[byteBuffer.limit()];

        String str = "abcdef";
        //存
        byteBuffer.put(str.getBytes());

        byteBuffer.flip();
        //读取前两个
        byteBuffer.get(dst, 0, 2);

        System.out.println(new String(dst));
        System.out.println(byteBuffer.position());//2

        //标记
        byteBuffer.mark();
        //读取后两个
        byteBuffer.get(dst, 2, 2);
        System.out.println(new String(dst, 2, 2));
        System.out.println(byteBuffer.position());//4


        // reset() 恢复到mark 标记 位置
        byteBuffer.reset();
        System.out.println(byteBuffer.position()); //2


        //常用方法
        //判断缓冲区是否剩余的数据
        if (byteBuffer.hasRemaining()) {
            //获取缓冲区剩余数据
            System.out.println(byteBuffer.remaining()); //4 刚才 position 被 reset 恢复到 2 所以剩4个
        }

FileChannel文件通道

Java NIO中的FileChannel是用于连接文件的通道。通过文件通道可以读、写文件的数据。Java NIO的FileChannel是相对标准Java IO API的可选接口。

FileChannel不可以设置为非阻塞模式,只能在阻塞模式下运行。

FileChannel 文件读取和复制

   // 1 利用通道完后曾文件的复制(非直接缓冲区)

        FileInputStream inputStream = null;
        FileOutputStream outputStream = null;

        FileChannel inputStreamChannel = null;
        FileChannel outputStreamChannel = null;


        try {
            //输入
            inputStream = new FileInputStream("1.jpg");
            //输出
            outputStream = new FileOutputStream("2.jpg");

            //1 获取通道
            inputStreamChannel = inputStream.getChannel();
            //输出通道
            outputStreamChannel = outputStream.getChannel();

            //2 分配指定大小的缓冲区
            ByteBuffer allocate = ByteBuffer.allocate(1024);

            //3 将通道中的数据存入缓冲区中
            while (inputStreamChannel.read(allocate) != -1) {
                //4 切换读模式
                allocate.flip();
                //5 将缓冲区数据写入通道中
                outputStreamChannel.write(allocate);
                //6 清空缓冲区
                allocate.clear();
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStreamChannel != null) {
                try {
                    outputStreamChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (inputStreamChannel != null) {
                try {
                    inputStreamChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }


            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }


            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }


        }

直接缓冲区复制文件

 //直接缓冲区 channel (内存映射文件方式   )   内存读取写文件 效率高,但是不稳定  垃圾回收机制不及时释放,会占用系统资源。
    static void channel2() {
        FileChannel open = null;
        FileChannel write = null;
        try {
            // StandardOpenOption 枚举 提供读 写 模式
            //将本地文件读到内存中
            open = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);

            //读写通道  将内存中的文件读到写通道然后再写到本地
            //CREATE 不管存不存在都创建覆盖;  CREATE_NEW  不存在就创建,存在就报错
            write = FileChannel.open(Paths.get("3.jpg"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);

            //内存映射文件
            // 读模式 从0开始读有多少复制多少  将本地文件读到内存映射文件中
            MappedByteBuffer inMappedBuf = open.map(FileChannel.MapMode.READ_ONLY, 0, open.size());

            //将内存文件 读到写通道中然后再写到本地
            MappedByteBuffer outMappedBuf = write.map(FileChannel.MapMode.READ_WRITE, 0, open.size());

            byte[] dst = new byte[inMappedBuf.limit()];
            //读文件
            inMappedBuf.get(dst);
            //写文件
            outMappedBuf.put(dst);


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (open != null) {
                try {
                    open.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (write != null) {
                try {
                    write.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

通道数据传输(直接缓冲区)



        FileChannel in = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
        FileChannel out = FileChannel.open(Paths.get("4.jpg"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);

        //从in通道传输到out通道  都是表示从in传输数据到out通道
        in.transferTo(0, in.size(), out);
        //out.transferFrom(out,0,in.size());

        out.close();
        in.close();

分散与读取

分散读取:把通道中的数据分散(按顺序)到多个缓冲区中。
聚集写入:将多个缓冲区的数据写入一个通道中。

 //读文件
        FileInputStream fileInputStream = new FileInputStream("NIO-Demo.iml");
        //写入
        FileOutputStream fileOutputStream = new FileOutputStream("NIO-Demo-test.txt");

        FileChannel inputStreamChannel = fileInputStream.getChannel();
        FileChannel outputStreamChannel = fileOutputStream.getChannel();

        //缓冲区
        ByteBuffer allocate1 = ByteBuffer.allocate(100);
        ByteBuffer allocate2 = ByteBuffer.allocate(1024);

        ByteBuffer[] byteBuffers = {allocate1, allocate2};

        //通道 分散读
        inputStreamChannel.read(byteBuffers);

        for (ByteBuffer b : byteBuffers) {
            //开启读模式
            b.flip();
        }

        //缓冲区读到的数据
        System.out.println(new String(byteBuffers[0].array(), 0, byteBuffers[0].limit()));
        System.out.println("------------- ");
        System.out.println(new String(byteBuffers[1].array(), 0, byteBuffers[1].limit()));

        //通道 聚集写入
        outputStreamChannel.write(byteBuffers);

        outputStreamChannel.close();
        inputStreamChannel.close();

管道

管道是两个线程之间单向数据连接
source通道负责读取,sink通道负责写

在这里插入图片描述

  //1 获取管道
        Pipe pipe = Pipe.open();
        //缓冲区数据
        ByteBuffer allocate = ByteBuffer.allocate(100);

        //2 将缓冲区数据写入管道中
        Pipe.SinkChannel sink = pipe.sink();
        //通过单向管道发送数据
        String str = "我是数据!";
        allocate.put(str.getBytes());
        //切换读模式
        allocate.flip();
        sink.write(allocate);


        //3 从管道读取数据
        Pipe.SourceChannel source = pipe.source();
        allocate.flip();
        int read = source.read(allocate);
        System.out.println(new String(allocate.array(),0,read));

        allocate.clear();
        source.close();
        sink.close();

阻塞NIO

阻塞NIO分以下两种tcp和udp

TCP:
ScoketChannel
ServerSocketChannel
UDP:
DatagramChannel

 //发送
    static void send() throws IOException {
        // 1 获取通道  SocketChannel 是 tcp 非阻塞
        SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8888));

        // 2 切换非阻塞魔术
        open.configureBlocking(false);

        // 3 分配指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //4 发送数据
        byteBuffer.put("发偶是那个数据!".getBytes());
        byteBuffer.flip();
        //数据
        open.write(byteBuffer);
        byteBuffer.clear();


        //5 关闭通道
        open.close();


    }


    //接受
    static void reveice() throws IOException {
        // 1 获取tcp 非阻塞通道
        SocketChannel open = SocketChannel.open();

        // 2 切换非阻塞模式
        open.configureBlocking(false);

        //3 绑定端口号
        open.bind(new InetSocketAddress(8888));

        //4 选择器
        Selector selector = Selector.open();

        //注册到选择器 同时指定模式  这里是读模式
        open.register(selector, SelectionKey.OP_READ);

        //轮训获取选择
        while (selector.select() > 0) {
            //获取所有选择键
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey sk = iterator.next();
                //判断可读状态
                if (sk.isReadable()) {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    //切换读模式
                    byteBuffer.flip();
                    System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
                }
            }
        }


        //发送数据
        open.close();
        selector.close();

    }