Selector

作者: JiinYuu | 来源:发表于2018-12-09 14:47 被阅读0次

Selector是一个可以监控多个Channel的Java NIO组件,它负责决定哪些Channel可以进行读或写。这样一个线程就可以管理多个Channel,从而管理多个网络连接。

Why Use a Selector?

单个线程管理多个Channel的好处就是你只需要少量的线程就可以处理所有的Channel。事实上,你可以只用一个线程来管理所有的Channel。对操作系统来说,线程间的切换是比较耗资源的,而且线程本身也也会占用一些资源(内存)。因此,线程越少越好。

记住,现代操作系统和CPU在多任务处理方面已经表现得越来越好。因此,随着时间的推移,多线程的开销会越来越小。事实上,如果一个CPU有多个核,而你又不执行多线程任务,实际上是对CPU计算能力的一种浪费。不管怎样,怎样设计线程数已经超出了本文讨论的范畴。这里只需说明,你可以通过Selector使用一个线程处理多个Channel

以下是通过Selector使用一个线程处理三个Channel的图示:

通过`Selector`使用一个线程处理三个`Channel`

Creating a Selector

你可以像下面这样通过调用Selector.open()方法来创建一个Selector

Selector selector = Selector.open();

Registering Channels with the Selector

为了使用带有ChannelSelector,你必须将Channel注册进Selector。注册操作可以通过调用SelectableChannel.register()方法来实现,如下所示:

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

Channel必须处于非阻塞模式,才能与Selector一起使用。意味着你不能将FileChannelSelector一起使用,因为FileChannel不能切换到非阻塞模式。但SocketChannel可以和Selector一起很好的使用。

注意register()方法的第二个参数。它是一个"interest set",意思是那些你感兴趣的发生在Channel上的事件。一共有以下四种事件你可以监听:

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

Channel触发事件也意味着它为该事件做好了准备。因此,一个Channel成功连接到一个远程服务就是"connect"就绪。一个ServerSocketChannel接收到一个连入连接就是accpet就绪。一个Channel准备好数据等待被读就是"read"就绪。一个Channel准备好等待你的数据写入就是"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;

SelectionKey's

正如你在前一节看到的,当你调用register()方法将Channel注册进Selector的时候,会返回一个SelectionKey类型的对象。这个对象包含一些有趣的属性:

  • interest set
  • ready set
  • Channel
  • Selector
  • attached object(可选)

Interest Set

interest set就是前一节里你注册的感兴趣的事件集。你可以通过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;   

如你所见,你可以用&)将interest setSelectionKey里的某个事件常量连接起来,用以判断该事件是否被包含在interest set里。

Ready Set

ready set就是Channel准备就绪的操作集。一般来说,在一次select()之后,你通常都需要访问ready set。至于select(),我们后面再讲。你可以像这样访问ready set

int readySet = selectionKey.readOps();

你可以像前一节判断interest set里是否包含某个特定事件一样的方法来判断某个特定的事件/操作是否已经就绪。但是,你可以用以下四个返回boolean类型值的方法来代替:

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

Attaching Objects

你可以附加一个对象到SelectionKey上,以便识别给定Channel或将进一步信息附加到Channel。例如,你可以附加一个正在和Channel一起使用的Buffer,或者一个包含更多聚合信息的对象。下面是如何附加对象的示例:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

你也可以在register()方法中,在向Selector注册Channel时附加一个对象。就像下面这样:

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

Selecting Channels via a Selector

当你向Selector注册了一个或多个Channel之后,你就可以调用select()方法了。select()方法会放回那些已经对你感兴趣的事件集(connect、accept、read或write)准备就绪的Channel。换句话说,如果你对Channel是否准备好读取数据,通过select()方法你就会收到那些已经准备好读取数据的Channel

以下是所有的select()方法:

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

select()方法会阻塞至至少一个你刚兴趣的事件已经准备好。

select(long timeout)方法和select()一样,但是它最多阻塞timeout毫秒。

selectNow()不会阻塞,无论有没有Channel准备好,它都会立即返回。

select方法返回的int代表到底有多少Channel已经就绪。也就是说,有多少Channel从你上次调用select()方法后又变得准备就绪了。如果有一个Channel准备好了,那么select方法就回返回1,再次调用select方法,又有一个Channel准备好了,它将再次返回1。如果你对第一个就绪的Channel啥也没干,现在你就又两个就绪的Channel了,但在你两次调用select方法之间只有一个Channel就绪。

selectedKeys()

如果你调用了select()方法,而它的返回值也指明了至少一个Channel已经就绪,那么你就可以通过调用SelectorselectedKeySet()方法返回的"selected key set"来访问这些Channel了。就像下面这样:

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

当你向Selector注册Channel的时候,Channel.register()方法会返回一个SelectionKey对象。这个key就代表着Selector里面的Channel。你可以通过selectedKeys()方法来访问这些key。

你可以遍历"selected key set"来访问就绪的Channel。就像下面这样:

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // ServerSocketChannel接受到一个新的连接
    } else if (key.isConnectable()) {
        // 连接远程服务成功
    } else if (key.isReadable()) {
        // 读就绪
    } else if (key.isWritable()) {
        // 写就绪
    }
    keyIterator.remove();
}

这个循环遍历了"selected key set"里面的所有key。对于每一个key,我们都测试这个key对应的Channel到底做好了什么准备。

注意每次遍历最后一句keyIterator.remove()Selector不会自己把SelectionKey实例从"selected key set"里面移除的。当你处理完Channel后,必须手动移除。下一次Channel准备就绪的时候,Selector会再次把它加到"selected key set"里面。

SelectionKey.channel()方法返回的Channel需要强制转换成你想要的类型,比如:ServerSocketChannelSocketChannel等等。

wakeUp()

调用select()方法的线程会被阻塞,但它同样可以被唤醒,就算没有任何Channel就绪。通过在另一个线程调用同一个Selectorwakeup()方法,可以唤醒正在调用这个Selectorselect()方法的线程。正在被select()方法阻塞的线程会立即返回。

如果一个不同的线程调用了wakeup()方法,并且现在没有任何线程正在调用select()方法,那么下一个调用select()方法的线程会立即被唤醒。

close()

当使用完Selector后,请调用close()方法。这会关闭Selector并使所有注册到该SelectorSelectionKey实例无效。但Channel本身不会被关闭。

Full Selector Example

以下是关于打开一个Selector,注册Channel(不包括Channel的初始化),并监控其四个事件(accpet、connect、read、write)的就绪的完整示例:

Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_ACCEPT);
while (true) {
    int readyChannels = selector.select();
    if (readyChannels == 0) {
        continue;
    }
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // ServerSocketChannel接受到一个新的连接
        } else if (key.isConnectable()) {
            // 连接远程服务成功
        } else if (key.isReadable()) {
            // 读就绪
        } else if (key.isWritable()) {
            // 写就绪
        }
        keyIterator.remove();
    }
}

说明

发现貌似有人在看这个系列文章了,有必要说明下,这个Java NIO系列来源于jenkov.com,本文只是翻译,希望大家千万不要误会,本文不是原创。原文地址:Java NIO

相关文章

网友评论

      本文标题:Selector

      本文链接:https://www.haomeiwen.com/subject/gmethqtx.html