【译】Java NIO Selector

Java NIO Selector是一个组件,它可以检测Channel对象,并且决定哪些channel是已经ready、readable、writable等状态。通过这个组件,一个线程可以管理多个channel,进而管理多个网络连接。

为什么使用Selector

只使用一个线程来处理多个Channel的优势是,需要更少的线程来处channel。事实上,你可以使用一个线程管理所有的channel。线程切换对于操作系统的消耗是十分巨大的,而且多个线程会占去系统更多的内存。因此线程的数量越少越好。

记住,现代操作系统里CPU处理多线程的能力变得越来越强,因此多线程的开销会随年代的推移越来越不那么重要。事实上,如果一个CPU是多核的,你不使用多线程会浪费CPU的性能。

您可以使用Selector在一个线程中处理多个Channel。如图所示:

创建一个Selector

调用Selector的静态方法open,就能创建一个Selector对象了。

Selector selector = Selector.open();

把Selector注册到Channel中

为了使用Selector管理channel,必须把selector注册到channel中。使用channel的register方法即可:

channel.configureBlocking(false);

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

channel必须是非阻塞的模式下才能使用selector,因此第一行就设置了非阻塞模式。(FileChannel就不能配合selector使用,因为它就不能设置成非阻塞模式,SocketChannel就没问题了)

注意第二个参数,它是用来表示“关注点”,它意味着你具体关心什么事件。下面有四种不同的事件你可以监听:

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

一个channel触发了某个事件,也可以称作某个事件已经就绪。因此一个channel成功的连接到一个服务应该触发“connect ready”(也就是Connect)事件。

一个SocketChannel收到了连接会触发“accept”事件。

一个channel有数据可以读取了会触发“read”事件。

一个channel已经准备好可以被写入了会触发“write”事件。

这四个事件有四种代码上的表达方式:

  1. Connect   ==  SelectionKey.OP_CONNECT
  2. Accept     ==   SelectionKey.OP_ACCEPT   
  3. Read        ==   SelectionKey.OP_READ
  4. Write        ==   SelectionKey.OP_WRITE

如果需要多个事件同时监听,可以使用“或”逻辑,像这样:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

SelectionKey

正如你看到的,像上一段代码中,把selector注册到channel中会返回一个SelectionKey的对象。这个对象包含一些重要属性:

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

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;   

可以使用 “与”逻辑符,来查看监听了哪些事件。

Ready Set

这部分是指channel已经就绪了哪些事件:

int readySet = selectionKey.readyOps();

可以使用Interest Set的检测方式来测试,也可以直接使用下面的四个方法来代替:

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上的简便方法。例如,可以将正在使用的buffer与channel或包含更多聚合数据的对象关联到一起。

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

也可以在向channel中注册selector时添加对象。

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

通过Selector选择Channel

一旦使用selector注册到一个或多个channel之后,就可以调用Selector的select方法了。会返回selector注册的事件已经触发了的channel。换言之,如果你注册了read事件,你就会收到可以读的channel。

几个类似的select方法:

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

select() 阻塞直到有指定事件触发为止

select(long timeout) 和上一个类似,多加了一个超时时间

selectNow() 绝对不会阻塞,它会立刻返回哪些channel是满足条件的(如果没有就是空,这个方法用来监控连接状态还不错)

select()方法返回的int值表示有多少通道已经就绪。也就是说,自从上次调用select()以来,已经准备好了多少通道。如果您调用select(),它将返回1,因为一个通道已经就绪,而您再次调用select(),又有一个通道已经就绪,那么它将再次返回1。如果对第一个就绪的通道不做任何操作,那么现在就有两个就绪通道,但是在每个select()调用之间只有一个通道就绪。

selectedKeys()

一旦你调用了select()方法中的一个,并且它的返回值表明一个或多个channel已经就绪,就可以通过“selected key set”来访问就绪channel,方法是调用selectors selectedKeys()方法。它是这样的:

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

当你使用selector注册一个channel时,channel.register()方法将返回一个SelectionKey对象。这个值就代表了注册的那个选择器。可以通过selectedKeySet()方法获取这些值。然后迭代这个Set:

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(); // notice this line
}

这个循环遍历集合中的key。对于每个key,都要判断以确定这个key所对应的channel是否已经准备好了。

请注意在每次迭代结束时调用keyIterator.remove()。选择器不会从所选的键集本身删除SelectionKey实例。下一次通道“就绪”时,选择器将再次将其添加到选定的键集。

应该将SelectionKey.channel()方法返回的通道类型转换为需要使用的通道类型。

wakeUp()

调用了被阻塞的select()方法的线程可以离开select()方法,即使还没有通道就绪。这是通过让另一个线程调用选择器上的select.wakeup()方法来实现的,第一个线程在这个选择器上调用了select()。在select()中等待的线程将立即返回。

close()

selector使用完毕后,调用它的close()方法关闭选择器,并把该选择器已注册的所有SelectionKey对象都置为失效状态。但是channel本身并没有关闭,不受selector的关闭影响。

完整Selector示例

下面的完整例子,包含了打开selector,注册到channel,持续监控状态:

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

 

下一篇:【译】Java NIO FileChannel

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值