我们先看看传统I/O模型工作模式:
每条socket链路都由一个单独的线程负责处理,这对于大容量、高并发的应用程序来说,使用上千万个线程来处理请求几乎是不可能实现的。
多路复用IO模型的工作模式:
在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,并负责处理对应I/O事件。这对于构建高并发、大容量的服务端应用程序来说是非常有意义。
从图中我们可以看出,多路复用IO模型中,使用了一个Selector对象来管理多个通道,这是实现单个线程可以高效地处理多个socket上I/O事件的关键所在。下面开始介绍Java NIO中Selector所实现的功能和原理。
Selector简介
简单来说,Selector就是SelectableChannel对象的多路复用器。通常调用Selector类的静态方法open来创建一个选择器对象,该方法使用系统默认SelectorProvider对象的openSelector方法来创建新的选择器。当然,还可以自定义实现SelectorProvider并重写openSelector方法来创建自定义选择器。
一个Channel对象注册到选择器之后,会返回一个SelectionKey对象,这个SelectionKey对象代表这个Channel和它注册的Selector间的关系。SelectionKey中维护着两个很重要的属性:interestOps、readyOps,并通过这两个属性管理通道上注册的事件。interestOps中保存了我们希望Selector监听Channel的哪些事件,在Selector每次做select操作时,若发现该Channel有我们所监听的事件发生时,就会将感兴趣的监听事件设置到readyOps中,这样我们可以根据事件的发生执行相应的I/O操作。
Selector的重要属性
每个选择器中管理着三个SelectionKey集合:
- keys:该集合中保存了所有注册到当前选择器上的通道的SelectionKey对象;
- selectedKeys:该集合中保存了上一次Selector选择期间,发生了就绪事件的通道的SelectionKey对象集合,它始终是keys的子集。
- cancelledKeys:该集合保存了已经被取消但其关联的通道还未被注销的SelectionKey对象集合,它始终是keys的子集。
初始化Selector对象时,这三个集合都为空。当我们调用Channel的register方法将通道注册到选择器时,一个SelectionKey对象会被加入到keys集合;调用通道的Close方法或直接调用选择器的cancel方法则会将一个SelectionKey对象添加到cancelledKeys集合,选择器下一次做选择操作时,将会清空cancelledKeys中保存的选择键,并从keys集合中删除;选择器做选择操作时,具有就绪事件的SelectionKey对象会被加入到selectedKeys集合中。
同时,每个Selector中还维护了publicKeys和publicSelectedKeys两个视图,供客户端使用。publicKeys是keys的视图,调用Selector的keys()方法返回的就是publicKeys,publicKeys不支持添加和删除操作;publicSelectedKeys是selectedKeys的视图,它是是个不可增长的集合,即不支持add操作,但支持remove操作,调用publicSelectedKeys集合的remove操作实际是从selectedKeys中删除一个SelectionKey对象。我们可以调用Selector的selectedKeys()方法访问publicSelectedKeys。
注意:selector.select()方法是阻塞的,而所关心的事件selectedkey.channel如果没有处理,那么下一次select的时候所关心的事件selectedkeys仍然会将此事件进行返回,可采取channel.cancle方法进行跳过。
Selector创建
通常我们使用Selector的静态工程方法open()来创建Selector对象:
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
open方法负责向SPI发出请求,获取一个SelectorProvider实例。SelectorProvider的静态工厂方法 provider()决定由哪个SelectorProvider对象来创建给定的Selector实例,通常是一个DefaultSelectorProvider实例。不同操作系统对应着不同的sun.nio.ch.DefaultSelectorProvider,Linux下DefaultSelectorProvider.create()会生成一个sun.nio.ch.EPollSelectorProvider类型的SelectorProvider,Windows环境下则生成sun.nio.ch.WindowsSelectorProvider类型的SelectorProvider。
当获取到SelectorProvider实例后,调用它的openSelector()即可创建一个特定的Selector对象。
Selection操作
Selector中提供了3种类型的selection操作:
select():该方法会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非当前线程发生中断或selector的wakeup方法被调用;
select(long timeout):该方法与select()类似,会一直阻塞直到至少一个channel中有感兴趣的事件发生,除非下面3种情况任意一种发生:1 设置的超时时间到达;2 当前线程发生中断;3 selector的wakeup方法被调用;
selectNow():该方法不会发生阻塞,无论是否有channel发生就绪事件,都会立即返回。
选择器中最重要的就是selection操作,当我们调用Selector的select方法时,selectedKeys集合会被更新,通过遍历selectedKeys,可以找到已经就绪的通道,从而处理各种I/O事件。select操作的大概过程如下:
- 检查cancelledKeys集合,如果它非空,从keys集合中移除所有存在于cancelledKeys集合中的SelectionKey对象,并将注销其通道,同时清空cancelledKeys;
- 向内核发起一个系统调用进行查询,以确定选择器上注册的每个通道所关心的事件是否就绪。如果没有通道已经准备好,线程可能会一直阻塞、阻塞指定时间,或立即返回,这主要依赖于特定select方法的调用;
- 系统调用返回,再次检查cancelledKeys集合;
- 系统调用返回后,对于那些没有就绪事件的通道将不会有任何的操作,对于那些已经有就绪事件的通道,将执行以下两种操作的一种:
- 如果通道的SelectionKey还未加入selectedKeys集合,将其添加到selectedKeys集合中,并修改ready集合,以便准确地标识该通道当前有哪些准备好的操作。先前记录在ready集合中的任何就绪信息都会被抛弃;
- 否则,通道的SelectionKey已经存在于selectedKeys集合,修改ready集合,以便准确地标识该通道当前有哪些准备好的操作。所有之前记录在ready集合中已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。由操作系统决定的ready集合是与之前的ready集合按位分离的,一旦键被放置于选择器的已选择的键的集合中,它的ready集合将是累积的。比特位只会被设置,不会被清理。
select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是selectedKeys集合中的通道总数。返回值不是已准备好的通道的总数,而是从上一个select( )调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是0。
Selector唤醒
Selector中提供了使线程从被阻塞的select( )方法中优雅地退出的能力:
public abstract Selector wakeup();
如果一个线程在调用select()或select(long)方法时被阻塞,调用wakeup()会使线程立即从阻塞中唤醒;如果调用wakeup()期间没有select操作,下次调用select相关操作会立即返回。在Select期间,多次调用wakeup()与调用一次效果是一样的。关于wakeup()的实现原理可参见文章Java NIO Selector的wakeup实现原理。
Selector关闭
当我们调用Selector的close()方法时,会首先执行wakeup操作,任何一个在选择操作中阻塞的线程都将被唤醒。同时会注销绑定在选择器上的所有通道,释放与此选择器相关联的任何其他资源。
如果选择已经处于关闭状态,再次调用close()方法不会由任何作用。若调用该选择器除close()和wakeup()之外的操作都会导致ClosedSelectorException异常。
SelectionKey
SelectionKey对象代表着一个Channel和它注册的Selector间的关系。其channel( )方法可返回与该键相关的SelectableChannel对象,而selector( )则返回相关的Selector对象。此外,SelectionKey中包含两个重要属性,两个以整数形式进行编码的比特掩码:
- interestOps:代表对注册Channel所感兴趣的事件集合。interest集合是使用注册通道时给定的值初始化的,可以通过调用键对象的interestOps( int ops)方法修改。同时,可以调用键对象的interestOps()方法获取当前interest集合。当相关的Selector上的select( )操作正在进行时改变键的interest集合,不会影响那个正在进行的选择操作。所有更改将会在select( )的下一个调用中体现出来;
- readyOps:代表interest集合中从上次调用select( )以来已经就绪的事件集合,它是interestOps的子集。注册通道时,初始化为0,只有在选择器选择操作期间可能被更新。可以调用键对象的readyOps()方法获取当前ready集合。需注意的是ready集合返回的就绪状态只是一个提示,不是保证。底层的通道在任何时候都会不断改变。其他线程可能在通道上执行操作并影响它的就绪状态。
SelectionKey中使用了四个常量来代表事件类型:
SelectionKey.OP_READ:通道已经准备好进行读取;
SelectionKey.OP_WRITE:通道已经准备好写入;
SelectionKey.OP_CONNECT:通道对应的socket已经准备好连接;
SelectionKey.OP_ACCEPT:通道对应的server socket已经准备好接受一个新连接。
注册通道时,如果我们不止对一种操作感兴趣,可以用“位或”操作符将多个常量连接起来。如下:
socketChannel.register(selector, SelectionKey.OP_CONNECT|SelectionKey.OP_READ|SelectionKey.OP_WRITE);
在一次selection之后,我们可以使用以下几个方法来检测channel中什么事件已经就绪:
selectionKey.isAcceptable():是否已准备好接受新连接;
selectionKey.isConnectable():是否已准备好连接;
selectionKey.isReadable():是否已准备好读取;
selectionKey.isWritable():是否已准备好写入。
我们也可以使用相关的比特掩码来检测就绪状态,与调用上面的方法是一致的。如:
if ((selectionkey.readyOps( ) & SelectionKey.OP_READ) != 0) {
readBuffer.clear( );
key.channel( ).read (readBuffer);
...
}
一个selectionKey被创建后将保持有效,调用selectionKey的cancel()方法或关闭其通道或关闭其选择器将导致其失效。我们可以调用isValid( )方法来检查selectionKey是否仍然有效。当我们调用selectionKey的cancel()方法后,它将被放在相关的选择器的cancelledKeys集合中。注册关系不会立即被取消,但是selectionKey会立即失效。当再次调用select( )方法时(或者一个正在进行的select()调用结束时),cancelledKeys中的被取消的键将被清理掉。
selectionKey除了维护Channel和Selector的注册关系外,还提供了保存“附件”的功能,并提供方法访问它。这是一种允许我们将任意对象与键关联的便捷方法。这个对象可以引用任何对象,例如业务对象、会话句柄、其他通道等等。当我们在遍历与选择器相关的键时,可以使用附加在selectionKey上的对象句柄来获取相关的上下文。attach( )方法将在selectionKey中保存所提供的对象的引用,attachment( )方法则用来获取与selectionKey关联的附件句柄。
关于SelectionKey还有最后一点需要注意,SelectionKey是线程安全的。修改interest集合的操作是通过Selector对象进行同步的,而选择器所使用的锁策略是依赖于具体实现的。因此如果Selector正在进行选择操作,则读取或写入interest集可能会阻塞不确定的时间。
示例代码
selector=Selector.open();
//此处可以拿到返回的selectionkey对象
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
//此时selector是阻塞的 知道有上面注册的感兴趣的事件(此处就是acception时间)发生了,才会往下执行
selector.select();
//如果此时只有一个serverSocketChannel 其实返回的就是上面注册完可以拿到的那个selectionkey对象
Set<SelectionKey> selectionKeys=selector.selectedKeys();
Iterator<SelectionKey> iterator=selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey=iterator.next();
if(selectionKey.isAcceptable()){
// a connection was accepted by a ServerSocketChannel.
// 这里其实就可以拿到acception事件发生后连接的客户端channel了
}else if(selectionKey.isReadable()){
// a channel is ready for reading
}else if(selectionKey.isWritable()){
// a channel is ready for writing
}
iterator.remove();
}