NIO | 什么是Java中的NIO —— 结合业务场景理解 NIO (一)
在 Java 中,NIO(New Input/Output)是 Java 1.4 引入的一个新的 I/O 类库,它提供了比传统的 I/O(如
java.io
包)更高效的 I/O 操作。NIO 提供了更好的性能,尤其是在需要处理 大量数据 的场景中(例如,文件操作、网络通信等)。
与传统 I/O 不同,NIO 是基于 缓冲区 和 通道 的,支持非阻塞模式,使得在处理 I/O 操作时可以更加高效地使用系统资源。
一、NIO 的核心概念
NIO 的设计 和 传统 I/O(流式 I/O) 有较大的不同,主要包括以下几个核心概念:
(一) Channel(通道)
在传统 I/O 中,数据是通过 流(Stream)来传输的,而在 NIO 中,数据通过 通道(Channel)进行传输。Channel
是双向的,它可以用来读取数据,也可以用来写入数据。Channel
与流的主要区别在于,Channel
是可以进行双向操作的。
常见的 Channel
实现类包括:
-
FileChannel
:用于文件 I/O 操作。SocketChannel
:用于网络 I/O 操作(TCP)。ServerSocketChannel
:用于网络 I/O 操作(监听 TCP 连接)。DatagramChannel
:用于 UDP 网络通信。
(二) Buffer(缓冲区)
在 NIO 中,数据并 不是直接 从 Channel
读入或写入,而是先通过 Buffer
存储,Buffer
是一个用于 读写数据 的容器。在进行数据操作时,数据会从 通道 读取到 缓冲区,或者从缓冲区 写入 到通道。
常见的 Buffer
类型:
ByteBuffer
:用于处理字节数据。CharBuffer
:用于处理字符数据。IntBuffer
:用于处理整型数据等。
(三) Selector(选择器)
NIO 提供了 Selector
,这是一个用于管理 多个 Channel
的机制,特别是支持 非阻塞 I/O。使用 Selector
,一个单独的线程可以处理多个 Channel
,从而减少线程数量,提高效率。Selector
主要用于 网络通信 中,尤其是在 一个服务器 需要 同时处理 多个客户端连接时,它能显著提高性能。
工作原理:
Selector
允许你检查一个或多个Channel
是否有就绪的 I/O 操作(例如,是否有数据可读,或者是否可以写数据)。Channel
与Selector
的结合,使得程序能够在非阻塞模式下轮询多个通道。
二、NIO 的工作模式
NIO 主要支持两种模式:
(一) 阻塞模式
默认情况下,NIO 的通道是阻塞的。在阻塞模式下,调用 I/O 操作(如读取、写入)时,操作会一直等待,直到操作完成为止。
(二) 非阻塞模式
在非阻塞模式下,I/O 操作不会阻塞线程。例如,SocketChannel
可以在 非阻塞模式下 进行读取,如果没有数据可读,它会立即返回,而不是阻塞等待数据。通过使用 Selector
,可以在 一个线程 中 轮询 多个 Channel
,使得可以同时处理多个 I/O 操作。
三、NIO 与传统 I/O 的区别
特性 | 传统 I/O (Stream-based I/O) | NIO (Buffer and Channel) |
数据传输方式 | 基于流(Stream),一端写一端读 | 基于缓冲区(Buffer),双向读写 |
操作方式 | 阻塞式 I/O | 支持非阻塞式 I/O |
性能 | 对于大量数据或并发连接性能较差 | 更适合高并发、大数据处理 |
文件处理 | 使用 , 等 | 使用 |
网络通信 | 使用 和 类 | 使用 和 |
(一) NIO 的优势
- 高效的内存管理:NIO 使用缓冲区(Buffer)存储数据,能够直接操作内存,更加高效。
- 非阻塞 I/O:在非阻塞模式下,NIO 能让你在同 一个线程 中处理 多个 I/O 操作,减少了线程的上下文切换,提高了效率。
- 支持选择器:通过
Selector
,NIO 使得可以在 单个线程 中管理 多个通道,提高了并发处理能力。 - 适合高并发场景:特别适合 网络服务器 或需要 处理大量并发连接 的场景,例如 Web 服务器。
(二) NIO 使用场景
- 文件操作:对于大量的文件读写,NIO 提供了更高效的方式,特别是当文件操作涉及大文件时。
- 网络通信:对于高并发的网络服务器(如 HTTP 服务器、Chat 应用等),使用 NIO 可以显著提升性能。特别是通过使用
Selector
,可以用少量的线程处理大量的并发连接。 - 数据库连接:在高并发的数据库访问场景下,NIO 可以更高效地处理数据库连接。
四、 示例一:使用 NIO 读取文件
简单的使用
FileChannel
和ByteBuffer
读取文件内容
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class NIOFileReadExample {
public static void main(String[] args) throws IOException {
// 1. 打开文件通道
// 通过 FileInputStream 打开一个文件输入流,然后通过 getChannel() 方法获取该流的 FileChannel
// FileChannel 允许我们直接操作文件,提供更高效的读写操作
FileInputStream fis = new FileInputStream("example.txt");
FileChannel fileChannel = fis.getChannel();
// 2. 创建缓冲区
// ByteBuffer 是 NIO 中用于存储数据的缓冲区,我们通过 ByteBuffer.allocate() 分配一个大小为 1024 字节的缓冲区
// 它用于临时存储从文件中读取的数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 读取文件内容到缓冲区
// 从文件通道读取数据到缓冲区中,返回值是读取的字节数
// 如果文件已经读取完毕,返回 -1
int bytesRead = fileChannel.read(buffer);
// 4. 循环直到文件内容读取完毕
// 当 bytesRead 不为 -1 时,表示文件仍有数据未读取
while (bytesRead != -1) {
// 5. 切换到读取模式
// 缓冲区默认是写模式,我们通过调用 flip() 方法将缓冲区切换为读取模式
buffer.flip();
// 6. 读取缓冲区中的数据
// 判断缓冲区中是否还有数据,hasRemaining() 方法返回是否有未读取的数据
while (buffer.hasRemaining()) {
// 使用 buffer.get() 获取一个字节的数据,并将其转换为字符后打印输出
System.out.print((char) buffer.get());
}
// 7. 清空缓冲区
// 读取完数据后,通过调用 clear() 方法清空缓冲区,准备下一次读操作
buffer.clear();
// 8. 继续读取文件内容
// 继续从文件通道读取更多的数据,直到文件读取完毕
bytesRead = fileChannel.read(buffer);
}
// 9. 关闭通道
// 关闭文件通道和文件输入流,以释放资源
fileChannel.close();
fis.close();
}
}
(一) 注释说明
- FileInputStream 和 FileChannel:
FileInputStream
是传统的 I/O 流,通过getChannel()
方法可以获取与之关联的FileChannel
。FileChannel
是 NIO 中专用于文件操作的通道,支持高效的异步 I/O 操作。 - ByteBuffer:
ByteBuffer
是 NIO 中用于存储数据的缓冲区。通过allocate()
方法分配一定大小的缓冲区,可以将数据从通道中读取到缓冲区,再从缓冲区读取数据。 - flip() 和 clear():
flip()
方法将缓冲区从写模式切换为读模式,使得可以从缓冲区中读取数据。clear()
方法则清空缓冲区,准备下一次读操作。 - hasRemaining() 和 get():
hasRemaining()
检查缓冲区是否还有未读取的数据,get()
从缓冲区中取出一个字节并返回。 - 关闭资源:使用完通道和流后,必须调用
close()
方法关闭它们,以释放系统资源。
这段代码演示了 如何使用 NIO 来高效地从文件中读取数据,并逐个字符打印出来,利用了 NIO 的缓冲区和通道机制。
五、 示例二:高并发的网络服务器
(一) 业务场景
假设你正在开发一个高并发的 Web 服务器,需要同时处理成千上万的 HTTP 请求。每个请求的处理可能涉及文件读取、数据库查询等操作。传统的阻塞 I/O 模式下,如果每个请求都创建一个新的线程来处理,随着并发请求的增加,系统会面临 线程上下文切换、内存占用、CPU 资源浪费等问题,从而影响性能。
(二) NIO 解决方案
NIO 提供了 非阻塞 I/O 和 选择器(Selector) 的机制,能够在 一个线程 中同时处理 多个连接 和 I/O 操作 。使用 Selector
,我们可以让一个线程监听多个客户端连接,而无需为每个连接都创建独立的线程。这样能大幅减少系统的开销,提高并发处理能力。
- Channel(通道):用于与客户端建立连接和发送接收数据。
- Buffer(缓冲区):用于在通道之间存取数据。
- Selector(选择器):用于一个线程管理多个通道。它可以判断哪些通道已经准备好进行读写操作,从而实现非阻塞 I/O。
示例: 假设你要实现一个简单的多客户端的 HTTP 服务器,每个客户端连接发送 HTTP 请求并接收 HTTP 响应。在高并发场景下,你希望只使用少量线程来同时处理大量的请求。
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.net.InetSocketAddress;
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. 创建 ServerSocketChannel 来监听客户端的连接
// ServerSocketChannel 是 NIO 中用于接受客户端连接的通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 将通道绑定到本地地址(8080端口)
// InetSocketAddress 用于指定服务器的端口地址
serverSocketChannel.bind(new InetSocketAddress(8080));
// 设置通道为非阻塞模式
// 非阻塞模式意味着不会因为等待连接或数据而阻塞当前线程
serverSocketChannel.configureBlocking(false);
// 2. 创建 Selector 用于多路复用 I/O 事件
// Selector 允许在一个线程中管理多个通道的 I/O 事件,避免了每个通道都需要一个线程的资源消耗
Selector selector = Selector.open();
// 将 serverSocketChannel 注册到 Selector,监听 ACCEPT 事件(客户端连接)
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started, listening on port 8080...");
// 3. 进入主循环,等待并处理 I/O 事件
while (true) {
// 4. 阻塞等待 I/O 事件的发生
// select() 方法会阻塞当前线程,直到至少有一个通道的 I/O 事件发生
selector.select();
// 5. 处理所有的 SelectionKey(已发生的事件)
// selectedKeys() 返回发生了事件的 SelectionKey 集合
for (SelectionKey key : selector.selectedKeys()) {
// 判断事件类型,如果是 ACCEPT 事件,表示有新的客户端连接到达
if (key.isAcceptable()) {
// 接受客户端连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// accept() 会返回一个新的 SocketChannel,用于与客户端进行通信
SocketChannel clientChannel = server.accept();
// 设置客户端通道为非阻塞模式
clientChannel.configureBlocking(false);
// 将客户端的通道注册到 Selector,并监听 READ 事件(数据可读)
clientChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理可读事件:客户端发送的数据已经准备好读取
// 获取对应的客户端 SocketChannel
SocketChannel clientChannel = (SocketChannel) key.channel();
// 创建一个缓冲区,用于存储从客户端读取的数据
ByteBuffer buffer = ByteBuffer.allocate(256);
// 从客户端通道读取数据到缓冲区
int bytesRead = clientChannel.read(buffer);
// 如果返回值为 -1,表示客户端关闭了连接
if (bytesRead == -1) {
clientChannel.close();
} else {
// 如果有数据被读取,处理客户端请求
buffer.flip(); // 将缓冲区从写模式切换到读模式
// 向客户端发送响应数据,HTTP 状态码 200 OK
// 使用 ByteBuffer.wrap() 方法将响应内容写入客户端
clientChannel.write(ByteBuffer.wrap("HTTP/1.1 200 OK\n".getBytes()));
}
}
}
// 6. 清除已处理的 SelectionKey
// 调用 selectedKeys().clear() 方法清空已处理的事件,
// 防止再次处理相同的 SelectionKey
selector.selectedKeys().clear();
}
}
}
(三) 注释说明
- ServerSocketChannel:用于监听来自客户端的连接请求。在 NIO 中,
ServerSocketChannel
提供了服务器端接受连接的能力。它需要被配置为非阻塞模式,这样可以避免阻塞等待客户端连接。 - Selector:一个多路复用器,允许你在单线程中监听多个通道的 I/O 事件。通过
select()
方法,Selector
会阻塞,直到至少有一个通道准备好进行 I/O 操作。你可以选择性地注册通道的事件,例如OP_ACCEPT
(连接请求)和OP_READ
(数据可读)。 - SelectionKey:每个通道注册到
Selector
时,会生成一个SelectionKey
,它包含了与该通道相关联的事件类型(如连接请求或可读数据)。selectedKeys()
方法返回所有已准备就绪的事件集合。 - 非阻塞 I/O:设置通道为非阻塞模式后,调用
select()
会让线程阻塞等待事件的发生,避免了为每个通道分配一个线程的开销。 - 处理客户端连接与数据读取:当有新的客户端连接时,
OP_ACCEPT
事件触发,服务器会接受客户端连接并注册到Selector
上,监听后续的读取事件(OP_READ
)。当客户端有数据发送时,OP_READ
事件触发,服务器读取数据并回应 HTTP 响应。 - 缓冲区操作:在 NIO 中,所有的数据都是通过
ByteBuffer
来操作的。flip()
切换缓冲区的读写模式,clear()
清空缓冲区准备下一次读写。 - 关闭连接:在读取过程中,如果客户端关闭了连接,
read()
方法会返回 -1,服务器需要关闭相应的通道。
该代码演示了一个简单的非阻塞式 HTTP 服务端实现,使用 Selector
来处理多个客户端连接,避免了为每个连接都创建一个独立线程,从而提高了服务器的性能。
(四) NIOServer
测试类(客户端)
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.net.InetSocketAddress;
import static java.lang.Thread.sleep;
public class NIOClient {
public static void main(String[] args) {
SocketChannel socketChannel = null;
try {
// 1. 打开客户端 SocketChannel
socketChannel = SocketChannel.open();
// 2. 连接到服务器(指定服务器的 IP 和端口)
socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 3. 配置为非阻塞模式
socketChannel.configureBlocking(false);
// 4. 等待连接完成,直到连接建立
while (!socketChannel.finishConnect()) {
// 在非阻塞模式下,finishConnect() 会检查连接是否已完成
System.out.println("Connecting to the server...");
}
System.out.println("Connected to the server.");
// 5. 向服务器发送请求数据(HTTP 请求示例)
String request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
ByteBuffer buffer = ByteBuffer.wrap(request.getBytes());
socketChannel.write(buffer);
System.out.println("Request sent to server: " + request);
// 6. 接收服务器的响应
buffer.clear(); // 清空缓冲区准备读取数据
sleep(1000); // 暂停一秒等待服务器响应 , 防止客户端过早的关闭,产生异常
int bytesRead = socketChannel.read(buffer);
if (bytesRead != -1) {
buffer.flip(); // 切换到读取模式
byte[] responseData = new byte[buffer.remaining()];
buffer.get(responseData);
// 打印服务器响应
System.out.println("Response from server: " + new String(responseData));
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 7. 关闭连接
try {
if (socketChannel != null && socketChannel.isOpen()) {
socketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1. 说明
- 连接到服务器:
- 客户端通过
SocketChannel.open()
打开一个套接字通道,接着通过connect()
方法连接到服务器。 - 服务器的地址为
localhost
,端口为8080
,与NIOServer
中的服务器端口一致。
- 非阻塞模式:
- 设置
SocketChannel
为非阻塞模式,使用finishConnect()
方法检查连接是否完成。由于是非阻塞操作,客户端会立即返回,连接完成后再继续进行下一步操作。
- 发送请求:
- 构造一个简单的 HTTP 请求字符串(GET 请求),并将其封装到
ByteBuffer
中。 - 使用
write()
方法将请求数据写入服务器。
- 接收响应:
- 客户端读取服务器的响应数据。服务器通过
SocketChannel.write()
返回简单的 HTTP 响应(如HTTP/1.1 200 OK
)。 - 使用
read()
方法从服务器读取响应数据,并将其输出。
- 关闭连接:
- 客户端操作完成后,关闭
SocketChannel
连接。
2. 测试流程
- 启动
NIOServer
类,它将监听 8080 端口等待连接。 - 运行
NIOClient
类,客户端将连接到服务器并发送一个简单的 HTTP 请求。 - 服务器会接收请求并发送响应,客户端接收到响应后打印输出。
原文地址:https://blog.csdn.net/qq_56435346/article/details/145303545
免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!