一、
前言
自从
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
进行了一些调查。当ta对我共享了
Selector
的一些底层机制的猜想和调查时候,我们觉得这是一件很有意思的事情,于是在进行过一系列的调查后,我俩发现了很多有趣的事情,于是导致了这篇文章的产生。
二、
故事开始
: 让
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
的
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机制解析.docJava_NIO类库Selector机制解析.docJava_NIO类库Selector机制解析.docJava_NIO类库Selector机制解析.doc
"Java NIO Selector 机制解析" ...了解Java NIO类库和Selector机制可以帮助程序员更好地使用Java NIO类库,提高I/O操作的性能。但需要注意到Java虚拟机对操作系统调用的影响,避免出现异常和错误。
4. **NIO(非阻塞I/O)**:`java.nio`包引入了新的I/O模型,允许异步读写,提高了大规模并发应用的性能。关键组件包括`Channel`、`Buffer`和`Selector`。 5. **多线程**:`java.lang.Thread`和`java.util....
【标签】"Java"明确了这个项目是用Java语言编写的,Java作为一门广泛应用的编程语言,其丰富的类库和强大的跨平台能力使得开发网络服务器成为可能。 至于【压缩包子文件的文件名称列表】中的“tiny-web-server-...
Java以其跨平台的特性、丰富的类库以及强大的性能,成为了后端开发的首选语言之一,尤其适合大型、复杂的游戏服务器开发。 1. **网络编程**:游戏服务器的核心是处理客户端的连接请求和数据传输。Java的Socket编程...
解决这些问题通常需要深入理解NIO的工作原理,包括缓冲区(Buffer)、通道(Channel)、选择器(Selector)等核心概念,并且需要对Servlet容器的内部机制有一定的了解。 在提供的文件列表中,"src"目录可能包含了源...
- Java NIO还包括缓冲区(Buffer)、通道(Channel)、选择器(Selector)等概念。 5. JVM类加载机制 - 类加载过程包括加载、验证、准备、解析、初始化等步骤。 - 类加载器分为启动类加载器(BootstrapClassLoader)、...
1. **NIO(Non-blocking I/O)**:Java的非阻塞I/O模型,提供了通道(Channel)和选择器(Selector)等概念,极大地提高了处理大量并发连接的能力。在`sun.nio`包下,你可以找到`sun.nio.ch`子包,它包含了Java NIO...
在`java.nio`包中,`FileChannel`、`SocketChannel`和`Selector`等类是核心部分,通过查看其源码,开发者可以学习到如何高效地进行多路复用I/O,以及如何利用非阻塞特性来优化网络通信。 最后,`misc`(杂项)一词...
Java是一种广泛使用的面向对象的编程语言,以其跨平台、高性能和丰富的类库而备受赞誉。在IT行业,尤其是软件开发领域,Java工程师是需求量极大的职位。为了在Java面试中脱颖而出,深入理解Java语言的核心概念和技术...
4. **I/O与NIO**:Java API 1.6不仅有传统的基于流的I/O,还引入了非阻塞I/O(New IO,即NIO)框架,提供了一种更高效的数据传输方式,包括`java.nio`包中的通道(Channel)、缓冲区(Buffer)和选择器(Selector)...
Java IO/NIO部分介绍了Java中的输入输出处理,包括不同的IO模型(阻塞IO、非阻塞IO、多路复用IO、信号驱动IO和异步IO),以及Java NIO包中Buffer、Channel、Selector等重要组件。NIO通过使用缓冲区作为数据临时存储...
JDK(Java Development Kit)是Oracle公司提供的用于开发Java应用程序的重要软件包,它包含了Java编译器、Java虚拟机(JVM)、Java类库以及各种开发和调试工具,是Java开发的基础。 JDK 1.4版本是Java历史上的一个...
2. **IO流**:Java 1.6对输入/输出流进行了优化,提供了NIO(New Input/Output)框架,它支持选择器(Selector)和通道(Channel)等特性,提高了IO操作的性能和并发性。 3. **网络编程**:Java API 1.6提供Socket...
3. **NIO(非阻塞I/O)**:Java NIO(New Input/Output)是Java 1.4引入的新特性,它提供了与传统IO不同的I/O操作方式,支持选择器(Selector)、通道(Channel)和缓冲区(Buffer)。NIO允许程序在数据准备好时进行...
相对于传统的IO模型,NIO提供了选择器(Selector)和通道(Channel)等机制,可以实现多路复用,提高网络通信的效率,尤其适合高并发场景。 通过这个Java网络教程,学习者将能够熟练地运用Java进行网络编程,包括...
NIO提供了一种非阻塞I/O模型,通过选择器(Selector)监控多个通道(Channel),实现同时处理多个输入输出任务。通道是NIO的核心概念,如FileChannel、SocketChannel等,它们可以从或向缓冲区(Buffer)读写数据。...
5. **java.nio**:非阻塞I/O,提供了Channel、Buffer和Selector等,提高了I/O性能。 6. **java.awt**和**javax.swing**:这两个包用于创建图形用户界面,其中java.awt提供基本组件,javax.swing提供了更为高级和...
`java.nio`包提供了选择器(Selector)、通道(Channel)和缓冲区(Buffer)等组件,可以实现高并发和低延迟的网络服务。AIO(Java NIO.2)引入了`AsynchronousServerSocketChannel`和`AsynchronousSocketChannel`,...