NIO网络编程底层逻辑分析

背景

最近在复习Java NIO的网络编程部分,针对NIO的多路复用,产生了以下疑问:

  1. ServerSocketChannel如何被创建的
  2. Selector是如何创建的
  3. Channel注册到Selector上的背后,执行了哪些操作;注册的SelectionKey.OP_ACCEPT在哪里起到作用的
  4. selector.select()为什么会阻塞
  5. selector的selectKeys是如何被赋值的
  6. 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();
    }
}
流程分析
  1. 创建ServerSocketChannel,设置为非阻塞状态

    • 获取ServerSocketChannel是通过SelectorProvider.provider().openServerSocketChannel()方法
    • SelectorProvider.provider()的过程,首先从系统变量查看是否有java.nio.channels.spi.SelectorProvider对应的值,若有,则通过反射创建,若无,进行下一步
    • 尝试通过SPI方式获取,若成功直接返回,若不成功,则进行下一步
    • 通过sun.nio.ch.DefaultSelectorProvider.create()方式获取,在Windows环境下,直接创建了一个sun.nio.ch.WindowsSelectorProvider返回
  2. 创建Selector

    • 获取ServerSocketChannel是通过SelectorProvider.provider().openSelector()方法
    • 获取SelectorProvider方式,同上
  3. 将ServerSocketChannel 注册到selector上,并指定关注的事件

    • 通过java.nio.channels.SelectableChannel#register(java.nio.channels.Selector, int ops, java.lang.Object)方法,注册到select上;注册的ops值,可以直接从SelectionKey中获取
    • 此时也可以直接传入ops值为0,表示不关注channel上任意事件,后续通过SelectionKey的interestOps方法,注册关注的事件。
  4. 通过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 代表如下:

      1. 参数readFds,接收关注:SelectionKey.OP_ACCEPT事件
      2. 参数writeFds,不接收此事件
      3. 参数exceptFds,代表OOB数据
    • poll0返回后, 通过sun.nio.ch.SelectorImpl#processReadyEvents来将selectionKey添加到Selector的selectedKey集合中。

  5. select执行返回影响selectedKey的数量

    • 此时获取到selector中selectdKey集合的大小
  6. selector.selectedKeys获取SelectionKey集合

    • selectionKeys的值,在select方法中被赋值,最终的放置值的方法为sun.nio.ch.SelectorImpl#processReadyEvents,可通过断点调试
  7. ServerSocketChannel方法在执行accept时,主要执行以下内容

    • 创建新的文件描述符,以及网络监听端口
    • 根据文件描述符和端口,创建SocketChannel
  8. 遍历selectedKeys集合,判断selectedKey上发生的事件,针对事件进行下一步的业务处理

结论

结合以上分析流程,可得出以下结论:

  1. ServerSocketChannel创建过程,在Windows环境下,直接调用SelectorProvider.provider()获取SelectorProvider,然后通过SelectorProvider 对应service提供的方法创建
  2. Selector的创建过程,同ServerSocketChannel
  3. Channel注册到Selector上,其实质是将创建一个SelectionKey实例,其中保存了Channel和selector两个实例;然后将SelectionKey,保存到AbstractSelectableChannel对应的属性keys中;同一个Channel可以同时注册多次selector,传入对应的interestOps操作。
  4. selector.select()在Windows环境下,最终调用了native方法sun.nio.ch.WindowsSelectorImpl.SubSelector#poll0,此方法执行了多路监听
  5. selector的selectedKeys是在poll0方法返回后,从其传回的参数值中,通过sun.nio.ch.SelectorImpl#processReadyEvents方法将影响的selectionKey添加到selectedKeys中
  6. ServerSocketChannel在执行accept方法时,创建了新的fd文件描述符,并启动本地网络监听端口,再创建SocketChannelImpl
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用 Java NIO(New I/O)进行网络编程的简单示例: ```java import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class NIOExample { public static void main(String[] args) throws IOException { // 创建一个线程池用于处理客户端连接 ExecutorService executor = Executors.newFixedThreadPool(10); // 创建 ServerSocketChannel 并绑定端口 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 8080)); System.out.println("Server started on port 8080"); while (true) { // 接受客户端连接 SocketChannel socketChannel = serverSocketChannel.accept(); // 使用线程池处理客户端连接 executor.execute(() -> handleClient(socketChannel)); } } private static void handleClient(SocketChannel socketChannel) { try { ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取客户端发送的数据 int bytesRead = socketChannel.read(buffer); while (bytesRead != -1) { buffer.flip(); while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); } buffer.clear(); bytesRead = socketChannel.read(buffer); } // 响应客户端 String response = "Hello from server"; ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes()); socketChannel.write(responseBuffer); // 关闭连接 socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } ``` 这个示例创建了一个简单的服务器,监听本地的 8080 端口。当客户端连接时,会使用线程池处理连接,并读取客户端发送的数据。然后,服务器会向客户端发送 "Hello from server" 的响应,并关闭连接。 请注意,这只是一个简单的示例,实际的网络编程可能涉及更复杂的逻辑和处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值