文章目录
JDK版本
- jdk8
Selector
Selector
主要作用是作为SelectableChannel
对象的多路复用器- 基本原理:当客户端发送数据到服务端,内核从网卡复制数据成功后会调用一个回调函数(将
Socket
加入到事件列表),回调函数中维护一个事件列表,应用层获取此事件列表即可以得到所有感兴趣的事件
Selector
提供选择执行已经就绪的事件的能力,而这些就绪事件就是上图中的事件列表。Selector
类似一个观察者,只要我们把需要探知的SocketChannel
告诉Selector
,当有事件发生时,Selector
会传回一组SelectorKey
,通过读取这些Key
,就会获得刚刚注册过的SocketChannel
,此时从这个SocketChannel
读取数据是一定可以读取到的。Selector
是SelectableChannel
对象的多路复用器,多路复用的核心目的就是使用最少的线程去操作更多的Channel
,当Selector
管理大量Channel
时,如何高效完成所有Channel
上就绪事件的检查?不同平台的实现机制不同,对于window平台来说(WindowsSelectorImpl实现类),创建线程的个数根据Channel
的数量决定,每注册1024(0-1023)个Channel
就创建1个新的线程,不满足1024个时,使用主线程。- 默认
Channel
接口是没有注册方法的,只有SelectableChannel
抽象类才有,所以以下无特殊说明,所说的Channel
都是指SelectableChannel
创建
-
创建
Selector
,方式一本质上是通过方式二创建的方式一: Selector selector = Selector.open( ); 方式二: SelectorProvider provider = SelectorProvider.provider(); Selector selector = provider.openSelector();
-
SelectorProvider
是用于Selector
和SelectableChannel
的服务提供者,SelectableChannel
和Selector
的open方法底层都是通过SelectorProvider
来创建实例的@Test public void testSelectorProvider() throws Exception { SelectorProvider provider = SelectorProvider.provider(); //两种方式等价 Selector selector = provider.openSelector(); Selector selector1 = Selector.open(); //两种方式等价 ServerSocketChannel serverSocketChannel = provider.openServerSocketChannel(); ServerSocketChannel serverSocketChanne2 = ServerSocketChannel.open(); //两种方式等价 SocketChannel socketChannel1 = provider.openSocketChannel(); SocketChannel socketChannel2 = SocketChannel.open(); }
注册Channel到Selector
-
SelectableChannel
类的SelectionKey register(Selector sel, int ops)
方法注册当前的SelectableChannel
到Selector
上(底层调用的其实是SelectorImpl
的未公开的SelectionKey register(AbstractSelectableChannel ch,int ops,Object attachment)
方法),并返回一个SelectionKey
。在将SelectableChannel
注册到Selector
之前必须将SelectableChannel
设置为非阻塞模式@Test public void testSelector() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //注册到Selector之前必须将Channel设置为非阻塞模式 serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress("localhost", 8888)); //创建多路复用选择器 Selector selector = Selector.open(); //false 新创建的Channel总是未注册的 System.out.println(serverSocketChannel.isRegistered()); SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //true System.out.println(serverSocketChannel.isRegistered()); //mac系统输出:sun.nio.ch.KQueueSelectorImpl@45ff54e6 System.out.println(selector); //sun.nio.ch.SelectionKeyImpl@6d5380c2 System.out.println(selectionKey); //true System.out.println(selector.isOpen()); selector.close(); //false System.out.println(selector.isOpen()); //true System.out.println(serverSocketChannel.isOpen()); serverSocketChannel.close(); //false System.out.println(serverSocketChannel.isOpen()); }
-
同一个
SelectableChannel
可以注册到不同的Selector
上,并返回不同SelectionKey
@Test public void SelectionKey() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress("localhost", 8888)); Selector selector = Selector.open(); SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //sun.nio.ch.SelectionKeyImpl@6d5380c2 System.out.println(selectionKey); //keyFor()方法获取当前Channel注册到指定Selector上的SelectionKey对象 SelectionKey selectionKey1 = serverSocketChannel.keyFor(selector); //sun.nio.ch.SelectionKeyImpl@6d5380c2 System.out.println(selectionKey1); //同一个Channel注册到多个Selector上 Selector selector2 = Selector.open(); SelectionKey selectionKey2 = serverSocketChannel.register(selector2, SelectionKey.OP_ACCEPT); //sun.nio.ch.SelectionKeyImpl@45ff54e6 System.out.println(selectionKey2); selector.close(); selector2.close(); serverSocketChannel.close(); }
-
不同的
SelectableChannel
注册到相同的Selector
,返回不同的SelectionKey
@Test public void SelectionKey2() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress("localhost", 8888)); Selector selector = Selector.open(); SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //sun.nio.ch.SelectionKeyImpl@6d5380c2 System.out.println(selectionKey); //不同的Channel注册到相同的Selector ServerSocketChannel serverSocketChannel2 = ServerSocketChannel.open(); serverSocketChannel2.configureBlocking(false); serverSocketChannel2.bind(new InetSocketAddress("localhost", 9999)); SelectionKey selectionKey2 = serverSocketChannel2.register(selector, SelectionKey.OP_ACCEPT); //sun.nio.ch.SelectionKeyImpl@45ff54e6 System.out.println(selectionKey2); selector.close(); serverSocketChannel.close(); }
-
不同的
SelectableChannel
注册到不同的Selector
,返回的SelectionKey
不是同一个对象@Test public void SelectionKey3() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress("localhost", 8888)); Selector selector = Selector.open(); SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //sun.nio.ch.SelectionKeyImpl@6d5380c2 System.out.println(selectionKey); //不同的Channel注册到不同的Selector Selector selector2 = Selector.open(); ServerSocketChannel serverSocketChannel2 = ServerSocketChannel.open(); serverSocketChannel2.configureBlocking(false); serverSocketChannel2.bind(new InetSocketAddress("localhost", 9999)); SelectionKey selectionKey2 = serverSocketChannel2.register(selector2, SelectionKey.OP_ACCEPT); //sun.nio.ch.SelectionKeyImpl@45ff54e6 System.out.println(selectionKey2); selector.close(); selector2.close(); serverSocketChannel.close(); serverSocketChannel2.close(); }
-
相同的
SelectableChannel
注册到相同的Selector
,返回的SelectionKey
是同一个对象@Test public void SelectionKey4() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress("localhost", 8888)); Selector selector = Selector.open(); SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //sun.nio.ch.SelectionKeyImpl@6d5380c2 System.out.println(selectionKey); //相同的Channel重复注册到相同的Selector SelectionKey selectionKey2 = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //sun.nio.ch.SelectionKeyImpl@6d5380c2 System.out.println(selectionKey2); selector.close(); serverSocketChannel.close(); }
-
总结
Channel
在被注册到Selector
之前,必须先设置非阻塞模式(socketChannel.configureBlocking(false))- 一个
Channel
可以被注册到多个Selector
上,但对每个Selector
而言,最好只注册一次。如果Selector
上多次注册同一个Channel
,返回的SelectionKey
总是同一个实例,后注册的感兴趣的操作类型会覆盖之前的感兴趣的操作类型 - 一个
Channel
在不同的Selector
上注册,每次返回的SelectionKey
都是不同的实例。
select()
-
selector.select()
阻塞直到发生以下- 已经注册的
Channel
准备就绪 - 通过调用
Selector
的wakeUp()
方法 - 线程被中断(调用阻塞线程的interrupt方法,同时线程对中断进行了处理)
- 调用
Selector
的close()
方法
- 已经注册的
-
验证阻塞
@Test public void testSelectorBlock() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress("localhost", 8888)); Selector selector = Selector.open(); SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //启动后阻塞,使用telnet 127.0.0.1 8888 访问后开始向下执行 int select = selector.select(); //输出:1 System.out.println(select); //16 System.out.println(selectionKey.readyOps()); serverSocketChannel.close(); }
-
select()
返回值:更新已就绪(选择)的SelectionKey
的集合的个数,该数目可能为0或者大于0。0表示已就绪(选择)的SelectionKey
的集合的个数没有更改。从上一个select()
调用之后进入就绪状态的通道的数量,之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。下面的iterator.remove();被注释,所以第二次输出会返回0/** * 测试selector为0 * 1. telent 127.0.0.1 8888 * 2. telnet 127.0.0.1 8888 * 第一次输出: * 已就绪的SelectionKey的集合:[sun.nio.ch.SelectionKeyImpl@6d5380c2] * 1 * 执行accept... * 第二次输出 * 已就绪的SelectionKey的集合:[sun.nio.ch.SelectionKeyImpl@6d5380c2] * 0 * 执行accept... * 两次已就绪的SelectionKey的集合没有变化,所以select返回0 * @throws Exception */ @Test public void testSelector0() throws Exception { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 8888)); serverSocketChannel.configureBlocking(false); Selector selector = Selector.open(); SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { int select = selector.select(); //获取已经注册的SelectionKey的集合 Set<SelectionKey> registered = selector.keys(); //获取准备就绪(已选择)的SelectionKey的集合 Set<SelectionKey> readyed = selector.selectedKeys(); System.out.println("已就绪的SelectionKey的集合:" + readyed); System.out.println(select); Iterator<SelectionKey> iterator = readyed.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if (key.isAcceptable()) { System.out.println("执行accept..."); ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); SocketChannel socketChannel = serverChannel.accept(); socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.wrap(new byte[]{'a', '\n'}); socketChannel.write(buffer); buffer.clear(); socketChannel.close(); } /** * 1. selector.selectedKeys()返回的已选择键集,包含了当前所有准备就绪的通道。 * 2. select()不阻塞时,一个新的Channel就绪了 * 3. 就绪一个Channel就要把此Channel的所有就绪的事件都处理完毕 * 4. selector不会从已选择的SelectionKey的集合中移除SelectionKey,所以需要手动移除 */ //iterator.remove(); } } }
-
select()
方法selectNow()
:如果没有Channel
准备就绪,立刻返回0select(long timeout)
:如果没有Channel准备就绪,阻塞规定时间
wakeup()
-
调用
wakeup()
可以使阻塞在select()
方法的线程返回,当然如果当前没有阻塞的select()
方法,那么会让下一次select()
直接返回,两个连续的select()操作之间多次调用wakeup()
和只调用一次的效果是一样的。不过很多时候,我们并不能确定是否有线程阻塞在select()
方法,也不想影响到下一次select()
(只是因为某些事件,临时唤醒一下),那可以在wakeup()
之后调用selectNow()
,他会立即返回,也抵消了wakeup()
的影响 -
为什么要唤醒?
- 注册了新的
Channel
或者事件 Channel
关闭,取消注册- 优先级更高的事件触发(如定时事件),希望得到及时处理
- 注册了新的
-
模拟唤醒
@Test public void testWakeUp() throws Exception { Selector selector = Selector.open(); new Thread(() -> { try { Thread.sleep(3000); //3s后唤醒主线程的阻塞的Selector selector.wakeup(); //1 System.out.println(selector.keys().size()); //0 System.out.println(selector.selectedKeys().size()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress("localhost", 8888)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //阻塞 int select = selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); if (selectionKey.isAcceptable()) { ServerSocketChannel ssk = (ServerSocketChannel) selectionKey.channel(); SocketChannel socketChannel = ssk.accept(); socketChannel.close(); } iterator.remove(); } selector.close(); serverSocketChannel.close(); }
SelectionKey
SelectionKey
封装了SelectableChannel
与特定的Selector
的注册关系Selector
会维护三种SelectionKey
集合- 已注册的
SelectionKey
的集合,通过keys()
方法返回,可能包括已经取消的键 - 已就绪(选择)的
SelectionKey
的集合,通过selectedKeys()
方法返回,此集合是已注册的SelectionKey
的集合的子集 - 已取消的
SelectionKey
的集合(但是还没注销),通过cancelledKeys()
方法返回,但是该方法是protected
,即不能在其他包中直接调用。此集合是已注册的SelectionKey
的集合的子集
- 已注册的
- 当我们调用
Selector
的select()
方法时,SelectedKeys
集合会被更新,通过遍历SelectedKeys
,可以找到已经就绪的Channel
,从而处理各种I/O事件 - select()调用时的逻辑
-
检查已取消的
SelectionKey
的集合,如果非空,从已注册的SelectionKey
的集合中移除所有存在于已取消的SelectionKey
的集合中的SelectionKey
对象,并将注销其Channel
,同时清空 -
向内核发起一个系统调用进行查询,以确定
Selector
上注册的每个Channel
所关心的事件是否就绪。如果没有Channel
已经准备好,线程可能会一直阻塞(select())、阻塞指定时间(select(timeout))或立即返回(selectNow()),这主要依赖于特定select方法的调用; -
系统调用返回,再次检查已取消的
SelectionKey
的集合 -
系统调用返回后,对于那些没有就绪事件的
Channel
将不会有任何的操作,对于那些已经有就绪事件的Channel
,将执行以下两种操作的一种:- 如果
Channel
的SelectionKey
还未加入已就绪(选择)SelectionKey
集合,将其添加到已就绪(选择)SelectionKey
集合中,修改readyOps
- 如果
Channel
的SelectionKey
已经存在于已就绪(选择)SelectionKey
集合,修改readyOps
,所有之前记录在readyOps
中已经不再是就绪状态的操作不会被清除
AbstractPollSelectorImpl代码 protected int updateSelectedKeys() { int numKeysUpdated = 0; // Skip zeroth entry; it is for interrupts only for (int i=channelOffset; i<totalChannels; i++) { int rOps = pollWrapper.getReventOps(i); if (rOps != 0) { SelectionKeyImpl sk = channelArray[i]; pollWrapper.putReventOps(i, 0); if (selectedKeys.contains(sk)) { if (sk.channel.translateAndSetReadyOps(rOps, sk)) { numKeysUpdated++; } } else { sk.channel.translateAndSetReadyOps(rOps, sk); //SelectionKeyImpl中的readyOps和interestOps if ((sk.nioReadyOps() & sk.nioInterestOps()) != 0) { selectedKeys.add(sk); numKeysUpdated++; } } } } return numKeysUpdated; }
- 如果
-
返回的是从上一个
select()
之后,处于就绪状态的Channel
数量,如果已经在就绪集合中,不会累计;比如我们注册read,并且在使用之后SelectionKey
不移出,那么下次再有read事件过来,select方法可能会返回0就不会被记录,但是不代表没有感兴趣的事件
-
已注册的SelectionKey
的集合
-
已注册的
SelectionKey
的集合,在sun.nio.ch.SelectorImpl
源码可以看到public abstract class SelectorImpl extends AbstractSelector { // The set of keys with data ready for an operation protected Set<SelectionKey> selectedKeys; // The set of keys registered with this Selector protected HashSet<SelectionKey> keys; // Public views of the key sets private Set<SelectionKey> publicKeys; // Immutable private Set<SelectionKey> publicSelectedKeys; // Removal allowed, but not addition protected SelectorImpl(SelectorProvider sp) { super(sp); keys = new HashSet<SelectionKey>(); selectedKeys = new HashSet<SelectionKey>(); if (Util.atBugLevel("1.4")) { publicKeys = keys; publicSelectedKeys = selectedKeys; } else { publicKeys = Collections.unmodifiableSet(keys); publicSelectedKeys = Util.ungrowableSet(selectedKeys); } } //keys返回不可变的keys public Set<SelectionKey> keys() { if (!isOpen() && !Util.atBugLevel("1.4")) throw new ClosedSelectorException(); return publicKeys; } ...省略... }
已就绪的SelectionKey
的集合
-
当调用select()方法的主要流程
-
查看
sun.nio.ch.SelectorImpl
类public abstract class SelectorImpl extends AbstractSelector { // The set of keys with data ready for an operation protected Set<SelectionKey> selectedKeys; ...省略... }
-
在Mac系统上,抽象类
SelectorImpl
的实现类是sun.nio.ch.KQueueSelectorImpl
,这里查看updateSelectedKeys
方法private int updateSelectedKeys(int entries) throws IOException { int numKeysUpdated = 0; boolean interrupted = false; updateCount++; for (int i = 0; i < entries; i++) { int nextFD = kqueueWrapper.getDescriptor(i); if (nextFD == fd0) { interrupted = true; } else { MapEntry me = fdMap.get(Integer.valueOf(nextFD)); if (me != null) { int rOps = kqueueWrapper.getReventOps(i); SelectionKeyImpl ski = me.ski; //selectedKeys就是就绪的selectedKeys的集合 if (selectedKeys.contains(ski)) { if (me.updateCount != updateCount) { if (ski.channel.translateAndSetReadyOps(rOps, ski)) { numKeysUpdated++; me.updateCount = updateCount; } } else { ski.channel.translateAndUpdateReadyOps(rOps, ski); } } else { ski.channel.translateAndSetReadyOps(rOps, ski); if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) { //添加就绪的selectedKey到就绪的selectedKeys集合中 selectedKeys.add(ski); numKeysUpdated++; me.updateCount = updateCount; } } } } } if (interrupted) { // Clear the wakeup pipe synchronized (interruptLock) { IOUtil.drain(fd0); interruptTriggered = false; } } return numKeysUpdated; }
已取消的SelectionKey
的集合
-
sun.nio.ch.SelectorImpl
类继承了父类AbstractSelector
, 已取消的SelectionKey
的集合定义在sun.nio.ch.SelectorImpl
类的父类AbstractSelector
中public abstract class AbstractSelector extends Selector { ...省略... //已取消的`SelectionKey`的集合 private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>(); void cancel(SelectionKey k) { synchronized (cancelledKeys) { cancelledKeys.add(k); } } //此方法在select()时调用,上面分析已就绪SelectionKeys集合时 protected final Set<SelectionKey> cancelledKeys() { return cancelledKeys; } ...省略... }
-
无论是调用该
SelectionKey
的cancel()
方法,还是通过关闭某个SelectionKey
的SelectableChannel
(关闭时会调用SelectionKey
的cancel()
方法),该SelectionKey
都会被加入到已取消的SelectionKey
的集合中 -
延迟注销
SelectionKey
的操作,是为了在选择的过程中减少不必要的同步,不然注销和选择就要形成一定的互斥,因为注销潜在的代价比较高,可能需要释放各种资源。
interestOps
-
interestOps()
代表感兴趣事件的集合,有四个常量public static final int OP_READ = 1 << 0 ==1 ==0000 0001 public static final int OP_WRITE = 1 << 2 ==4 ==0000 0100 public static final int OP_CONNECT = 1 << 3 ==8 ==0000 1000 public static final int OP_ACCEPT = 1 << 4 ==16 ==0001 0000 其实还有一个就是0,0表示取消所有监听键
-
运算
sk.interestOps(sk.interestOps()& ~SelectionKey.OP_READ) 等价于(~ 按位取反) sk.interestOps(sk.interestOps()& 1111 1110) "&~xx" 代表取消xx事件 sk.interestOps()| SelectionKey.OP_READ "|xx" 代表添加xx事件 int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 表示对多个事件感兴趣(read和write)
readyOps()
-
readyOps()
返回Channel
已经准备就绪事件的集合,readyOps()
集合是interestOps()
集合的子集。并且表示了interest集合中从上次调用select()以来已经就绪的那些操作。例如:注册channel感兴趣的动作是OP_READ,OP_WRITE
,sc.register(sel, SelectionKey.OP_WRITE|SelectionKey.OP_READ)
;调用interestOps()
可以得到他对read和write感兴趣,但是如果该channel中没有数据,则只能是key.readyOps()==SelectionKey.OP_WRITE
。 -
检查等价方式
(key.isWritable()) 等价于: if ((key.readyOps() & SelectionKey.OP_WRITE) != 0)
cancel()
SelectionKey
代表Channel
与Selector
的注册关系,当Channel
在选择器里注册后,Channel
在注销之前将一直保持注册状态。可以调用SelectionKey
类的cancel()
取消这种关系,但是这种注销关系并不是立即生效(此时仅仅添加到其Selector
维护的已取消的SelectionKey
集合中),而是在Selector
下一次调用select()
时取消这种关系- 无论是调用
Channel
的close()
方法,还是中断阻塞于该通道上I/O操作的线程来关闭该Channel
,都会隐式地取消该该Channel
的所有SelectionKey
- 如果
Selector
本身已关闭,则将注销该Channel
,并且表示其注册的SelectionKey
将立即无效