NIO-Selector源码分析


[TOC]html

前言

原本是想学习Netty的,可是Netty是一个NIO框架,所以在学习netty以前,仍是先梳理一下NIO的知识。经过剖析源码理解NIO的设计原理。java

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

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

什么是Selector

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

关于I/O模型相关知识能够查看《高性能网络通信原理》windows

为了能知道哪些链接已就绪,在一开始咱们须要定时轮询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实现不同。能够在jdk\src\[macosx|windows|solaris]\classes\sun\nio\ch找到实现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,能够在jdk\src\[macosx|windows|solaris|share]\classes\sun\nio\ch找到实现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

  • 微信扫一扫二维码关注订阅号杰哥技术分享
  • 出处:www.cnblogs.com/Jack-Blog/p…
  • 做者:杰哥很忙
  • 本文使用「CC BY 4.0」创做共享协议。欢迎转载,请在明显位置给出出处及连接。
相关文章
相关标签/搜索