NIO 三大组件之Selector

一、Selector 简介

Selector 一般称其为“选择器”,在Java NIO中起着多路复用的作用。它可用于检查一个或多个Channel(通道)的状态,是否处于可读、可写。如此便可实现单个线程管理多个channels,也就可以管理多个网络链接。因此使用Selector 的好处便是使用更少的线程来就可以来处理多个通道了,相比传统I/O,这里也就避免了线程上下文切花的开销了。这里看下Selector类的代码:

public abstract class Selector implements Closeable {

    protected Selector() { }

    // 打开一个选择器
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

    public abstract boolean isOpen();

    public abstract SelectorProvider provider();

    public abstract Set<SelectionKey> keys();

    public abstract Set<SelectionKey> selectedKeys();

    public abstract int selectNow() throws IOException;

    public abstract int select(long timeout)
        throws IOException;

    public abstract int select() throws IOException;

    public abstract Selector wakeup();

    public abstract void close() throws IOException;

}

从上边的代码中,可以了解到 Selector 实现了 Closeable,表示它是可关闭的。这个类中主要关注的是函数了java.nio.channels.Selector#open,在这个函数中调用的是SelectorProvider.provider().openSelector(),首先调用的是java.nio.channels.spi.SelectorProvider#provider方法,代码如下:

public static SelectorProvider provider() {
    synchronized (lock) {
        if (provider != null)
            return provider;
        return AccessController.doPrivileged(
            new PrivilegedAction<SelectorProvider>() {
                public SelectorProvider run() {
                        // 根据条件判断来创建的provider:反射、SPI、默认provider
                        if (loadProviderFromProperty())
                            return provider;
                        if (loadProviderAsService())
                            return provider;
                        provider = sun.nio.ch.DefaultSelectorProvider.create();
                        return provider;
                    }
                });
    }
}

从上面的方法中,可以看到需要根据条件判断使用什么样的方式创建provider,这里有三种方式,分别是根据反射、SPI、默认provider。先看一下java.nio.channels.spi.SelectorProvider#loadProviderFromProperty,代码如下:

private static boolean loadProviderFromProperty() {
    String cn = System.getProperty("java.nio.channels.spi.SelectorProvider");
    if (cn == null)
        return false;
    try {
        Class<?> c = Class.forName(cn, true,
                                   ClassLoader.getSystemClassLoader());
        provider = (SelectorProvider)c.newInstance();
        return true;
    } catch (ClassNotFoundException x) {
        throw new ServiceConfigurationError(null, x);
    } catch (IllegalAccessException x) {
        throw new ServiceConfigurationError(null, x);
    } catch (InstantiationException x) {
        throw new ServiceConfigurationError(null, x);
    } catch (SecurityException x) {
        throw new ServiceConfigurationError(null, x);
    }
}

SelectorProvider#loadProviderFromProperty方法实现很简单,就是根据反射的方式创建Provider,那么继续看一下java.nio.channels.spi.SelectorProvider#loadProviderAsService方法,实现代码如下:

private static boolean loadProviderAsService() {
    // 
    ServiceLoader<SelectorProvider> sl =
        ServiceLoader.load(SelectorProvider.class,
                           ClassLoader.getSystemClassLoader());
    Iterator<SelectorProvider> i = sl.iterator();
    for (;;) {
        try {
            if (!i.hasNext())
                return false;
            provider = i.next();
            return true;
        } catch (ServiceConfigurationError sce) {
            if (sce.getCause() instanceof SecurityException) {
                // Ignore the security exception, try the next provider
                continue;
            }
            throw sce;
        }
    }
}

最后就是那个默认provider了,默认所创建的是KQueueSelectorProvider对象。

二、SelectableChannel 与 Selector

SelectableChannel 是一个抽象类,它提供了实现通道的可选择性所需要的公共方法,也是所有支持就绪检查的Channel类的父类。但不是所有的Channel都可以被Selector 复用的,譬如 FileChannel 就不能被选择器复用。判断一个Channel是否能被Selector 复用,看他是否继承了 SelectableChannel 类即可。如果继承了 SelectableChannel,则可以被复用,否则不能。首先看下SelectableChannel代码,如下:

public abstract class SelectableChannel extends AbstractInterruptibleChannel
    implements Channel {
    protected SelectableChannel() { }

    public abstract SelectorProvider provider();

    public abstract int validOps();

    public abstract boolean isRegistered();

    public abstract SelectionKey keyFor(Selector sel);

    public abstract SelectionKey register(Selector sel, int ops, Object att)
        throws ClosedChannelException;

    public final SelectionKey register(Selector sel, int ops)
        throws ClosedChannelException
    {
        return register(sel, ops, null);
    }

    public abstract SelectableChannel configureBlocking(boolean block)
        throws IOException;

    public abstract boolean isBlocking();

    public abstract Object blockingLock();

}

上面代码中的java.nio.channels.SelectableChannel#register(java.nio.channels.Selector sel, int ops)方法中所调用的java.nio.channels.spi.AbstractSelectableChannel#register,告诉我们这里是将一个Channel注册到一个给定的Selector上的(第一个参数,指定通道要注册的选择器。第二个参数指定选择器需要查询的通道操作。),然后返回一个 SelectionKey 对象。一个通道可以被注册到多个选择器上,但是每个选择器只能被注册一次,Channel 和 Selector 之间的关系是使用注册的方式完成的,且SelectableChannel 可以被注册到Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的。这需要注意的是通道在被注册到一个选择器上之前,必须先设置其为非阻塞模式。

可以供选择器查询的通道操作,从类型来分,包括以下四种:

  • 可读 : SelectionKey.OP_READ
  • 可写 : SelectionKey.OP_WRITE
  • 连接 : SelectionKey.OP_CONNECT
  • 接收 : SelectionKey.OP_ACCEPT

如果 Selector 对通道的多操作类型感兴趣,可以用“位或”操作符来实现:比如:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE 。

选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。什么是操作的就绪状态?一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪,就可以被 Selector 查询到,程序可以对通道进行对应的操作。比方说,某个SocketChannel 通道可以连接到一个服务器,则处于“连接就绪”(OP_CONNECT)。再比方说,一个 ServerSocketChannel 服务器通道准备好接收新进入的连接,则处于“接收就绪”(OP_ACCEPT)状态。还比方说,一个有数据可读的通道,可以说是“读就绪”(OP_READ)。一个等待写数据的通道可以说是“写就绪”(OP_WRITE)。

三、选择键(SelectionKey)

选择键封装了特定的通道与特定选择器的注册关系,选择键对象被java.nio.channels.SelectableChannel#register(java.nio.channels.Selector, int, java.lang.Object)返回并提供一个表示这种注册关系的标记。先看下SelectionKey 类的代码实现,如下:

public abstract class SelectionKey {
    protected SelectionKey() { }

    public abstract SelectableChannel channel();

    public abstract Selector selector();

    public abstract boolean isValid();

    public abstract void cancel();

    public abstract int interestOps();

    public abstract SelectionKey interestOps(int ops);
    
    public abstract int readyOps();

    public static final int OP_READ = 1 << 0;

    public static final int OP_WRITE = 1 << 2;

    public static final int OP_CONNECT = 1 << 3;

    public static final int OP_ACCEPT = 1 << 4;

    public final boolean isReadable() {
        return (readyOps() & OP_READ) != 0;
    }

    public final boolean isWritable() {
        return (readyOps() & OP_WRITE) != 0;
    }

    public final boolean isConnectable() {
        return (readyOps() & OP_CONNECT) != 0;
    }

    public final boolean isAcceptable() {
        return (readyOps() & OP_ACCEPT) != 0;
    }

    private volatile Object attachment = null;

    private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
        attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
            SelectionKey.class, Object.class, "attachment"
        );

    public final Object attach(Object ob) {
        return attachmentUpdater.getAndSet(this, ob);
    }

    public final Object attachment() {
        return attachment;
    }

}

(1)Channel 注册到后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器 Selector 的 select()方法完成。select 方法的作用,对感兴趣的通道操作,进行就绪状态的查询。

(2)Selector 可以不断的查询 Channel 中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是 Selector 感兴趣的操作,就会被 Selector 选中,放入选择键集合中。

(3)一个选择键,首先是包含了注册在 Selector 的通道操作的类型,比方说SelectionKey.OP_READ。也包含了特定的通道与特定的选择器之间的注册关系。开发应用程序是,选择键是编程的关键。NIO 的编程,就是根据对应的选择键,进行不同的业务逻辑处理。

(4)选择键的概念,和事件的概念比较相似。一个选择键类似监听器模式里边的一个事件。由于 Selector 不是事件触发的模式,而是主动去查询的模式,所以不叫事件Event,而是叫 SelectionKey 选择键。

猜你喜欢

转载自blog.csdn.net/zfy163520/article/details/121757810