IO、NIO简述

75 阅读4分钟

前言

操作系统中,大多数应用运行在用户空间里。在用户空间中,执行的代码并不能之间操控硬件。因此,用户空间中的代码需要用过调用内核代码来读写存储设备上的数据。 image.png 内核代码将磁盘上的数据,存储到内核空间的缓冲区,再被用户空间读取到用户空间的缓冲区。数据来往与用户空间与存储设备的过程中,内核充当着中间人的角色。

但是,当用户空间有多个缓冲区进行读写时,多次调用内核代码的代价不小。对于这种情况,用户进程可以选择利用一次系统调用,将多个缓冲区地址传递给内核,由内核进行发散、汇聚,同时满足多个缓冲的需求。

image.png

IO

java io 相关的类比较多,先很粗糙的切分一下:字节流与字符流,也就是 Stream 与 Reader/Writer。然后输入、输出流往往也是对应的,因此,任选其一后(如 InputStream)后,大体剩下了这些类:

  • ByteArrayInputStream、StringBufferInputStream、FileInputStream、PipedInputStream、ObjectInputStream
  • FilterInputStream
    • BufferedInputStream
    • DataInputStream
    • LineNumberInputStream
    • PushBackInputStream

可以看到,以上的类被分为了两类:一类是各种数据类型的输入(数组、字符串、文件输入等等),一类则继承了 FilterInputStream 类:将基本数据进行转换或者提供其他功能的类,也就是装饰者模式的主要对象。

总体来说,io 包下,首先划分成了 stream 与 Reader/Writer,其下有分成了输入、输出两种,其下再次分为了基本数据传输流与功能性装饰流。

stream 与 Reader/Writer 的区别显而易见,一个按字节读取,一个按字符读取:

    // OutputStream,按字节数组写
    public void write(byte b[]) throws IOException {
        write(b, 0, b.length);
    }

    // Writer,按字符数组写
    public void write(char cbuf[]) throws IOException {
        write(cbuf, 0, cbuf.length);
    }

根据不同数据类型而区分的数据流更是直观:

    // FileInputStream,从file读取
    public FileInputStream(File file) throws FileNotFoundException {
    }

    // ByteArrayInputStream,从字节数组读取
    public ByteArrayInputStream(byte buf[]) {
    }

我们来以常见的装饰流 BufferedInputStream 举例:

    // BufferedInputStream
    protected volatile byte[] buf;

    public synchronized int read() throws IOException {
        if (pos >= count) {
            fill(); // 读入更多的数据到buf
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
    }

fill()方法会读取更多的数据到 buf,也就是缓存之中。将下次读取数据的时候,优先从缓存中读取,因此减少了本地 io api 的调用。

而基本的数据传输流,是没有 fill 与 buf 的,每次读将会直接调用本地 api,如 FileInputStream

    public int read() throws IOException {
        return read0();
    }

    private native int read0() throws IOException;

NIO

java nio与io相比,多了几个概念:buffer(缓冲)、channel(通道)、selector(选择器)。相信有了多缓冲io的那张图,理解这些概念并不困难。简而言之,从buffer中读写数据,channel传输数据,selector控制channel。

image.png 与io相比,nio无疑复杂了许多,那么nio的优势在哪里呢?这里,我们得先提一下io模型: image.png 关于io模型,推荐阅读:漫话:如何给女朋友解释什么是Linux的五种IO模型?

io是阻塞式,而nio则是I/O复用形式。通过将多个channel注册到一个selector上,等待数据的时间会被分摊,空等数据的时间将会减少。

public static void main(String[] args) throws  Exception{
        //创建ServerSocketChannel,-->> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(5555);
        serverSocketChannel.socket().bind(inetSocketAddress);
        serverSocketChannel.configureBlocking(false); //设置成非阻塞

        //开启selector,并注册accept事件
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while(true) {
            selector.select(2000);  //监听所有通道
            //遍历selectionKeys
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if(key.isAcceptable()) {  //处理连接事件
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);  //设置为非阻塞
                    System.out.println("client:" + socketChannel.getLocalAddress() + " is connect");
                    socketChannel.register(selector, SelectionKey.OP_READ); //注册客户端读取事件到selector
                } else if (key.isReadable()) {  //处理读取事件
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(byteBuffer);
                    System.out.println("client:" + channel.getLocalAddress() + " send " + new String(byteBuffer.array()));
                }
                iterator.remove();  //事件处理完毕,要记得清除
            }
        }

    }

至于异步IO(AIO)模型,则是java nio 2了:

    AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(80));

    server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
        final ByteBuffer buffer = ByteBuffer.allocate(1024);

        @Override
        public void completed(AsynchronousSocketChannel result, Object attachment) {
            Future<Integer> writeResult = null;
            try {
                buffer.clear();
                result.read(buffer).get(100, TimeUnit.SECONDS);
                buffer.flip();
                writeResult = result.write(buffer);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    server.accept(null, this);
                    writeResult.get();
                    result.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            System.out.println("failed: " + exc);
        }
    });