Java 入门指南:Java NIO —— Selector(选择器)
NIO 的引入
在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。
为了解决这个问题,在 Java1.4 版本引入了 NIO
(New I/O or Non-Blocking I/O)java.nio
。提供了一种基于缓冲区、选择器和非阻塞 IO 模型的 IO 处理方式。相比于之前的 BIO
模型,NIO
可以实现更高的并发、更低的延迟以及更少的资源消耗。
I/O 包和 NIO
已经很好地集成了,java.io
也已经以 NIO
为基础重新实现了,所以现在它可以利用 NIO
的一些特性。例如,java.io
包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
Java NIO 概要介绍:初识 Java NIO
使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。
Selector
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector
通过轮询的方式去监听多个通道 Channel
上的事件,从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel
为非阻塞,那么当 Channel
上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel
,找到 IO 事件已经到达的 Channel
执行。
由于创建和切换线程的开销很大,所以使用一个线程来处理多个事件具有更好的性能。
Selector
是 Java NIO(New I/O)库中的一个重要组件,它用于实现非阻塞 I/O 操作。它可以用于管理多个通道(如网络套接字或文件通道)的事件,从而使单个线程能够有效地处理多个通道的 I/O 操作。
使用 Selector
,可以注册一个或多个通道(通道必须为非阻塞模式!),并指定感兴趣的事件类型,例如连接操作、读操作或写操作。然后,Selector
会监视这些通道上发生的事件,并且只有当感兴趣的事件发生时,才会通知我们。这样就可以在单个线程中同时处理多个通道的 I/O 事件,而无需为每个通道分配一个独立的线程。
常用方法
-
open()
:打开一个选择器。 -
select()
:选择一组 I/O 操作已经准备就绪的通道。该方法是阻塞的,直到至少有一个通道就绪,或者调用线程被中断。 -
select(long timeout)
:选择一组 I/O 操作已经准备就绪的通道,但最多等待指定的超时时间(以毫秒为单位)。该方法是阻塞的,直到至少有一个通道就绪、超时时间到达或调用线程被中断。 -
selectNow()
:选择一组 I/O 操作已经准备就绪的通道,但不会阻塞。如果没有任何通道就绪,该方法会立即返回0。 -
selectedKeys()
:获取当前已经选择(就绪)进行 I/O 操作的通道的SelectionKey
集合。 -
wakeup()
:唤醒阻塞在select()
或select(long timeout)
方法上的线程。 -
keys()
:返回当前注册在监听器上的所有选键集合,即返回一个包含所有已经注册过的通道的SelectionKey
集合。包括已经取消注册但还未从选键集合中移除的对象,因此需要进行有效性判断,例如使用isValid()
先判断选键是否有效。 -
close()
:关闭选择器。
SelectionKey
SelectionKey
是 Selector
的注册对象,用于表示注册在 Selector
上的通道和感兴趣的事件。它是 NIO 中 Selector API 的核心之一。
在使用 Selector
进行事件驱动的网络编程时,每个注册到 Selector 上的通道都会关联一个 SelectionKey
对象。SelectionKey
维护了通道的状态以及感兴趣的事件。
使用 SelectionKey
可以实现基于事件驱动的处理模式,实现高效的并发网络编程。
事件类别
-
SelectionKey.OP_CONNECT
:表示连接已经建立,适用于客户端的 SocketChannel。 -
SelectionKey.OP_ACCEPT
:表示通道已经准备好接受新的连接请求,适用于服务端的 ServerSocketChannel。 -
SelectionKey.OP_READ
:表示通道已经准备好进行读操作,即可以从通道中读取数据。 -
SelectionKey.OP_WRITE
:表示通道已经准备好进行写操作,即可以向通道中写入数据。
它们在 SelectionKey
的定义如下:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
每个事件可以被当成一个位域,从而组成事件集整数。例如:
int interestSet = SelectionKey.OP_READ |
SelectionKey.OP_WRITE;
方法
SelectionKey
包含以下重要的属性和方法:
-
channel()
:返回与此选择键关联的通道。 -
selector()
:返回创建此 SelelctionKey 所属的选择器。 -
interestOps()
:返回选择键当前感兴趣的操作集合,即注册时指定的操作集合。 -
readyOps()
:返回通道当前已经准备就绪的操作集合。可以与interestOps()
方法的结果进行位运算判断具体的就绪事件类型。 -
isAcceptable()
:判断通道是否已经准备好接受新的连接。 -
isConnectable()
:判断通道是否已经准备好完成连接。 -
isReadable()
:判断通道是否已经准备好进行读取操作。 -
isWritable()
:判断通道是否已经准备好进行写入操作。 -
attach(Object obj)
和attachment()
:用于在选择键上附加一个对象,以便在后续处理中获取或更新相关信息。 -
cancel()
:取消该 SelectionKey 的注册,通道不再与 Selector 相关联。 -
remove()
:移除指定的SelectionKey
对象,以便下一次调用select()
方法时不会再次触发该事件。
使用 SelectionKey
对象时,应注意它的生命周期和正确的使用方式,以避免出现资源泄漏或其他问题。可以通过选择键集合(在选择器上调用 selectedKeys()
方法)来获取就绪的选择键,并在处理完后进行适当的移除或取消注册操作。
使用流程
-
通过
ServerSocketChannel
或SocketChannel
的register(Selector sel, int ops)
方法将通道注册到Selector
上,返回一个SelectionKey
对象。 -
可以通过
SelectionKey
对象获取通道、选择器、事件集合、选择键集合等信息。 -
通过
Selector
的selectedKeys()
方法可以获取当前已经就绪的SelectionKey
集合,可以遍历集合处理就绪事件。
注意事项
-
一个通道只能注册到一个
Selector
上,且注册后会返回一个唯一的SelectionKey
。 -
SelectionKey
的事件集合可以使用interestOps(int ops)
方法进行更新,但更新后并不会立即生效,需要再次调用Selector
的select()
方法。 -
使用附件对象可以将自定义的数据与
SelectionKey
相关联,以便在事件处理时获取和使用。 -
取消
SelectionKey
后,通道仍然保持打开状态,需要手动关闭。
使用流程
使用 Java NIO 中的选择器(Selector)时,通常可以遵循以下流程:
- 创建一个选择器:使用
Selector.open()
方法打开一个选择器对象。
Selector selector = Selector.open();
- 向选择器注册通道:通过调用通道的
register(Selector selector, int interestOps)
方法将通道注册到选择器上,指定对于该通道感兴趣的 I/O 事件类型(如读、写、连接等)。
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress("localhost",8888));
// 将通道设置为非阻塞模式
channel.configureBlocking(false);
channel.register(selector,Selelction.OP_ACCEPT);
一个选择器可以同时注册多个通道。
- 不断循环选择就绪的通道:在循环中使用选择器的
select()
方法或select(long timeout)
方法等待通道就绪,并返回已经准备就绪的通道数量。可以根据返回值判断是否有通道就绪。
while(true){
selector.select();
// 对事件进行操作
// ...
}
- 处理就绪的通道:通过调用选择器的
selectedKeys()
方法,获取已经准备就绪的通道的选择键集合。遍历选择键集合,可以使用SelectionKey
对象来获取具体的就绪通道,以及就绪的I/O事件类型。
Set<SelelctionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while(iter.hasNext()){
Selection key = iter.next();
// 处理事件
// ...
}
- 根据事件类型进行相应的业务处理:根据通道的可操作事件类型(读、写、连接等),使用相应的方法处理相应的业务逻辑,并可能对通道进行读写操作。
// 如果是连接事件,accept 并注册关注 OP_READ 事件
if(key.isAcceptable()){
ServerSocketChannel server = (ServerSocketChannel)key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector,SelectionKey.OP_READ);
}
// 如果是读事件,读取数据并响应
else if(key.isReadable()){
ServerSocket client = (ServerSocket)key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
buffer.flip();
String request = new String(buffer.array(),
0,
buffer.limit(),
StandardCharsets.UTF_8)
.trim();
System.out.println("Client request: " + request);
String response = "Response from server: ";
ByteBuffer outBuffer = ByteBuffer.wrap(response.getBytes());
client.write(outBuffer);
}
//手动从集合中移除当前事件,避免重复处理
iter.remove();
- 取消通道的注册:在处理完就绪的通道后,如果不再关注该通道的 I/O 事件,可以调用选择键的
cancel()
方法取消通道的注册。
key.cancel();
-
处理其他操作(可选):根据具体需求,可能要处理一些其他操作,如再次注册通道、关闭选择器等。
-
关闭选择器:当不再需要使用选择器时,应调用选择器的
close()
方法关闭选择器
selector.close();