一、NIO简介
NIO 是 Java SE 1.4 引入的一组新的 I/O 相关的 API,它提供了非阻塞式 I/O、选择器、通道、缓冲区等新的概念和机制。相比与传统的 I/O 多出的 N 不是单纯的 New,更多的是代表了 Non-blocking 非阻塞,NIO具有更高的并发性、可扩展性以及更少的资源消耗等优点。
二、NIO 与传统BIO
NIO:是同步非阻塞的,服务器实现模式为 一个线程处理多个连接。服务端只会创建一个线程负责管理Selector(多路复用器),Selector(多路复用器)不断的轮询注册其上的Channel(通道)中的 I/O 事件,并将监听到的事件进行相应的处理。每个客户端与服务端建立连接时会创建一个 SocketChannel 通道,通过 SocketChannel 进行数据交互。
BIO:全称是Blocking IO,同步阻塞式IO,是JDK1.4之前的传统IO模型,服务器实现模式为一个连接一个线程。每当客户端有连接请求时服务器端就需要启动一个线程进行处理。
两者主要区别如下:
阻塞和非阻塞:NIO 使用非阻塞式 I/O,而 BIO 使用阻塞式 I/O。在阻塞式 I/O 中,当一个 I/O 操作完成之前,线程会一直被阻塞,直到 I/O 操作完成;在非阻塞式 I/O 中,线程可以继续执行其他任务,直到 I/O 操作完成并返回结果。
线程模型:NIO 中的线程模型是基于事件驱动的,当一个 I/O 操作完成时,会触发相应的事件通知线程处理;而在 BIO 中,每个线程都负责处理一个客户端连接,需要不断地轮询客户端的输入输出流,以便及时响应客户端的请求。
内存消耗:NIO 中使用的缓冲区(Buffer)可以重复利用,减少了频繁的内存分配和回收,从而减少了内存的消耗;而在 BIO 中,每个客户端连接都需要单独分配一个缓冲区,容易造成内存的浪费。
并发性能:NIO 中使用非阻塞式 I/O,可以同时处理多个客户端连接,从而提高了并发处理能力;而在 BIO 中,由于每个客户端连接都需要一个线程来处理,当连接数量增加时,容易出现线程饥饿和资源耗尽的问题。
三、NIO 工作流程
创建 Selector:Selector 是 NIO 的核心组件之一,它可以同时监听多个通道上的 I/O 事件,并且可以通过 select() 方法等待事件的发生。
注册 Channel:通过 Channel 的 register() 方法将 Channel 注册到 Selector 上,这样 Selector 就可以监听 Channel 上的 I/O 事件。
等待事件:调用 Selector 的 select() 方法等待事件的发生,当有事件发生时,Selector 就会通知相应的线程进行处理。
处理事件:根据不同的事件类型,调用对应的处理逻辑。
关闭 Channel:当 Channel 不再需要使用时,需要调用 Channel 的 close() 方法关闭 Channel,同时也需要调用 Buffer 的 clear() 方法清空 Buffer 中的数据,以释放内存资源。
Java NIO 的工作流程可以简单概括为:通过 Selector 监听多个 Channel 上的 I/O 事件,当事件发生时,通过对应的 Channel 进行读写操作,并在 Channel 不再需要使用时关闭 Channel。
四、NIO 核心的组件
1. Channel(通道)
Channel 是应用程序与操作系统之间交互事件和传递内容的直接交互渠道,应用程序可以从管道中读取操作系统中接收到的数据,也可以向操作系统发送数据。Channel和传统IO中的Stream很相似,其主要区别为:通道是双向的,通过一个Channel既可以进行读,也可以进行写;而Stream只能进行单向操作,通过一个Stream只能进行读或者写,比如InputStream只能进行读取操作,OutputStream只能进行写操作。
1.1 常用的Channel实现类
FileChannel:本地文件IO通道,从文件中读写数据。一般流程为:
1.获取文件通道,通过 FileChannel 的静态方法 open() 来获取,获取时需要指定文件路径和文件打开方式
FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
2.创建字节缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
3.读/写操作
(1)、读操作
// 循环读取通道中的数据,并写入到 buf 中
while (channel.read(buf) != -1){
// 缓存区切换到读模式
buf.flip();
// 读取 buf 中的数据
while (buf.position() < buf.limit()){
// 将buf中的数据追加到文件中
text.append((char)buf.get());
}
// 清空已经读取完成的 buffer,以便后续使用
buf.clear();
}
(2)、写操作
// 循环读取文件中的数据,并写入到 buf 中
for (int i = 0; i < text.length(); i++) {
// 填充缓冲区,需要将 2 字节的 char 强转为 1 自己的 byte
buf.put((byte)text.charAt(i));
// 缓存区已满或者已经遍历到最后一个字符
if (buf.position() == buf.limit() || i == text.length() - 1) {
// 将缓冲区由写模式置为读模式
buf.flip();
// 将缓冲区的数据写到通道
channel.write(buf);
// 清空已经读取完成的 buffer,以便后续使用
buf.clear();
}
}
4.将数据刷出到物理磁盘
channel.force(false);
5.关闭通道
channel.close();
1.获取文件通道,通过 FileChannel 的静态方法 open() 来获取,获取时需要指定文件路径和文件打开方式
FileChannel channel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
2.创建字节缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
3.读/写操作
(1)、读操作
// 循环读取通道中的数据,并写入到 buf 中
while (channel.read(buf) != -1){
// 缓存区切换到读模式
buf.flip();
// 读取 buf 中的数据
while (buf.position() < buf.limit()){
// 将buf中的数据追加到文件中
text.append((char)buf.get());
}
// 清空已经读取完成的 buffer,以便后续使用
buf.clear();
}
(2)、写操作
// 循环读取文件中的数据,并写入到 buf 中
for (int i = 0; i < text.length(); i++) {
// 填充缓冲区,需要将 2 字节的 char 强转为 1 自己的 byte
buf.put((byte)text.charAt(i));
// 缓存区已满或者已经遍历到最后一个字符
if (buf.position() == buf.limit() || i == text.length() - 1) {
// 将缓冲区由写模式置为读模式
buf.flip();
// 将缓冲区的数据写到通道
channel.write(buf);
// 清空已经读取完成的 buffer,以便后续使用
buf.clear();
}
}
4.将数据刷出到物理磁盘
channel.force(false);
5.关闭通道
channel.close();
SocketChannel:网络套接字IO通道,TCP协议,客户端通过 SocketChannel 与服务端建立TCP连接进行通信交互。与传统的Socket操作不同的是,SocketChannel基于非阻塞IO模式,可以在同一个线程内同时管理多个通信连接,从而提高系统的并发处理能力。
1.打开一个 SocketChannel 通道
SocketChannel channel = SocketChannel.open();
2.连接到服务端
channel.connect(new InetSocketAddress("localhost", 9001));
3.分配缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
4.配置是否为阻塞方式(默认为阻塞方式)
channel.configureBlocking(false); // 配置通道为非阻塞模式
5.将channel的连接、读、写等事件注册到selector中,每个chanel只能注册一个事件,最后注册的一个生效,
同时注册多个事件可以使用"|"操作符将常量连接起来
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE | SelectionKey.OP_READ);
6.与服务端进行读写操作
channel.read(buf);
channel.write(buf);
7.关闭通道
channel.close();
1.打开一个 SocketChannel 通道
SocketChannel channel = SocketChannel.open();
2.连接到服务端
channel.connect(new InetSocketAddress("localhost", 9001));
3.分配缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
4.配置是否为阻塞方式(默认为阻塞方式)
channel.configureBlocking(false); // 配置通道为非阻塞模式
5.将channel的连接、读、写等事件注册到selector中,每个chanel只能注册一个事件,最后注册的一个生效,
同时注册多个事件可以使用"|"操作符将常量连接起来
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_WRITE | SelectionKey.OP_READ);
6.与服务端进行读写操作
channel.read(buf);
channel.write(buf);
7.关闭通道
channel.close();
ServerSocketChannel:网络套接字IO通道,TCP协议,服务端通过ServerSocketChannel监听来自客户端的连接请求,并创建相应的SocketChannel对象进行通信交互。ServerSocketChannel同样也是基于非阻塞IO模式,可以在同一个线程内同时管理多个通信连接,从而提高系统的并发处理能力。
1.打开一个 ServerSocketChannel 通道
ServerSocketChannel serverChannel = ServerSocketChannel.open()