Java NIO类库Selector机制解析(上)

一、  前言

 
自从J2SE 1.4 版本以来,JDK 发布了全新的I/O 类库,简称NIO ,其不但引入了全新的高效的I/O 机制,同时,也引入了多路复用的异步模式。NIO 的包中主要包含了这样几种抽象数据类型:
 
  • Buffer:包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的I/O操作。
  • Charset:它提供Unicode字符串影射到字节序列以及逆映射的操作。
  • Channels:包含socketfilepipe三种管道,都是全双工的通道。
  • Selector:多个异步I/O操作集中到一个或多个线程中(可以被看成是Unixselect()函数的面向对象版本)。
 
我的大学同学赵锟在使用NIO 类库书写相关网络程序的时候,发现了一些Java 异常RuntimeException ,异常的报错信息让他开始了对NIO Selector 进行了一些调查。当赵锟对我共享了Selector 的一些底层机制的猜想和调查时候,我们觉得这是一件很有意思的事情,于是在伙同赵锟进行过一系列的调查后,我俩发现了很多有趣的事情,于是导致了这篇文章的产生。这也是为什么本文的作者署名为我们两人的原因。
 
先要说明的一点是,赵锟和我本质上都是出身于Unix/Linux/C/C++ 的开发人员,对于Java ,这并不是我们的长处,这篇文章本质上出于对Java Selector 的好奇,因为从表面上来看Selector 似乎做到了一些让我们这些C/C++ 出身的人比较惊奇的事情。
 
下面让我来为你讲述一下这段故事。
 

二、  故事开始 : C++程序员写Java程序!

 
没有严重内存问题,大量丰富的SDK 类库,超容易的跨平台,除了在性能上有些微辞,C++ 出身的程序员从来都不会觉得Java 是一件很困难的事情。当然,对于长期习惯于使用操作系统API (系统调用System Call )的C/C++ 程序来说,面对Java 中的比较“另类”地操作系统资源的方法可能会略感困惑,但万变不离其宗,只需要对面向对象的设计模式有一定的了解,用不了多长时间,Java SDK 类库也能玩得随心所欲。
 
在使用Java 进行相关网络程序的的设计时,出身C/C++ 的人,首先想到的框架就是多路复用,想到多路复用,Unix/Linux 下马上就能让从想到select, poll, epoll 系统调用。于是,在看到Java NIO 中的Selector 类时必然会倍感亲切。稍加查阅一下SDK 手册以及相关例程,不一会儿,一个多路复用的框架便呈现出来,随手做个单元测试,没啥问题,一切和C/C++ 照旧。然后告诉兄弟们,框架搞定,以后咱们就在Windows 上开发及单元测试,完成后到运行环境Unix 上集成测试。心中并暗自念到,跨平台就好啊,开发活动都可以跨平台了。
 
然而,好景不长,随着代码越来越多,逻辑越来越复杂。好好的框架居然在Windows 上单元测试运行开始出现异常,看着Java 运行异常出错的函数栈,异常居然由Selector.open() 抛出,错误信息居然是Unable to establish loopback connection
 
“Selector.open() 居然报 loopback connection 错误,凭什么?不应该啊? open 的时候又没有什么 loopback socket 连接,怎么会报这个错?
 
长期使用C/C++ 的程序当然会对操作系统的调用非常熟悉,虽然Java 的虚拟机搞的什么系统调用都不见了,但C/C++ 的程序员必然要比Java 程序敏感许多。
 

三、  开始调查 : 怎么Java这么“傻”!

 
于是,C/C++ 的老鸟从 SystemInternals 上下载 Process Explorer 来查看一下究竟是什么个Loopback Connection 果然,打开java 运行进程,发现有一些自己连接自己的localhost TCP/IP 链接。于是另一个问题又出现了,
 
凭什么啊?为什么会有自己和自己的连接?我程序里没有自己连接自己啊,怎么可能会有这样的链接啊?而自己连接自己的端口号居然是些奇怪的端口。
 
问题变得越来越蹊跷了。难道这都是Selector.open() 在做怪?难道Selector.open() 要创建一个自己连接自己的链接?写个程序看看:
 
import java.nio.channels.Selector;
import java.lang.RuntimeException;
import java.lang.Thread;
public class TestSelector {
    private static final int MAXSIZE=5;
    public static final void main( String argc[] ) {
        Selector [] sels = new Selector[ MAXSIZE];
 
            try{
                for( int i = 0 ;i< MAXSIZE ;++i ) {
                    sels[i] = Selector.open();
                    //sels[i].close();
                }
                Thread.sleep(30000);
            }catch( Exception ex ){
                throw new RuntimeException( ex );
            }
    }
}
 
这个程序什么也没有,就是做5 Selector.open() ,然后休息30 秒,以便我使用Process Explorer 工具来查看进程。程序编译没有问题,运行起来,在Process Explorer 中看到下面的对话框:(居然有 10 个连接,从连接端口我们可以知道,互相连接, 如:第一个连第二个,第二个又连第一个
 
 
 
 
不由得赞叹我们的Java 啊,先不说这是不是一件愚蠢的事。至少可以肯定的是,Java 在消耗宝贵的系统资源方面,已经可以赶的上某些蠕虫病毒了。
 
如果不信,不妨把上面程序中的那个MAXSIZE 的值改成65535 试试,不一会你就会发现你的程序有这样的错误了:(在我的XP 机器上大约运行到2000 Selector.open() 左右)
 
Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Unable to establish loopback connection
        at Test.main(Test.java:18)
Caused by: java.io.IOException: Unable to establish loopback connection
        at sun.nio.ch.PipeImpl$Initializer.run(Unknown Source)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.nio.ch.PipeImpl.<init>(Unknown Source)
        at sun.nio.ch.SelectorProviderImpl.openPipe(Unknown Source)
        at java.nio.channels.Pipe.open(Unknown Source)
        at sun.nio.ch.WindowsSelectorImpl.<init>(Unknown Source)
        at sun.nio.ch.WindowsSelectorProvider.openSelector(Unknown Source)
        at java.nio.channels.Selector.open(Unknown Source)
        at Test.main(Test.java:15)
Caused by: java.net.SocketException: No buffer space available (maximum connections reached?): connect
        at sun.nio.ch.Net.connect(Native Method)
        at sun.nio.ch.SocketChannelImpl.connect(Unknown Source)
        at java.nio.channels.SocketChannel.open(Unknown Source)
        ... 9 more
 
 

四、  继续调查 : 如此跨平台

 
当然,没人像我们这么变态写出那么多的Selector.open() ,但这正好可以让我们来明白Java 背着大家在干什么事。上面的那些“愚蠢连接”是在Windows 平台上,如果不出意外,Unix/Linux 下应该也差不多吧。
 
于是我们把上面的程序放在Linux 下跑了跑。使用netstat 命令,并没有看到自己和自己的Socket 连接。貌似在Linux 上使用了和Windows 不一样的机制?!
 
如果在Linux 上不建自己和自己的TCP 连接的话,那么文件描述符和端口都会被省下来了,是不是也就是说我们调用65535 Selector.open() 的话,应该不会出现异常了。
 
可惜,在实现运行过程序当中,还是一样报错:(大约在400 Selector.open() 左右,还不如Windows
 
Exception in thread "main" java.lang.RuntimeException: java.io.IOException: Too many open files
        at Test1.main(Test1.java:19)
Caused by: java.io.IOException: Too many open files
        at sun.nio.ch.IOUtil.initPipe(Native Method)
        at sun.nio.ch.EPollSelectorImpl.<init>(EPollSelectorImpl.java:49)
        at sun.nio.ch.EPollSelectorProvider.openSelector(EPollSelectorProvider.java:18)
        at java.nio.channels.Selector.open(Selector.java:209)
        at Test1.main(Test1.java:15)
 
我们发现,这个异常错误是“Too many open files” ,于是我想到了使用 lsof 命令来查看一下打开的文件。
 
看到了有一些pipe 文件,一共5 对,10 个(当然,管道从来都是成对的)。如下图所示。
 
 
 
 
可见,Selector.open() Linux 下不用TCP 连接,而是用pipe 管道。看来,这个pipe 管道也是自己给自己的。所以,我们可以得出下面的结论:
 
1)Windows 下,Selector.open() 会自己和自己建立两条TCP 链接。不但消耗了两个TCP 连接和端口,同时也消耗了文件描述符。
2)Linux 下,Selector.open() 会自己和自己建两条管道。同样消耗了两个系统的文件描述符。
 
估计,在Windows 下,Sun JVM 之所以选择TCP 连接,而不是Pipe ,要么是因为性能的问题,要么是因为资源的问题。可能,Windows 下的管道的性能要慢于TCP 链接,也有可能是Windows 下的管道所消耗的资源会比TCP 链接多。这些实现的细节还有待于更为深层次的挖掘。
 
但我们至少可以了解,原来Java Selector 在不同平台上的机制。

你可能感兴趣的:(java,类库,机制解析)