7. Java NIO Selector

Selector是Java NIO的一个组件,可以检查一个或者多个NIO channel,并且决定哪个channel已经准备好去读写了。通过这种方式,单个线程可以管理多个channel,当然也可以管理多个网络连接。

为什么使用selector ?

使用单个线程处理多个channel的优势是,你只需要很多的线程去处理大量channel。实际上,你可以只使用一个线程来处理所有的channel。对操作系统来说切换线程是需要大量开销的,而且每个线程都会占用一些资源(内存)。所以你使用的线程越少越好(译者注:达到目的的情况下,尽可能使用少的线程)。

不过要记住,当代操作系统和CPU的多任务处理性能已经越来越高了,所以多线程的开销已经变得越来越小了。实际上,如果CPU有多核,你不进行多任务处理则是对CPU性能的浪费。不管怎么样,关于这部分内容的讨论会在其他教程中给出。但是足够的是,你可以使用selector来达到使用单线程处理多个channel的目的。

下面的示意图是表示单个线程来处理三个channel:

7. Java NIO Selector_第1张图片
1个线程处理3个channel

创建Selector

通过调用Selector.open()来创建Selector:


Selector selector = Selector.open();

使用Selector注册Channel

为了通过Selector来使用Channel,你需要使用Selector来注册Channel。这可以通过调用SelectableChannel.register()方法来实现,如下所示:


channel.configureBlocking(false);

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

Channel要被Selector使用,必须要是非阻塞(non-blocking)模式。这意味着你不能使用Selector来管理FileChannel,因为它不能选择非阻塞模式。但是socket channel是没有问题的。

注意register()的第二个参数。这是一个“偏好设置”,意思是你向监听channel的什么事件,通过Selector.你可以选择不同的监听事件,共有四种不同的监听事件:

  • Connect
  • Accept
  • Read
  • Write

所以,一个channel已经成功连接到另一个server就是上面的connect事件。一个server socket正在等待一个连接接入的过程就是上面的accept事件。一个channel有数据准备好去被读取就是read事件。一个channel已经准备好让你往里面写入数据,这就是write事件。

这四个事件用下面四个常量来表示:


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

如果你想注册多个事件,可以像下面这样:


int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;   

SelectionKey's

如上面所述,当你用selector注册一个channel,register()方法会返回一个SelectionKey对象。这个对象包含一些有意思的属性:

  • interest set
  • ready set
  • Channel
  • Selector
  • 附加对象 (optional)

下面来具体介绍这些属性。

Interest Set

Interest Set表示的是你扫描的事件的集合。你可以通过SelectionKey来读写这个interest set像下面这样:


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常量,去找出某一事件是否在interest set中。

Ready Set

ready set是channel准备好的操作的集合。主要是在selection后访问ready set。至于selection,会在后面给出解释。你可以像下面这样访问ready set:


int readySet = selectionKey.readyOps();

你可以用interest set以同样的方式来测试channe已经做好什么事件的准备了。但是,你可以利用下面的四个方法来代替,它们都会返回一个布尔值:


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

Channel 和 Selector

从SelectionKey来访问channel和seector是无关紧要的,下面展示了如何进行此操作:


Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();   

附加对象

你可以将一个对象附加到SelectionKey中,或者给出更多的信息添加至channel,这可以很方便的途径去识别一个给定的channel。例如,你可以添加使用的buffer,或者一个包含更多数据的对象。下面是相关例子:


selectionKey.attach(theObject);

 Object attachedObj = selectionKey.attachment();

你也可以使用register()方法,在使用Selector注册channel时添加这个对象。如下所示:


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

通过Selector选择Channel

当你用Selector注册了一个或多个channel,你可以调用select()方法以及其重载的方法。这些方法会返回你监听的事件(connect, accept, read 和 write)。换句话说,你可以从这个方法接受到已经准备好的事件。下面是select()方法以及其相关重载:


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

select()方法会一直阻塞,直到至少一个你注册的channel已经准备好相应事件。

select(long timeout)和select()方法做了同样的事儿,不过它是阻塞一段时间(多长时间取决于你传入的事件参数)。

selectNow() 一点也不会阻塞。不管channel准备好没有,它立即回返回。

select() 方法返回的int值表示有多少channel已经准备好。即:在上一次调用select()方法之后一共有多少channel已经准备好。如果你调用select()方法,它返回 1,因为此时只有一个channel准备好,那么你再一次调用select(),然后期间又有一个channel准备好,它会再返回1,而不是2。如果你没有对第一个准备好的channel有任何操作,你现在其实已经有两个准备好的channel了,但是只有一个channel是你两次调用之间准备好的channel。

selectedKeys()

调用select()方法之后,它会返回一个int值,这表示一个或多个channel已经准备好了,你可以通过“selected key set”访问准备好的channel,即调用selectedKeys()方法:下面是如何操作的:


Set selectedKeys = selector.selectedKeys();   

当你注册一个channel时候,Channel.register()返回一个SelectionKey对象。这个key表示表示的是这个channel使用了哪个selector来注册的。可以通过selectedKeySet()方法来访问key。

你可以迭代它来访问准备好的channel:


Set selectedKeys = selector.selectedKeys();
Iterator 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();
}

这个循环迭代selected key集合中的key。她再测试每个key以便去确定通过key引用的channel是否已经准备好。

注意,在循环的最后调用了keyIterator.remove()方法。Selector本身并没有从selected key集合中删除SelectionKey实例。当你操作完channel后你需要这么做。下次channel准备好后,selector会再次将其添加至select key集合中。

方法SelectionKey.channel()返回的channel可以被强转成你需要使用的channel,例如ServerSocketChannel o或 SocketChannel

wakeUp()

调用select()方法的线程被阻塞后,你可以让这个线程离开select()方法,即使channel没有准备好呢。这需要另一个线程在selector上调用Selector.wakeup()方法。

如果一个不同的线程调用了wakeup()方法,并且当前已经没有线程在select()阻塞了,那么下一个调用select()方法的线程会立即唤醒(也就是不阻塞了)。

close()

当使用完Selector你可以调用她的close()方法。这个操作会关闭selector,并且使所有在此selector上注册的SelectKey实例失效。channel本身并没有关闭。

完整的Selector例子

下面是一个完成的例子,包括打开Selector,并使用其注册channel(channel实例化不考虑),然后保持selector监控四个事件(accept, connect, read, write)。


Selector selector = Selector.open();

channel.configureBlocking(false);

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

while(true) {
  int readyChannels = selector.select();

  if(readyChannels == 0) continue;

  Set selectedKeys = selector.selectedKeys();

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


想要查看此教程的目录请点击:Java NIO教程目录贴地址

你可能感兴趣的:(7. Java NIO Selector)