• NIO-Selector源码分析




    NIO-Selector源码分析

    目录

    NIO-概览
    NIO-Buffer
    NIO-Channel
    NIO-Channel接口分析
    NIO-SocketChannel源码分析
    NIO-FileChannel源码分析
    NIO-Selector源码分析
    NIO-WindowsSelectorImpl源码分析
    NIO-EPollSelectorIpml源码分析

    前言

    本来是想学习Netty的,但是Netty是一个NIO框架,因此在学习netty之前,还是先梳理一下NIO的知识。通过剖析源码理解NIO的设计原理。

    本系列文章针对的是JDK1.8.0.161的源码。

    前几篇文章对Buffer和Channel的源码的常用功能进行了研究,本篇将对Selector源码进行解析。

    什么是Selector

    在网络传输时,客户端不定时的会与服务端进行连接,而在高并发场景中,大多数连接实际上是空闲的。因此为了提高网络传输高并发的性能,就出现各种I/O模型从而优化CPU处理效率。不同选择器实现了不同的I/O模型算法。同步I/O在linux上有EPoll模型,mac上有KQueue模型,windows上则为select模型。

    关于I/O模型相关知识可以查看《高性能网络通讯原理》

    为了能知道哪些连接已就绪,在一开始我们需要定时轮询Socket是否有接收到新的连接,同时我们还要监控是否接收到已建立连接的数据,由于大多数情况下大多数网络连接实际是空闲的,因此每次都遍历所有的客户端,那么随着并发量的增加,性能开销也是呈线性增长。

    有了Selector,我们可以让它帮我们做"监控"的动作,而当它监控到连接接收到数据时,我们只要去将数据读取出来即可,这样就大大提高了性能。要Selector帮我们做“监控”动作,那么我们需要告知它需要监控哪些Channel

    注意,只有网络通讯的时候才需要通过Selector监控通道。从代码而言,Channel必须继承AbstractSelectableChannel

    创建Selector

    首先我们需要通过静态方法Selector.open()从创建一个Selector

    Selector selector = Selector.open();
    

    需要注意的是,Channel必须是非阻塞的,我们需要手动将Channel设置为非阻塞。调用Channel的实例方法SelectableChannel.configureBlocking(boolean block)

    注册通道

    需要告诉Selector监控哪些Channel,通过channel.register将需要监控的通道注册到Selector

    注册是在AbstractSelectableChannel中实现的,当新的通道向Selector注册时会创建一个SelectionKey,并将其保存到 SelectionKey[] keys缓存中。

    
    public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException
    {
        synchronized (regLock) {
            if (!isOpen())
                throw new ClosedChannelException();
                //当前Channel是否支持操作
            if ((ops & ~validOps()) != 0)
                throw new IllegalArgumentException();
                //阻塞不支持
            if (blocking)
                throw new IllegalBlockingModeException();
            SelectionKey k = findKey(sel);
            if (k != null) {
                //已经存在,则将其注册支持的操作
                k.interestOps(ops);
                //保存参数
                k.attach(att);
            }
            if (k == null) {
                // New registration
                synchronized (keyLock) {
                    if (!isOpen())
                        throw new ClosedChannelException();
                    //注册
                    k = ((AbstractSelector)sel).register(this, ops, att);
                    //添加到缓存
                    addKey(k);
                }
            }
            return k;
        }
    }
    
    

    新的SelectionKey会调用到AbstractSelector.register,首先会先创建一个SelectionKeyImpl,然后调用方法implRegister执行实际注册,该功能是在各个平台的SelectorImpl的实现类中做具体实现。

    k = ((AbstractSelector)sel).register(this, ops, att);
    protected final SelectionKey register(AbstractSelectableChannel ch, int ops, Object attachment)
    {
        if (!(ch instanceof SelChImpl))
            throw new IllegalSelectorException();
            //创建SelectionKey
        SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
        k.attach(attachment);
        synchronized (publicKeys) {
            //注册
            implRegister(k);
        }
        //设置事件
        k.interestOps(ops);
        return k;
    }
    

    创建了SelectionKey后就将他加入到keys的缓存中,当keys缓存不足时,扩容两倍大小。

    private void addKey(SelectionKey k) {
        assert Thread.holdsLock(keyLock);
        int i = 0;
        if ((keys != null) && (keyCount < keys.length)) {
            // Find empty element of key array
            for (i = 0; i < keys.length; i++)
                if (keys[i] == null)
                    break;
        } else if (keys == null) {
            keys =  new SelectionKey[3];
        } else {
            // 扩容两倍大小
            int n = keys.length * 2;
            SelectionKey[] ks =  new SelectionKey[n];
            for (i = 0; i < keys.length; i++)
                ks[i] = keys[i];
            keys = ks;
            i = keyCount;
        }
        keys[i] = k;
        keyCount++;
    }
    
    

    SelectorProvider

    在讨论Selector如何工作之前,我们先看一下Selector是如何创建的。我们通过Selector.open()静态方法创建了一个Selector。内部实际是通过SelectorProvider.openSelector()方法创建Selector

    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    

    创建SelectorProvider

    通过SelectorProvider.provider()静态方法,获取到SelectorProvider,首次获取时会通过配置等方式注入,若没有配置,则使用DefaultSelectorProvider生成。

    public static SelectorProvider provider() {
        synchronized (lock) {
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            //通过配置的java.nio.channels.spi.SelectorProvider值注入自定义的SelectorProvider
                            if (loadProviderFromProperty())
                                return provider;
                            //通过ServiceLoad注入,然后获取配置的第一个服务
                            if (loadProviderAsService())
                                return provider;
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }
    

    若我们没有做特殊配置,则会使用默认的DefaultSelectorProvider创建SelectorProvider
    不同平台的DefaultSelectorProvider实现不一样。可以在jdksrc[macosx|windows|solaris]classessun ioch找到实现DefaultSelectorProvider.java。下面是SelectorProvider的实现。

    //windows
    public class DefaultSelectorProvider {
        private DefaultSelectorProvider() { }
        public static SelectorProvider create() {
            return new sun.nio.ch.WindowsSelectorProvider();
        }
    }
    //linux
    public class DefaultSelectorProvider {
    
        private DefaultSelectorProvider() { }
    
        @SuppressWarnings("unchecked")
        private static SelectorProvider createProvider(String cn) {
            Class<SelectorProvider> c;
            try {
                c = (Class<SelectorProvider>)Class.forName(cn);
            } catch (ClassNotFoundException x) {
                throw new AssertionError(x);
            }
            try {
                return c.newInstance();
            } catch (IllegalAccessException | InstantiationException x) {
                throw new AssertionError(x);
            }
        }
    
        public static SelectorProvider create() {
            String osname = AccessController
                .doPrivileged(new GetPropertyAction("os.name"));
            if (osname.equals("SunOS"))
                return createProvider("sun.nio.ch.DevPollSelectorProvider");
            if (osname.equals("Linux"))
                return createProvider("sun.nio.ch.EPollSelectorProvider");
            return new sun.nio.ch.PollSelectorProvider();
        }
    
    }
    

    创建Selector

    获取到SelectorProvider后,创建Selector了。通过SelectorProvider.openSelector()实例方法创建一个Selector

    //windows
    public class WindowsSelectorProvider extends SelectorProviderImpl {
    
        public AbstractSelector openSelector() throws IOException {
            return new WindowsSelectorImpl(this);
        }
    }
    //linux
    public class EPollSelectorProvider
        extends SelectorProviderImpl
    {
        public AbstractSelector openSelector() throws IOException {
            return new EPollSelectorImpl(this);
        }
        ...
    }
    

    windows下创建了WindowsSelectorImpl,linux下创建了EPollSelectorImpl

    所有的XXXSelectorImpl都继承自SelectorImpl,可以在jdksrc[macosx|windows|solaris|share]classessun ioch找到实现XXXSelectorImpl.java。继承关系如下图所示。
    20200102205431.png

    接下里我们讨论一下Selector提供的主要功能,后面在分析Windows和Linux下Selector的具体实现。

    SelectorImpl

    在创建SelectorImpl首先会初始化2个HashSet,publicKeys存放用于一个存放所有注册的SelectionKey,selectedKeys用于存放已就绪的SelectionKey。

    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);
        }
    }
    

    关于Util.atBugLevel找到一篇文章有提到该方法。似乎是和EPoll的一个空指针异常相关。这个bug在nio bugLevel=1.4版本引入,这个bug在jdk1.5中存在,直到jdk1.7才修复。

    前面我们已经向Selector注册了通道,现在我们需要调用Selector.select()实例方法从系统内存中加载已就绪的文件描述符。

    
    public int select() throws IOException {
        return select(0);
    }
    public int select(long timeout)
        throws IOException
    {
        if (timeout < 0)
            throw new IllegalArgumentException("Negative timeout");
        return lockAndDoSelect((timeout == 0) ? -1 : timeout);
    }
    
    private int lockAndDoSelect(long timeout) throws IOException {
        synchronized (this) {
            if (!isOpen())
                throw new ClosedSelectorException();
            synchronized (publicKeys) {
                synchronized (publicSelectedKeys) {
                    return doSelect(timeout);
                }
            }
        }
    }
    protected abstract int doSelect(long timeout) throws IOException;
    

    最终会调用具体SelectorImpldoSelect,具体内部主要执行2件事

    1. 调用native方法获取已就绪的文件描述符。
    2. 调用updateSelectedKeys更新已就绪事件的SelectorKey

    当获取到已就绪的SelectionKey后,我们就可以遍历他们。根据SelectionKey的事件类型决定需要执行的具体逻辑。

    //获取到已就绪的Key进行遍历
    Set<SelectionKey> selectKeys = selector.selectedKeys();
    Iterator<SelectionKey> it = selectKeys.iterator();
    while (it.hasNext()) {
        SelectionKey key = it.next();
        //处理事件。
        if(key.isAcceptable()){
            doAccept(key);
        }
        else if(key.isReadable())
        {
            doRead(key);
        }
        ...
        it.remove();
    }
    

    总结

    本文对SelectorSelectorProvider的创建进行分析,总的流程可以参考下图

    对于后面步骤的EpollArrayWarpper()会在SelectorImpl个平台具体实现进行讲解。后面会分2对WindowsSelectorImplEpollSelectorImpl进行分析。

    相关文献

    1. ServiceLoader详解
    2. SelectorImpl分析
    3. NIO源码分析(一)

    20191127212134.png
    微信扫一扫二维码关注订阅号杰哥技术分享
    出处:https://www.cnblogs.com/Jack-Blog/p/12367953.html
    作者:杰哥很忙
    本文使用「CC BY 4.0」创作共享协议。欢迎转载,请在明显位置给出出处及链接。

  • 相关阅读:
    事件对象3
    事件对象2
    事件对象1
    编码、摘要、加密
    身份证号码的组成
    Oracle的TO_CHAR()格式化数字为百分数的字符串
    转载和补充:Oracle中的一些特殊字符
    linux Shell(待学)
    linux 管道相关命令(待学)
    linux用户权限、系统信息相关命令(待学)
  • 原文地址:https://www.cnblogs.com/Jack-Blog/p/12367953.html
Copyright © 2020-2023  润新知