Java NIO(New Input/Output)是Java中用于处理非阻塞I/O操作的一组API。它引入了一种新的I/O模型,提供了更高效、更可伸缩的方式来处理I/O操作。
Java NIO的核心组件是通道(Channel)和缓冲区(Buffer)。通道是对原始I/O操作的一种抽象,可以通过它读取和写入数据。缓冲区是一块内存区域,用于存储数据,它使得数据的读写更加高效。
相对于传统的Java I/O(也称为流式I/O),Java NIO具有以下优点:
-
非阻塞式:Java NIO提供非阻塞I/O操作,可以在等待I/O完成时同时执行其他任务,提高系统的效率。
-
选择器(Selector):选择器是Java NIO的一个重要组件,可以监视多个通道的I/O事件,从而实现单线程处理多个通道的能力。
-
内存管理:Java NIO使用直接缓冲区,将数据直接存储到物理内存中,避免了传统I/O中数据在Java堆上的复制过程,提高了效率。
-
网络编程支持:Java NIO提供了更多的网络编程支持,例如Socket通信和服务器端的Channel和Selector。
总结来说,Java NIO提供了一种更高效、更可伸缩的方式来处理I/O操作,尤其适用于开发高性能的网络应用程序。
1. NIO和传统I/O的区别:
-
NIO是非阻塞I/O,而传统I/O是阻塞式的。在传统I/O中,当一个线程在进行I/O操作时会被阻塞,而NIO可以在进行I/O操作时继续执行其他任务。
-
NIO使用了通道和缓冲区的概念,而传统I/O使用了流的概念。
-
NIO提供了选择器(Selector)的机制,可以同时监控多个通道的事件,而传统I/O需要使用多个线程来处理多个连接。
2. 通道和缓冲区的基本操作:
-
通道(Channel)是数据的载体,负责读取和写入数据。可以通过FileChannel、SocketChannel、ServerSocketChannel等类获取通道。
-
缓冲区(Buffer)是用来存储数据的区域。可以通过ByteBuffer、CharBuffer、IntBuffer等类创建不同类型的缓冲区。
-
通道的读取和写入都是通过缓冲区来进行的。可以使用read方法从通道将数据读取到缓冲区,使用write方法将数据从缓冲区写入到通道。
3. 选择器和选择键:
-
选择器(Selector)是用来监听通道事件的对象。可以使用Selector.open()来创建一个选择器。
-
选择键(SelectionKey)是与通道一起使用的对象,它表示了注册在选择器上的通道以及对应的事件类型。
-
选择器通过register方法将通道注册到选择器上,并指定监听的事件类型。
-
选择器使用select方法监控通道事件,并返回有事件发生的通道数。
-
可以使用selectedKeys方法获取已经发生事件的选择键,然后依次处理相应的事件。
4. 非阻塞式通信的原理和优势:
-
非阻塞式通信在进行I/O操作时不会阻塞当前线程,而是立即返回,无论数据是否就绪。
-
非阻塞式通信通过不断轮询来判断数据是否就绪,从而实现对多个通道的同时管理和处理。
-
与传统I/O相比,非阻塞式通信可以更高效地处理多个并发连接,并减少线程的开销。
5. 非阻塞式通信的实现方式:通过Selector监听通道事件并处理非阻塞读写操作。
-
首先创建一个选择器,并将通道注册到选择器上。
-
使用一个线程不断调用选择器的select方法来监听通道事件。
-
当有通道事件发生时,通过获取选择键,可以判断具体是哪个通道有事件发生,然后进行相应的读写操作。
Selector 是 Java NIO 中的关键组件,它提供了一种监视多个通道的机制,以检测哪些通道已经准备好进行 I/O 操作(如读、写、连接等)。通过 Selector,你可以使用单个线程来管理多个通道,从而实现高效的 I/O 复用。
以下是关于 Selector 类的一些重要说明:
- 创建 Selector 对象:可以通过调用 Selector.open() 方法创建一个 Selector 实例。例如:Selector selector = Selector.open();
- 注册通道:通道(Channel)需要注册到 Selector 上才能被 Selector 进行监视。通常情况下,你需要调用通道的 register() 方法将通道注册到 Selector 上,并指定所感兴趣的事件(如读、写、连接等)。例如:channel.register(selector, SelectionKey.OP_READ);
- 选择操作:一旦通道注册到 Selector 上,你可以调用 Selector 的 select() 方法阻塞地等待通道就绪事件的发生。select() 方法会返回就绪通道的数量。例如:int readyChannels = selector.select();
- 获取就绪的通道:通过调用 selector.selectedKeys() 方法可以获取到一组已经就绪的 SelectionKey 集合。你可以遍历这个集合,逐个处理就绪的通道。例如:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
for (SelectionKey key : selectedKeys) {
if (key.isReadable()) {
// 处理可读事件
}
if (key.isWritable()) {
// 处理可写事件
}
// 其他事件处理...
}
-
取消通道的注册:当你不再需要监听某个通道时,可以调用通道的 cancel() 方法取消通道的注册。例如:key.cancel();
-
关闭 Selector:当你不再需要 Selector 时,应该调用其 close() 方法关闭 Selector,释放相关资源。例如:selector.close();
以上是关于 Selector 类的简要说明。使用 Selector 可以极大地提高网络编程的效率和性能,特别适用于需要管理多个并发连接的场景。
Selector和SelectionKey是Java NIO中与选择器和选择键相关的两个重要类。下面是它们常用的方法:
Selector类的方法:
-
open():创建一个新的选择器。
-
isOpen():检查选择器是否处于打开状态。
-
close():关闭选择器。
-
select():阻塞直到至少有一个通道已经准备就绪,返回就绪通道的数量。
-
select(timeout):最多阻塞timeout毫秒,或者直到至少有一个通道已准备就绪。
-
selectNow():立即返回,不阻塞,如果有通道就绪则返回就绪通道的数量。
-
selectedKeys():返回已选择键集合的集合视图。
-
keys():返回现在已经注册到选择器上的键集合。
-
wakeup():唤醒正在select操作中阻塞的线程。
-
selectableChannels():返回当前已注册到选择器的通道的集合。
SelectionKey类的方法:
-
channel():返回与此键关联的通道。
-
selector():返回此键的选择器。
-
isValid():检查此键是否有效。
-
interestOps():获取或设置此键的感兴趣的操作集。
-
interestOps(int ops):设置此键的感兴趣的操作集。
-
isReadable():检查键的通道是否已准备好读取操作。
-
isWritable():检查键的通道是否已准备好写入操作。
-
isConnectable():检查键的通道是否已准备好连接操作。
-
isAcceptable():检查键的通道是否已准备好接受操作。
-
attach(Object obj):关联一个对象到此键。
-
attachment():获取此键的附加对象。
-
cancel():取消此键的通道在其选择器中的注册。
以上是Selector和SelectionKey类的一些常用方法。通过使用它们,你可以实现对通道的事件监听和处理,以及管理多个通道的并发操作。详情可参考Java官方文档中关于Selector和SelectionKey的说明。
以下是Java官方文档中关于Selector和SelectionKey的说明(摘自Java 11文档):
Selector类的说明:
-
Selector是可选择通道的多路复用器。它可以通过单个线程,使用选择操作来监视多个通道的事件。
-
可以使用Selector.open()方法打开一个选择器,使用register()方法将通道注册到选择器上。
-
一旦通道注册到选择器上,通过选择器的select()方法可以阻塞地等待通道就绪事件的发生。
-
选择器是线程安全的,可以在多线程环境下共享使用。
SelectionKey类的说明:
-
SelectionKey表示注册在选择器上的通道,并维护了通道与选择器之间的关联关系。
-
通过SelectionKey可以检索与通道关联的选择器,以及感兴趣的操作集合(interest set)和就绪的操作集合(ready set)。
-
可以使用isReadable()、isWritable()、isConnectable()和isAcceptable()等方法来检查通道就绪的特定操作。
-
通过修改interest set,可以动态改变对通道感兴趣的操作,通过修改ready set,可以处理通道已准备就绪的操作。
-
可以通过attach()和attachment()方法关联和检索自定义的附加对象到SelectionKey上。
-
SelectionKey是不可重用的,一旦取消注册后将会失效。
请注意,以上是对官方文档中关于Selector和SelectionKey的简要说明。如需详细了解它们的更多细节和使用方法,请参考Java官方文档中关于java.nio.channels包的相关章节。
以下是使用Selector和SelectionKey类的实例:
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SelectionKey;
import java.util.Iterator;
import java.util.Set;
public class SelectorExample {
public static void main(String[] args) throws IOException {
// 创建选择器
Selector selector = Selector.open();
// 创建ServerSocketChannel并绑定端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8888));
serverSocketChannel.configureBlocking(false);
// 将通道注册到选择器上,并指定感兴趣的事件为接收连接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞等待就绪的通道,最多等待10秒
int readyChannels = selector.select(10000);
if (readyChannels == 0) {
continue;
}
// 获取已经就绪的通道集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 处理连接事件
// ...
}
if (key.isReadable()) {
// 处理读事件
// ...
}
if (key.isWritable()) {
// 处理写事件
// ...
}
// 处理完事件后,需要从已选择键集合中移除
keyIterator.remove();
}
}
}
}
上述示例演示了如何使用Selector和SelectionKey类来监听和处理通道事件。在主循环中,使用select()方法阻塞等待就绪的通道,然后使用迭代器遍历就绪的选择键集合,判断具体是哪种事件发生,并进行相应的处理。最后,需要使用迭代器的remove()方法将已处理的选择键从集合中移除。
请注意,在实际应用中,你可能还需要处理异常、关闭通道、写入数据等其他操作。以上示例仅提供了基本的框架。根据你的实际需求和业务逻辑,你可以根据选择键的就绪状态进行相应的操作。
解释一下这两行代码的具体作用:
-
selector.selectedKeys():这个方法返回一个Set,包含了当前已经就绪的选择键(SelectionKey)的集合。就绪的选择键指的是那些可以执行操作(如读、写、连接、接受等)的通道所对应的选择键。这个集合是动态的,会随通道的就绪状态的变化而变化。
-
selectedKeys.iterator():通过调用集合的iterator()方法,可以获取到一个迭代器(Iterator),用来遍历集合中的元素。在这里,我们将选择键集合的迭代器赋值给了keyIterator变量,后续可以使用这个迭代器来依次访问集合中的选择键元
Java NIO的缓冲区(Buffer)是用于存储数据的一块连续内存区域,它是NIO中数据读写的中间载体。缓冲区提供了一组API来方便地读取和写入数据,并且可以跟踪读写位置、限制和容量等信息。
Java NIO中的缓冲区是抽象类java.nio.Buffer的子类,**常用的缓冲区实现类有ByteBuffer、CharBuffer、**ShortBuffer、IntBuffer、LongBuffer、FloatBuffer和DoubleBuffer,分别用于存储不同类型的数据。
以下是缓冲区的一些重要概念:
-
容量(Capacity):缓冲区的容量是它所能存储的数据的大小,一旦定义后不可变。
-
限制(Limit):限制是缓冲区中有效数据的末尾位置,初始时与容量相等,随着读写操作而改变。
-
位置(Position):位置表示下一个要读取或写入的元素的索引,初始时为0,随着读写操作而改变。
-
标记(Mark):标记是一个备忘位置,通过调用mark()方法设置,之后可以通过调用reset()方法将位置重置为标记的位置。
缓冲区具有以下几个基本操作: -
写入数据:使用put()方法向缓冲区中写入数据,可以一次性写入多个元素或逐个写入。
-
读取数据:使用get()方法从缓冲区中读取数据,可以一次性读取多个元素或逐个读取。
-
翻转(Flip):翻转缓冲区,将限制设置为当前位置,将位置设置为0,用于从缓冲区读取数据。
-
清空(Clear):清空缓冲区,将位置设置为0,将限制设置为容量,用于向缓冲区写入数据。
-
压缩(Compact):压缩缓冲区,将未读数据复制到缓冲区的起始位置,并将位置设置为未读数据的末尾,用于继续写入数据。
缓冲区在进行数据读写时,会根据当前位置和限制来确定可读写的数据范围,避免了对整个缓冲区的操作,提高了效率。
总结来说,Java NIO的缓冲区是用于存储数据的中间载体,提供了一组API来方便地读取和写入数据。通过合理使用缓冲区,可以提高数据读写的效率和性能。
java
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;
public class NioServer {
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);
while (true) {
if (selector.select() > 0) {
for (SelectionKey key : selector.selectedKeys()) {
if (key.isAcceptable()) {
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
System.out.println("Received from client: " + new String(buffer.array(), 0, bytesRead));
buffer.flip();
clientChannel.write(buffer);
}
}
}
selector.selectedKeys().clear();
}
}
}
}
// 在这个例子中,服务器端使用 ServerSocketChannel 监听端口,当有客户端连接时,接受连接并使用 SocketChannel 与客户端通信。
// Selector 用于监听多个客户端连接,并使用 SelectionKey 来管理每个连接的状态。
// 客户端使用 SocketChannel 连接到服务器,然后使用 ByteBuffer 发送和接收数据。
// 请注意,这个例子仅用于演示 Java NIO 的基本使用,在实际应用中,您可能需要处理更多的细节,例如异常处理、线程管理等。
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NioClient {
public static void main(String[] args) throws IOException {
SocketChannel clientChannel = SocketChannel.open();
clientChannel.connect(new InetSocketAddress("localhost", 9999));
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello, server!".getBytes());
buffer.flip();
clientChannel.write(buffer);
buffer.clear();
int bytesRead = clientChannel.read(buffer);
if (bytesRead > 0) {
System.out.println("Received from server: " + new String(buffer.array(), 0, bytesRead));
}
clientChannel.close();
}
}