Android NIO 系列教程(四) -- Selector

系列文章:
Android NIO 系列教程(一) NIO概述
Android NIO 系列教程(二) – Channel
Android NIO 系列教程(三) – Buffer
Android NIO 系列教程(四) – Selector
Android NIO 系列教程(五) – FileChannel
Android NIO 系列教程(六) – SocketChannel
Android NIO 系列教程(七) – ServerSocketChannel
Android NIO 系列教程(八) --NIO简易聊天室

前面几篇文章,我们已经认识了 selector ,它是一个可以检测 一个 或 多个 channel ,并且能够知道该 channel 的读写状态的组件,通过这种方式,一个线程可以管理多个channel,从而管理这个网络连接。

一、为什么使用 selector ?

一个好处是你可以使用一个线程去管理多个channel。对于操作系统来说,线程的切换开销很大,且占用资源(内存),因此,使用的线程越少越好。
但是,实际上,现在的 CPU 和 系统 性能越来越好,多线程的开销,随着时间的推移,也变得越来越少;实际上,多喝 cpu 不使用多线程去开发,是很浪费资源的。但是我们不深入讨论这个问题,只需要知道 selector 是使用单个线程去处理多个 channel 的即可。
如下 一个 selector 监听 三个 channel 的案例:
在这里插入图片描述

二、创建 selector 的实例

如何创建 selector 的实例呢,我们需要使用 open 方法:

Selector selector = Selector.open();

三、向 selector 注册 channel

接着我们需要注册 channel,但这里的 channel 必须是非阻塞的,所以,我们将不能注册 FileChannel,因为它为 非阻塞 IO, Socket 的channel是可以使用的,事实上,NIO 也是偏向网络的IO.如:

channel.configureBlocking(false);

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

注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:

  • Connect
  • Accept
  • Read
  • Write

channel 的触发事件也可以叫做准备事件,所以,当一个 channel 跟服务器连接成功叫做 Connected状态,server socket 的channel 等待接收事件到来叫做 Accept状态;一个可读的 channel 则被称为 Read状态;同理一个可写的 chanenl 被称为Write状态。

这四种事件,可以用 SelectionKey 里的常量表示:

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

如果对多个事件感兴趣,可以使用以下方式:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;   

四、SelectionKey

在上面,当我们通过 register() 方法向 selector 注册 channel 时,会返回一个 SelectionKey 的对象,该 SelectionKey 可以获得以下数据:

  • Interest 集合
  • Ready 集合
  • Channel 通道
  • Selector
  • 附加对象

4.1 Interest 集合

Interest 集合如上面所述,是你感兴趣的集合,可以通过SelectionKey读写interest集合,像这样:

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 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。

4.2 Ready 集合

ready 集合是 channel 准备就绪的集合,在选择(Selection)之后,会优先选择 ready 集合,selection 后面讲,这里我们就能拿到 ready 集合了:

int readySet = selectionKey.readyOps();

可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。当然,也可以使用以下四个方法,它们都会返回一个布尔类型:

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

4.3 Channel + Selector

可以通过 SelctionKey 访问到 channel 和 selector 的实例,如下:

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

4.4 附加对象

你可以将对象附加到 SelectionKey 上,这是给channel 或附加更多信息的一个比较简便的方法。比如,你可以把channel 正在使用的 buffer ,或其他信息附加到 selectionkey 上,如:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

你也可以通过 register 方法,在 channel 注册给 selector 的时候,把对象附加进去:

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

五、通过 selector 选择 channel

当你想 selector 注册一个或多个 channel 时,你可以使用 select() 方法,来选择你感兴趣的事件 (如 connect, accept, read or write),已经准备就绪的事件,换句话,如果你对 channel 中的 read 事件感兴趣,selector 就会返回已经就绪的那些通道。
selector 有以下方法:

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

select() 会一直阻塞,直到有注册且准备就绪的channel到来
select(long timeout) 和select()一样,除了最长会阻塞timeout毫秒
selectNow() 不会阻塞,无论什么通道就绪立即返回。

select()方法返回的int值告诉我们有多少通道已经准备好了。也就是说,自上次调用select()以来,已经准备好了多少通道。如果你调用select(),它返回1,因为一个通道已经就绪,再次调用select(),并且一个通道已经就绪,它将再次返回1。如果没有对第一个就绪的通道执行任何操作,那么现在就有了两个就绪通道,但是在每个select()调用之间只有一个通道已经就绪。

六、selectedKeys()

一旦你调用了 select() 方法,且有返回一个或多个就绪的 channel,就可以使用 selector 的 selectionKey 集合了。像这样:

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

当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。

可以遍历这个已选择的键集合来访问就绪的通道。如下:

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
    }

 // 记得要 remove 移除实例,不然下次事件过来就接收不到了
    keyIterator.remove();
}

这个循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件。

注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。

SelectionKey.channel()方法返回的通道需要转型成你要处理的类型,如ServerSocketChannel或SocketChannel等。

七、wakeUp()

当我们调用 select 方法,线程会一直阻塞;但几遍没有就绪的事件,我们也可以唤醒,只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)

八、close()

当使用完 selector ,可以调用它的 close 方法,该方法会使 selector 关闭,并且让 SelectionKey 注册的事件失效。但 channel 本身不会关闭

完整实例

下面是一个实例,通过 open 创建 selector,通过register 向 selector 注册 channel,并监听自己感兴趣的事件(accept, connect, read, write):

//获取 selector 实例
Selector selector = Selector.open();
//设置非阻塞状态
channel.configureBlocking(false);
//向 selector 注册 channel 事件
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {
//监听感兴趣的事件,当有就绪事件时立即返回
  int readyChannels = selector.selectNow();
//防止 cpu 空转100%的问题
  if(readyChannels == 0) continue;

//拿到 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
    }
// 移除 selector 实例,方便下次接入
    keyIterator.remove();
  }
}

下一章,我们继续学习 Channel 的一些实例

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值