文件就是一串二进制流而已,在信息交换的过程中,对这些流进行数据的收发操作、简称为 I/O 操作,IO 有内存IO、网络IO和磁盘IO三种、通常说的 IO 指的是后两者。
阻塞IO
在进行同步 I/O 操作时,如果读取数据,代码会进行阻塞,直到有可读取的数据,同样,写入数据也会进行阻塞,直至数据能够写入。
传统的 Server/Client 模式,服务器会为每一个客户端请求建立一个线程,该线程单独负责客户端请求。缺点是请求增加,线程也会增加,增大服务器开销。超出配置的线程数量后,服务端无法处理客户端的请求。也就是说内核将数据准备好之前,系统调用会一直等待,默认的是阻塞方式。
非阻塞IO
NIO 中实现非阻塞 I/0 的核心对象就是 Selector, Selector 就是注册各种 I/O 事件地方,通过一个 Selector 实时监听多个通道,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 Key,同时从 Key 中可以找到发生的事件和该事件所发生的具体的通道,以获得客户端发送过来的数据。
NIO 概述
Channel
Channel 就像是一个“水管”,网络数据通过 Channel 这个水管进行写入和读取,通道与IO流不同在于通道是双向的,一个流必须是 inputStream 或 outputStream 的子类,而通道可以同时用于读写。
通过 Channel 可以操作数据源,可以是文件、网络等,Channel 用于字节缓冲区和另一侧实体之间进行数据传输。
Channel 是一个对象,所有的数据操作都通过 buffer 处理,Channel 依赖于 buffer 实现,Channel 本身并不直接写入或读取数据而是通过 buffer 进行。可以理解为 Channel 是水管,而 buffer 是蓄水池。
Channel 主要是实现:FileChannel(从文件中读取数据)、DatagramChannel(通过 UDP 读取数据)、SocketChannel(通过 TCP 读取数据)、ServerSocketChannel(可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel)
如何打开一个 Channel
无法直接打开一个 Channel,需要通过 inputStream、outputSteam 或者 RandomAccessFile。
FileChannel
从 FileChannel 读取数据
- 分配一个 Buffer,
ByteBuffer buffer = ByteBuffer.allocate(1024); - 调用 read()方法
int byteRead = channel.read(buffer);从文件通道中读取数据,并将数据存储到 buffer 里,该方法返回实际读取的字节数,如果到达文件末尾,则返回 -1
从 FileChannel 写入数据
使用 FileChannel.write() 方法,但是注意 FileChannel.write() 是在 while 循环中调用的,因为无法保证一次能向 FileChannel 写入多少字节
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put("test data".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.close();
position 方法
有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用 position() 方法获取 FileChannel 的当前位置。也可以通过调用 position(long pos) 方法设置 FileChannel 的当前位置。
size 方法
FileChannel 实例的 size() 方法将返回该实例所关联文件的大小
truncate 方法
可以使用 FileChannel.truncate() 方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。如: channel.truncate(1024);,这个例子截取文件的前 1024个字节。
force 方法
FileChannel.force() 方法将通道里尚未写磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存,所以无法保证写入到 FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()。方法force() 方法有一个 boolean 类型的参数。指明是否同时将文件元数据(权限信息等)写到磁盘上。
transferFrom 和 transferTo 方法
两者都是用于将数据从一个文件通道传输到另一个通道中。它们的主要区别在于数据的传输方向。
transferFrom(),参数1:源通道,即要从中读取数据的通道;参数2:源通道中的起始位置,从该位置开始读取数据;参数3:要传输的字节数
transferTo(),参数1:源通道中的起始位置,从该位置开始读取数据;参数2:要传输的字节数;参数3:目标通道,即要将数据写入的通道
Socket 通道
特点
- 可以以非阻塞方式实现,通过一个 Selector 监听多个 socket 通道;
- DatagramChannel、SocketChannel、ServerSocketChannel 都继承 AbstractSelectableChannel,ServerSocketChannel 只负责监听传入的连接和信新创建的 SocketChannel 对象,本身并不传输数据,而 DatagramChannel 和 SocketChannel 实现读、写功能的接口;
- Socket 通道可以被大多数协议重复使用,而 Socket 不会被重复使用
- Socket 通道可以设置为非阻塞模式
ServerSocketChannel
ServerSocketChannel 对应 ServerSocket,是基于 Socket 的监听器,本身并不传输数据,只负责监听传入的连接和信新创建的 SocketChannel 对象,能够在非阻塞模式下运行。
ServerSocketChannel 没有 bind() 方法(绑定方法),但和 ServerSocket 一样有 accept() 方法,于从 ServerSocketChannel 中接受一个新的连接。该方法会阻塞当前线程,直到有新的连接到达,如果返回为 null 则表示没有连接
SocketChannel
SocketChannel 对应 Socket,是一个基于 TCP 的处理网络IO的通道,实现了可选择通道,可以被多路复用。
通过 open() 方法创建,通过 connect() 方法连接到指定地址,支持阻塞和非阻塞。
DatagramChannel
DatagramChannel 对应 DatagramSocket,是一个基于 UDP 的无连接通道,可以发送单独数据给不同地址,也可以接受任意地址数据。
read() 和 write() 只有在 connect() 后才能使用,否则会抛出异常
分散和聚集
分散
从 Channel 中读取到的数据写入多个 Buffer,会按照数组顺序进行操作
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body= ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header, body};
channel.read(bufferArray);
聚集
从多个 Buffer 里的数据写入同一个 Channel,会按照数组顺序进行操作
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body= ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header, body};
channel.write(bufferArray);
Buffer
缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。可以理解为本质就是内存,只不过是包装成了一种对象。
为什么要引入缓冲区
高速设备与低速设备的不匹配,势必会让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区。
缓冲区的作用
- 可以解除两者的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率。例如:使用打印机打印文档,由于打印机的打印速度相对较慢,先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时 CPU 可以处理别的事情。
- 可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。
buffer 读写数据的一般步骤
- 写入数据到 buffer
- 调用 flip() 方法,从写模式切换为读模式
- 从 buffer 中读取数据
- 调用 clear() 或 compact() 方法,一旦读完了数据就需要清空缓冲区,让它可以再次写入。
NIO 中 buffer 实现有ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer等,分别对应基本数据类型: byte、char、double、float。
capacity
capacity 是 buffer 固定大小的值,buffer 满了必须清空才能继续写入数据。
position
- 写数据到 buffer:position 表示写入数据的位置,初始值为 0,最大值就是 capacity-1。
- 读数据到 buffer:position 表示读入数据的位置,调用 flip() 方法切换为读模式时,position 会被重置为 0。
limit
- 写数据到 buffer:limit 表示对 buffer 最多写入多少数据,limit 等于 capacity。
- 读数据到 buffer:limit 表示 buffer 里有多少可读数据(not null的数据),因此能读到之前写入的所有数据。
buffer 分配
不管什么类型的 buffer,都可以调用 allocate 方法,参数为分配的字节大小
向 buffer 中写数据
- 从 channel 写到 buffer,
channe.read(buffer) - 通过 buffer 的 put() ,
buffer.put()
向 buffer 中读数据
- 从 buffer 读取数据到 channel,
channe.write (buffer) - 使用 get() 方法从 buffer 中读取数据,
buffer.get()
buffer 从写切换为读
调用 flip() 方法
rewind() 方法
将 position 设置为 0,可以重读 buffer 中的所有数据,limit 保持不变
clear() 和 compact()
clear() 方法会清空整个缓冲区,compact() 只会清除已经读过的数据。
mark() 和 reset()
mark() 方法可以标记 buffer 中的一个特定的 position,之后通过 reset() 方法恢复到这个 position。
缓冲区分片
根据现有的缓冲对象创建一个子缓冲区,它们之间数据共享,通过调用 slice() 方法实现
只读缓冲区
只能读取数据,不能写入数据,通过调用 asReadOnlyBuffer() 方法将任何常规缓冲区转换为只读缓冲区。
直接缓冲区
给定一个字节的缓冲区,JVM 直接对它进行 IO 操作,加快操作速度,调用 allocateDirect() 方法进行创建而不是 allocate() 方法,使用方法无区别。
内存映射文件 IO
内存映射文件 IO 是一种读写文件数据的方法,文件中的实际读取或写入的数据会映射到内存中,通过 map() 方法使用。
Selector
Selector 一般成为选择器,也称为多路复用器。Selector 运行单线程处理多个 Channe,如果应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在一个聊天服务器中。要使用 Selector, 得向 Selector 注册 Channel,然后调用它的 select() 方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件。
SelectableChannel 可选择通道
不是所有的 channel 都可以被 Selector 复用,比如FileChannel就不可以被选择器复用,判断一个 channel 能被 Selector 复用,有一个前提:判断他是否继承了 SelectableChannel,如果继承了就可以复用,否则不能。
一个通道可以被注册到多个选择器上,但每个选择器只能被注册一次,通道和选择器之间的关系,使用注册的方式完成。SelectableChannel 可以被注册到 Selector 对象上,在注册的时候,需要指定通道的哪些操作是 Selector 感兴趣的。
channel 注册到 selector
使用 Channel.register() 方法,参数1:指定通道要注册的选择器,参数2:指定选择器需要查询的通道操作。
可以供选择器查询的通道操作,分为四种:
- 可读:SelectionKey.OP.READ
- 可写:SelectionKey.OP.WRITE
- 连接:SelectionKey.OP.CONNECT
- 接收:SelectionKey.OP.ACCEPT
如果多选,可通过“位或”操作符实现,比如:int key = SelectionKey.OP.READ | SelectionKey.OP.WRITE
选择查询器不是通道的操作,只是通道的某个操作的一种就绪状态。
Selector 可以不断的查询 Channel 中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是 Selector 感兴趣的操作,就会被 Selector 选中,放入选择键集合中。
注意:
- channel 必须为非阻塞,否则抛出异常,所以 FileChannel 不能和 Selector 一起使用,因为 FileChannel 不能设置为非阻塞,而 Socket 相关的通道都可以。
- 一个通道并不一定都支持四种操作,可以通过 validOps() 方法获取特定通道下所有支持的操作合集
轮询查询就绪操作
通过 select() 方法可以进行就绪状态的查询。就绪状态集合包含在一个元素为 selectionKey 对象的 Set 集合里。
- select():阻塞到至少有一个通道在你注册的事件上就绪了
- select(long timeout):和select0)一样,但最长阻塞事件为 timeout 毫秒
- selectNow():非阻塞,只要有通道就绪就立刻返回
方法返回的 int 值,表示有多少通道已经就绪,一旦调用 select() 方法,并且返回值不为 0 时,在 Selector 中有一个 selectedKeys()方法,用来访问已选择键集合,迭代集合的每一个选择键元素,根据就绪操作的类型完成对应的操作
停止选择的方法
选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在 select ()方法中阻塞的线程:
wakeup() :让处在阻塞状态的 select() 方法立刻返回 close() :关闭 selector,同时所有的 channel 的键被注销,但是 channel 不会注销
开发的一般步骤
- 创建 ServerSocketChannel 通道,绑定监听端口
- 创建 Selector 选择器
- 把 Channel 注册到 Selector 选择器上,监听事件
- 循环调用 selector 的 select 方法,监听通道的就绪状况
- 调用 selectKeys 方法获取就绪 channel 集合
- 遍历就绪 channel 集合,判断就绪事件类型,实现具体的业务操作
- 根据业务,是否需要再次注册监听事件,重复执行第三步操作
Pipe
Pipe是一个类,它用于在两个线程之间进行管道通信。它提供了一种简单的方式来在两个线程之间传递数据,而不需要显式地使用锁或其他同步机制。Pipe 有一个 source 通道和 sink 通道。数据会被写到 sink 通道, source 通道读取。
创建管道
通过 Pipe.open() 打开通道
数据写入管道
要向通道写入数据,需要先访问通道,调用 Pipe.sink() 方法,返回 SinkChannel,通过调用 SinkChannel.write() 方法
管道读取数据
要向管道读取数据,需要先访问通道,调用 Pipe.source() 方法,返回 SourceChannel,通过调用 SourceChannel.read() 方法
FileLock
文件锁在 OS 中很常见,如果多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题。给文件加一个锁,同一时间,只能有一个程序修改此文件或者程序都只能读此文件,这就解决了同步问题。
文件锁是进程级别的,不是线程级别的。文件锁可以解决多个进程并发访问、修改同一个文件的问题,但不能解决多线程并发访问、修改同一文件的问题。
使用文件锁时,同一进程内的多个线程,可以同时访问、修改此文件。
文件锁是当前程序所属的 JVM 实例持有的,一旦获取到文件锁 (对文件加锁),要调用 release(),或者关闭对应的 FileChannel 对象,或者当前JVM 退出,才会释放这个锁。
文件锁的分类
- 排他锁:又叫独占锁,对文件加排它锁后,该进程可以对此文件进行读写,该进程独占此文件,其他进程不能读写此文件,直到该进程释放文件锁。
- 共享锁:某个进程对文件加共享锁,其他进程也可以访问此文件,但这些进程都只能读此文件,不能写。线程是安全的。只要还有一个进程持有共享锁,此文件就只能读,不能写。
获取文件锁的四张方法
- lock():对整个文件加锁,默认为排它锁
- lock(long position, long size, booean shared),自定义加锁方式,前两个参数指定要加锁的部分,第三个参数值指定是否是共享锁。
- tryLock():对整个文件加锁,默认为排
- tryLock(long position, long size, booean shared):自定义加锁方式
如果指定为共享锁,则其它进程可读此文件,所有进程均不能写此文件,如果某进程试图对此文件进行写操作,会抛出异常。
lock 与 tryLock 的区别
lock 是阻塞式的,如果未获取到文件锁,会一直阻塞当前线程,直到获取文件锁 tryLock 和 lock 的作用相同,只不过 tryLock 是非阻塞式的,tryLock 是尝试获取文件锁,获取成功就返回锁对象,否则返回 null,不会阻塞当前线程。
FileLock 两个方法
- isShard():此文件是否是共享锁
- isValid():此文件锁是否还有效
Path
Java Path 实例表示文件系统中的路径。一个路径可以指向一个文件或一个目录。路径可以是绝对路径,也可以是相对路径。绝对路径包含从文件系统的根目录到它指向的文件或目录的完整路径。相对路径包含相对于其他路径的文件或目录的路径。
创建 Path 实例
通过 Paths.get() 方法创建 Path 实例,参数为文件路径
绝对路径:Paths.get("e:\Users\bruce\Desktop\a.txt")
相对路径:Paths.get(basePath,relativePath),指向路径为 basePath\relativePath
路径标准化
Path.normalize() 使路径标准化,标准化意味着将移除所有在路径字符串中间的 . 和 .. 代码,并解析路径字符串所引用的路径。
创建文件目录
Files.createDirectory() 用于根据 Path 实例创建一个新目录
Path path = Paths.get("E:\\test");
Files.createDirectory(path);
拷贝文件
Files.copy() 方法从一个路径拷贝一个文件到另外一个目录,参数1:源地址,参数2:目标地址,参数3:StandardCopyOption.REPLACE EXISTING 选填,如果不加目标地址已有相同文件会报异常,如果加上会覆盖文件
文件移动/重命名
Files.move() 方法,参数1:源地址,参数2:目标地址,参数3:StandardCopyOption.REPLACE EXISTING 覆盖目标地址上的文件
文件删除
Files.delete() 方法
AsynchronousFileChannel
AsynchronousFileChannel 提供了异步读取和写入文件数据的功能。
创建 AsynchronousFileChannel
通过 AsynchronousFileChannel.open() 方法进行创建,参数1:文件路径,参数2:操作方式
读取数据和写入数据的两种方式
通过 Future 通过 CompletionHandler
Future 和 CompletionHandler区别
CompletionHandler 是一种回调机制,用于在异步操作完成后通知调用者。CompletionHandler 通常在异步操作开始时被注册,并在操作完成后被调用。CompletionHandler 的优点是简单易用,适用于简单的异步操作。但是,它不支持取消操作或检查操作是否已经完成。
Future 是一种更高级的异步操作结果表示形式,它表示一个异步操作的最终结果。Future 提供了一些方法,例如 isDone()、get() 和 cancel(),用于检查操作是否已经完成、获取操作结果或取消操作。Future 的优点是更灵活,可以支持更复杂的异步操作。但是,它的使用方式比 CompletionHandler 更复杂。
Charset 字符集的常用方法
Charset.forName()
通过编码类型获取 Charset 对象
Charset.defaultCharset()
获得虚拟机默认的编码方式
Charset.name()
获得 Charset 对象的编码类型
Charset.newEncoder()
获得编码器对象
Charset.newDecoder()
获得解码器对象