什么是 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 核心技术》张洪亮