主要参考:NIO学习文档这个大神写的非常好,通俗易懂!!!!!!!!
注意,“旧”的I/O包已经使用NIO重新实现过,即使我们不显式的使用NIO编程,也能从中受益。
一、NIO与IO对比记忆
IO | NIO |
---|---|
面向流 | 面向缓存区 |
阻塞IO | 非阻塞IO |
选择器 |
面向流与面向缓存区
- 面向流
Java IO是面向流的,这意味着你一次从一个流中读取一个或多个字节。如何处理读取的字节由你决定。它们不会被缓存到任何地方。此外,不能操作在数据流中来回移动。如果需要在从流读取的数据中来回移动,则需要先将其缓存到缓冲区中。 - 面向缓冲区
Java NIO的面向缓冲区的方法略有不同。数据被读入一个缓冲区,然后从这个缓冲区中进行处理。您可以根据操作在缓冲区中来回移动。这在处理过程中提供了更多的灵活性。
阻塞与非阻塞
- Java IO是阻塞的,一次连接对应一个线程,当进行流的读写时,该线程会一直阻塞至读写完毕
- Java NIO是非阻塞的,当线程从通道中读取数据时,若此时没有可用数据,此时线程不会被阻塞,可以被其他资源继续使用,直到有可用数据可被读取。非阻塞写也是如此。
选择器
使用单线程控制选择器管理多个通道。
二、NIO
三个主要概念
通道
通道与流的对比
- 通道可以读也可以写,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)。
- 通道可以异步读写。
- 通道总是基于缓冲区Buffer来读写。
通道的实现
- FileChannel: 用于文件的数据读写
- DatagramChannel: 用于UDP的数据读写
- SocketChannel: 用于TCP的数据读写,一般是客户端实现
- ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现
Java NIO:从Channels读数据到Buffers,从Buffers写数据到Channels
缓存
- 容量
- 位置
- 限制
选择器
Selector是一个组件,它可以检查一个或多个Java NIO 通道 实例,并确定哪些通道准备好进行读取或写入等操作
使用选择器可以让一个线程处理多个通道,减少线程切换的资源消耗和线程的管理
Java NIO:一个线程使用一个选择器来处理 3 个通道的
三、NIO的非阻塞服务器
读:非阻塞服务器需要定时检查是否传入数据,以及检查数据的完整性。服务器可能需要多次检查,直到收到一条或多条完整消息,所以是一个定时的查询。
同样,写:非阻塞服务器需要定时检查是否有数据要写入。如果是,服务器需要检查写入数据的完整性,避免数据被部分写入。
所以服务器的channel需要定时执行的三个检查器是:
- 读管道,新连接数据的传入;
- 检查数据完整性;
- 写管道,检查它是否可以将任何传出的消息写到任何打开的连接上。
四、RandomAccessFile类:动态读取文件内容
需求:上传文件到抖音或者快手等发布渠道,当文件过大,上传时可能出现连接超时等网络问题,渠道官方一般推荐使用分片上传机制,降低大文件上传的失败频率。使用IO的RandomAccessFile完成需求。类似于一次NIO文件操作,针对缓存区做操作,http请求类似于一个文件上传通道。
RandomAccessFile支持对随机访问文件的读写。同时,RandomAccessFile支持“随机访问”的方式。局限的是它只能读写文件。
RandomAccessFile的一个重要使用场景就是网络请求中的多线程下载及断点续传。当分片上传中的某一片上传失败时,可以根据分片号和文件位移量的位置得到需要重新操作的文件片段,进行重新上传,我使用的是@Retryable注解使用在分片上传的方法上。
@Retryable(maxAttempts = 5, backoff = @Backoff(value = 1000, multiplier = 1.5))
四种访问模式:
- “r”
只读 - “rw”
可读可写。如果文件不存在,则会尝试创建它。 - “rws”
可读可写,与“rw”一样,还要求每次对文件内容或元数据的更新都同步写入底层存储设备。 - “rwd”
可读可写,与“rw”一样,还要求每次对文件内容的更新都同步写入底层存储设备。
代码示例:
// 只读模式
RandomAccessFile raf = new RandomAccessFile(file, "r");
try {
long length = file.length();
//分片号 从1开始
int startOffset = 1;
int byteCount;
// 缓存区 取文件长度和每片大小中小的值作为缓存区的容量
byte[] buff = new byte[(int) Math.min(SIZE, length)];
boolean finish = false;
// 使用while而不是if
while (!finish) {
// 指针位置 从 0 开始,每次位移量 = 1 * 每片大小
raf.seek((startOffset - 1) * SIZE);
// 每片的字节数
byteCount = raf.read(buff);
// 当本次上传的字节数未到达需要分片的大小 | 指针已经到末尾 证明是最后一片
if (byteCount < SIZE || (startOffset) * SIZE == length) {
byte[] realChunkData = new byte[byteCount];
System.arraycopy(buff, 0, realChunkData, 0, byteCount);
buff = realChunkData;
finish = true;
}
//分片上传 nio:通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理
this.uploadVideoShard(startOffset, buff);
// 修改指针位置 未结束每次 + 1
startOffset = finish ? startOffset : startOffset + 1;
//分片上传完成 开始文件分片合片
if (finish) {
JSONObject result = restTemplate.postForObject(completeUrl, HttpEntity.EMPTY, JSONObject.class);
}
}
} finally {
if (file.exists()) {
file.delete();
}
// 关闭文件
raf.close();
}