Java IO--Selector

 

一、概述

jdk1.4之前提供的io主要是阻塞io(bloking io),服务端需要为每个请求创建一个处理线程,如果没有请求则阻塞等待直到有请求到达;客户端发起请求时,需要判断服务端是否有线程响应,如果有响应,则在响应返回值后继续执行,否则阻塞等待直到响应返回或出现异常。

非阻塞io(no-bloking io)使用单线程或者少量线程等待事件通知,处于等待的线程可以释放出来处理其他请求,当有事件(通过事件驱动模型)通知后,主线程分配资源(线程)处理相关事件。selector就是此模型中的观察者,用户可以把感兴趣的时间注册到selector,当有事件到达时,selector通知主线程处理此事件,当没有感兴趣事件到达时,selector处于阻塞状态。

二、概念模型

无阻塞io使用单线程少量线程等待事件通知,其中selector就是处于观察者模式的核心组件,在没有请求到达时,selector处于阻塞模式,知道请求事件到达或者出现异常,请求事件到达时,selector会从阻塞模式唤醒,selector会调用主线程处理当前请求事件。
    客户端请求模型如下图所示:

Java IO--Selector_第1张图片
    服务端请求模型如下图所示:

Java IO--Selector_第2张图片
    客户端和服务端所支持的事件并不完全一致,客户端关注读、写和请求链接事件,服务端关注读、写和接受链接事件,ServerSocketChannel和SocketChannel有着共同的父类,除了小部分差异外,两个通道可以互相转换。

三、继承体系及功能
    1、Selector的继承体系

Java IO--Selector_第3张图片
    2、SelectorProvider的继承体系

Java IO--Selector_第4张图片
    3、SelectionKey继承体系

Java IO--Selector_第5张图片

四、Selector提供的API

1、Selector   API

名称

返回值类型

功能

open

Selector

用系统默认的SelectorProvider打开一个selector

isOpen

boolean

判断选择器是否已打开

provider

SelectorProvider

返回创建选择器的选择器服务提供者

keys

Set

返回选择器已注册的key集合

selectedKeys

Set

返回选择器已选择的key集合、及操作事件已就绪的key集合

selectNow

int

返回关联通道已经就绪的key数量,非阻塞运行,没有通道可选择,则返回0

Select(long timeout)

int

返回关联通道已经就绪的key数量,非阻塞模式运行,至少有一个通道可选择、wakeup方法被调用、当前线程中断、超时四种情况会导致方法返回

wakeup

Selector

唤醒一个等待选取key的可用线程,如果当前没有选择操作在进行,则下一个调用选择器的方法将返回。

close

void

关闭选择器

Selector主要用户通道的选择,包括返回当前已注册的key集合,返回当前准备就绪的key集合,唤醒一个阻塞的可用线程等,返回的SelectionKey可以获取当前绑定的通道和选择器。

2、SelectorProvider   api

名称

返回值

功能

loadProviderFromProperty

boolean

根据系统属性配置加载SelectorProvider实例

loadProviderAsService

boolean

获取系统加载路径下所有的SelectorProvider实现类,以最后一个为当前SelectorProvider

provider

SelectorProvider

返回jvm默认的SelectorProvider,如果系统属性配置存在,则加载;否则判断系统加载路径下是否配置实例,存在则加载;加载系统默认的实例(区分平台windows、linux)。

openDatagramChannel

DatagramChannel

打开一个支持UDP通信协议的通道并返回

openPipe

Pipe

打开一个管道pipe并返回

openSelector

AbstractSelector

打开一个选择器并返回

openServerSocketChannel

ServerSocketChannel

打开一个ServerSocketChannel并返回

openSocketChannel

SocketChannel

打开一个SocketChannel并返回

inheritedChannel

Channel

返回继承虚拟机创建实例的通道

SelectorProvider是为了创建Selector、SocketChannel、ServerSocketChannel、DatagramChannel而存在的,在相应的通道和选择器的open方法中调用系统默认的SelectorProvider的open*方法,创建响应的通道和选择器。

3、SelectionKey   api

名称

返回值

功能

channel

SelectableChannel

返回SelectionKey关联的通道,即使SelectionKey取消后,这个方法依然会返回通道

selector

Selector

返回Selector关联的通道,即使Selector取消后,这个方法依然会返回选择器

isValid

boolean

判断一个SelectionKey是否有效

cancel

void

取消SelectionKey与通道的注册关系,key将无效,并且将key添加到取消注册集合,在选择器的下一次选择中将会被移除。

interestOps

int

获取selectionkey感兴趣的操作集合

interestOps

SelectionKey

设置selectionkey感兴趣的事件

readyOps

int

获取已经准备就绪的操作事件集

isReadable

boolean

判断selectionkey的通道是否已经准备好读操作

isWritable

boolean

判断selectionkey的通道是否已经准备好写操作

isConnectable

boolean

判断selectionkey的通道是否已经准备好接受链接操作

isAcceptable

boolean

判断selectionkey的通道是否已经准备好请求链接

attach

Object

将指定对象设置为附加对象

attachment

Object

返回当前附加对象

 SelectionKey表示一个可选择通道与选择器关联的注册器,可以简单理解为一个token。SelectionKey包含两个操作集,分别是兴趣操作事件集interestOps和通道就绪操作事件集readyOps,每个操作集用一个Integer来表示。interestOps用于选择器判断在下一个选择操作的过程中,操作事件是否是通道关注的。兴趣操作事件集在SelectionKey创建时,初始化为注册选择器时的opt值,这个值可能通过interestOps(int)会改变。SelectionKey的readyOps表示一个通道已经准备就绪的操作事件,但不能保证在没有引起线程阻塞的情况下,就绪的操作事件会被线程执行。在一个选择操作完成后,大部分情况下就绪操作事件集会立即更新。如果外部的事件或在通道有IO操作,就绪操作事件集可能不准确。

如果需要经常关联一些应用的特殊数据到SelectionKey,比如一个object表示一个高层协议的状态,object用于通知实现协议处理器。所以,SelectionKey支持通过attach方法将一个对象附加的SelectionKey的attachment上。attachment可以通过#attachment方法进行修改。SelectionKey定义了所有的操作事件,但是具体通道支持的操作事件依赖于具体的通道。所有可选择的通道都可以通过validOps方法,判断一个操作事件是否被通道所支持。测试一个不被通道所支持的通道,将会抛出相关的运行时异常。

SelectionKey多线程并发访问时,是线程安全的。读写兴趣操作事件集的操作都将同步到,选择器的具体操作。同步器执行过程是依赖实现的:在一个本地实现版本中,如果一个选择操作正在进行,读写兴趣操作事件集也许会不确定地阻塞;在一个高性能的实现版本中,可能会简单阻塞。无论任何时候,一个选择操作在操作开始时,选择器总是占用着兴趣操作事件集的值。SelectionKey可以简单理解为通道和选择器的映射关系,并定义了相关的操作事件,分别为OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT值分别是,int的值的第四为分别为1,级1,4,8,16。用一个AtomicReferenceFieldUpdater原子更新attachment。

五、selector源码分析

selector设计到的功能主要有打开open()、注册register()、选择select()、和主动唤醒wakeup(),整个阻塞和唤醒的过程设计到的内容非常多,先上一张整体的流程图。

Java IO--Selector_第6张图片

1、Selector.open()

Selector代码如下:

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

主要是返回SelectorProvider,然后调用Provider的openSelector()方法,看下SelectorProvider.provider方法做了什么操作

    public static SelectorProvider provider() {
        synchronized (lock) {
            if (provider != null)
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction() {
                    public SelectorProvider run() {
                            if (loadProviderFromProperty())
                                return provider;
                            if (loadProviderAsService())
                                return provider;
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }

由SelectorProvider   api得知,该方法会根据配置返回Provider,首先查看系统配置,如果系统属性已配置,则加载;如果系统属性未配置,则查看系统类加载路径下是否存在provider实例,如果存在,则返回第一个provider作为实例;如果以上都未正确返回provider,则返回系统默认实例 (区分平台,Windows,Linux)。查看create()方法

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

可以看到,如果是高版本的Linux,则返回EPollSelectorProvider,如果是低版本的Linux,则返回PollSelectorProvider,终于找到了SelectorProvider的创建方式,我们继续查看SelectorProvider的openSlector方法,以WindowsSelectorProvider为例

public AbstractSelector openSelector() throws IOException {
        return new WindowsSelectorImpl(this);
    }
    WindowsSelectorImpl(SelectorProvider sp) throws IOException {
        super(sp);
        pollWrapper = new PollArrayWrapper(INIT_CAP);
        wakeupPipe = Pipe.open();
        wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal();

        // Disable the Nagle algorithm so that the wakeup is more immediate
        SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
        (sink.sc).socket().setTcpNoDelay(true);
        wakeupSinkFd = ((SelChImpl)sink).getFDVal();

        pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
    }

WindowsSelectorImpl的初始化做了如下几件事,Pipe.open()打开了一个管道;拿到wakeupSoureFd和wakeupSinkFd两个文件描述符;把唤醒端的文件描述符(wakeupSoureFd)放到了pollWrapper中。为什么需要管道,管道是如何实现的,管道的实现过程中都做了什么工作?

    public static Pipe open() throws IOException {
        return SelectorProvider.provider().openPipe();
    }

熟悉的SelectorProvider返回过程,我们跳过直接看openPipe()方法

    public Pipe openPipe() throws IOException {
        return new PipeImpl(this);
    }
    PipeImpl(SelectorProvider sp) {
        long pipeFds = IOUtil.makePipe(true);
        int readFd = (int) (pipeFds >>> 32);
        int writeFd = (int) pipeFds;
        FileDescriptor sourcefd = new FileDescriptor();
        IOUtil.setfdVal(sourcefd, readFd);
        source = new SourceChannelImpl(sp, sourcefd);
        FileDescriptor sinkfd = new FileDescriptor();
        IOUtil.setfdVal(sinkfd, writeFd);
        sink = new SinkChannelImpl(sp, sinkfd);
    }

这里是创建pipe的过程,Windows平台使用SourceChannelImpl和SinkChannleImpl实现了管道的source端和sink端,source端由千面的WindowsSlectorImpl放到了PollWrapper中  pollWrapper.addWakeupSocket(wakeupSourceFd, 0);

    void addWakeupSocket(int fdVal, int index) {
        putDescriptor(index, fdVal);
        putEventOps(index, POLLIN);
    }

这里讲source的POLLIN事件标识为感兴趣,当sink端有数据写入时,source对应的文件描述符wakeupSourceFd就会处于就绪状态。到此终于完成了Selector.open()方法,主要创建了pipe,并把pipe的wakeupSourceFd放入pollArray中,这个pollArray是selector的枢纽,完成事件流转工作。

2、ServerSocketChannel.register()

    public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException {
        synchronized (regLock) {
            if (!isOpen())
                throw new ClosedChannelException();
            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;
        }
    }
    protected final SelectionKey register(AbstractSelectableChannel ch,
                                          int ops,
                                          Object attachment)
    {
        if (!(ch instanceof SelChImpl))
            throw new IllegalSelectorException();
        SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
        k.attach(attachment);
        synchronized (publicKeys) {
            implRegister(k);
        }
        k.interestOps(ops);
        return k;
    }

继续查看implRegister()方法,以Windows平台的WindowsSelectorImpl为例

    protected void implRegister(SelectionKeyImpl ski) {
        synchronized (closeLock) {
            if (pollWrapper == null)
                throw new ClosedSelectorException();
            growIfNeeded();
            channelArray[totalChannels] = ski;
            ski.setIndex(totalChannels);
            fdMap.put(ski);
            keys.add(ski);
            pollWrapper.addEntry(totalChannels, ski);
            totalChannels++;
        }
    }

注册register功能主要就是把SocketChannel的文件描述符放到pollArray中。

3、Selector.select()

    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 int doSelect(long timeout) throws IOException {
        if (channelArray == null)
            throw new ClosedSelectorException();
        this.timeout = timeout; // set selector timeout
        processDeregisterQueue();
        if (interruptTriggered) {
            resetWakeupSocket();
            return 0;
        }
        // Calculate number of helper threads needed for poll. If necessary
        // threads are created here and start waiting on startLock
        adjustThreadsCount();
        finishLock.reset(); // reset finishLock
        // Wakeup helper threads, waiting on startLock, so they start polling.
        // Redundant threads will exit here after wakeup.
        startLock.startThreads();
        // do polling in the main thread. Main thread is responsible for
        // first MAX_SELECTABLE_FDS entries in pollArray.
        try {
            begin();
            try {
                subSelector.poll();
            } catch (IOException e) {
                finishLock.setException(e); // Save this exception
            }
            // Main thread is out of poll(). Wakeup others and wait for them
            if (threads.size() > 0)
                finishLock.waitForHelperThreads();
          } finally {
              end();
          }
        // Done with poll(). Set wakeupSocket to nonsignaled  for the next run.
        finishLock.checkForException();
        processDeregisterQueue();
        int updated = updateSelectedKeys();
        // Done with poll(). Set wakeupSocket to nonsignaled  for the next run.
        resetWakeupSocket();
        return updated;
    }

这里主要是调用了subSelector.poll(),继续查看

        private int poll() throws IOException{ // poll for the main thread
            return poll0(pollWrapper.pollArrayAddress,
                         Math.min(totalChannels, MAX_SELECTABLE_FDS),
                         readFds, writeFds, exceptFds, timeout);
        }
        private native int poll0(long pollAddress, int numfds,
             int[] readFds, int[] writeFds, int[] exceptFds, long timeout);

到这里,就清楚一些了,退出阻塞(select()方法返回)的方式有:注册在Selector上的SocketChannel处于就绪状态(放在pollArray中的socketchannel的文件描述符就绪);放在pollArray中的wakeupSourceFd就绪。前者就绪就是正常事件到达,也就是正常的阻塞--事件驱动--唤醒的过程,后者是NIO的主动唤醒过程。

4、Selector.wakeup()

    public Selector wakeup() {
        synchronized (interruptLock) {
            if (!interruptTriggered) {
                setWakeupSocket();
                interruptTriggered = true;
            }
        }
        return this;
    }
    private void setWakeupSocket() {
        setWakeupSocket0(wakeupSinkFd);
    }
private native void setWakeupSocket0(int wakeupSinkFd);

这里是个native方法,查看native方法源码:

Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this,  
                                                jint scoutFd)  
{  
    /* Write one byte into the pipe */  
    send(scoutFd, (char*)&POLLIN, 1, 0);  
}  

这里完成了想最开始创建的Pipe的sink端写入一个字节,source的文件描述符处于就绪状态,poll方法就会返回,从而导致select方法返回。所以wakeup方法就是自己给自己写了一个字节,传入了写事件,从而中断select的阻塞,并返回。

你可能感兴趣的:(Java,网络知识,Java--IO)