NIO-Selector

Posted jack-blog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NIO-Selector相关的知识,希望对你有一定的参考价值。



NIO-Selector

目录

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

前言

本来是想学习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。继承关系如下图所示。
技术图片

接下里我们讨论一下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源码分析(一)

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

以上是关于NIO-Selector的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段——CSS选择器

谷歌浏览器调试jsp 引入代码片段,如何调试代码片段中的js

片段和活动之间的核心区别是啥?哪些代码可以写成片段?

VSCode自定义代码片段——.vue文件的模板

VSCode自定义代码片段6——CSS选择器

VSCode自定义代码片段——声明函数