1.简介
前面的文章说了缓冲区,说了通道,本文就来说说 NIO 中另一个重要的实现,即选择器 Selector。在更早的文章中,我简述了几种 IO 模型。如果大家看过之前的文章,并动手写过代码的话。再看 Java 的选择器大概就会知道它是什么了,以及怎么用了。选择器是 Java 多路复用模型的一个实现,可以同时监控多个非阻塞套接字通道。示意图大致如下:

如果大家了解过多路复用模型,那应该也会知道几种复用模型的实现。比如 select,poll 以及 Linux 下的 epoll 和 BSD 下的 kqueue。Java 的选择器并非凭空创造,而是在底层操作系统提供的接口的基础上封装而来。相关的细节,我随后会进行分析。
关于 Java 选择器的简介这里先说到这,接下来进入正题。
2.基本操作及实现
本章我将对 Selector 的创建,通道的注册,Selector 的选择过程进行分析。内容篇幅较大,希望大家耐心看完。由于 Selector 相关类在不同操作系统下的实现是不同的,加之个人对 Linux epoll 更为熟悉,所以本文所分析的源码也是和 epoll 相关的。好了,进入正题吧。
2.1 创建选择器
选择器 Selector 是一个抽象类,所以不能直接创建。Selector 提供了一个 open 方法,通过 open 方法既可以创建选择器实例。示例代码如下:
Selector selector = Selector.open();
上面的代码比较简单,只有一行。不过不要被表象迷惑,这行代码仅是完整实现的冰山一角,更复杂的逻辑则隐藏在水面之下。 在简介一节,我已经说了 Java 选择器是对底层多路复用接口的一个包装,这里的 open 方法也不例外。假设我们的 Java 运行在 Linux 平台下,那么 open 最终所做的事情应该是调用操作系统的epoll_create函数,用于创建 epoll 实例。真实情况是不是如此呢?答案就在冰山深处,接下来就让我们一起去求索吧。下面我们将沿着 open 方法一路走下去,如下:
1public abstract class Selector implements Closeable { 2 public static Selector open() throws IOException { 3 // 创建 SelectorProvider,再通过其 openSelector 方法创建 Selector 4 return SelectorProvider.provider().openSelector(); 5 } 6 // 省略无关代码 7} 8 9public abstract class SelectorProvider { 10 public static SelectorProvider provider() { 11 synchronized (lock) { 12 if (provider != null) 13 return provider; 14 return AccessController.doPrivileged( 15 new PrivilegedAction<SelectorProvider>() { 16 public SelectorProvider run() { 17 if (loadProviderFromProperty()) 18 return provider; 19 if (loadProviderAsService()) 20 return provider; 21 // 创建默认的 SelectorProvider 22 provider = sun.nio.ch.DefaultSelectorProvider.create(); 23 return provider; 24 } 25 }); 26 } 27 } 28} 29 30public class DefaultSelectorProvider { 31 private DefaultSelectorProvider() { } 32 33 /** 34 * 根据系统名称创建相应的 SelectorProvider 35 */ 36 public static SelectorProvider create() { 37 String osname = AccessController 38 .doPrivileged(new GetPropertyAction("os.name")); 39 if (osname.equals("SunOS")) 40 return createProvider("sun.nio.ch.DevPollSelectorProvider"); 41 if (osname.equals("Linux")) 42 return createProvider("sun.nio.ch.EPollSelectorProvider"); 43 44 // 45 return new sun.nio.ch.PollSelectorProvider(); 46 } 47 48 /** 49 * 加载 SelectorProvider 类,并创建实例 50 */ 51 @SuppressWarnings("unchecked") 52 private static SelectorProvider createProvider(String cn) { 53 Class<SelectorProvider> c; 54 try { 55 c = (Class<SelectorProvider>)Class.forName(cn); 56 } catch (ClassNotFoundException x) { 57 throw new AssertionError(x); 58 } 59 try { 60 return c.newInstance(); 61 } catch (IllegalAccessException | InstantiationException x) { 62 throw new AssertionError(x); 63 } 64 65 } 66} 67 68/** 69 * 创建完 SelectorProvider,接下来要调用 openSelector 方法 70 * 创建 Selector 的继承类了。 71 */ 72public class EPollSelectorProvider extends SelectorProviderImpl { 73 public AbstractSelector openSelector() throws IOException { 74 return new EPollSelectorImpl(this); 75 } 76} 77 78class EPollSelectorImpl extends SelectorImpl { 79 EPollSelectorImpl(SelectorProvider sp) throws IOException { 80 // 调用父类构造方法 81 super(sp); 82 long pipeFds = IOUtil.makePipe(false); 83 fd0 = (int) (pipeFds >>> 32); 84 fd1 = (int) pipeFds; 85 86 // 创建 EPollArrayWrapper,EPollArrayWrapper 是一个重要的实现 87 pollWrapper = new EPollArrayWrapper(); 88 89 pollWrapper.initInterrupt(fd0, fd1); 90 fdToKey = new HashMap<>(); 91 } 92} 93 94public abstract class SelectorImpl extends AbstractSelector { 95 protected SelectorImpl(SelectorProvider sp) { 96 super(sp); 97 keys = new HashSet<SelectionKey>(); 98 selectedKeys = new HashSet<SelectionKey>(); 99 100 /* 初始化 publicKeys 和 publicSelectedKeys, 101 * publicKeys 即 selector.keys() 方法所返回的集合, 102 * publicSelectedKeys 则是 selector.selectedKeys() 方法返回的集合 103 */ 104 if (Util.atBugLevel("1.4")) { 105 publicKeys = keys; 106 publicSelectedKeys = selectedKeys; 107 } else { 108 publicKeys = Collections.unmodifiableSet(keys); 109 publicSelectedKeys = Util.ungrowableSet(selectedKeys); 110 } 111 } 112} 113 114/** 115 * EPollArrayWrapper 一个重要的实现,这一层再往下就是 C 代码了 116 */ 117class EPollArrayWrapper { 118 EPollArrayWrapper() throws IOException { 119 // 调用 epollCreate 方法创建 epoll 文件描述符 120 epfd = epollCreate(); 121 122 // the epoll_event array passed to epoll_wait 123 // 初始化 pollArray,该对象用于存储就绪文件描述符和事件 124 int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT; 125 pollArray = new AllocatedNativeObject(allocationSize, true); 126 pollArrayAddress = pollArray.address(); 127 128 // eventHigh needed when using file descriptors > 64k 129 if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE) 130 eventsHigh = new HashMap<>(); 131 } 132 133 // epollCreate 方法是 native 类型的 134 private native int epollCreate(); 135}
以上代码时 Java 层面的,Java 层调用栈最下面的类是 EPollArrayWrapper(源码路径可以在附录中查找)。EPollArrayWrapper 是一个重要的实现,起着承上启下的作用。上层是 Java 代码,下层是 C 代码。上层的代码看完了,接下来看看冰山深处的 C 代码:
1JNIEXPORT jint JNICALL 2Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this) 3{ 4 // 调用 epoll_create 函数创建 epoll 实例,并返回文件描述符 epfd 5 int epfd = epoll_create(256); 6 if (epfd < 0) { 7 JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed"); 8 } 9 return epfd; 10}
上面的代码很简单,仅做了创建 epoll 实例这一件事。看到这里,答案就明了了。最后在附一张时序图帮助大家理清代码调用顺序,如下:

2.2 选择键
2.2.1 几种事件
选择键 SelectionKey 包含4种事件,分别是:
1public static final int OP_READ = 1 << 0; 2public static final int OP_WRITE = 1 << 2; 3public static final int OP_CONNECT = 1 << 3; 4public static final int OP_ACCEPT = 1 << 4;
事件之间可以通过或运算进行组合,比如:
int interestOps = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
2.2.2 两种事件集合:interestOps 和 readyOps
interestOps 即感兴趣的事件集合,通道调用 register 方法注册时会设置此值,interestOps 可通过 SelectionKey interestOps() 方法获取。readyOps 是就绪事件集合,可通过 SelectionKey readyOps() 获取。
interestOps 和 readyOps 被声明在 SelectionKey 子类 SelectionKeyImpl 中,代码如下:
1public class SelectionKeyImpl extends AbstractSelectionKey { 2 private volatile int interestOps; 3 private int readyOps; 4}
接下来再来看看与 readyOps 事件集合相关的几个方法,如下:
1selectionKey.isAcceptable(); 2selectionKey.isConnectable(); 3selectionKey.isReadable(); 4selectionKey.isWritable();
以上方法从字面意思上就可以知道有什么用,这里就不解释了。接下来以 isReadable 方法为例,简单看一下这个方法是如何实现。
1public final boolean isReadable() { 2 return (readyOps() & OP_READ) != 0; 3}
上面说到可以通过或运算组合事件,这里则是通过与运算来测试某个事件是否在事件集合中。比如
1readyOps = SelectionKey.OP_READ | SelectionKey.OP_WRITE = 0101, 2readyOps & OP_READ = 0101 & 0001 = 0001, 3readyOps & OP_CONNECT = 0101 & 1000 = 0
readyOps & OP_READ != 0,所以 OP_READ 在事件集合中。readyOps & OP_CONNECT == 0,所以 OP_CONNECT 不在事件集合中。
2.2.3 attach 方法
attach 是一个好用的方法,通过这个方法,可以将对象暂存在 SelectionKey 中,待需要的时候直接取出来即可。比如本文对应的练习代码实现了一个简单的 HTTP 服务器,在读取用户请求数据后(即 selectionKey.isReadable() 为 true),会去解析请求头,然后将请求头信息通过 attach 方法放入 selectionKey 中。待通道可写后,再从 selectionKey 中取出请求头,并根据请求头回复客户端不同的消息。当然,这只是一个应用场景,attach 可能还有其他的应用场景,比如标识通道。不过其他的场景我没使用过,就不说了。attach 使用方式如下:
1selectionKey.attach(obj); 2Object attachedObj = selectionKey.attachment();
2.3 通道注册
通道注册即将感兴趣的事件告知 Selector,待事件发生时,Selector 即可返回就绪事件,我们就可以去做后续的事情了。比如 ServerSocketChannel 通道通常对 OP_ACCEPT 事件感兴趣,那么我们就可以把这个事件注册给 Selector。待事件发生,即服务端接受客户端连接后,我们即可获取这个就绪的事件并做相应的操作。通道注册的示例代码如下:
1channel.configureBlocking(false); 2SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
起初我以为通道注册操作会调用操作系统的 epoll_ctl 函数,但最终通过看源码,发现自己的理解是错的。既然通道注册阶段不调用 epoll_ctl 函数。那么,epoll_ctl 什么时候才会被调用呢?如果不调用 epoll_ctl,那么注册过程都干了什么事情呢?关于第一个问题,本节还无法解答,不过第二个问题则可以说说。接下来让我们深入通道类 register 方法的调用栈中去探寻答案吧。
1public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel { 2 public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException { 3 return register(sel, ops, null); 4 } 5 6 public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException; 7} 8 9public abstract class AbstractSelectableChannel extends SelectableChannel { 10 11 private SelectionKey[] keys = null; 12 13 public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException { 14 synchronized (regLock) { 15 // 省去一些校验代码 16 17 // 从 keys 数组中查找,查找条件为 k.selector() == sel 18 SelectionKey k = findKey(sel); 19 20 // 如果 k 不为空,则修改 k 所感兴趣的事件 21 if (k != null) { 22 k.interestOps(ops); 23 k.attach(att); 24 } 25 26 // k 为空,则创建一个 SelectionKey,并存储到 keys 数组中 27 if (k == null) { 28 // New registration 29 synchronized (keyLock) { 30 if (!isOpen()) 31 throw new ClosedChannelException(); 32 k = ((AbstractSelector)sel).register(this, ops, att); 33 addKey(k); 34 } 35 } 36 return k; 37 } 38 } 39} 40 41public abstract class AbstractSelector extends Selector { 42 protected abstract SelectionKey register(AbstractSelectableChannel ch, 43 int ops, Object att); 44} 45 46public abstract class SelectorImpl extends AbstractSelector { 47 protected final SelectionKey register(AbstractSelectableChannel ch, int ops, Object attachment) { 48 if (!(ch instanceof SelChImpl)) 49 throw new IllegalSelectorException(); 50 // 创建 SelectionKeyImpl 实例 51 SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this); 52 k.attach(attachment); 53 synchronized (publicKeys) { 54 implRegister(k); 55 } 56 k.interestOps(ops); 57 return k; 58 } 59} 60 61class EPollSelectorImpl extends SelectorImpl { 62 protected void implRegister(SelectionKeyImpl ski) { 63 if (closed) 64 throw new ClosedSelectorException(); 65 SelChImpl ch = ski.channel; 66 int fd = Integer.valueOf(ch.getFDVal()); 67 // 存储 fd 和 SelectionKeyImpl 的映射关系 68 fdToKey.put(fd, ski); 69 70 pollWrapper.add(fd); 71 // 将 SelectionKeyImpl 实例存储到 keys 中(这里的 keys 声明在 SelectorImpl 类中),keys 集合可由 selector.keys() 方法获取 72 keys.add(ski); 73 } 74} 75 76public class SelectionKeyImpl extends AbstractSelectionKey { 77 public SelectionKey interestOps(int ops) { 78 ensureValid(); 79 return nioInterestOps(ops); 80 } 81 82 public SelectionKey nioInterestOps(int ops) { 83 if ((ops & ~channel().validOps()) != 0) 84 throw new IllegalArgumentException(); 85 // 转换并设置感兴趣的事件 86 channel.translateAndSetInterestOps(ops, this); 87 // 设置 interestOps 变量 88 interestOps = ops; 89 return this; 90 } 91} 92 93class SocketChannelImpl extends SocketChannel implements SelChImpl { 94 public void translateAndSetInterestOps(int ops, SelectionKeyImpl sk) { 95 int newOps = 0; 96 // 转换事件 97 if ((ops & SelectionKey.OP_READ) != 0) 98 newOps |= PollArrayWrapper.POLLIN; 99 if ((ops & SelectionKey.OP_WRITE) != 0) 100 newOps |= PollArrayWrapper.POLLOUT; 101 if ((ops & SelectionKey.OP_CONNECT) != 0) 102 newOps |= PollArrayWrapper.POLLCONN; 103 // 设置事件 104 sk.selector.putEventOps(sk, newOps); 105 } 106} 107 108class class EPollSelectorImpl extends SelectorImpl { 109 public void putEventOps(SelectionKeyImpl ski, int ops) { 110 if (closed) 111 throw new ClosedSelectorException(); 112 SelChImpl ch = ski.channel; 113 // 设置感兴趣的事件 114 pollWrapper.setInterest(ch.getFDVal(), ops); 115 } 116} 117 118class EPollArrayWrapper { 119 void setInterest(int fd, int mask) { 120 synchronized (updateLock) { 121 // 扩容 updateDescriptors 数组,并存储文件描述符 fd 122 int oldCapacity = updateDescriptors.length; 123 if (updateCount == oldCapacity) { 124 int newCapacity = oldCapacity + INITIAL_PENDING_UPDATE_SIZE; 125 int[] newDescriptors = new int[newCapacity]; 126 System.arraycopy(updateDescriptors, 0, newDescriptors, 0, oldCapacity); 127 updateDescriptors = newDescriptors; 128 } 129 updateDescriptors[updateCount++] = fd; 130 131 // events are stored as bytes for efficiency reasons 132 byte b = (byte)mask; 133 assert (b == mask) && (b != KILLED); 134 // 存储事件 135 setUpdateEvents(fd, b, false); 136 } 137 } 138 139 private void setUpdateEvents(int fd, byte events, boolean force) { 140 if (fd < MAX_UPDATE_ARRAY_SIZE) { 141 if ((eventsLow[fd] != KILLED) || force) { 142 eventsLow[fd] = events; 143 } 144 } else { 145 Integer key = Integer.valueOf(fd); 146 if (!isEventsHighKilled(key) || force) { 147 eventsHigh.put(key, Byte.valueOf(events)); 148 } 149 } 150 } 151}
到 setUpdateEvents 这个方法,整个调用栈就结束了。但是我们并未在调用栈中看到调用 epoll_ctl 函数的地方,也就是说,通道注册时,并不会立即调用 epoll_ctl,而是先将事件集合 events 存放在 eventsLow。至于 epoll_ctl 函数何时调用的,需要大家继续往下看了。
2.4 选择过程
2.4.1 选择方法
Selector 包含3种不同功能的选择方法,分别如下:
- int select()
- int select(long timeout)
- int selectNow()
select() 是一个阻塞方法,仅在至少一个通道处于就绪状态时才返回。 select(long timeout) 同样也是阻塞方法,不过可对该方法设置超时时间(timeout > 0),使得线程不会被一直阻塞。如果 timeout = 0,会一直阻塞线程。 selectNow() 为非阻塞方法,调用后立即返回。
以上3个方法均返回 int 类型值,表示每次调用 select 或 selectNow 方法后,新就绪通道的数量。如果某个通道在上一次调用 select 方法时就已经处于就绪状态,但并未将该通道对应的 SelectionKey 对象从 selectedKeys 集合中移除。假设另一个的通道在本次调用 select 期间处于就绪状态,此时,select 返回1,而不是2。
2.4.2 选择过程
选择方法用起来虽然简单,但方法之下隐藏的逻辑还是比较复杂的。大致分为下面几个步骤:
- 检查已取消键集合 cancelledKeys 是否为空,不为空则将 cancelledKeys 的键从 keys 和 selectedKeys 中移除,并将键和通道注销。
- 调用操作系统的 epoll_ctl 函数将通道感兴趣的事件注册到 epoll 实例中
- 调用操作系统的 epoll_wait 函数监听事件
- 再次执行步骤1
- 更新 selectedKeys 集合,并返回就绪通道数量
上面五个步骤对应于 EPollSelectorImpl 类中 doSelect 方法的逻辑,如下:
1protected int doSelect(long timeout) throws IOException { 2 if (closed) 3 throw new ClosedSelectorException(); 4 // 处理已取消键集合,对应步骤1 5 processDeregisterQueue(); 6 try { 7 begin(); 8 // select 方法的核心,对应步骤2和3 9 pollWrapper.poll(timeout); 10 } finally { 11 end(); 12 } 13 // 处理已取消键集合,对应步骤4 14 processDeregisterQueue(); 15 16 // 更新 selectedKeys 集合,并返回就绪通道数量,对应步骤5 17 int numKeysUpdated = updateSelectedKeys(); 18 if (pollWrapper.interrupted()) { 19 // Clear the wakeup pipe 20 pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0); 21 synchronized (interruptLock) { 22 pollWrapper.clearInterrupted(); 23 IOUtil.drain(fd0); 24 interruptTriggered = false; 25 } 26 } 27 return numKeysUpdated; 28}
接下来,我们按照上面的步骤顺序去分析代码实现。先来看看步骤1对应的代码:
1+----SelectorImpl.java 2void processDeregisterQueue() throws IOException { 3 // Precondition: Synchronized on this, keys, and selectedKeys 4 Set<SelectionKey> cks = cancelledKeys(); 5 synchronized (cks) { 6 if (!cks.isEmpty()) { 7 Iterator<SelectionKey> i = cks.iterator(); 8 // 遍历 cancelledKeys,执行注销操作 9 while (i.hasNext()) { 10 SelectionKeyImpl ski = (SelectionKeyImpl)i.next(); 11 try { 12 // 执行注销逻辑 13 implDereg(ski); 14 } catch (SocketException se) { 15 throw new IOException("Error deregistering key", se); 16 } finally { 17 i.remove(); 18 } 19 } 20 } 21 } 22} 23 24+----EPollSelectorImpl.java 25protected void implDereg(SelectionKeyImpl ski) throws IOException { 26 assert (ski.getIndex() >= 0); 27 SelChImpl ch = ski.channel; 28 int fd = ch.getFDVal(); 29 // 移除 fd 和选择键键的映射关系 30 fdToKey.remove(Integer.valueOf(fd)); 31 // 从 epoll 实例中删除事件 32 pollWrapper.remove(fd); 33 ski.setIndex(-1); 34 35 // 从 keys 和 selectedKeys 中移除选择键 36 keys.remove(ski); 37 selectedKeys.remove(ski); 38 39 // 注销选择键 40 deregister((AbstractSelectionKey)ski); 41 42 // 注销通道 43 SelectableChannel selch = ski.channel(); 44 if (!selch.isOpen() && !selch.isRegistered()) 45 ((SelChImpl)selch).kill(); 46}
上面的代码代码逻辑不是很复杂,首先是获取 cancelledKeys 集合,然后遍历集合,并对每个选择键及其对应的通道执行注销操作。接下来再来看看步骤2和3对应的代码,如下:
1+----EPollArrayWrapper.java 2int poll(long timeout) throws IOException { 3 // 调用 epoll_ctl 函数注册事件,对应步骤3 4 updateRegistrations(); 5 6 // 调用 epoll_wait 函数等待事件发生,对应步骤4 7 updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd); 8 for (int i=0; i<updated; i++) { 9 if (getDescriptor(i) == incomingInterruptFD) { 10 interruptedIndex = i; 11 interrupted = true; 12 break; 13 } 14 } 15 return updated; 16} 17 18/** 19 * Update the pending registrations. 20 */ 21private void updateRegistrations() { 22 synchronized (updateLock) { 23 int j = 0; 24 while (j < updateCount) { 25 // 获取 fd 和 events,这两个值在调用 register 方法时被存储到数组中 26 int fd = updateDescriptors[j]; 27 short events = getUpdateEvents(fd); 28 boolean isRegistered = registered.get(fd); 29 int opcode = 0; 30 31 if (events != KILLED) { 32 // 确定 opcode 的值 33 if (isRegistered) { 34 opcode = (events != 0) ? EPOLL_CTL_MOD : EPOLL_CTL_DEL; 35 } else { 36 opcode = (events != 0) ? EPOLL_CTL_ADD : 0; 37 } 38 if (opcode != 0) { 39 // 注册事件 40 epollCtl(epfd, opcode, fd, events); 41 // 设置 fd 的注册状态 42 if (opcode == EPOLL_CTL_ADD) { 43 registered.set(fd); 44 } else if (opcode == EPOLL_CTL_DEL) { 45 registered.clear(fd); 46 } 47 } 48 } 49 j++; 50 } 51 updateCount = 0; 52 } 53 54 // 下面两个均是 native 方法 55 private native void epollCtl(int epfd, int opcode, int fd, int events); 56 private native int epollWait(long pollAddress, int numfds, long timeout, int epfd) throws IOException; 57}
看到 updateRegistrations 方法的实现,大家现在知道 epoll_ctl 这个函数是在哪里调用的了。在 3.2 节通道注册的结尾给大家埋了一个疑问,这里就是答案了。注册通道实际上只是先将事件收集起来,等调用 select 方法时,在一起通过 epoll_ctl 函数将事件注册到 epoll 实例中。
上面 epollCtl 和 epollWait 方法是 native 类型的,接下来我们再来看看这两个方法是如何实现的。如下:
1+----EPollArrayWrapper.c 2JNIEXPORT void JNICALL Java_sun_nio_ch_EPollArrayWrapper_epollCtl(JNIEnv *env, jobject this, jint epfd, jint opcode, jint fd, jint events) { 3 struct epoll_event event; 4 int res; 5 6 event.events = events; 7 event.data.fd = fd; 8 9 // 调用 epoll_ctl 注册事件 10 RESTARTABLE(epoll_ctl(epfd, (int)opcode, (int)fd, &event), res); 11 12 if (res < 0 && errno != EBADF && errno != ENOENT && errno != EPERM) { 13 JNU_ThrowIOExceptionWithLastError(env, "epoll_ctl failed"); 14 } 15} 16 17JNIEXPORT jint JNICALL Java_sun_nio_ch_EPollArrayWrapper_epollWait(JNIEnv *env, jobject this, jlong address, jint numfds, jlong timeout, jint epfd) { 18 struct epoll_event *events = jlong_to_ptr(address); 19 int res; 20 21 if (timeout <= 0) { /* Indefinite or no wait */ 22 // 调用 epoll_wait 等待事件 23 RESTARTABLE(epoll_wait(epfd, events, numfds, timeout), res); 24 } else { /* Bounded wait; bounded restarts */ 25 res = iepoll(epfd, events, numfds, timeout); 26 } 27 28 if (res < 0) { 29 JNU_ThrowIOExceptionWithLastError(env, "epoll_wait failed"); 30 } 31 return res; 32}
上面的C代码没什么复杂的逻辑,这里就不多说了。如果大家对 epoll_ctl 和 epoll_wait 函数不了解,可以参考 Linux man-page。关于 epoll 的示例,也可以参考我的另一篇文章“基于epoll实现简单的web服务器”。
说完步骤2和3对应的代码,接下来再来说说步骤4和5。由于步骤4和步骤1是一样的,这里不再赘述。最后再来说说步骤5的逻辑。代码如下:
1+----EPollSelectorImpl.java 2private int updateSelectedKeys() { 3 int entries = pollWrapper.updated; 4 int numKeysUpdated = 0; 5 for (int i=0; i<entries; i++) { 6 /* 从 pollWrapper 成员变量的 pollArray 中获取文件描述符, 7 * pollArray 中的数据由 epoll_wait 设置 8 */ 9 int nextFD = pollWrapper.getDescriptor(i); 10 SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD)); 11 // ski is null in the case of an interrupt 12 if (ski != null) { 13 // 从 pollArray 中获取就绪事件集合 14 int rOps = pollWrapper.getEventOps(i); 15 16 /* 如果 selectedKeys 已包含选择键,则选择键必须由新的事件发生时, 17 * 才会将 numKeysUpdated + 1 18 */ 19 if (selectedKeys.contains(ski)) { 20 if (ski.channel.translateAndSetReadyOps(rOps, ski)) { 21 numKeysUpdated++; 22 } 23 } else { 24 // 转换并设置就绪事件集合 25 ski.channel.translateAndSetReadyOps(rOps, ski); 26 if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) { 27 // 更新 selectedKeys 集合,并将 numKeysUpdated + 1 28 selectedKeys.add(ski); 29 numKeysUpdated++; 30 } 31 } 32 } 33 } 34 35 // 返回 numKeysUpdated 36 return numKeysUpdated; 37} 38 39+----SocketChannelImpl.java 40public boolean translateReadyOps(int ops, int initialOps, SelectionKeyImpl sk) { 41 int intOps = sk.nioInterestOps(); // Do this just once, it synchronizes 42 int oldOps = sk.nioReadyOps(); 43 int newOps = initialOps; 44 45 if ((ops & PollArrayWrapper.POLLNVAL) != 0) { 46 return false; 47 } 48 49 if ((ops & (PollArrayWrapper.POLLERR 50 | PollArrayWrapper.POLLHUP)) != 0) { 51 newOps = intOps; 52 sk.nioReadyOps(newOps); 53 // No need to poll again in checkConnect, 54 // the error will be detected there 55 readyToConnect = true; 56 return (newOps & ~oldOps) != 0; 57 } 58 59 /* 60 * 转换事件 61 */ 62 if (((ops & PollArrayWrapper.POLLIN) != 0) && 63 ((intOps & SelectionKey.OP_READ) != 0) && 64 (state == ST_CONNECTED)) 65 newOps |= SelectionKey.OP_READ; 66 67 if (((ops & PollArrayWrapper.POLLCONN) != 0) && 68 ((intOps & SelectionKey.OP_CONNECT) != 0) && 69 ((state == ST_UNCONNECTED) || (state == ST_PENDING))) { 70 newOps |= SelectionKey.OP_CONNECT; 71 readyToConnect = true; 72 } 73 74 if (((ops & PollArrayWrapper.POLLOUT) != 0) && 75 ((intOps & SelectionKey.OP_WRITE) != 0) && 76 (state == ST_CONNECTED)) 77 newOps |= SelectionKey.OP_WRITE; 78 79 // 设置事件 80 sk.nioReadyOps(newOps); 81 82 // 如果新的就绪事件和老的就绪事件不相同,则返回true,否则返回 false 83 return (newOps & ~oldOps) != 0; 84}
上面就是步骤5的逻辑了,简单总结一下。首先是获取就绪通道数量,然后再获取这些就绪通道对应的文件描述符 fd,以及就绪事件集合 rOps。之后调用 translateAndSetReadyOps 转换并设置就绪事件集合。最后,将选择键添加到 selectedKeys 集合中,并累加 numKeysUpdated 值,之后返回该值。
以上就是选择过程的代码讲解,贴了不少代码,可能不太好理解。Java NIO 和操作系统接口关联比较大,所以在学习 NIO 相关原理时,也应该去了解诸如 epoll 等系统调用的知识。没有这些背景知识,很多东西看起来不太好懂。好了,本节到此结束。
2.5 模板代码
使用 NIO 选择器编程时,主干代码的结构一般比较固定。所以把主干代码写好后,就可以往里填业务代码了。下面贴一个服务端的模板代码,如下:
1ServerSocketChannel ssc = ServerSocketChannel.open(); 2ssc.socket().bind(new InetSocketAddress("localhost", 8080)); 3ssc.configureBlocking(false); 4 5Selector selector = Selector.open(); 6ssc.register(selector, SelectionKey.OP_ACCEPT); 7 8while(true) { 9 int readyNum = selector.select(); 10 if (readyNum == 0) { 11 continue; 12 } 13 14 Set<SelectionKey> selectedKeys = selector.selectedKeys(); 15 Iterator<SelectionKey> it = selectedKeys.iterator(); 16 17 while(it.hasNext()) { 18 SelectionKey key = it.next(); 19 20 if(key.isAcceptable()) { 21 // 接受连接 22 } else if (key.isReadable()) { 23 // 通道可读 24 } else if (key.isWritable()) { 25 // 通道可写 26 } 27 28 it.remove(); 29 } 30}
2.6 实例演示
原本打算将示例演示的代码放在本节中展示,奈何文章篇幅已经很大了,所以决定把本节的内容独立成文。在下一篇文章中,我将会演示使用 Java NIO 完成一个简单的 HTTP 服务器。这里先贴张效果图,如下:

3.总结
到这里,本文差不多就要结束了。原本只是打算简单说说 Selector 的用法,然后再写一份实例代码。但是后来发现这样写显得比较空洞,没什么深度。所以后来翻了一下 Selector 的源码,大致理解了 Selector 的逻辑,然后就有了上面的分析。不过 Selector 的逻辑并不止我上面所说的那些,还有一些内容我现在还没看,所以就没有讲。对于已写出来的分析,由于我个人水平有限,难免会有错误。如果有错误,也欢迎大家指出来,共同进步!
好了,本文到此结束,感谢大家的阅读。
参考
附录
文中贴的一些代码是没有包含在 JDK src.zip 包里的,这里单独列举出来,方便大家查找。
文件名
路径
DefaultSelectorProvider.java
jdk/src/solaris/classes/sun/nio/ch/DefaultSelectorProvider.java
EPollSelectorProvider.java
jdk/src/solaris/classes/sun/nio/ch/EPollSelectorProvider.java
SelectorImpl.java
jdk/src/share/classes/sun/nio/ch/SelectorImpl.java
EPollSelectorImpl.java
jdk/src/solaris/classes/sun/nio/ch/EPollSelectorImpl.java
EPollArrayWrapper.java
jdk/src/solaris/classes/sun/nio/ch/EPollArrayWrapper.java
SelectionKeyImpl.java
jdk/src/share/classes/sun/nio/ch/SelectionKeyImpl.java
SocketChannelImpl.java
jdk/src/share/classes/sun/nio/ch/SocketChannelImpl.java
EPollArrayWrapper.c
jdk/src/solaris/native/sun/nio/ch/EPollArrayWrapper.c
本文在知识共享许可协议 4.0 下发布,转载需在明显位置处注明出处 作者:coolblog 本文同步发布在我的个人博客:http://www.coolblog.xyz/?r=cb
<a rel="license" href="http://creativecommons.org/licenses/by-nc-nd/4.0/">
</a><br />本作品采用<a rel="license" href="http://creativecommons.org/licenses/by-nc-nd/4.0/">知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议</a>进行许可。