Java NIO之选择器Selector
1、前言
Selector一般称为选择器,也可以翻译为多路复用器,是Java NIO核心组件之一,主要功能是用于检查一个或者多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个Channel(通道),当然也可以管理多个网络连接。使用Selector的好处在于,可以使用更少的线程来处理更多的通道,相比使用更多的线程,避免了线程上下文切换带来的开销等。
Selector提供选择执行已经就绪的任务的能力,这使得多路I/O成为了可能,就绪执行和多元选择使得单线程能够有效地同时管理多个I/O通道。某种程度上来说,理解选择器比理解缓冲区和通道类更困难一些和复杂一些,因为涉及了三个主要的类,它们都会同时参与到这整个过程中,这里先将选择器的执行分解为几条细节:
1、创建一个或者多个可选择的通道(SelectableChannel) 2、将这些创建的通道注册到选择器对象中 3、选择键会记住开发者关心的通道,它们也会追踪对应的通道是否已经就绪 4、开发者调用一个选择器对象的select()方法,当方法从阻塞状态返回时,选择键会被更新 5、获取选择键的集合,找到当时已经就绪的通道,通过遍历这些键,开发者可以选择对已就绪的通道要做的操作
对于选择器的操作,大致就是这么几步,OK,接下去再进一步,看一下和选择器相关的三个类。
选择器管理一个被注册的通道集合的信息和它们的就绪状态,通道和选择器一起被注册,并且选择器可更新通道的就绪状态,也可将被唤醒的线程挂起,直到有通道就绪。
2、选择器、可选择通道和选择键类
2.1、选择器(Selector)
选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。
2.2、可选择通道(SelectableChannel)
SelectableChannel 可被注册到 Selector 对象上,同时可以指定对那个选择器而言,哪种操作是感兴趣的。
这个抽象类提供了实现通道的可选择性所需要的公共方法,它是所有支持就绪检查的通道类的父类,FileChannel对象不是可选择的,因为它们没有继承SelectableChannel,所有Socket通道都是可选择的,包括从管道(Pipe)对象中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以设定对哪个选择器而言哪种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通道在被注册到一个选择器上之前,必须先设置为非阻塞模式,通过调用通道的configureBlocking(false)方法即可。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字通道都可以。
要想Channel注册到Selector中,那么这个Channel必须是非阻塞的。所以FileChannel不适合Selector,因为FileChannel不能切换为非阻塞模式,更准确的说是因为FileChannel没有继承SelectableChannel。但是SocketChannel可以正常使用。
2.3、选择键(SelectionKey)
选择键封装了特定的通道与特定的选择器的注册关系。调用SelectableChannel.register()方法会返回选择键并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
用一张UML图来描述一下选择器、可选择通道和选择键:
3、建立选择器
前面讲了,选择器的作用是管理了被注册的通道集合和它们的就绪状态,假设我们有三个Socket通道的选择器,可能会有类似的代码:
... channel1.configureBlocking(false);//让这个Channel(通道)是非阻塞的。它是SelectableChannel抽象类里的方法,用于使通道处于阻塞模式或非阻塞模式,false表示非阻塞,true表示阻塞。 Selector selector = Selector.open(); channel1.register(selector, SelectionKey.OP_READ); channel2.register(selector, SelectionKey.OP_WRITE); channel3.register(selector, SelectionKey.OP_READ | OP_WRITE); channel4.register(selector, SelectionKey.OP_READ | OP_ACCEPT); ready = selector.select(10000); ...
这种操作用图表示就是:
代码创建了一个新的选择器,然后将这四个(已经存在的)Socket通道注册到选择器上,而且感兴趣的操作各不相同。select()方法在将线程置于睡眠状态直到这些感兴趣的事件中的一个发生或者10秒钟过去,这就是所谓的事件驱动。
再稍微看一下Selector的API细节:
public abstract class Selector { ... public static Selector open() throws IOException; public abstract boolean isOpen(); public abstract void close() throws IOException; public abstract SelectionProvider provider(); ... }
Selector是通过调用静态工厂方法open()来实例化的,这个从前面的代码里面也看到了,选择器不是像通道或流那样的基本I/O对象----数据从来没有通过他们进行传递。
通道是调用register方法注册到选择器上的,从代码里面可以看到register()方法接受一个Selector对象作为参数,以及一个名为ops的整数型参数,第二个参数表示关心的通道操作。在JDK1.4中,有四种被定义的可选择操作:读(read)、写(write)、连接(connect)和接受(accept)。第二个参数,它是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:Connect、Accept、Read、Write。
- Connect:成功连接到另一个服务器称为“连接就绪”;
- Accept:ServerSocketChannel准备好接收新进入的连接称为“接收就绪”;
- Read:有数据可读的通道称为“读就绪”;
- Write:等待写数据的通道称为“写就绪”;
如果对不止一种事件感兴趣,可以使用或( | )运算符来操作:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
注意并非所有的操作都在所有的可选择通道上被支持,例如SocketChannel就不支持accept。
4、使用选择键
接下来看看选择键,选择键的API大致如下:
public abstract class SelectionKey { public static final int OP_READ; public static final int OP_WRITE; public static final int OP_CONNECT; public static final int OP_ACCEPT; public abstract SelectableChannel channel(); public abstract Selector selector(); public abstract void cancel(); public abstract boolean isValid(); public abstract int interestOps(); public abstract void iterestOps(int ops); public abstract int readyOps();//获取相关的通道的已经就绪的操作 public final boolean isReadable(); public final boolean isWritable(); public final boolean isConnectable(); public final boolean isAcceptable(); public final Object attach(Object ob); public final Object attachment();////返回SelectionKey的attachment,attachment可以在注册channel的时候指定。 }
关于这些API,总结几点:
1、就像前面提到的,一个键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系,channel()方法和selector()方法反映了这种关系,通道不会在键被取消的时候立即注销。直到下一次操作发生为止,它们仍然会处于被注册的状态。
一个 SelectionKey 对象包含两个以整数形式进行编码的比特掩码:一个用于指示那些通道/选择器组合体所关心的操作(insterest 集合),另一个表示通道准备好要执行的操作(ready 集合)。可以通过调用键的 readyOps( )方法来获取相关的通道的已经就绪的操作,ready集合是interest集合的子集,表示interest集合中从上次调用select( )以来已经就绪的那些操作。
2、开发者可以使用cancel()方法终结这种关系,可以使用isValid()方法来检查这种有效的关系是否仍然存在,可以使用readyOps()方法来获取相关的通道已经就绪的操作。
3、第2点有提到readyOps()方法,不过我们往往不需要使用这个方法,SelectionKey类定义了四个便于使用的布尔方法来为开发者测试通道的就绪状态,例如:
if (key.isWritable()){...} 这种写法就等价于: if ((key.readyOps() & SelectionKeys.OPWRITE) != 0){...} isWritable()、isReadable()、isConnectable()、isAcceptable()四个方法在任意一个SelectionKey对象上都能安全地调用。
4、当通道关闭时,所有相关的键会自动取消(一个通道可以被注册到多个选择器上);当选择器关闭时,所有被注册到该选择器的通道都会被注销并且相关的键立即被取消。
5、选择器维护的选择键
选择器维护者注册过的通道的集合,并且这些注册关系中的任意一个都是封装在SelectionKey对象中的。每一个Selector对象维护三种键的集合:
public abstract class Selector { ... public abstract Set keys(); public abstract Set selectedKeys(); public abstract int select() throws IOException; public abstract int select(long timeout) throws IOException; public abstract int selectNow() throws IOException; public abstract void wakeup(); ... }
由这个API看下去,这三种键是:
5.1、已注册的键的集合(Registered key set)
与选择器关联的已经注册的键的集合,并不是所有注册过的键都有效,这个集合通过keys()方法返回,并且可能是空的。这些键的集合是不可以直接修改的,试图这么做将引发java.lang.UnsupportedOperationException。
5.2、已选择的键的集合(Selected key set)
已注册的键的集合的子集,这个集合的每个成员都是相关的通道被选择器判断为已经准备好的并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(有可能是空的)。
键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。
5.3、已取消的键的集合(Cancelled key set)
已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。
注意:当键被取消(可以通过isValid()方法来判断)时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用select()方法时(或者一个正在进行的select()调用结束时),已取消的键的集合中的被取消的键将会被清理掉,并且相应的注销也将会完成。通道会被注销,新的SelectionKey将被返回。当通道关闭时,所有相关的键会自动取消(一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相应的键将立即被无效化(取消),一旦键被无效化,调用它的与相关的方法就将抛出CancelledKeyException 异常。
6、选择过程
接着就是Selector的核心选择过程了。基本上来说,选择器是对select()、poll()、epoll()等本地调用或者类似的操作系统特定的系统调用的一个包装。但是Selector所做的不仅仅是简单地向本地代码传送参数,每个操作都有特定的过程,对这个过程的理解是合理地管理键和它们所表示的状态信息的基础。
选择操作是当三种形式的select()中的任意一种被调用时,由选择器执行的。不管是哪一种形式的调用,下面步骤将被执行:
1、已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。此步骤结束,已取消的键的集合将是空的。
2、已注册的键的集合中的键的interest集合将被检查,此步骤结束,对interest集合的改动不会影响剩余的检查过程。一旦就绪条件被定下来,底层操作系统将会进行查询,以确定每个通道所关心的操作的真实就绪状态,依赖于特定的select()方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。
3、步骤2可能会花费很长时间,特别是线程处于阻塞状态时。与该选择器相关的键可能会同时被取消,当步骤2结束时,步骤1将重新执行,以完成任意一个在选择进行的过程中,键已经被取消的通道的注册。
4、select操作的返回值不是已准备好的通道的总数,而是从上一个select()调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。
6.1 select方法
在刚初始化的Selector对象中,上面讲述的三个集合都是空的。通过Selector的select()方法可以选择已经准备就绪的通道(这些通道包含你感兴趣的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector重载的几个select()方法:
- int select():阻塞到至少有一个通道在你注册的事件上就绪了;
- int select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒;
- int selectNow():非阻塞,执行就绪检查过程,但不阻塞,如果当前没有通道就绪,立刻返回0;
select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在调用select()时进入就绪的通道不会在本次调用中被计入,而在前一次select()调用进入就绪但现在已经不在于就绪状态的通道也不会被计入。例如:首次调用select()方法,如果有一个通道变成了就绪状态,返回了1,若再次调用select()方法,如果一个另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
一旦调用了select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键的集合。如下:
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(); }
请注意keyIterator.remove()每次迭代结束时的呼叫。在Selector删除SelectionKey作为自己选择的关键实例,当你完成处理后,必须这样做。这样的话才能在通道下一次变为“就绪”时,Selector将再次将其添加到所选的键集合。
6.2停止选择
选择器执行选择的过程,系统底层会一次询问每个通道是否就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有一下二种方式来唤醒在Select()方法中阻塞的线程。
(1)wakeup()方法:一个线程调用select()方法的那个对象上调用Selector.wakeup()方法。阻塞在select()方法上的线程会立马返回。如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。wakeup( )方法将使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有在进行中的选择,那么下一次对 select( )方法的调用将立即返回,后续的选择操作将正常进行。有时并不想要这种延迟的唤醒行为,而只想唤醒一个睡眠中的线程,后续的选择继续正常地进行,此时可以通过在调用 wakeup( )方法后调用 selectNow( )方法解决该问题。
(2)close()方法:该方法使得任何一个在选择操作中阻塞的线程都被唤醒,用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。调用wakeup()方法将使得选择器上的第一个还没有返回的选择操作立即返回,如果当前没有正在进行中的选择,那么下一次对select()方法的一种形式的调用将立即返回,后续的选择操作将正常进行。close( )方法会使得任何一个在select操作中阻塞的线程都将被唤醒,如同调用wakeup( )方法,与选择器相关的通道将被注销,而键将被取消。
(3)interrupt方法:如果睡眠中的线程的 interrupt( )方法被调用,它的返回状态将被设置。如果被唤醒的线程之后将试图在通道上执行 I/O 操作,通道将立即关闭,然后线程将捕捉到一个异常。
7、Selector和通道的使用实例
使用Selector完成客户端与服务端的通信,其中SelectorServerSocketChannel为服务端,SelectorSocketChannel为客户端,先启动服务端,然后启动客户端,连接成功后,客户端发送信息至服务端,服务端收到信息后,反馈信息给客户端。
SelectorServerSocketChannel:
public class SelectorServerSocketChannel { public static void main(String[] args) throws Exception{ ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); ServerSocket serverSocket = serverSocketChannel.socket(); serverSocketChannel.configureBlocking(false); serverSocket.bind(new InetSocketAddress("localhost", 1234)); Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while(true) { selector.select(); Iterator iter = selector.selectedKeys().iterator(); while(iter.hasNext()) { SelectionKey key = (SelectionKey) iter.next(); if(key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); System.out.println("Connected: " + channel.socket().getRemoteSocketAddress()); } if(key.isReadable()) { ByteBuffer byteBuffer = ByteBuffer.allocate(512); SocketChannel socketChannel = (SocketChannel) key.channel(); socketChannel.read(byteBuffer); byteBuffer.flip(); System.out.println("server received message: " + getString(byteBuffer)); byteBuffer.clear(); String message = "server sending message " + System.currentTimeMillis(); System.out.println("server sending message: " + message); byteBuffer.put(message.getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer); } iter.remove(); } } } private static String getString(ByteBuffer byteBuffer) { Charset charset; CharsetDecoder decoder; CharBuffer charBuffer; try { charset = Charset.forName("UTF-8"); decoder = charset.newDecoder(); charBuffer = decoder.decode(byteBuffer.asReadOnlyBuffer()); return charBuffer.toString(); } catch (Exception ex) { ex.printStackTrace(); return ""; } } }
SelectorSocketChannel:
public class SelectorSocketChannel { public static void main(String[] args) throws Exception { SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); Selector selector = Selector.open(); socketChannel.connect(new InetSocketAddress("localhost",1234)); socketChannel.register(selector, SelectionKey.OP_CONNECT); while (true) { selector.select(); Iterator it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); it.remove(); if (key.isConnectable()) { if (socketChannel.isConnectionPending()) { if (socketChannel.finishConnect()) { key.interestOps(SelectionKey.OP_READ); sendMessage(socketChannel); } else { key.cancel(); } } } if(key.isReadable()) { ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { byteBuffer.clear(); int count = socketChannel.read(byteBuffer); if (count > 0) { byteBuffer.flip(); System.out.println("client receive message: " + getString(byteBuffer)); break; } } } } } } private static void sendMessage(SocketChannel socketChannel) throws Exception { String message = "client sending message " + System.currentTimeMillis(); ByteBuffer byteBuffer = ByteBuffer.allocate(512); byteBuffer.clear(); System.out.println("client sending message: " + message); byteBuffer.put(message.getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer); } private static String getString(ByteBuffer buffer) { Charset charset; CharsetDecoder decoder; CharBuffer charBuffer; try { charset = Charset.forName("UTF-8"); decoder = charset.newDecoder(); charBuffer = decoder.decode(buffer.asReadOnlyBuffer()); return charBuffer.toString(); } catch (Exception ex) { ex.printStackTrace(); return ""; } } }
selector和channel的例子:
public class SelectorDemo { public static int PORT_NUMBER = 1234; public static void main(String[] argv) throws Exception { new SelectorDemo().go(argv); } private void go(String[] argv) throws Exception { int port = PORT_NUMBER; if(argv.length > 0) { port = Integer.parseInt(argv[0]); } System.out.println("Listening on port " + port); ServerSocketChannel serverChannel = ServerSocketChannel.open(); ServerSocket serverSocket = serverChannel.socket(); Selector selector = Selector.open(); serverSocket.bind(new InetSocketAddress(port)); serverChannel.configureBlocking(false); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { int n = selector.select(); if (n == 0) { continue; } Iterator it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); if (key.isAcceptable() || key.isReadable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); registerChannel(selector, channel, SelectionKey.OP_READ); sayHello(channel); } if (key.isReadable()) { readDataFromSocket(key); } it.remove(); } } } private void registerChannel(Selector selector, SelectableChannel channel, int ops) throws Exception { if (channel == null) { return; } channel.configureBlocking(false); channel.register(selector, ops); } private ByteBuffer buffer = ByteBuffer.allocateDirect(1024); private void readDataFromSocket(SelectionKey key) throws Exception { SocketChannel socketChannel = (SocketChannel) key.channel(); int count; buffer.clear(); while ((count = socketChannel.read(buffer)) > 0) { buffer.flip(); while (buffer.hasRemaining()) { socketChannel.write(buffer); } buffer.clear(); } if (count < 0) { socketChannel.close(); } } private void sayHello(SocketChannel channel) throws Exception { buffer.clear(); buffer.put("Hi there!\r\n".getBytes()); buffer.flip(); channel.write(buffer); } }