I/O 基础笔记

335 阅读8分钟

什么是 I/O 流?

I/O 是 Input/Output 的缩写,也就是输入与输出。外部数据输入到计算机的内存中称为输入,相反的,内存中的数据输入到外部为输出,流是一个很形象的概念,在这些传输的过程中,数据就像水一样流动,所以我们经常将此称为输入流、输出流。

I/O 流的分类

按照流的方向可以分为输入流和输出流,按照数据的传输的单位可以分为字节流和字符流。

InputStream、OutputStream、Reader、Writer

在 Java 中操作字节类型的主要操作类是 InputStream 和 OutputStream 的子类,操作字符类型的主要操作类是 Reader 和 Writer 的子类,Java IO 流的 40 多个类都是从这 4 个抽象类基类中派生出来的。

字节缓冲流

I/O 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。

通过一个测试去对比字节流和字节缓冲流复制一个文件的耗时,代码如下:

public class Main {

    public static void main(String[] args) {
        String filePath = "/Users/BrantleyFan/Downloads/test.pdf";
        copyFileByStream(filePath);
        copyFileByBufferedStream(filePath);
    }

    private static void copyFileByStream(String filePath) {
        long start = System.currentTimeMillis();
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(filePath);
            fos = new FileOutputStream(filePath);
            int content;
            while((content = fis.read()) != -1) {
                fos.write(content);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("使用字节流复制文件用时:" + (end - start) + "毫秒");
    }

    private static void copyFileByBufferedStream(String filePath) {
        long start = System.currentTimeMillis();
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            bis = new BufferedInputStream(new FileInputStream(filePath));
            bos = new BufferedOutputStream(new FileOutputStream(filePath));
            int content;
            while((content = bis.read()) != -1) {
                bos.write(content);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bis != null) {
                    bis.close();
                }
                if (bos != null) {
                    bos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("使用字节缓冲流复制文件用时:" + (end - start) + "毫秒");
    }

}
使用字节流复制文件用时:105毫秒
使用字节缓冲流复制文件用时:0毫秒

Linux 的五种 I/O 模型

先来了解一些基础的概念,便于后续的理解。

一些重要的概念

Socket

socket 中文翻译为套接字,套接字可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。

在操作系统中,通常会为应用程序提供一组应用程序接口(API),称为套接字接口。应用程序可以通过套接字接口,来使用网络套接字,以进行资料交换。

表示方法为套接字 Socket = IP地址 : 端口号,如 210.76.125.1

内核空间与用户空间

操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。如果用户进程想访问系统资源,比如读写磁盘文件,那么就必须通过系统调用进入内核空间操作,recv() 函数就是一个系统调用。

同步与异步

举个例子,A 调用 B

如果是同步,B 在收到 A 的调用命令后,会立马执行,A 的本次调用可以得到结果。

如果是异步,B 在收到 A 的调用命令后,不保证会立马执行,但是保证会去完成,B 完成之后会通知 A,A 的本次调用不一定得到结果。

阻塞与非阻塞

举个例子,A 调用 B

如果是阻塞,A 在发出调用命令后,会一直等待 B 返回结果。

如果是非阻塞,A 在发出调用命令后,不需要等待,可以去做自己的事情。

五种 I/O 模型

阻塞 I/O 模型

阻塞 I/O 模型一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去,如果条件满足,则进行下一步操作。

应用进程通过系统调用 recvfrom 接收数据,但由于内核还没有准备好数据报,应用进程会阻塞,直到内核准备好数据报,recvfrom 完成数据报的复制工作,应用进程才会结束阻塞状态。

非阻塞 I/O 模型

应用进程与内核交互,目的未达到之前,不再一昧地等待,而是直接返回。然后通过轮询的方式,不停地询问内核数据有没有准备好,如果某一次轮询发现数据已经准备好了,那么就把数据复制到用户空间。

应用进程通过 recvfrom 不停地与内核交互,直到内核准备好数据报,如果没有准备好数据,内核返回 error,应用进程在得到 error 后,过一段时间再发送 recvfrom 请求。在两次发送请求的间隔中,进程可以做其他事情。

信号驱动 I/O 模型

应用进程在读取文件时通知内核,当某个 Socket 事件发生时,请向我发一个信号。应用进程收到信号后,信号对应的处理函数会进行后续的处理。

应用进程事先向内核注册一个信号处理函数,然后用户进程不阻塞直接返回,当内核数据准备就绪时就会发送一个信号给进程,用户进程在信号处理函数中把数据复制到用户空间。

I/O 复用模型

多个进程的 I/O 可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据复制到用户空间。

多个进程注册到同一个 select 上,当用户进程调用该 select 时,select 会监听所有注册好的 I/O,如果所有被监听的 I/O 需要的数据都没有准备好,那么 select 调用进程会阻塞。当任意一个 I/O 所需的数据准备好之后,select 调用就会返回,然后进程通过 recvfrom 实现数据复制。这里并没有向内核注册信号处理函数,所以,I/O 复用模型并不是非阻塞的。

异步 I/O 模型

应用进程把 I/O 请求传给内核后,完全由内核去完成文件的复制。内核完成相关操作后,会发送信号告诉应用进程本次 I/O 操作已经完成。

用户进程发起 aio_read 操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了。当内核收到 aio_read 后,会立刻返回,然后开始等待数据准备,数据准备好以后,直接把数据复制到用户空间,然后通知进程本次 I/O 操作已经完成。

BIO、NIO和AIO

Java 的 I/O 可以分为三种同步阻塞 I/O-BIO、同步非阻塞 I/O-NIO和异步非阻塞 I/O-AIO。

BIO

一种同步阻塞 I/O 模型,数据的读取和写入必须阻塞在一个线程内等待其完成。

适用于连接数目较小且固定的架构,这种方式对服务器资源要求比较高。

使用 BIO 实现文件的读取和写入:

public class Main {

    public static void main(String[] args) {
        User user = new User();
        user.setId(1L);
        user.setName("fanzibang");
        user.setAge(23);
        // 将对象写入文件中
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("temp"));
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (oos != null) {
                    oos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 从文件中读取对象
        ObjectInputStream ois = null;
        try {
            File file = new File("temp");
            ois = new ObjectInputStream(new FileInputStream(file));
            User userFromFile = (User) ois.readObject();
            System.out.println(userFromFile);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                if (ois != null) {
                    ois.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

NIO

同时支持阻塞与非阻塞模式,和 BIO 最大的不同就是,NIO 支持同步非阻塞。

NIO 适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。

使用 NIO 实现文件的读取和写入:

public class Main {

    public static void main(String[] args) {
        readNIO();
        writeNIO();
    }

    private static void readNIO() {
        String pathName = "/Users/BrantleyFan/Downloads/temp.rtf";
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(new File(pathName));
            FileChannel channel = fis.getChannel();
            // 字节
            int capacity = 2;
            ByteBuffer bf = ByteBuffer.allocate(capacity);
            System.out.println("限制是:" + bf.limit() + "容量是:" + bf.capacity() + "位置是:" + bf.position());

            int length = -1;
            while((length = channel.read(bf)) != -1) {
                bf.clear();
                byte[] bytes = bf.array();
                System.out.write(bytes, 0, length);
                System.out.println();
                System.out.println("限制是:" + bf.limit() + "容量是:" + bf.capacity() + "位置是:" + bf.position());
            }
            channel.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static void writeNIO() {
        String fileName = "out.txt";
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(new File(fileName));
            FileChannel channel = fos.getChannel();
            ByteBuffer src = Charset.forName("utf8").encode("你好,世界!");
            // 字节缓冲的容量和limit会随着数据长度变化,不是固定不变的
            System.out.println("初始化容量和limit:" + src.capacity() + "," + src.limit());

            int length = 0;
            while ((length = channel.write(src)) != 0) {
                System.out.println("写入长度:" + length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

AIO

异步非阻塞 I/O 模型。

适用于连接数目多且连接比较长的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂。

参考

《深入理解 Java 核心技术》张洪亮

Java IO基础知识总结