Java NIO实现非阻塞I/O
在Java中,阻塞I/O(Blocking I/O)和非阻塞I/O(Non-blocking I/O)是两种不同的I/O模式。
阻塞I/O模式下,当应用程序进行输入/输出操作时,线程会一直阻塞,直到数据传输完成或者发生异常。在此期间,线程无法执行其他任务,因此阻塞I/O模式具有较低的效率和响应性能。
非阻塞I/O模式下,当应用程序进行输入/输出操作时,线程会立即返回,并且不会等待数据传输完成。在此期间,线程可以执行其他任务,因此非阻塞I/O模式具有较高的效率和响应性能。
Java NIO中的非阻塞I/O是基于选择器(Selector)和通道(Channel)的。选择器可以监听多个通道上的I/O事件,并在有事件发生时通知应用程序,从而实现非阻塞I/O操作。通道则是用于输入/输出操作的对象,可以是文件通道或网络通道。
Java NIO是非阻塞的,因为它基于选择器和通道实现了非阻塞I/O,支持同时处理多个通道的I/O事件,从而提高了I/O操作的效率和响应性能。相比之下,传统的Java IO(也称为IO流)是阻塞的,因为它只能同时处理一个输入/输出流,当进行输入/输出操作时,线程会一直阻塞,直到数据传输完成或者发生异常。
1、创建通道
通道是Java NIO中用于输入/输出操作的对象,可以通过SocketChannel、ServerSocketChannel、DatagramChannel等创建网络通道,或者通过FileChannel创建文件通道。在这里,我们以SocketChannel为例创建网络通道。
SocketChannel channel = SocketChannel.open();
2、将通道设置为非阻塞模式
通过调用通道的configureBlocking(false)方法,将通道设置为非阻塞模式。在非阻塞模式下,通道的读取和写入操作不会阻塞线程,而是立即返回。
channel.configureBlocking(false);
3、创建选择器
选择器是Java NIO中用于监听多个通道的I/O事件的对象,用于实现非阻塞I/O。可以通过Selector.open()方法创建选择器。
Selector selector = Selector.open();
4、将通道注册到选择器上
通过调用通道的register()方法,将通道注册到选择器上,并指定要监听的事件类型,例如读取事件、写入事件、连接事件、接受事件等。在这里,我们注册了读取事件。
channel.register(selector, SelectionKey.OP_READ);
5、轮询选择器
通过调用选择器的select()方法,轮询选择器上注册的通道,当有通道上的I/O事件就绪时,select()方法会返回就绪的通道数量。
while (true) {
selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 处理就绪的通道
keyIterator.remove();
}
}
6、处理就绪的通道
通过调用选择器的selectedKeys()方法,获取所有就绪的通道,并进行相应的读取或写入操作。在这里,我们实现了从通道读取数据的操作。
while (true) {
selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
while (bytesRead > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
}
if (bytesRead == -1) {
channel.close();
}
}
keyIterator.remove();
}
}
需要注意的是,在非阻塞I/O模式下,读取和写入操作通常需要多次调用,直到完整的数据传输完成。在读取操作中,需要将数据从通道读取到缓冲区,并判断缓冲区中是否已经读取完毕。
此外,在非阻塞I/O模式下,发生异常的可能性比较高,因此需要进行异常处理。可以通过选择器的selectedKeys()方法和SelectionKey的readyOps()方法,判断通道是否出现异常,并进行相应的处理。
以下是完整的示例代码。在这个例子中,我们使用了一个简单的Echo服务器,将客户端发送的消息原样返回。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NonBlockingServer {
public static void main(String[] args) throws IOException {
// 创建服务器套接字通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(9999));
serverChannel.configureBlocking(false);
// 创建选择器
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 9999");
while (true) {
selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理连接事件
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = channel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + clientChannel.getRemoteAddress());
} else if (key.isReadable()) {
// 处理读取事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
while (bytesRead > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
buffer.clear();
bytesRead = channel.read(buffer);
}
if (bytesRead == -1) {
channel.close();
}
}
keyIterator.remove();
}
}
}
}
问题:selector.select()是阻塞,为什么还说NIO是非阻塞的呢?
selector.select()方法确实会阻塞,直到有至少一个通道准备好进行I/O操作或者等待超时或中断。但是,需要注意的是,这种阻塞只会影响当前的线程,不会影响应用程序的其他线程。
在服务端线程调用选择器的select()方法时,只有当前服务端线程会被阻塞,而不是客户端线程。
客户端的阻塞和非阻塞I/O操作取决于具体的实现。对于阻塞I/O模式,客户端线程在进行输入/输出操作时,会一直阻塞,直到数据传输完成或者发生异常。对于非阻塞I/O模式,客户端线程在进行输入/输出操作时,会立即返回,并且不会等待数据传输完成。在此期间,客户端线程可以执行其他任务。
因此,Java NIO仍然可以称为非阻塞I/O。
Java NIO提供了一种基于事件驱动的I/O模型,应用程序使用选择器(Selector)来注册通道(Channel)上的I/O事件,并在有事件发生时进行相应的处理。在选择器上调用select()方法会阻塞当前线程,直到至少有一个通道上注册的事件发生,此时select()方法会返回,应用程序可以通过selectedKeys()方法获取就绪的事件。由于选择器可以同时监听多个通道,因此Java NIO可以同时处理多个通道上的I/O事件,从而提高了I/O操作的效率和响应性能。
需要注意的是,虽然选择器的select()方法会阻塞当前线程,但是可以通过调用选择器的wakeup()方法中断阻塞,使得select()方法立即返回。此外,可以在选择器上设置超时时间,使得select()方法在指定时间内返回,避免长时间的无限阻塞。
实战Java NIO中实现文件I/O(File I/O)和网络I/O(Network I/O)
文件I/O(File I/O)
Java NIO中的文件I/O是通过FileChannel来实现的。FileChannel类提供了读取和写入文件的方法,而ByteBuffer类则用于存储读取和写入的数据。
以下是实现文件I/O的详细步骤:
步骤1:获取FileChannel实例
在进行文件I/O之前,需要先获取FileChannel实例。可以通过FileInputStream或FileOutputStream来获取FileChannel实例,例如:
FileInputStream fileInputStream = new FileInputStream("file.txt");
FileChannel fileChannel = fileInputStream.getChannel();
步骤2:创建ByteBuffer
在进行文件I/O之前,需要先创建ByteBuffer实例,用于存储读取和写入的数据。可以通过ByteBuffer的allocate方法创建ByteBuffer实例,例如:
ByteBuffer buffer = ByteBuffer.allocate(1024);
步骤3:读取文件数据
(1)从FileChannel中读取数据
可以通过FileChannel的read方法从文件中读取数据,并将数据存储到ByteBuffer中。read方法有两个重载版本:
int read(ByteBuffer dst) throws IOException;
long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
第一个版本的read方法将数据读取到单个ByteBuffer中,返回值为读取的字节数。如果返回值为-1,表示已经读取到了文件的末尾。
第二个版本的read方法将数据读取到多个ByteBuffer中,返回值为读取的字节数。如果返回值为-1,表示已经读取到了文件的末尾。
以下是使用第一个版本read方法的示例代码:
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的read方法将数据读取到ByteBuffer中,并返回读取的字节数。随后,通过flip方法将ByteBuffer从写模式切换为读模式,并通过get方法读取ByteBuffer中的数据。当ByteBuffer中的数据被读取完毕后,通过clear方法将ByteBuffer从读模式切换为写模式,并再次调用FileChannel的read方法读取文件中的数据,直到文件中的所有数据被读取完毕。
(2)向FileChannel中写入数据
可以通过FileChannel的write方法向文件中写入数据,例如:
byte[] data = "Hello, World!".getBytes();
ByteBuffer buffer = ByteBuffer.wrap(data);
int bytesWritten = fileChannel.write(buffer);
上述代码首先将数据存储到ByteBuffer中,随后调用FileChannel的write方法将数据写入到文件中。
步骤4:关闭FileChannel
在使用完FileChannel后,需要调用其close方法关闭FileChannel,例如:
fileChannel.close();
完整的代码示例:
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileIODemo {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("file.txt");
FileChannel fileChannel = fileInputStream.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();
}
}
网络I/O(Network I/O)
Java NIO中的网络I/O是通过SocketChannel和ServerSocketChannel来实现的,它们分别用于客户端和服务端的网络通信。
以下是实现网络I/O的详细步骤:
步骤1:获取SocketChannel或ServerSocketChannel实例
在进行网络I/O之前,需要先获取SocketChannel或ServerSocketChannel实例。可以通过SocketChannel或ServerSocketChannel的open方法获取相应的实例,例如:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.example.com", 80));
或:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
步骤2:创建ByteBuffer
在进行网络I/O之前,需要先创建ByteBuffer实例,用于存储读取和写入的数据。可以通过ByteBuffer的allocate方法创建ByteBuffer实例,例如:
ByteBuffer buffer = ByteBuffer.allocate(1024);
步骤3:读取网络数据
(1)从SocketChannel中读取数据
可以通过SocketChannel的read方法从网络中读取数据,并将数据存储到ByteBuffer中。read方法的用法与文件I/O中的read方法相同,这里不再赘述。
以下是使用SocketChannel的read方法的示例代码:
int bytesRead = socketChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = socketChannel.read(buffer);
}
上述代码首先通过SocketChannel的read方法将数据读取到ByteBuffer中,并返回读取的字节数。随后,通过flip方法将ByteBuffer从写模式切换为读模式,并通过get方法读取ByteBuffer中的数据。当ByteBuffer中的数据被读取完毕后,通过clear方法将ByteBuffer从读模式切换为写模式,并再次调用SocketChannel的read方法读取网络中的数据,直到网络中的所有数据被读取完毕。
(2)向SocketChannel中写入数据
可以通过SocketChannel的write方法向网络中写入数据,例如:
byte[] data = "Hello, World!".getBytes();
ByteBuffer buffer = ByteBuffer.wrap(data);
int bytesWritten = socketChannel.write(buffer);
上述代码首先将数据存储到ByteBuffer中,随后调用SocketChannel的write方法将数据写入到网络中。
步骤4:关闭SocketChannel或ServerSocketChannel
在使用完SocketChannel或ServerSocketChannel后,需要调用其close方法关闭SocketChannel或ServerSocketChannel,例如:
socketChannel.close();
或:
serverSocketChannel.close();
:完整的代码示例:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NetworkIODemo {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.example.com", 80));
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = socketChannel.read(buffer);
}
socketChannel.close();
}
}