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

![[BIO vs NIO.png]]
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 事件,而无需为每个通道分配一个独立的线程。

常用方法

  1. open():打开一个选择器。

  2. select():选择一组 I/O 操作已经准备就绪的通道。该方法是阻塞的,直到至少有一个通道就绪,或者调用线程被中断。

  3. select(long timeout):选择一组 I/O 操作已经准备就绪的通道,但最多等待指定的超时时间(以毫秒为单位)。该方法是阻塞的,直到至少有一个通道就绪、超时时间到达或调用线程被中断。

  4. selectNow():选择一组 I/O 操作已经准备就绪的通道,但不会阻塞。如果没有任何通道就绪,该方法会立即返回0。

  5. selectedKeys():获取当前已经选择(就绪)进行 I/O 操作的通道的 SelectionKey 集合。

  6. wakeup():唤醒阻塞在 select()select(long timeout) 方法上的线程。

  7. keys():返回当前注册在监听器上的所有选键集合,即返回一个包含所有已经注册过的通道的 SelectionKey 集合。包括已经取消注册但还未从选键集合中移除的对象,因此需要进行有效性判断,例如使用 isValid() 先判断选键是否有效。

  8. close():关闭选择器。

SelectionKey

SelectionKeySelector 的注册对象,用于表示注册在 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 包含以下重要的属性和方法:

  1. channel():返回与此选择键关联的通道。

  2. selector():返回创建此 SelelctionKey 所属的选择器。

  3. interestOps():返回选择键当前感兴趣的操作集合,即注册时指定的操作集合。

  4. readyOps():返回通道当前已经准备就绪的操作集合。可以与interestOps() 方法的结果进行位运算判断具体的就绪事件类型。

  5. isAcceptable():判断通道是否已经准备好接受新的连接。

  6. isConnectable():判断通道是否已经准备好完成连接。

  7. isReadable():判断通道是否已经准备好进行读取操作。

  8. isWritable():判断通道是否已经准备好进行写入操作。

  9. attach(Object obj)attachment():用于在选择键上附加一个对象,以便在后续处理中获取或更新相关信息。

  10. cancel():取消该 SelectionKey 的注册,通道不再与 Selector 相关联。

  11. remove():移除指定的 SelectionKey 对象,以便下一次调用 select() 方法时不会再次触发该事件。

使用 SelectionKey 对象时,应注意它的生命周期和正确的使用方式,以避免出现资源泄漏或其他问题。可以通过选择键集合(在选择器上调用 selectedKeys() 方法)来获取就绪的选择键,并在处理完后进行适当的移除或取消注册操作。

使用流程
  1. 通过 ServerSocketChannelSocketChannelregister(Selector sel, int ops) 方法将通道注册到 Selector 上,返回一个 SelectionKey 对象。

  2. 可以通过 SelectionKey 对象获取通道、选择器、事件集合、选择键集合等信息。

  3. 通过 SelectorselectedKeys() 方法可以获取当前已经就绪的 SelectionKey 集合,可以遍历集合处理就绪事件。

注意事项
  • 一个通道只能注册到一个 Selector 上,且注册后会返回一个唯一的 SelectionKey

  • SelectionKey 的事件集合可以使用 interestOps(int ops) 方法进行更新,但更新后并不会立即生效,需要再次调用 Selectorselect() 方法。

  • 使用附件对象可以将自定义的数据与 SelectionKey 相关联,以便在事件处理时获取和使用。

  • 取消 SelectionKey 后,通道仍然保持打开状态,需要手动关闭。

使用流程

使用 Java NIO 中的选择器(Selector)时,通常可以遵循以下流程:

  1. 创建一个选择器:使用 Selector.open() 方法打开一个选择器对象。
Selector selector = Selector.open();
  1. 向选择器注册通道:通过调用通道的 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);

一个选择器可以同时注册多个通道。

  1. 不断循环选择就绪的通道:在循环中使用选择器的 select() 方法或 select(long timeout) 方法等待通道就绪,并返回已经准备就绪的通道数量。可以根据返回值判断是否有通道就绪。
while(true){
	selector.select();
	// 对事件进行操作
	// ...
}
  1. 处理就绪的通道:通过调用选择器的 selectedKeys() 方法,获取已经准备就绪的通道的选择键集合。遍历选择键集合,可以使用 SelectionKey 对象来获取具体的就绪通道,以及就绪的I/O事件类型。
Set<SelelctionKey> keys =  selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while(iter.hasNext()){
	Selection key = iter.next();
	// 处理事件
	// ...
}
  1. 根据事件类型进行相应的业务处理:根据通道的可操作事件类型(读、写、连接等),使用相应的方法处理相应的业务逻辑,并可能对通道进行读写操作。
// 如果是连接事件,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();
  1. 取消通道的注册:在处理完就绪的通道后,如果不再关注该通道的 I/O 事件,可以调用选择键的 cancel() 方法取消通道的注册。
key.cancel();
  1. 处理其他操作(可选):根据具体需求,可能要处理一些其他操作,如再次注册通道、关闭选择器等。

  2. 关闭选择器:当不再需要使用选择器时,应调用选择器的 close() 方法关闭选择器

selector.close();
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值