《Java 源码分析》Java NIO 之 Selector

《Java 源码分析》 :Java NIO 之 Selector(第一部分Selector.open())
关于Selector类主要涉及两个重要的方法,如下:

1、Selector.open()

2、select()

由于篇幅限制,这篇主要从源码的角度来介绍Selector selector = Selector.open()背后主要做了什么,发生了什么。

Selector类中的open()源码如下:

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


函数功能:打开一个选择器。这个新的选择器的是通过调用系统级默认 SelectorProvider 对象的 openSelector 方法来创建的。

SelectorProvider.provider().openSelector();这行代码中我们先看SelectorProvider.provider();具体做了些什么。

provider()方法的源码如下:

 

    public static SelectorProvider provider() {
        synchronized (lock) {
            if (provider != null)//保证只有一个provider对象实例
                return provider;
            return AccessController.doPrivileged(
                new PrivilegedAction<SelectorProvider>() {
                    public SelectorProvider run() {
                            if (loadProviderFromProperty())
                                return provider;
                            if (loadProviderAsService())
                                return provider;
                            provider = sun.nio.ch.DefaultSelectorProvider.create();
                            return provider;
                        }
                    });
        }
    }


此函数功能:返回此时调用JVM的系统级默认的SelectorProvider

在代码中

    而if (provider != null)
            returnprovider;


是用来保证了整个程序中只有一个WindowsSelectorProvider对象;

由于我自己看的源码不是openjdk,因此在类库中根本就没有sun.nio.ch这个包。

在这里可以看到:http://www.docjar.com/html/api/sun/nio/ch/DefaultSelectorProvider.java.html。或者是直接下载openjdk来进行源码的追踪。

provider由sun.nio.ch.DefaultSelectorProvider.create();创建


所以从上面我们就可以看到:DefaultSelectorProvider 类只有一个私有的构造函数和一个create方法。其中在类 SelectorProvider 中的provider()方法中 
provider = sun.nio.ch.DefaultSelectorProvider.create();会根据操作系统来返回不同的实现类,windows平台就返回WindowsSelectorProvider对象实例;

以上就是 SelectorProvider.provider() 产生的一个 SelectorProvider (子类 WindowsSelectorProvider)对象实例的过程。

结论:有上面我们知道Selector.open()方法中的SelectorProvider.provider()实际上就是实例化了一个WindowsSelectorProvider对象,其中WindowsSelectorProvider为SelectorProvider的子类。


有了上面的基础,我们接着看上面代码块中的后面一半:SelectorProvider.openSelector()方法的具体实现过程。

SelectorProvider.provider().openSelector();根据前面的分析实际上就是 WindowsSelectorProvider. openSelector()。

因此,下面主要看下:WindowsSelectorProvider. openSelector()这个主要做了些什么操作。

WindowsSelectorProvider类的源码如下:

final class WindowsSelectorImpl extends SelectorImpl
      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);
         }

这段代码中做了如下几个事情

 

1、Pipe.open()打开一个管道(打开管道的实现后面再看);

2、拿到wakeupSourceFd和wakeupSinkFd两个文件描述符;

3、把唤醒端的文件描述符(wakeupSourceFd)放到pollWrapper里;PollArrayWrapper类会在文章后面进行介绍。

那么为什么需要一个管道,这个管道是怎么实现的?接下来看Pipe.open()做了什么

Pipe类在 java.nio.channels包下面

    //函数功能:打开一个管道
    public static Pipe open() throws IOException {
        return SelectorProvider.provider().openPipe();
    }


Pipe.open()方法直接是 调用了 SelectorProvider.openPipe()方法来实现的。

进一步来看 SelectorProvider.openPipe() 这个方法。

在SelectorProvider中的openPipe()是一个抽象方法,如下:

public abstract Pipe openPipe()
    throws IOException;


由于SelectorProvider.provider()实际上返回的是SelectorProvider的子类WindowSelectorProvider的实例对象。沿着 WindowsSelectorProvider的继承关系找了下,SelectorProvider中的openPipe()抽象方法 
是在 SelectorProviderImpl类中实现的,即是在 WindowSelectorProvider类的父类来实现的。

这里有必要说下 WindowsSelectorProvider的继承关系:

WindowSelectorProvider 的直接父类是 SelectorProviderImpl;SelectorProviderImpl 的直接父类是 SelectorProvider。

SelectorProviderImpl 类的代码如下:

      public abstract class SelectorProviderImpl
              extends SelectorProvider
          {
                //...省略了一些不相干的函数
              public Pipe openPipe() throws IOException {
                  return new PipeImpl(this);
              }
               //...
          }


从上面可知:

打开管道Pipe.open()方法 直接调用的 SelectorProvider.openPipe()方法,而SelectorProvider类中的openPipe()方法 直接返回的是:new PipeImpl(this),即PipeImpl类的对象实例。

看到这里的时候,我在想为什么不从最开始的Pipe类的open()中直接返回PipeImpl的实例对象呢,而是要委托给SelectorProviderImpl(具体代码看下面)呢,原因可能在于PipeImpl实例需要一个WindowsSelectorProvider且所有环境有且只有一个,如果不采用这种方式可能会更复杂,不想了,继续往后面看

Pipe类的open()方法

    //函数功能:打开一个管道
    public static Pipe open() throws IOException {
        return SelectorProvider.provider().openPipe();
    }



经过上面的分析,我们已经知道了Pipe.open()在代码层面的表现为:实例化了一个PipeImpl对象。

下面看下PipeImpl 类的构造函数


        PipeImpl(final SelectorProvider sp) throws IOException {
             try {
                 AccessController.doPrivileged(new Initializer(sp));
             } catch (PrivilegedActionException x) {
                 throw (IOException)x.getCause();
             }
         }


这个构造方法中的代码虽然比较不熟悉,是自己第一次见到,但是我们还是要想办法来看下,是吧。

先不看Initializer这个类里面的具体实现,我们来看下PipeImpl类的构造函数中

AccessController.doPrivileged(new Initializer(sp)) 
这行代码中所涉及的:dePrivileged这个方法是干什么的?

AccessController 类中的 doPrivileged(PrivilegedAction action) 方法是一个native方法,如下:

@CallerSensitive
public static native <T> T doPrivileged(PrivilegedAction<T> action);
1
2
关于 AccessController.doPrivileged方法的介绍,可以参考下篇博文:

1、http://www.blogjava.net/DLevin/archive/2012/11/02/390637.html(自己目前也没有太理解)

2、http://huangyunbin.iteye.com/blog/1942509

看了一些关于AccessController.doPrivileged的资料,还没有怎么懂,但是可以这里来理解:

首先:AccessController.doPrivileged意思是这个是特别的,不用做权限检查.

在什么地方会用到呢

答:假设1.jar中有类可以读取一个文件,现在我们要使用1.jar去做这个事情. 
但是我们的类本生是没有权限去读取那个文件的,一般情况下就是眼睁睁的看着了. 但是java提供了doPrivileged.在1.jar中如果读取文件的方法是通过doPrivileged来实现的. 
就不会有后面的检查了,现在我们就可以使用1.jar去读取那个文件了.

利用doPrivileged就实现了没有权限的人借用有权限的人来达到一定的目的。

回到原题:

AccessController.doPrivileged(new Initializer(sp)) 经过权限的检查之后就会直接执行Initializer中的run方法 
,下面来看下Initializer这个类中的run方法。

Initializer是PipeImpl类的内部类,源代码如下:

       private class Initializer
              implements PrivilegedExceptionAction<Void>
          {

              private final SelectorProvider sp;

              private Initializer(SelectorProvider sp) {
                  this.sp = sp;
              }

              public Void run() throws IOException {
                  ServerSocketChannel ssc = null;
                  SocketChannel sc1 = null;
                  SocketChannel sc2 = null;

                  try {
                      // loopback address
                      InetAddress lb = InetAddress.getByName("127.0.0.1");
                      assert(lb.isLoopbackAddress());

                      // bind ServerSocketChannel to a port on the loopback address
                      // 将ServerSocketChannel绑定本地环回地址,端口号为 0
                      ssc = ServerSocketChannel.open();
                      ssc.socket().bind(new InetSocketAddress(lb, 0));

                      // Establish connection (assumes connections are eagerly
                      // accepted)
                      // 建立连接
                      InetSocketAddress sa
                          = new InetSocketAddress(lb, ssc.socket().getLocalPort());
                      sc1 = SocketChannel.open(sa);
                      //向SocketChannel中写入数据
                      ByteBuffer bb = ByteBuffer.allocate(8);
                     long secret = rnd.nextLong();
                     bb.putLong(secret).flip();
                     sc1.write(bb);

                     // Get a connection and verify it is legitimate
                     for (;;) {
                         sc2 = ssc.accept();
                         bb.clear();
                         sc2.read(bb);
                         bb.rewind();
                         if (bb.getLong() == secret)
                             break;
                         sc2.close();
                     }

                     // Create source and sink channels
                     source = new SourceChannelImpl(sp, sc1);
                     sink = new SinkChannelImpl(sp, sc2);
                 } catch (IOException e) {
                     try {
                         if (sc1 != null)
                             sc1.close();
                         if (sc2 != null)
                             sc2.close();
                     } catch (IOException e2) { }
                     IOException x = new IOException("Unable to establish"
                                                     + " loopback connection");
                     x.initCause(e);
                     throw x;
                 } finally {
                     try {
                         if (ssc != null)
                             ssc.close();
                     } catch (IOException e2) { }
                 }
                 return null;
             }
         }


从 Initializer中run方法中,我们可以得到的一点是:建立了一个 loopback connection.

windows下的实现是创建两个本地的socketChannel,然后连接(链接的过程通过写一个随机long做两个socket的链接校验),两个socketChannel分别实现了管道的source与sink端。 
source端由前面提到的WindowsSelectorImpl放到了pollWrapper中(pollWrapper.addWakeupSocket(wakeupSourceFd, 0))

最后,看下PollArrayWrapper这个类

PollArrayWrapper类主要在前面的WindowsSelectorImpl的构造函数中有这样一行代码:pollWrapper.addWakeupSocket(wakeupSourceFd, 0)(作用:把唤醒端的文件描述符(wakeupSourceFd)放到pollWrapper里)

PollArrayWrapper类中addWakeupSocket方法的源代码如下:

    // Adds Windows wakeup socket at a given index.
    void addWakeupSocket(int fdVal, int index) {
        putDescriptor(index, fdVal);
        putEventOps(index, POLLIN);
    }
    // Access methods for fd structures
    void putDescriptor(int i, int fd) {
        pollArray.putInt(SIZE_POLLFD * i + FD_OFFSET, fd);
    }

    void putEventOps(int i, int event) {
        pollArray.putShort(SIZE_POLLFD * i + EVENT_OFFSET, (short)event);
    }


这里将source的POLLIN事件标识为感兴趣的,当sink端有数据写入时,source对应的文件描述符wakeupSourceFd就会处于就绪状态

到这里从源码的角度来看了Selector selector = Selector.open()主要做了些什么

主要完成建立Pipe,并把pipe的wakeupSourceFd放入pollArray中,这个pollArray是Selector的枢纽。这里是以Windows的实现来看,在windows下通过两个链接的socketChannel实现了Pipe,linux下则是直接使用系统的pipe。

小结
Selector selector = Selector.open();实际上就是new 了一个 WindowsSelectorImpl对象实例。

以及建立了Pipe,并把pipe的wakeupSourceFd放入pollArray中,这个pollArray是Selector的枢纽。这里是以Windows的实现来看,在windows下通过两个链接的socketChannel实现了Pipe,linux下则是直接使用系统的pipe。

关于第二部分就是从源码的角度来看下selector.select()背后做了些什么,敬请期待。

最后上一张关于Selector工作原理的图:(来源自参考资料所贴出的博客)

这张图自己目前也还没有全部弄懂(只知道大概流程确实是这样),有待于自己进一步的接触后才能更好的理解。

《Java 源码分析》Java NIO 之 Selector

参考资料
1、http://goon.iteye.com/blog/1775421

2、http://www.myexception.cn/program/1598318.html
原文链接

https://blog.****.net/u010412719/article/details/52809669

博主亲测 也看过NIO 源码。 类似的理解不做重复。遂转载之。