JAVANIO学习笔记

JAVANIO学习笔记

文章发布于 2020-08-26 19:46:44,最后更新于 2020-08-31 10:38:35

上一节说到JAVA NIO,他属于第三种模型,就是IO多路复用模型。它由三部分组成,分别是Channel(通道)、Buffer(缓冲区)、Selector(选择器)。

首先我们将NIO和OIO(阻塞式IO)做个简单对比;

  1. OIO是面向字节流或者字符流的,NIO是面向缓冲区的。
  2. OIO操作是阻塞的,NIO是非阻塞的。
  3. OIO没有选择器概念,而NIO有(如果你不懂阻塞和选择器概念请看上一篇文章)。

当然或许你会问了什么是面向流,什么是是面向缓冲区?

在一般的OIO中以流式的方式(字节流、字符流)顺序的从一个流(stream)中读取一个或多个字节,因此不能随意的改变指针位置。而NIO不同,引入了Channel和Buffer,读取和写入,只需要从通道中读取数据到缓冲区中或者将数据从缓冲区写入通道。NIO不是OIO那种顺序操作,可以随意的读取Buffer中任意位置的数据。

通道Channel

OIO中一个网络连接对应两个流,输入流和输出流。在NIO中,一个网络连接对应一个通道,既可以向通道写入也可以从通道读取。

重要的四种Channel(通道)实现:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。

(1)FileChannel文件通道,用于文件的数据读写。

(2)SocketChannel套接字通道,用于Socket套接字TCP连接的数据读写。

(3)ServerSocketChannel服务器嵌套字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道。

(4)DatagramChannel数据报通道,用于UDP协议的数据读写。

FileChannel文件通道

FileChannel是专门操作文件的通道。通过FileChannel,既可以从一个文件中读取数据,也可以将数据写入到文件中。特别申明一下,FileChannel为阻塞模式,不能设置为非阻塞模式。

四个操作分别是获取、读取、写入、关闭

/**
     * 复制两个资源目录下的文件
     */
    public static void nioCopyResouceFile() {
        String sourcePath = NioDemoConfig.FILE_RESOURCE_SRC_PATH;
        String srcPath = IOUtil.getResourcePath(sourcePath);
        Logger.debug("srcPath=" + srcPath);

        String destShortPath = NioDemoConfig.FILE_RESOURCE_DEST_PATH;
        String destdePath = IOUtil.builderResourcePath(destShortPath);
        Logger.debug("destdePath=" + destdePath);

        nioCopyFile(srcPath, destdePath);
    }


    /**
     * 复制文件
     *
     * @param srcPath
     * @param destPath
     */
    public static void nioCopyFile(String srcPath, String destPath) {

        File srcFile = new File(srcPath);
        File destFile = new File(destPath);

        try {
            //如果目标文件不存在,则新建
            if (!destFile.exists()) {
                destFile.createNewFile();
            }


            long startTime = System.currentTimeMillis();

            FileInputStream fis = null;
            FileOutputStream fos = null;
            FileChannel inChannel = null;
            FileChannel outchannel = null;
            try {
                fis = new FileInputStream(srcFile);
                fos = new FileOutputStream(destFile);
                inChannel = fis.getChannel();
                outchannel = fos.getChannel();

                int length = -1;
                ByteBuffer buf = ByteBuffer.allocate(1024);
                //从输入通道读取到buf
                while ((length = inChannel.read(buf)) != -1) {

                    //翻转buf,变成成读模式
                    buf.flip();

                    int outlength = 0;
                    //将buf写入到输出的通道
                    while ((outlength = outchannel.write(buf)) != 0) {
                        System.out.println("写入字节数:" + outlength);
                    }
                    //清除buf,变成写入模式
                    buf.clear();
                }

                //强制刷新磁盘
                outchannel.force(true);
            } finally {
                IOUtil.closeQuietly(outchannel);
                IOUtil.closeQuietly(fos);
                IOUtil.closeQuietly(inChannel);
                IOUtil.closeQuietly(fis);
            }
            long endTime = System.currentTimeMillis();
            Logger.debug("base 复制毫秒数:" + (endTime - startTime));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

SocketChannel套接字通道

在NIO中,涉及网络连接的通道有两个,一个是SocketChannel负责连接传输,另一个是ServerSocketChannel负责连接的监听。

ServerSocketChannel应用于服务器端,而SocketChannel同时处于服务器端和客户端。换句话说,对应于一个连接,两端都有一个负责传输的SocketChannel传输通道。

(1)socketChannel.configureBlocking(false)设置为非阻塞模式。

(2)socketChannel.configureBlocking(true)设置为阻塞模式。

在阻塞模式下,SocketChannel通道的connect连接、read读、write写操作,都是同步的和阻塞式的,在效率上与Java旧的OIO的面向流的阻塞式读写操作相同。因此,在这里不介绍阻塞模式下的通道的具体操作。在非阻塞模式下,通道的操作是异步、高效率的,这也是相对于传统的OIO的优势所在。下面详细介绍在非阻塞模式下通道的打开、读写和关闭操作等操作。


DatagramChannel数据报通道

和Socket套接字的TCP传输协议不同,UDP协议不是面向连接的协议。使用UDP协议时,只要知道服务器的IP和端口,就可以直接向对方发送数据。在Java中使用UDP协议传输数据,比TCP协议更加简单。在Java NIO中,使用DatagramChannel数据报通道来处理UDP协议的数据传输。


选择器Selector

首先什么是选择器,他是一个IO事件的查询器,通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。思考一下实现一个IO多路复用有哪些步骤?

一方面注册通道到选择器中,另一方面通过选择器内部的机制,查询已经注册了的通道是否有就绪的IO事件。很明显,这是非常高效的一个线程只需要监控一个选择器,而一个选择器可以去管理多个通道。这就是他的优势,不用为每个连接创建线程。

缓冲区Buffer

​ 应用程序和通道交互就是为了完成读写操作,所以第三个组件Buffer就是为了完成这个操作的核心。读取就是从通道读入缓冲区,写就是从缓冲区写入通道。缓冲区是NIO独有的面向流的OIO是没有的。

Buffer类和属性

Buffer本质上是一个内存块数组,可写可读。NIO的buffer类是一个抽象类。Buffer类是非线程安全类。

在NIO中有8种缓冲区类,分别如下:ByteBuffer(用的最多)、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer。

​ 通过源码可以发现Buffer类有四个重要属性分别是:capacity(容量)、position(读写位置)、limit(读写的限制)、mark(标记)。值得一提的是可以将当前position的值临时存入mark中,需要的时候可以从mark标记恢复到position位置。

capacity表示内部容量的大小。写入超过他的值就代表缓冲区满了,不能再写入。而且一旦初始化后,就无法改变,因为Buffer类在初始化时,会按照capacity的值分配内存,内存分配好后就无法改变。(capacity容量不是byte[]数组的字节数量,而是写入数据对象的数量)**最后要注意Buffer是一个抽象类,不能直接创建对象,需要用子类。**例如使用DoubleBuffer,则写入的数据是double类型,如果其capacity是100,那么我们最多可以写入100个double数据。

position表示当前的位置。他和缓冲区的读写模式有关,不同模式下position值不同。读写模式改变时,他的值也会改变。

写模式:

  1. 刚进入写模式,position值为0,表示从头开始写。
  2. 写入一个数据,position就后移到下一个可写的位置。
  3. 初始position为0,最大为limit-1,到达limit时,缓冲区无空间可写。

读模式:

  1. 开始读模式,position会被重置成0
  2. 从缓冲区读数据时,从position开始,读完后后移下一个可读位置。
  3. position最大值为最大可读上限limit,当position达到limit表示无数据读了。

当然你会关心起点。新建缓冲区,状态属于写入模式。调用flip()翻转状态变为读模式。

limit表示读写的最大上限。写模式表示最大写上限,刚进入写模式,limit会被设置成capacity容量值,表示可以一直写,直到满。

  1. 首先创建缓冲区,初始为写模式,position为0,limit为capacity。
  2. 写数据,每写入一个数据,position后移一个位置(position+1),假设一共写了5个数据。
  3. 调用flip,写模式换成读模式,limit变成position值也就是5,新的position会被重置为0,表示从头开始读。

Buffer类的方法

​ 通过源码查看我们会发现所有的Buffer类中有rewind() 倒带指的是读完的数据如果需要再读一次就可以调用这个方法、flip()、mark( )、reset( )、clear( )方法,他的子类都有allocate()、put()、get()方法。关于这些方法这里就不做详细介绍了,如果感兴趣可以自己去阅读源码。

​ 主要就把Buffer类使用的流程做个简单介绍。

  1. 使用创建子类实例对象的allocate()方法创建一个Buffer类。
  2. 调用put方法,将数据写入缓冲区。
  3. 写入完成后,在开始读数据之前调用Buffer.flip()方法,将缓冲区模式变为读。
  4. 调用get方法读数据
  5. 读取完成后调用Buffer.claer()或者Buffer.compact()方法,将缓冲区转为写入模式。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://mxyblogs.club/archives/javanio

Buy me a cup of coffee ☕.