我对java nio多路复用器的理解和源码深入剖析

本文使用的java版本是jdk8
查看本文的前提是对nio有过基本的使用了解
先附上一个server端带selector版本的使用方式

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

public class NioSelectorServer {



    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(9000));
        serverSocket.configureBlocking(false);

        // 注册到多路复用器中
        Selector sel = Selector.open();
        serverSocket.register(sel, SelectionKey.OP_ACCEPT);
        System.out.println("nio server started.");

        while (true) {

            System.out.println("to select");
            sel.select();
            Set<SelectionKey> selectionKeys = sel.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel)key.channel();
                    SocketChannel socketChannel = server.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(sel, SelectionKey.OP_READ);
                    System.out.println("a client conntect.");

                } else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel)key.channel();
                    // 同步处理
                    //handlerRead(socketChannel);

                    // 异步处理
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                handlerRead(socketChannel);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();

                }
                iterator.remove();
            }




        }
    }

    private static void handlerRead(SocketChannel socketChannel) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        int len = socketChannel. read(byteBuffer);

        if (len > 0) {
            String data = new String(byteBuffer.array());
            System.out.println("receive msg:" + data);

            if (data.startsWith("88\r\n")) {
                socketChannel.write(ByteBuffer.wrap("886".getBytes(StandardCharsets.UTF_8)));
                socketChannel.close();
                System.out.println("a client close.");
            } else {
                socketChannel.write(ByteBuffer.wrap("hello client".getBytes(StandardCharsets.UTF_8)));
            }

        } else if (len == -1) {
            System.out.println("a client close.");
            socketChannel.close();
        }
    }
}

我们知道java nio中的关键对象是多路复用器selector,那么它是如何实现多路复用的呢,我们来扒一扒
先说说关键方法
Selector.open()
在这里插入图片描述

我们跟进去会发现调用了SelectorProvider.provider()方法去获取一个provider
在这里插入图片描述
再跟进去会看到调用了sun.nio.ch.DefaultSelectorProvider.create()
在这里插入图片描述
再跟进去会发现创建了一个KQueueSectorProvider(),笔者用的是mac,所以看到创建了一个KQueueSectorProvider(),但是实际上产环境我们一般都是在linux系统中运行,所以这里应该找一下linux下的实现类是哪个。
笔者这里只告诉大家如何去找到对应的实现类,我们可以去官网把openjdk的源码拉下来,然后导入到ide中,源码项目结构是这样子的
在这里插入图片描述
linux系统对应的是solaris包,我们可以看到linux版本jdk,DefaultSelectorProvider.create(),创建的是EPollSelectorProvider, 它的openSelector()方法创建的是EPollSelectorImpl
在这里插入图片描述
EPollSelectorImpl初始化的时候初始化了pollWrapper是EpollArrayWrapper在这里插入图片描述
我们再跟进去看看EpollArrayWrapper是如何初始化的
在这里插入图片描述
这里我们看到很关键的一行代码调用epollCreate(), 实际上是调用了native方法创建了一个epoll 文件描述符,我们再看看该native方法的实现。
这里科普一个native方法的查找方式,用我们java中的类签名 + 下划线 + native方法名例如我们现在要找的这个方法可以在jdk源码中搜索Java_sun_nio_ch_EPollArrayWrapper_epollCreate,后面就不再阐述了。
在这里插入图片描述
这里我们又看到了很关键的一行代码epoll_create(256),这个函数实际上是linux系统提供的一个系统库,我们可以看到上面导入了系统的epoll.h
在这里插入图片描述
这里我们可以借助linux系统的man命令查看该函数的使用说明(如果man命令找不到对应的函数的话,可以尝试安装man-pages试试 yum install -y man-pages)

man epoll_create

在这里插入图片描述
可以看到该命令为我们打开了一个epoll文件描述符
小结:
跟到这一步我们就知道Selector.open()方法实际上是帮我们调用了系统库创建了一个epoll,并返回了EPollSelectorImpl实例,在这个实例中实际操作epoll的是pollWrapper

接下来我们来看一下我们写的server第二行关键代码
sel.select();
在这里插入图片描述

我们知道现在selector实例是EPollSelectorImpl它继承了SelectorImpl,最终会调用到EPollSelectorImpl的doSelect方法
在这里插入图片描述
这里又调用了pollWrapper的poll方法,我们可以大胆猜测,这里就要去操作epoll了
在这里插入图片描述
果然这里又调用了一个native 的 epollCtl方法,并且把epoll的文件描述符, 操作类型,socketChannel以及感兴趣的事件作为入参传入进去,这里初次调用操作类型是EPOLL_CTL_ADD 我们来看看这个native方法
在这里插入图片描述
这里又调用了系统函数 epoll_ctl 我们来看看这个函数是干嘛的

man epoll_ctl

在这里插入图片描述
根据描述可知,我们本次调用的EPOLL_CTL_ADD是把socketChannel注册到epoll上,并关联相关的事件(这里注册的是serverSocket所以感兴趣的事件是OP_ACCEPT,也就是对建立连接事件感兴趣)

随后又调用了native 的 epollWait函数,把我们上面分配的pollArray地址,系统ulimit,超时时间还有epoll文件描述符传入进去
在这里插入图片描述
在这里插入图片描述
老规矩

man epoll_wait

在这里插入图片描述
翻译为:系统调用等待文件描述符epfd引用的epoll(7)实例上的事件。事件指向的内存区域将包含调用者可用的事件。最多maxevents由epoll wait()返回。maxevents参数必须大于零。timeout参数指定epoll wait()将阻塞的最小毫秒数。(这个间隔将被舍入到系统时钟粒度,而内核调度延迟意味着阻塞间隔可能会超出一小部分。)
笔者理解为epoll 阻塞等待rdlist中的数据,网卡接收到数据,就会发起硬中断事件通知内核,内核会回调epoll函数把数据放到rdlist中,这个回调函数是epoll_ctl创建的时候向内核注册的,当rdlist中有数据时epoll_wait就会跳出等待,然后把rdlist中的事件放到EPollArrayWrapper分配的pollArray内存区域中,随后更新selectedKeys
在这里插入图片描述
随后在我们的代码中调用 sel.selectedKeys() 就能获取到网络发生的事件进行处理。
在这里插入图片描述

总结:
java8中的多路复用器在linux系统中,实际上就是对epoll的封装,底层调用关键系统库函数

epoll_create
epoll_ctl
epoll_wait

另外sel.select()方法存在空轮询bug,不确定是否已修复,在netty中对其进行了探测和修复,具体我们在后续文章中再阐述讨论

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值