背景
最近在复习Java NIO的网络编程部分,针对NIO的多路复用,产生了以下疑问:
- ServerSocketChannel如何被创建的
- Selector是如何创建的
- Channel注册到Selector上的背后,执行了哪些操作;注册的SelectionKey.OP_ACCEPT在哪里起到作用的
- selector.select()为什么会阻塞
- selector的selectKeys是如何被赋值的
- SocketChannel是如何被创建的
基于以上疑问,写了一个Demo,单步调试详细了解NIO的网络编程中,如何工作的。本流程源码部分,参考openjdk14版本
原理分析
示例代码
示例代码使用openjdk14版本,后续分析也是基于此jdk版本进行
public static void main(String[] args) throws IOException {
log.info("server start");
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(8899));
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.forEach(selectionKey -> {
final SocketChannel client;
try {
if (selectionKey.isAcceptable()) {
ServerSocketChannel socketChannel = (ServerSocketChannel)selectionKey.channel();
client = socketChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
clientSet.add(client);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
});
selectionKeys.clear();
}
}
流程分析
-
创建ServerSocketChannel,设置为非阻塞状态
- 获取ServerSocketChannel是通过SelectorProvider.provider().openServerSocketChannel()方法
- SelectorProvider.provider()的过程,首先从系统变量查看是否有java.nio.channels.spi.SelectorProvider对应的值,若有,则通过反射创建,若无,进行下一步
- 尝试通过SPI方式获取,若成功直接返回,若不成功,则进行下一步
- 通过sun.nio.ch.DefaultSelectorProvider.create()方式获取,在Windows环境下,直接创建了一个sun.nio.ch.WindowsSelectorProvider返回
-
创建Selector
- 获取ServerSocketChannel是通过SelectorProvider.provider().openSelector()方法
- 获取SelectorProvider方式,同上
-
将ServerSocketChannel 注册到selector上,并指定关注的事件
- 通过java.nio.channels.SelectableChannel#register(java.nio.channels.Selector, int ops, java.lang.Object)方法,注册到select上;注册的ops值,可以直接从SelectionKey中获取
- 此时也可以直接传入ops值为0,表示不关注channel上任意事件,后续通过SelectionKey的interestOps方法,注册关注的事件。
-
通过select.select() 阻塞等待有客户端连接
-
在Windows环境下,select最终执行sun.nio.ch.WindowsSelectorImpl.SubSelector#poll0,监听多路复用事件。poll 方法签名如下:
private native int poll0(long pollAddress, int numfds, int[] readFds, int[] writeFds, int[] exceptFds, long timeout, long fdsBuffer)
-
poll0方法是native本地方法,当有事件发生时,将返回;并且将结果写入到poll0传入的参数中。对于不同的Channel,readFds/writeFds/exceptFds代表的含义不同,共同点是都是数组存储,并且数组的第一个原始代表变化的数量,后面的元素,代表对应Channel的FD的值,针对ServerSocketChannelImpl 代表如下:
- 参数readFds,接收关注:SelectionKey.OP_ACCEPT事件
- 参数writeFds,不接收此事件
- 参数exceptFds,代表OOB数据
-
poll0返回后, 通过sun.nio.ch.SelectorImpl#processReadyEvents来将selectionKey添加到Selector的selectedKey集合中。
-
-
select执行返回影响selectedKey的数量
- 此时获取到selector中selectdKey集合的大小
-
selector.selectedKeys获取SelectionKey集合
- selectionKeys的值,在select方法中被赋值,最终的放置值的方法为sun.nio.ch.SelectorImpl#processReadyEvents,可通过断点调试
-
ServerSocketChannel方法在执行accept时,主要执行以下内容
- 创建新的文件描述符,以及网络监听端口
- 根据文件描述符和端口,创建SocketChannel
-
遍历selectedKeys集合,判断selectedKey上发生的事件,针对事件进行下一步的业务处理
结论
结合以上分析流程,可得出以下结论:
- ServerSocketChannel创建过程,在Windows环境下,直接调用SelectorProvider.provider()获取SelectorProvider,然后通过SelectorProvider 对应service提供的方法创建
- Selector的创建过程,同ServerSocketChannel
- Channel注册到Selector上,其实质是将创建一个SelectionKey实例,其中保存了Channel和selector两个实例;然后将SelectionKey,保存到AbstractSelectableChannel对应的属性keys中;同一个Channel可以同时注册多次selector,传入对应的interestOps操作。
- selector.select()在Windows环境下,最终调用了native方法sun.nio.ch.WindowsSelectorImpl.SubSelector#poll0,此方法执行了多路监听
- selector的selectedKeys是在poll0方法返回后,从其传回的参数值中,通过sun.nio.ch.SelectorImpl#processReadyEvents方法将影响的selectionKey添加到selectedKeys中
- ServerSocketChannel在执行accept方法时,创建了新的fd文件描述符,并启动本地网络监听端口,再创建SocketChannelImpl