Java NIO类库Selector机制解析

from:http://haoel.blog.51cto.com/313033/124582

一、 前言

自从j2se 1.4版本以来,jdk发布了全新的i/o类库,简称nio,其不但引入了全新的高效的i/o机制,同时,也引入了多路复用的异步模式。nio的包中主要包含了这样几种抽象数据类型:

· buffer:包含数据且用于读写的线形表结构。其中还提供了一个特殊类用于内存映射文件的i/o操作。

· charset:它提供unicode字符串影射到字节序列以及逆映射的操作。

· channels:包含socket,file和pipe三种管道,都是全双工的通道。

· selector:多个异步i/o操作集中到一个或多个线程中(可以被看成是unix中select()函数的面向对象版本)。

我的大学同学赵锟在使用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

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.(unknown source)

at sun.nio.ch.selectorproviderimpl.openpipe(unknown source)

at java.nio.channels.pipe.open(unknown source)

at sun.nio.ch.windowsselectorimpl.(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.(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的new i/o要设计成这个样子?如果说老的i/o不能多路复用,如下图所示,要开n多的线程去挨个侦听每一个channel (文件描述符) ,如果这样做很费资源,且效率不高的话。那为什么在新的i/o机制依然需要自己连接自己,而且,还是重复连接,消耗双倍的资源?

通过web搜索引擎没有找到为什么。只看到n多的人在报bug,但sun却没有任何解释。

下面一个图展示了,老的io和新io的在网络编程方面的差别。看起来nio的确很好很强大。但似乎比起c/c++来说,java的这种实现会有一些不必要的开销。

六、 它山之石 : 从apache的mina框架了解selector

上面的调查没过多长时间,正好同学赵锟的一个同事也在开发网络程序,这位仁兄使用了apache的mina框架。当我们把mina框架的源码研读了一下后。发现在mina中有这么一个机制:

1)mina框架会创建一个work对象的线程。

2)work对象的线程的run()方法会从一个队列中拿出一堆channel,然后使用selector.select()方法来侦听是否有数据可以读/写。

3)最关键的是,在select的时候,如果队列有新的channel加入,那么,selector.select()会被唤醒,然后重新select最新的channel集合。

4)要唤醒select方法,只需要调用selector的wakeup()方法。

对于熟悉于系统调用的c/c++程序员来说,一个阻塞在select上的线程有以下三种方式可以被唤醒:

1) 有数据可读/写,或出现异常。

2) 阻塞时间到,即time out。

3) 收到一个non-block的信号。可由kill或pthread_kill发出。

所以,selector.wakeup()要唤醒阻塞的select,那么也只能通过这三种方法,其中:

1)第二种方法可以排除,因为select一旦阻塞,应无法修改其time out时间。

2)而第三种看来只能在linux上实现,windows上没有这种信号通知的机制。

所以,看来只有第一种方法了。再回想到为什么每个selector.open(),在windows会建立一对自己和自己的loopback的tcp连接;在linux上会开一对pipe(pipe在linux下一般都是成对打开),估计我们能够猜得出来——那就是如果想要唤醒select,只需要朝着自己的这个loopback连接发点数据过去,于是,就可以唤醒阻塞在select上的线程了。

七、 真相大白 : 可爱的java你太不容易了

使用linux下的strace命令,我们可以方便地证明这一点。参看下图。图中,请注意下面几点:

1) 26654是主线程,之前我输出notify the select字符串是为了做一个标记,而不至于迷失在大量的strace log中。

2) 26662是侦听线程,也就是select阻塞的线程。

3) 图中选中的两行。26654的write正是wakeup()方法的系统调用,而紧接着的就是26662的epoll_wait的返回。

从上图可见,这和我们之前的猜想正好一样。可见,jdk的selector自己和自己建的那些tcp连接或是pipe,正是用来实现selector的notify和wakeup的功能的。

这两个方法完全是来模仿linux中的的kill和pthread_kill给阻塞在select上的线程发信号的。但因为发信号这个东西并不是一个跨平台的标准(pthread_kill这个系统调用也不是所有unix/linux都支持的),而pipe是所有的unix/linux所支持的,但windows又不支持,所以,windows用了tcp连接来实现这个事。

关于windows,我一直在想,windows的防火墙的设置是不是会让java的类似的程序执行异常呢?呵呵。如果不知道java的sdk有这样的机制,谁知道会有多少个程序为此引起的问题度过多少个不眠之夜,尤其是java程序员。

八、 后记

文章到这里是可以结束了,但关于java nio的selector引出来的其它话题还有许多,比如关于gnu 的java编译器又是如何,它是否会像sun的java解释器如此做傻事?我在这里先卖一个关子,关于gnu的java编译器,我会在另外一篇文章中讲述,近期发布,敬请期待。

关于本文中所使用的实验平台如下:

· windows:windows xp + sp2, sun j2se (build 1.7.0-ea-b23)

· linux:ubuntu 7.10 + linux kernel 2.6.22-14-generic, j2se (build 1.6.0_03-b05)

本文主要的调查工作由我的大学同学赵锟完成,我帮其验证调查成果及猜想。在此也向大家介绍我的大学同学赵锟,他也是一个技术高手,在软件开发方面,特别是unix/linux c/c++方面有着相当的功底,相信自此以后,会有很多文章会由我和他一同发布。

本篇文章由我成文。但其全部著作权和版权归赵锟和我共同所有。我们欢迎大家转载,但希望保持整篇文章的完整性,并请勿用于任何商业用途。谢谢。

相关文章:java nio 类库selector机制解析(续)http://haoel.blog.51cto.com/313033/124570

technorati 标签: java,nio,mina,selector


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值