什么是NIO?
这是一个新的IO处理类库,相对于BIO来说,它是新增的,所以它是(New IO)的简称。
但同时,NIO相对BIO来说,BIO是阻塞式IO,NIO是一个非阻塞式的IO,所以它也可称之为(Non-block IO)非阻塞IO。
NIO提供了那些新的特性?
缓冲区(Buffer)
缓冲区Buffer本质是一个对象,这个对象包含写入或者是读出的数据。在BIO中数据读出或者写入是通过Stream流来操作,流是单相的,要么写入,要么读出。但是Buffer是一个双向的。
在NIO中所有的数据都是通过缓冲区来处理,读取数据时,直接读到缓冲区中;写入数据时,写入到缓冲区。访问数据时,都是通过缓冲区进行操作。
public static void main(String[] args) {
final ByteBuffer allocate = ByteBuffer.allocate(10);
allocate.put("a".getBytes(StandardCharsets.UTF_8));
allocate.put("A".getBytes(StandardCharsets.UTF_8));
allocate.put("b".getBytes(StandardCharsets.UTF_8));
allocate.put("c".getBytes(StandardCharsets.UTF_8));
allocate.flip();
System.out.println(String.valueOf(allocate.get(0)));
System.out.println(String.valueOf(allocate.get(1)));
System.out.println(String.valueOf(allocate.get(2)));
System.out.println(String.valueOf(allocate.get(3)));
}
缓冲区通常是一个字节数组,但是它也可以是其它类型的数组,比如下面的类图中,所有类型的Buffer父类都是java.nio.Buffer
Buffer中定义了几种重要的数据:
private int mark = -1;标志位,允许在执行一些操作后返回此位置。mark 值在调用 mark() 方法时设置为当前的 position。调用 reset() 方法可以将 position 恢复到之前标记的 mark 位置。如果未设置标记,调用 reset() 会抛出 InvalidMarkException。
private int position = 0;定义了读写当前位置,从0开始到limit,每次读写一个元素,position自动增加,使用 flip() 方法切换缓冲区的读写模式时,position 会被重置为 0
private int limit;定义当前读写数据的限制,比如写入模式下,limit <= capacity,在读取模式下,limit表示的是写入数据的末尾位置。
private int capacity;定义了数组的容量
- 初始化
- 写入数据
- 切换为读
- 读取数据之后
- clear动作之后
- compact动作
channel通道
Channel是一个“通道“,网络数据通过Channel读取和写入。它有点类似于Stream流,但是它是更为灵活,支持异步的读写操作和双向的数据传输。
因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全全双工的,同时支持读写操作。
- 双向传输:
Channel 可以同时支持读和写操作,这与传统 I/O 的单向流不同。流在输入时只能读取,在输出时只能写入,而通道则可以同时读写。
- 非阻塞操作:
Channel 支持非阻塞模式,可以让线程在执行 I/O 操作时不会被阻塞。比如在读取文件时,通道可以立即返回,线程可以继续执行其他任务。
- 与缓冲区(Buffer)配合使用:
通道与缓冲区密切配合,用来进行高效的数据操作。数据会先从通道读取到缓冲区,再从缓冲区写入到通道中。通过 Buffer 对象,开发者可以更灵活地控制数据的读写操作。
Channel的类图关系:
可以看出,Channel提供了接口,下面有不同的实现,其主要可以分为两类,用于网络读写SelectableChannel 和用于文件操作的FileChannel
操作文件的实例:
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelExample {
public static void main(String[] args) throws Exception {
// 创建一个 RandomAccessFile 实例并获取 FileChannel
RandomAccessFile file = new RandomAccessFile("example.txt", "r");
FileChannel fileChannel = file.getChannel();
// 创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据从通道读取到缓冲区
int bytesRead = fileChannel.read(buffer);
// 读取数据
while (bytesRead != -1) {
buffer.flip(); // 切换缓冲区为读模式
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear(); // 清空缓冲区用于下次读取
bytesRead = fileChannel.read(buffer);
}
fileChannel.close();
file.close();
}
}
selector 多路复用器
主要是在单个线程上管理多个Channel的IO操作。通过 Selector,可以在一个线程中同时处理多个通道的 I/O 事件(如读、写、连接、接受等),从而有效提升性能,减少线程资源的消耗。
Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
Selector的特点:
- 单线程管理多连接:
• 使用 Selector,可以让一个线程同时处理多个通道,而无需为每个通道创建单独的线程,极大地提高了资源利用率。
- 非阻塞 I/O:
• Selector 支持非阻塞模式,可以检测出哪些通道准备好进行 I/O 操作,避免传统阻塞 I/O 模型中线程等待的问题。
- 事件驱动:
• Selector 监听通道的事件,通道一旦准备好特定的操作(如读、写),就会通知 Selector 处理相关事件。这种事件驱动机制让 Selector 更加高效。
Selector 监听的事件类型
Selector 可以监听的事件包括:
• OP_READ:通道的读事件,表示通道中有数据可供读取。
• OP_WRITE:通道的写事件,表示通道可以进行写操作。
• OP_CONNECT:连接事件,表示客户端通道成功连接到服务器。
• OP_ACCEPT:接收事件,表示服务器通道可以接受新的客户端连接。