课程《一站式学习Java网络编程 全面理解BIO/NIO/AIO》的学习笔记(三):
NIO概念 & 使用NIO进行文件拷贝
源码地址:https://github.com/NoxWang/web-program
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(一):java IO与内核IO
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(二):BIO聊天室
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(四):NIO聊天室
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(五):AIO聊天室
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(六):思维导图
一、NIO概述
NIO(Non-blocking IO 或 New IO),非阻塞式IO。与BIO不同,NIO使用Channel代替Stream,特征如下:
- Stream具有方向性,分为输入流和输出流,而Channel无方向,一个Channel既可以写入数据也可以读取数据。
- Stream的读写均为阻塞式(例如InputStream.read(),OutputStream.write()),而Channnel的读写具有两种模式,既可以阻塞式读写,也提供了非阻塞式读写的方法
- NIO使用Selector监控多条Channel
- 可以在一个线程里处理多个Channel I/O
1.1 Buffer
NIO向Channel中读写数据都需要通过一个Buffer类来实现。
Buffer,顾名思义,是一个缓冲区,代表内存中我们可以进行读写的一个区域。Channel可以支持双向操作,因此Buffer也可进行读写双向操作。
1.1.1 向Buffer中写入数据
写模式主要用到两个指针:
- position:当前写入的位置
- capacity:最远可写入位置(缓冲区最大容量)
1.1.2 由写模式转换为读模式
调用flip()
方法实现从写模式到读模式的转换,步骤如下:
- 将position指针移至缓冲区头部
- 将limit指针移至写入的最远位置,limit指针表示的是读模式下最远所能读取的位置
1.1.3 读模式①
第一种读取模式是将写入的数据全部读取完,读取完后position指针将和limit指针指向相同的位置
调用clear()
方法转换为写模式,步骤如下:
- 将position指针移回头部
- 将limit指针移回capacity处
从图中可以看出,clear()方法其实并没有清除已读取的数据,而是通过指针操作,使得再次写入的数据覆盖之前的数据。
1.1.4 读模式②
第二种读取模式是只读取部分已写入的数据,读取完后position指针的位置在limit指针之上。
此时若想转换为写模式,需要调用compact()
方法,步骤如下:
- 将已写入但未读取的数据复制到缓冲区头部
- 将position指针移至未读取数据的下面一个区域
- 将limit指针移回capacity处
1.2 Channel
1.2.1 Channel的基本操作
- 通过Buffer写入/读取数据
- Channel之间可以直接进行数据传输:每个Channel可以向其他Channel数据传输数据,也可以接受从其他Channel传输过来的数据
1.2.2 几个重要的Channel
1.3 Selector
Channel可以进行非阻塞式的读写,而并不是每个时刻Channel上都有数据可供读写,所以我们需要不停地询问Channel是否处于可操作的状态。Jahannlelva提供了一个Selector类
来实现这个功能,该类负责监听各个Channel的状态。
Selector也称为 I/O多路复用器。
1.3.1 Channel的状态
Channel的状态并不是固定不变的,Channel的状态会随着不同事件的发生而在如下状态间发生变化:
- CONNECT:客户端与服务端建立连接后,客户端的
SocketChannel
会处于CONNECT状态 - ACCEPT:服务端接受了客户端的连接建立请求后,服务端的
ServerSocketChannel
会处于ACCEPT状态 - READ:Channel上有了可读取信息后
- WRITE:可以向Channel写入数据的状态
1.3.2 Channel的注册
要想使用Selector监听Channel,我们首先需要将该Channel注册到Selector上。
注册后,我们会得到一个SelectionKey
对象,该对象可理解为每一个注册在Selector上的Channel的ID。通过SelectionKey
,我们可以得到如下信息:
interestOps()
:得到一组Selector需要监听的该Channel的状态readyOps()
:在需要监听的状态中,哪些是处于准备好、可操作的channel()
:返回监听的Channel对象selector()
:返回被注册的Selectorattachment()
:根据具体业务需求,可以是需要加载在channel上的任意对象
二、使用NIO进行文件拷贝
使用FileChannel实现本地文件拷贝。如上所述,有通过Buffer传输和Channel之间直接传输两种方式
2.1 通过Buffer进行数据传输
几个需要注意的点:
-
通过文件输入输出流获得文件通道:
FileChannel fin = new FileInputStream(source).getChannel();
-
注意Channel和Buffer的方向:将数据从文件Channel中读取出来(
fin.read(buffer)
),写入缓冲区(缓冲区为写模式);将缓冲区转换为读模式(buffer.flip()
);将数据从缓冲区中读出(缓冲区为读模式),写入文件Channel(fout.write(buffer)
);将缓冲区转换为写模式(buffer.clear()
)
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class NioBufferCopy {
private static void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void copyFile(File source, File target) {
// 声明文件通道
FileChannel fin = null;
FileChannel fout = null;
try {
// 通过文件输入输出流得到文件通道
fin = new FileInputStream(source).getChannel();
fout = new FileOutputStream(target).getChannel();
// 创建ByteBuffer类型的缓冲区(按字节读取)
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据从文件通道中读取出来,写进Buffer
while (fin.read(buffer) != -1) {
// 将Buffer从写模式转换为读模式
buffer.flip();
while (buffer.hasRemaining()) { // 确保Buffer中的内容被读完
// 将Buffer中的数据写入文件通道
fout.write(buffer);
}
// 将Buffer从读模式转换为写模式
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close(fin);
close(fout);
}
}
public static void main(String[] args) {
File file = new File("E:/JavaProject/web/nio-file-copy/tmp/smallFile.jpg");
File fileCopy = new File("E:/JavaProject/web/nio-file-copy/tmp/smallFile-copy.jpg");
System.out.println("--- Copying small file ---");
copyFile(file, fileCopy);
}
}
2.2 直接在Channel间进行数据传输
- 调用通道的
transferTo()
方法实现Channel间的数据传输 - 与
write()
方法一样,transferTo()不能保证拷贝通道中的所有数据,因此需要使用一个while循环,确保文件被完整拷贝
import java.io.*;
import java.nio.channels.FileChannel;
public class NioTransferCopy {
private static void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void copyFile(File source, File target) {
// 声明文件通道
FileChannel fin = null;
FileChannel fout = null;
try {
// 通过输入输出流创建通道
fin = new FileInputStream(source).getChannel();
fout = new FileOutputStream(target).getChannel();
long transferred = 0L;
long size = fin.size();
while (transferred != size) {
// 从位置0开始,拷贝fin通道中size长度的数据,至fout通道,返回的是已拷贝长度
// transferTo不能保证拷贝通道中的所有数据,因此使用while循环
transferred += fin.transferTo(0, size, fout);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
close(fin);
close(fout);
}
}
public static void main(String[] args) {
File file = new File("E:/JavaProject/web/nio-file-copy/tmp/smallFile.jpg");
File fileCopy = new File("E:/JavaProject/web/nio-file-copy/tmp/smallFile-copy.jpg");
System.out.println("--- Copying small file ---");
copyFile(file, fileCopy);
}
}