日期:2014-05-16  浏览次数:20310 次

实现非阻塞套接字的一种简单方法 JSSE 和 NIO
转自http://www.ibm.com/developerworks/cn/java/j-sslnb/index.html

阻塞,还是非阻塞?这就是问题所在。无论在程序员的头脑中多么高贵……当然这不是莎士比亚,本文提出了任何程序员在编写 Internet 客户程序时都应该考虑的一个重要问题。通信操作应该是阻塞的还是非阻塞的?

许多程序员在使用 Java 语言编写 Internet 客户程序时并没有考虑这个问题,主要是因为在以前只有一种选择――阻塞通信。但是现在 Java 程序员有了新的选择,因此我们编写的每个客户程序也许都应该考虑一下。

非阻塞通信在 Java 2 SDK 的 1.4 版被引入 Java 语言。如果您曾经使用该版本编过程序,可能会对新的 I/O 库(NIO)留下了印象。在引入它之前,非阻塞通信只有在实现第三方库的时候才能使用,而第三方库常常会给应用程序引入缺陷。

NIO 库包含了文件、管道以及客户机和服务器套接字的非阻塞功能。库中缺少的一个特性是安全的非阻塞套接字连接。在 NIO 或者 JSSE 库中没有建立安全的非阻塞通道类,但这并不意味着不能使用安全的非阻塞通信。只不过稍微麻烦一点。

要完全领会本文,您需要熟悉:
Java 套接字通信的概念。您也应该实际编写过应用程序。而且不只是打开连接、读取一行然后退出的简单应用程序,应该是实现 POP3 或 HTTP 之类协议的客户机或通信库这样的程序。

SSL 基本概念和加密之类的概念。基本上就是知道如何设置一个安全连接(但不必担心 JSSE ――这就是关于它的一个“紧急教程”)。

NIO 库。
在您选择的平台上安装 Java 2 SDK 1.4 或以后的版本。(我是在 Windows 98 上使用 1.4.1_01 版。)

如果需要关于这些技术的介绍,请参阅 参考资料部分。

那么到底什么是阻塞和非阻塞通信呢?

阻塞和非阻塞通信

阻塞通信意味着通信方法在尝试访问套接字或者读写数据时阻塞了对套接字的访问。在 JDK 1.4 之前,绕过阻塞限制的方法是无限制地使用线程,但这样常常会造成大量的线程开销,对系统的性能和可伸缩性产生影响。java.nio 包改变了这种状况,允许服务器有效地使用 I/O 流,在合理的时间内处理所服务的客户请求。

没有非阻塞通信,这个过程就像我所喜欢说的“为所欲为”那样。基本上,这个过程就是发送和读取任何能够发送/读取的东西。如果没有可以读取的东西,它就中止读操作,做其他的事情直到能够读取为止。当发送数据时,该过程将试图发送所有的数据,但返回实际发送出的内容。可能是全部数据、部分数据或者根本没有发送数据。

阻塞与非阻塞相比确实有一些优点,特别是遇到错误控制问题的时候。在阻塞套接字通信中,如果出现错误,该访问会自动返回标志错误的代码。错误可能是由于网络超时、套接字关闭或者任何类型的 I/O 错误造成的。在非阻塞套接字通信中,该方法能够处理的唯一错误是网络超时。为了检测使用非阻塞通信的网络超时,需要编写稍微多一点的代码,以确定自从上一次收到数据以来已经多长时间了。

哪种方式更好取决于应用程序。如果使用的是同步通信,如果数据不必在读取任何数据之前处理的话,阻塞通信更好一些,而非阻塞通信则提供了处理任何已经读取的数据的机会。而异步通信,如 IRC 和聊天客户机则要求非阻塞通信以避免冻结套接字。

创建传统的非阻塞客户机套接字
Java NIO 库使用通道而非流。通道可同时用于阻塞和非阻塞通信,但创建时默认为非阻塞版本。但是所有的非阻塞通信都要通过一个名字中包含 Channel 的类完成。在套接字通信中使用的类是 SocketChannel, 而创建该类的对象的过程不同于典型的套接字所用的过程,如清单 1 所示。

清单 1. 创建并连接 SocketChannel 对象
SocketChannel sc = SocketChannel.open();
sc.connect("www.ibm.com",80);
sc.finishConnect();

必须声明一个 SocketChannel 类型的指针,但是不能使用 new 操作符创建对象。相反,必须调用 SocketChannel 类的一个静态方法打开通道。打开通道后,可以通过调用 connect() 方法与它连接。但是当该方法返回时,套接字不一定是连接的。为了确保套接字已经连接,必须接着调用 finishConnect() 。

当套接字连接之后,非阻塞通信就可以开始使用 SocketChannel 类的 read() 和 write() 方法了。也可以把该对象强制转换成单独的 ReadableByteChannel 和 WritableByteChannel 对象。无论哪种方式,都要对数据使用 Buffer 对象。因为 NIO 库的使用超出了本文的范围,我们不再对此进一步讨论。

当不再需要套接字时,可以使用 close() 方法将其关闭:
sc.close();
这样就会同时关闭套接字连接和底层的通信通道。

创建替代的非阻塞的客户机套接字
上述方法比传统的创建套接字连接的例程稍微麻烦一点。不过,传统的例程也能用于创建非阻塞套接字,不过需要增加几个步骤以支持非阻塞通信。

SocketChannel 对象中的底层通信包括两个 Channel 类: ReadableByteChannel 和 WritableByteChannel。 这两个类可以分别从现有的 InputStream 和 OutputStream 阻塞流中使用 Channels 类的 newChannel() 方法创建,如清单 2 所示:

清单 2. 从流中派生通道
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream());
WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());

Channels 类也用于把通道转换成流或者 reader 和 writer。这似乎是把通信切换到阻塞模式,但并非如此。如果试图读取从通道派生的流,读方法将抛出 IllegalBlockingModeException 异常。

相反方向的转换也是如此。不能使用 Channels 类把流转换成通道而指望进行非阻塞通信。如果试图读从流派生的通道,读仍然是阻塞的。但是像编程中的许多事情一样,这一规则也有例外。

这种例外适合于实现 SelectableChannel 抽象类的类。 SelectableChannel 和它的派生类能够选择使用阻塞或者非阻塞模式。 SocketChannel 就是这样的一个派生类。

但是,为了能够在两者之间来回切换,接口必须作为 SelectableChannel 实现。对于套接字而言,为了实现这种能力必须使用 SocketChannel 而不是 Socket 。

回顾一下,要创建套接字,首先必须像通常使用 Socket 类那样创建一个套接字。套接字连接之后,使用 清单 2中的两行代码把流转换成通道。

清单 3. 创建套接字的另一种方法
Socket s = new Socket("www.ibm.com", 80);
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream());
WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());

如前所述,这样并不能实现非阻塞套接字通信――所有的通信仍然在阻塞模式下。在这种情况下,非阻塞通信必须模拟实现。模拟层不需要多少代码。让我们来看一看。

从模拟层读数据
模拟层在尝试读操作之前首先检查数据的可用性。如果数据可读则开始读。如果没有数据可用,可能是因为套接字被关闭,则返回表示这种情况的代码。在清单 4 中要注意仍然使用了 ReadableByteChannel 读,尽管 InputStream 完全可以执行这个动作。为什么这样做呢?为了造成是 NIO 而不是模拟层执行通信的假象。此外,还可以使模拟层与其他通道更容易结合,比如向文件通道内写入数据。

清单 4. 模拟非阻塞的