Java NIO选择器

时间:2020-01-09 10:36:16  来源:igfitidea点击:

Java NIO"选择器"是一个组件,可以检查一个或者多个Java NIO Channel实例,并确定哪些通道可供使用,例如阅读或者写作。这样,单个线程可以管理多个通道,从而可以管理多个网络连接。

为什么要使用选择器?

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

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

创建一个选择器

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

Selector selector = Selector.open();

向选择器注册频道

为了将"通道"与"选择器"一起使用,我们必须在"选择器"中注册"通道"。这是使用SelectableChannel.register()方法完成的,如下所示:

channel.configureBlocking(false);

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

"通道"必须处于非阻塞模式才能与"选择器"一起使用。这意味着我们不能将FileChannel和Selector一起使用,因为FileChannel不能切换到非阻塞模式。套接字通道将正常工作。

注意" register()"方法的第二个参数。这是一个"兴趣集",表示我们希望通过"选择器"在"频道"中收听哪些事件。我们可以听四种不同的事件:

  • Connect
  • Accept
  • Read
  • Write

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

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

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

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

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

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

选择键

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

  • interest集
  • ready集
  • 通道
  • 选择器
  • 复制对象(可选)

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

interest集

兴趣集是我们对"选择"感兴趣的事件集,如"在选择器中注册通道"一节中所述。我们可以通过" 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"常量进行"与"运算,以找出某个事件是否在兴趣集中。

Ready集

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

int readySet = selectionKey.readyOps();

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

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

通道+选择器

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

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();

附加的对象

我们可以将对象添加到" SelectionKey",这是识别给定通道或者将更多信息添加到通道的便捷方法。例如,我们可以在通道中添加正在使用的"缓冲区",或者包含更多聚合数据的对象。这是添加对象的方式:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

register()方法中,用Selector注册Channel时,我们还可以已经添加了一个对象。看起来是这样的:

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

通过选择器选择通道

一旦使用"选择器"注册了一个或者多个通道,就可以调用"选择()"方法之一。这些方法返回我们感兴趣的事件(连接,接受,读取或者写入)的"就绪"通道。换句话说,如果我们对准备阅读的频道感兴趣,我们将通过select()方法接收准备阅读的频道。

这是select()方法:

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

select()会阻塞,直到至少有一个通道可以为我们注册的事件准备好为止。

select(long timeout)与select()一样,除了它最多阻塞timeout毫秒(参数)。

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

select()方法返回的int告诉我们准备了多少个通道。就是说,自从我们上次调用" select()"以来,已经准备好了多少个通道。如果我们调用select()并返回1,因为一个通道已准备就绪,而我们再次调用select()又有一个通道已准备就绪,则它将再次返回1. 如果我们没有对第一个就绪的通道进行任何操作,则我们现在有2个就绪的通道,但是在每次select()调用之间只有一个通道已就绪。

selectedKeys()

一旦调用了select()方法之一,并且其返回值表明一个或者多个通道已就绪,就可以通过调用选择器selectedKeys()通过" selected key set"访问就绪通道。方法。看起来是这样的:

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

当我们使用"选择器"注册频道时," 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()调用。 "选择器"不会从所选键集本身中删除"选择键"实例。处理完频道后,我们必须执行此操作。下次频道变为"就绪"状态时,"选择器"会将其再次添加到所选键集中。

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

wakeUp()

即使尚未准备好任何通道,也可以使调用了被阻塞的select()方法的线程退出select()方法。这是通过让另一个线程在"选择器"上调用" Selector.wakeup()"方法来完成的,第一个线程在其上调用了" select()"。然后在select()中等待的线程将立即返回。

如果一个不同的线程调用wakeup()并且在select()内部当前没有线程被阻塞,则下一个调用select()的线程将立即"唤醒"。

close()

选择器完成后,调用其close()方法。这将关闭"选择器",并使在该"选择器"中注册的所有"选择键"实例无效。通道本身未关闭。

全选择器示例

这是一个完整的示例,该示例打开一个"选择器",向其注册一个通道(忽略通道实例化),并继续监视"选择器"以了解四个事件(接受,连接,读取,写入)的"就绪"状态。

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();
  }
}