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
包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
使用 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();