当前位置: 首页 > 教程 > Java NIO >

Java NIO (Selector)选择器

精华
小牛编辑
164浏览
2023-03-14

Java NIO的选择器(Selector)是一个组件,可以选择一个或多个通道(Channel) 实例,并确定准备好进行读取或写入的通道。这样,单个线程可以管理多个通道(Channel),从而可以管理多个网络连接。

1 为什么要使用选择器?

仅使用单个线程来处理多个通道的优点是您需要更少的线程来处理通道。实际上,您只能使用一个线程来处理所有通道。线程之间的切换对于操作系统来说是昂贵的,并且每个线程也占用操作系统中的一些资源(内存)。因此,使用的线程越少越好。

但是请记住,现代操作系统和CPU在多任务处理方面变得越来越好,因此多线程的开销随着时间的推移而变得越来越小。实际上,如果一个CPU有多个内核,则可能由于不执行多任务处理而浪费了CPU功率。无论如何,该设计讨论属于不同的文本。足以在这里说,您可以使用来通过一个线程处理多个通道Selector。

下图表示一个线程使用选择器处理3个通道:

Java NIO:线程使用选择器来处理3个通道

2 创建一个选择器

您可以Selector通过调用Selector.open()方法来创建一个,如下所示:

Selector selector = Selector.open();

3 向选择器注册频道

为了使用一个Channel与Selector你必须注册Channel用Selector。使用SelectableChannel.register()方法完成的 ,如下所示:

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

在Channel必须在非阻塞模式与使用Selector。这意味着您不能将FileChannel搭配使用,Selector因为 FileChannel无法将切换到非阻止模式。套接字通道将正常工作。

注意方法的第二个参数register()。这是一个“interest set”,这意味着Channel在监听的事件,通过Selector。您可以监听四种不同的事件:

  1. Connect
  2. Accept
  3. Read
  4. Write

也可以说“触发事件”的频道已为该事件“就绪”。因此,已成功连接到另一台服务器的通道是“连接就绪”。接受传入连接的服务器套接字通道已“接受”就绪。具有准备好读取数据的通道已“读取”就绪。准备好向其写入数据的通道已“写入”就绪。

这四个事件由四个SelectionKey常量表示:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

如果您对多个事件或常量或多个常量感兴趣,如下所示:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;    

在本文中,我将进一步探讨一下兴趣点。

4 选择键

如上一节中所见,当Channel向a注册时,Selector 该register()方法将返回一个SelectionKey对象。该SelectionKey 对象包含一些有趣的属性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

我将在下面描述这些属性。

4.1 InterestSet

兴趣集是您对“选择”感兴趣的事件集,如“在选择器中注册通道”一节中所述。您可以通过以下方式读取和写入该兴趣集SelectionKey:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;    

如您所见,您可以将兴趣集与给定的SelectionKey常数进行“与”运算,以确定某个事件是否在兴趣集中。

4.2 ReadySet

准备集是通道准备进行的一组操作。选择后,您将主要访问准备好的设置。选择将在后面的部分中说明。您可以按以下方式访问就绪集:

int readySet = selectionKey.readyOps();

您可以使用兴趣设定相同的方式测试频道准备就绪的事件/操作。但是,您也可以改用这四个方法,它们都产生一个布尔值:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

4.3 Channel+Selector

从中访问通道+选择器SelectionKey很简单。这是完成的过程:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();  

4.4 attachedObj

您可以将对象附加SelectionKey到此,这是识别给定通道或将更多信息附加到该通道的便捷方法。例如,您可以将Buffer您正在使用的频道或包含更多聚合数据的对象附加到频道中。这是附加对象的方式:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

您还可以在方法中Channel使用 进行注册时附加对象。看起来是这样的: Selectorregister()

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

5 通过选择器选择频道

向a注册一个或多个通道后,Selector您可以调用其中一种select()方法。这些方法返回您感兴趣的事件(连接,接受,读取或写入)的“就绪”通道。换句话说,如果您对准备阅读的通道感兴趣,您将从方法中接收准备阅读的通道select()。

下面是select()方法:

  • int select()
  • int select(long timeout)
  • int selectNow()

select() 阻止,直到至少有一个频道准备好您注册的事件。

select(long timeout)select()除了阻塞最多timeout毫秒(参数)外,其他功能与之相同。

selectNow()完全不会阻塞。无论准备好任何通道,它都会立即返回。

在int由返回的select()方法告诉很多渠道如何准备。也就是说,自您上次致电以来已准备好多少个频道select()。如果您呼叫select()并且由于一个信道已准备好而返回1,并且您再呼叫select()一次并且又有一个信道已准备好,它将再次返回1。如果您对第一个就绪的通道没有做任何事情,则现在有2个就绪的通道,但是在每次select()通话之间只有一个通道已就绪。

5.1 selectedKeys()方法

一旦调用了其中一种select()方法,并且其返回值指示一个或多个通道已就绪,则可以通过调用选择器selectedKeys()方法,通过“选定键集”访问就绪通道。看起来是这样的:

Set<SelectionKey> selectedKeys = selector.selectedKeys();    

当您使用注册一个通道时,Selector该Channel.register()方法将返回一个SelectionKey对象。该键表示使用该选择器注册频道。您可以通过selectedKeySet()方法访问这些键。从 SelectionKey。

您可以迭代此选定的键集以访问就绪频道。看起来是这样的:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

此循环迭代所选键集中的键。对于每个键,它都会测试该键,以确定该键引用的通道已准备就绪。

注意keyIterator.remove()每次迭代结束时的调用。在 Selector不删除SelectionKey从选定的键实例设置本身。处理完频道后,必须执行此操作。下次频道变为“就绪”状态时,Selector它将再次将其添加到所选键集中。

该SelectionKey.channel()方法返回的通道应强制转换为您需要使用的通道,例如ServerSocketChannel或SocketChannel等。

6 wakeup()方法

即使尚未准备好通道select(),也可以使 调用了被阻塞方法的线程退出该select()方法。这是由具有不同的线程调用完成Selector.wakeup() 的方法,Selector其中第一线呼吁 select()上。然后在内部等待的线程select()将立即返回。

如果调用了其他线程,wakeup()并且当前没有任何线程被阻塞select(),则下一个调用的线程select()将立即“唤醒”。

7 close()方法

完成后,Selector调用其close() 方法。这将关闭Selector并使所有SelectionKey 在this中注册的实例无效Selector。通道本身未关闭。

8 完整的选择器示例

这是一个完整的示例,其中打开一个Selector,向其注册一个通道(忽略通道实例化),并持续监视Selector 四个事件(接受,连接,读取,写入)的“就绪”状态。

Selector selector = Selector.open();

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


while(true) {

  int readyChannels = selector.selectNow();

  if(readyChannels == 0) continue;


  Set<SelectionKey> selectedKeys = selector.selectedKeys();

  Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

  while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
  }
}