Java NIO Selector

98 阅读13分钟

Java NIO Selector是一个可以检查一个或多个Java NIO Channel实例的组件,并且决定了哪个channel已经准备好读写。这个方法可以用一个单线程管理多个channel,和多个网络连接。

Why User a Selector

只使用一个单线程去处理多个channel的优势是使用更少的线程去处理channel。实际上,你可以使用一个线程去处理你所有的channel。对于一个操作系统来说进行线程之间的切换是非常昂贵的,并且在操作系统里的每个线程也都携带了一些资源(内存),因此,使用的线程越少越好。

用心记住,现代化的操作系统和CPU变得越来越好在多任务层面,所以多线程的负载比以前要小。实际上,如果一个CPU有多个核心,你如果没有使用多任务也许会浪费CPU的能力。无论如何,设计上的讨论属于其他的内容。这里说的已经足够了,你可以使用单线程处理多个channel,使用一个Selector。

这里是单线程使用一个Selector去处理3个channel的例子:

Java NIO: 一个线程使用一个Selector去处理3个channel

Createing a Selector

可以调用Selector.open()方法来创建一个Selector,像这样:

Selector selector = Selector.open()

Registering Channel with the Selector

为了使用带有Channel的Selector你必须注册Channel到Selector。使用SelectableChannel.register()方法来完成这个,就像下面这个示例代码一样:

channel.configureBlocking(false);

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

Channel必须是非阻塞的模式在搭配Selector的时候。这就意味着你不能在Selector里使用FileChannel因为FileChannel不能切换回非阻塞的模式。Socket Channel能很好的使用。

注意register()方法的第二个参数。这个是一个"inter set"意思是你在Channel里监听的什么事件感兴趣,通过Selector。这里有四种你可以监听到的不同的事件:

1、Connect(连接)

2、Accept(接受)

3、Read(读)

4、Write(写)

一个channel“fires an event”也就是说已经"read"为事件。所以,一个channel成功连接到另一个服务器是"connect ready"。一个server socket channel 接受入栈的连接是"accept ready"。一个channel已经准备好读是"read read"。一个channel准备好写是"write ready"。

下面是由四个SelectionKey常量包含的四种事件

1、SelectionKey.OP_CONNECT

2、SelectionKey.OP_ACCEPT

3、SelectionKey.OP_READY

4、SelectionKey.OP_WRITE

如果你对更多事件感兴趣,或者对常量里的几个事件都感兴趣,像这样:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;   

这节的后面会进一步说明这个集合。

SelectionKey

跟你在前面章节看到的一样,当你注册一个Channel到Selector的时候,register()方法返回一个SelectionKey对象。这个SelectionKey对象包含了一些感兴趣的属性。

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

将在下面的内容描述这些属性。

Interset Set

这个感兴趣的集合是在"selecting"里感兴趣的事件的集合,如同在"Registering Channel with the Selector "描述的一样。你可以使用SelectionKey读写相关的集合像下面这样:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = SelectionKey.OP_ACCEPT  == (interests & SelectionKey.OP_ACCEPT);
boolean isInterestedInConnect = SelectionKey.OP_CONNECT == (interests & SelectionKey.OP_CONNECT);
boolean isInterestedInRead    = SelectionKey.OP_READ    == (interests & SelectionKey.OP_READ);
boolean isInterestedInWrite   = SelectionKey.OP_WRITE   == (interests & SelectionKey.OP_WRITE);

如同你看到的,你可以用and操作符与给定的SelectionKey常量在interset集合里找出确切的事件。

Ready Set

ready set是已经准备好的channel的集合。在Selection后你可以访问准备好的集合。Selection在后面的内容里进行解释。你可以访问准备好的集合像下面这样:

int readySet = selectionKey.readyOps();

你可以用相同的方法测试interest集合里,channel对于哪些事件/操作已经准备好。但是,你也可以用下面这四个方法替代,所有的都返回一个boolean变量:

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

Channel + Selector

从SelectionKey里访问channel + selector是很琐碎的事。这里展示了这个是怎么做的:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();

Attaching Objects

你可以附加一个对象到SelectionKey,这是一种简单的方法去识别给定的channel,或者是附加进一步的信息到channel。例如,你可以附加你使用channel的Buffer,或者一个包含更多聚合数的对象。这里展示了附加对象:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

你也可以在注册Channel到Selector的时候附加一个对象,在register()方法。这里是具体的用法:

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

Selecting Channel via a Selector

一旦你注册一个或多个channel到Selector你可以调用select()方法。这些方法返回了你那些感兴趣的并且已经"ready"的Channel(连接,接受,读或者写)。在另一方面,如果你对在channel理已经准备好读的感兴趣,你将收到那个channel,从select()方法收到这个已经准备好读的channel。

这里是select()方法:

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

select()阻塞直到至少有一个channel对于你感兴趣的事件准备好。

select(long timeout) 做的事情跟select()一样除了它的阻塞有一个timeout的最大值(由那个参数决定)。

selectNow()从来不阻塞。它立即返回哪些channel已经准备好。

select()的int返回值告诉有多少channel已经准备好。也就是从上次你调用select()方法之后有多少channel变成就绪状态。如果你调用select()并且它返回1是因为有一个channel变成了就绪态,并且你多次调用select()而且有多个channel它会再次返回1.如果你在第一个channel就绪的状态下没有做任何事,你将有两个就绪的channels,但是只有一个channel变成就绪态在每次调用select()之间。

selectedKeys()

一旦你调用select()其中一个方法并且它的返回值已经指明一个或者多个channel已经就绪,你可以使用"selected key set"来访问就绪的channel,通过调用selectors的selectedKeys()方法。这里是如何调用:

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

当你使用Channel.register()方法将channel注册到Selector它返回一个SelectionKey对象。这个key描述了selector的channel注册登记。它是你可以使用selectedKeySet()方法从SelectionKey里访问的key。

你可以枚举这个selected Key集合来访问就绪的channels。这里是怎么使用:

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

这个循环枚举了selected key set里的key。对于每个key它测试key来决定这个key引用的channel已经就绪为了某些事件。

注意keyIterator.remove()在每次枚举的末尾调用。Selector不会从它自己的selected key集合里移除SelectionKey实例。你必须这么做,当你已经处理完channel。下次channel变成“ready”Selector将会再次将它添加到selected key集合。

SelectionKey.channel返回的channel将会被转成你需要的channel,例如ServerSocketChannel或者SocketChannel等等。

wakeUp()

线程调用select()方法会阻塞,可以离开select()方法,即使还没有channel就绪。这个通过不同的线程在第一个线程调用select()的Selector上调用Selector.wakeup()。在select()里等待的线程将会直接返回。

如果不同的线程调用wakeup()并且没有线程正阻塞在select(),下一个调用select()的线程将会立刻"wake up"。

close()

当你完成了Selector之后调用它的close()方法。这个关闭Selector并且将注册到这个Seletor的所有SelectionKey实例失效。channel本身没有关闭。

Full Selector Example

这里是一个完整的例子,打开Selector,注册一个channel到上面(不包含channel的实例化),并且持续监控四种事件准备就绪的Selector(accept,connect,read,write)。

Java NIO Selector是一个可以检查一个或多个Java NIO Channel实例的组件,并且决定了哪个channel已经准备好读写。这个方法可以用一个单线程管理多个channel,和多个网络连接。

Why User a Selector

只使用一个单线程去处理多个channel的优势是使用更少的线程去处理channel。实际上,你可以使用一个线程去处理你所有的channel。对于一个操作系统来说进行线程之间的切换是非常昂贵的,并且在操作系统里的每个线程也都携带了一些资源(内存),因此,使用的线程越少越好。

用心记住,现代化的操作系统和CPU变得越来越好在多任务层面,所以多线程的负载比以前要小。实际上,如果一个CPU有多个核心,你如果没有使用多任务也许会浪费CPU的能力。无论如何,设计上的讨论属于其他的内容。这里说的已经足够了,你可以使用单线程处理多个channel,使用一个Selector。

这里是单线程使用一个Selector去处理3个channel的例子:

Java NIO: 一个线程使用一个Selector去处理3个channel

Createing a Selector

可以调用Selector.open()方法来创建一个Selector,像这样:

Selector selector = Selector.open()

Registering Channel with the Selector

为了使用带有Channel的Selector你必须注册Channel到Selector。使用SelectableChannel.register()方法来完成这个,就像下面这个示例代码一样:

channel.configureBlocking(false);

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

Channel必须是非阻塞的模式在搭配Selector的时候。这就意味着你不能在Selector里使用FileChannel因为FileChannel不能切换回非阻塞的模式。Socket Channel能很好的使用。

注意register()方法的第二个参数。这个是一个"inter set"意思是你在Channel里监听的什么事件感兴趣,通过Selector。这里有四种你可以监听到的不同的事件:

1、Connect(连接)

2、Accept(接受)

3、Read(读)

4、Write(写)

一个channel“fires an event”也就是说已经"read"为事件。所以,一个channel成功连接到另一个服务器是"connect ready"。一个server socket channel 接受入栈的连接是"accept ready"。一个channel已经准备好读是"read read"。一个channel准备好写是"write ready"。

下面是由四个SelectionKey常量包含的四种事件

1、SelectionKey.OP_CONNECT

2、SelectionKey.OP_ACCEPT

3、SelectionKey.OP_READY

4、SelectionKey.OP_WRITE

如果你对更多事件感兴趣,或者对常量里的几个事件都感兴趣,像这样:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;   

这节的后面会进一步说明这个集合。

SelectionKey

跟你在前面章节看到的一样,当你注册一个Channel到Selector的时候,register()方法返回一个SelectionKey对象。这个SelectionKey对象包含了一些感兴趣的属性。

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

将在下面的内容描述这些属性。

Interset Set

这个感兴趣的集合是在"selecting"里感兴趣的事件的集合,如同在"Registering Channel with the Selector "描述的一样。你可以使用SelectionKey读写相关的集合像下面这样:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = SelectionKey.OP_ACCEPT  == (interests & SelectionKey.OP_ACCEPT);
boolean isInterestedInConnect = SelectionKey.OP_CONNECT == (interests & SelectionKey.OP_CONNECT);
boolean isInterestedInRead    = SelectionKey.OP_READ    == (interests & SelectionKey.OP_READ);
boolean isInterestedInWrite   = SelectionKey.OP_WRITE   == (interests & SelectionKey.OP_WRITE);

如同你看到的,你可以用and操作符与给定的SelectionKey常量在interset集合里找出确切的事件。

Ready Set

ready set是已经准备好的channel的集合。在Selection后你可以访问准备好的集合。Selection在后面的内容里进行解释。你可以访问准备好的集合像下面这样:

int readySet = selectionKey.readyOps();

你可以用相同的方法测试interest集合里,channel对于哪些事件/操作已经准备好。但是,你也可以用下面这四个方法替代,所有的都返回一个boolean变量:

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

Channel + Selector

从SelectionKey里访问channel + selector是很琐碎的事。这里展示了这个是怎么做的:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();

Attaching Objects

你可以附加一个对象到SelectionKey,这是一种简单的方法去识别给定的channel,或者是附加进一步的信息到channel。例如,你可以附加你使用channel的Buffer,或者一个包含更多聚合数的对象。这里展示了附加对象:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

你也可以在注册Channel到Selector的时候附加一个对象,在register()方法。这里是具体的用法:

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

Selecting Channel via a Selector

一旦你注册一个或多个channel到Selector你可以调用select()方法。这些方法返回了你那些感兴趣的并且已经"ready"的Channel(连接,接受,读或者写)。在另一方面,如果你对在channel理已经准备好读的感兴趣,你将收到那个channel,从select()方法收到这个已经准备好读的channel。

这里是select()方法:

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

select()阻塞直到至少有一个channel对于你感兴趣的事件准备好。

select(long timeout) 做的事情跟select()一样除了它的阻塞有一个timeout的最大值(由那个参数决定)。

selectNow()从来不阻塞。它立即返回哪些channel已经准备好。

select()的int返回值告诉有多少channel已经准备好。也就是从上次你调用select()方法之后有多少channel变成就绪状态。如果你调用select()并且它返回1是因为有一个channel变成了就绪态,并且你多次调用select()而且有多个channel它会再次返回1.如果你在第一个channel就绪的状态下没有做任何事,你将有两个就绪的channels,但是只有一个channel变成就绪态在每次调用select()之间。

selectedKeys()

一旦你调用select()其中一个方法并且它的返回值已经指明一个或者多个channel已经就绪,你可以使用"selected key set"来访问就绪的channel,通过调用selectors的selectedKeys()方法。这里是如何调用:

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

当你使用Channel.register()方法将channel注册到Selector它返回一个SelectionKey对象。这个key描述了selector的channel注册登记。它是你可以使用selectedKeySet()方法从SelectionKey里访问的key。

你可以枚举这个selected Key集合来访问就绪的channels。这里是怎么使用:

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

这个循环枚举了selected key set里的key。对于每个key它测试key来决定这个key引用的channel已经就绪为了某些事件。

注意keyIterator.remove()在每次枚举的末尾调用。Selector不会从它自己的selected key集合里移除SelectionKey实例。你必须这么做,当你已经处理完channel。下次channel变成“ready”Selector将会再次将它添加到selected key集合。

SelectionKey.channel返回的channel将会被转成你需要的channel,例如ServerSocketChannel或者SocketChannel等等。

wakeUp()

线程调用select()方法会阻塞,可以离开select()方法,即使还没有channel就绪。这个通过不同的线程在第一个线程调用select()的Selector上调用Selector.wakeup()。在select()里等待的线程将会直接返回。

如果不同的线程调用wakeup()并且没有线程正阻塞在select(),下一个调用select()的线程将会立刻"wake up"。

close()

当你完成了Selector之后调用它的close()方法。这个关闭Selector并且将注册到这个Seletor的所有SelectionKey实例失效。channel本身没有关闭。

Full Selector Example

这里是一个完整的例子,打开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.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();
  }
}