- 浏览: 115184 次
- 性别:
- 来自: 济南
文章分类
最新评论
-
yangbaodi516:
XMLInputFactory2 xmlif = (XMLIn ...
基于Woodstox的StAX 2 解析XML -
AK53pro:
SSL证书怎么伪造啊...有数字签名的啊...
SSL中间人攻击及防范 -
lysino:
若把此问题交给oracle的sequence来解决岂不是很简单 ...
一个循环流水号实现,求评 -
zuzong:
写的时候,考虑过用indexof查一次,删一次,后来写着写着就 ...
过滤掉非指定保留的html元素,保留元素间的内容和指定的html -
zuzong:
我一开始用的stringbuffer,发现删除了那些不需要的h ...
过滤掉非指定保留的html元素,保留元素间的内容和指定的html
转自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. 模拟非阻塞的读操作
/* The checkConnection method returns the character read when
determining if a connection is open.
*/
y = checkConnection();
if(y <= 0) return y;
buffer.putChar((char ) y);
return rbc.read(buffer);
向模拟层写入数据
对于非阻塞通信,写操作只写入能够写的数据。发送缓冲区的大小和一次可以写入的数据多少有很大关系。缓冲区的大小可以通过调用 Socket 对象的 getSendBufferSize() 方法确定。在尝试非阻塞写操作时必须考虑到这个大小。如果尝试写入比缓冲块更大的数据,必须拆开放到多个非阻塞写操作中。太大的单个写操作可能被阻塞。
清单 5. 模拟非阻塞的写操作
int x, y = s.getSendBufferSize(), z = 0;
int expectedWrite;
byte [] p = buffer.array();
ByteBuffer buf = ByteBuffer.allocateDirect(y);
/* If there isn't any data to write, return, otherwise flush the stream */
if(buffer.remaining() == 0) return 0;
os.flush()
for(x = 0; x < p.length; x += y)
{
if(p.length - x < y)
{
buf.put(p, x, p.length - x);
expectedWrite = p.length - x;
}
else
{
buf.put(p, x, y);
expectedWrite = y;
}
/* Check the status of the socket to make sure it's still open */
if(!s.isConnected()) break;
/* Write the data to the stream, flushing immediately afterward */
buf.flip();
z = wbc.write(buf); os.flush();
if(z < expectedWrite) break;
buf.clear();
}
if(x > p.length) return p.length;
else if(x == 0) return -1;
else return x + z;
与读操作类似,首先要检查套接字是否仍然连接。但是如果把数据写入 WritableByteBuffer 对象,就像清单 5 那样,该对象将自动进行检查并在没有连接时抛出必要的异常。在这个动作之后开始写数据之前,流必须立即被清空,以保证发送缓冲区中有发送数据的空间。任何写操作都要这样做。发送到块中的数据与发送缓冲区的大小相同。执行清除操作可以保证发送缓冲不会溢出而导致写操作被阻塞。
因为假定写操作只能写入能够写的内容,这个过程还必须检查套接字保证它在每个数据块写入后仍然是打开的。如果在写入数据时套接字被关闭,则必须中止写操作并返回套接字关闭之前能够发送的数据量。
BufferedOutputReader 可用于模拟非阻塞写操作。如果试图写入超过缓冲区两倍长度的数据,则直接写入缓冲区整倍数长度的数据(缓冲余下的数据)。比如说,如果缓冲区的长度是 256 字节而需要写入 529 字节的数据,则该对象将清除当前缓冲区、发送 512 字节然后保存剩下的 17 字节。
对于非阻塞写而言,这并非我们所期望的。我们希望分次把数据写入同样大小的缓冲区中,并最终把全部数据都写完。如果发送的大块数据留下一些数据被缓冲,那么在所有数据被发送的时候,写操作就会被阻塞。
模拟层类模板
整个模拟层可以放到一个类中,以便更容易和应用程序集成。如果要这样做,我建议从 ByteChannel 派生这个类。这个类可以强制转换成单独的 ReadableByteChannel 和 WritableByteChannel 类。
清单 6 给出了从 ByteChannel 派生的模拟层类模板的一个例子。本文后面将一直使用这个类表示通过阻塞连接执行的非阻塞操作。
清单 6. 模拟层的类模板
public class nbChannel implements ByteChannel
{
Socket s;
InputStream is; OutputStream os;
ReadableByteChannel rbc;
WritableByteChannel wbc;
public nbChannel(Socket socket);
public int read(ByteBuffer dest);
public int write(ByteBuffer src);
public void close();
protected int checkConnection();
}
使用模拟层创建套接字
使用新建的模拟层创建套接字非常简单。只要像通常那样创建 Socket 对象,然后创建 nbChannel 对象就可以了,如清单 7 所示:
清单 7. 使用模拟层
Socket s = new Socket("www.ibm.com", 80);
nbChannel socketChannel = new nbChannel(s);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
创建传统的非阻塞服务器套接字
服务器端的非阻塞套接字和客户端上的没有很大差别。稍微麻烦一点的只是建立接受输入连接的套接字。套接字必须通过从服务器套接字通道派生一个阻塞的服务器套接字绑定到阻塞模式。清单 8 列出了需要做的步骤。
清单 8. 创建非阻塞的服务器套接字(SocketChannel)
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket ss = ssc.socket();
ss.bind(new InetSocketAddress(port));
SocketChannel sc = ssc.accept();
与客户机套接字通道相似,服务器套接字通道也必须打开而不是使用 new 操作符或者构造函数。在打开之后,必须派生服务器套接字对象以便把套接字通道绑定到一个端口。一旦套接字被绑定,服务器套接字对象就可以丢弃了。
通道使用 accept() 方法接收到来的连接并把它们转给套接字通道。一旦接收了到来的连接并转给套接字通道对象,通信就可以通过 read() 和 write() 方法开始进行了。
创建替代的非阻塞服务器套接字
实际上,并非真正的替代。因为服务器套接字通道必须使用服务器套接字对象绑定,为何不完全绕开服务器套接字通道而仅使用服务器套接字对象呢?不过这里的通信不使用 SocketChannel ,而要使用模拟层 nbChannel。
清单 9. 建立服务器套接字的另一种方法
ServerSocket ss = new ServerSocket(port);
Socket s = ss.accept();
nbChannel socketChannel = new nbChannel(s);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
创建 SSL 连接
创建SSL连接,我们要分别从客户端和服务器端考察。
从客户端
创建 SS L连接的传统方法涉及到使用套接字工厂和其他一些东西。我将不会详细讨论如何创建SSL连接,不过有一本很好的教程,“Secure your sockets with JSSE”(请参阅 参考资料),从中您可以了解到更多的信息。
创建 SSL 套接字的默认方法非常简单,只包括几个很短的步骤:
创建套接字工厂。
创建连接的套接字。
开始握手。
派生流。
通信。
清单 10 说明了这些步骤:
清单 10. 创建安全的客户机套接字
SSLSocketFactory sslFactory =
(SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
ssl.startHandshake();
InputStream is = ssl.getInputStream();
OutputStream os = ssl.getOutputStream();
默认方法不包括客户验证、用户证书和其他特定连接可能需要的东西。
从服务器端
建立SSL服务器连接的传统方法稍微麻烦一点,需要加上一些类型转换。因为这些超出了本文的范围,我将不再进一步介绍,而是说说支持SSL服务器连接的默认方法。
创建默认的 SSL 服务器套接字也包括几个很短的步骤:
创建服务器套接字工厂。
创建并绑定服务器套接字。
接受传入的连接。
开始握手。
派生流。
通信。
尽管看起来似乎与客户端的步骤相似,要注意这里去掉了很多安全选项,比如客户验证。
清单 11 说明这些步骤:
清单 11. 创建安全的服务器套接字
SSLServerSocketFactory sslssf =
(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
SSLSocket ssls = (SSLSocket)sslss.accept();
ssls.startHandshake();
InputStream is = ssls.getInputStream();
OutputStream os = ssls.getOutputStream();
创建安全的非阻塞连接
要精心实现安全的非阻塞连接,也需要分别从客户端和服务器端来看。
从客户端
在客户端建立安全的非阻塞连接非常简单:
创建并连接 Socket 对象。
把 Socket 对象添加到模拟层上。
通过模拟层通信。
清单 12 说明了这些步骤:
清单 12. 创建安全的客户机连接
/* Create the factory, then the secure socket */
SSLSocketFactory sslFactory =
(SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
/* Start the handshake. Should be done before deriving channels */
ssl.startHandshake();
/* Put it into the emulation layer and create separate channels */
nbChannel socketChannel = new nbChannel(ssl);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
利用前面给出的 模拟层类 就可以实现非阻塞的安全连接。因为安全套接字通道不能使用 SocketChannel 类打开,而 Java API 中又没有完成这项工作的类,所以创建了一个模拟类。模拟类可以实现非阻塞通信,无论使用安全套接字连接还是非安全套接字连接。
列出的步骤包括默认的安全设置。对于更高级的安全性,比如用户证书和客户验证, 参考资料 部分提供了说明如何实现的文章。
从服务器端
在服务器端建立套接字需要对默认安全稍加设置。但是一旦套接字被接收和路由,设置必须与客户端的设置完全相同,如清单 13 所示:
清单 13. 创建安全的非阻塞服务器套接字
/* Create the factory, then the socket, and put it into listening mode */
SSLServerSocketFactory sslssf =
(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
SSLSocket ssls = (SSLSocket)sslss.accept();
/* Start the handshake on the new socket */
ssls.startHandshake();
/* Put it into the emulation layer and create separate channels */
nbChannel socketChannel = new nbChannel(ssls);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;同样,要记住这些步骤使用的是默认安全设置。
集成安全的和非安全的客户机连接
多数 Internet 客户机应用程序,无论使用 Java 语言还是其他语言编写,都需要提供安全和非安全连接。Java Secure Socket Extensions 库使得这项工作非常容易,我最近在编写一个 HTTP 客户库时就使用了这种方法。
SSLSocket 类派生自 Socket。 您可能已经猜到我要怎么做了。所需要的只是该对象的一个 Socket 指针。如果套接字连接不使用SSL,则可以像通常那样创建套接字。如果要使用 SSL,就稍微麻烦一点,但此后的代码就很简单了。清单 14 给出了一个例子:
清单 14. 集成安全的和非安全的客户机连接
Socket s;
ReadableByteChannel rbc;
WritableByteChannel wbc;
nbChannel socketChannel;
if(!useSSL) s = new Socket(host, port);
else
{
SSLSocketFactory sslsf = SSLSocketFactory.getDefault();
SSLSocket ssls = (SSLSocket)SSLSocketFactory.createSocket(host, port);
ssls.startHandshake();
s = ssls;
}
socketChannel = new nbChannel(s);
rbc = (ReadableByteChannel)socketChannel;
wbc = (WritableByteChannel)socketChannel;
...
s.close();
创建通道之后,如果套接字使用了SSL,那么就是安全通信,否则就是普通通信。如果使用了 SSL,关闭套接字将导致握手中止。
这种设置的一种可能是使用两个单独的类。一个类负责处理通过套接字沿着与非安全套接字的连接进行的所有通信。一个单独的类应该负责创建安全的连接,包括安全连接的所有必要设置,无论是否是默认的。安全类应该直接插入通信类,只有在使用安全连接时被调用。
最简单的集成方法
本文提出的方法是我所知道的把 JSSE 和 NIO 集成到同一代码中以提供非阻塞安全通信的最简单方法。尽管还有其他方法,但是都需要准备实现这一过程的程序员花费大量的时间和精力。
一种可能是使用 Java Cryptography Extensions 在 NIO 上实现自己的 SSL 层。另一种方法是修改现有的称为 EspreSSL (以前称为 jSSL)的定制 SSL 层, 把它改到 NIO 库中。我建议只有在您有很充裕的时间时才使用这两种方法。
参考资料 部分的可下载 zip 文件提供了示例代码,帮助您实践本文所述的技术,其中包括:
nbChannel,清单 7 所介绍的模拟层的源代码。
连接到 Verisign's Web 站点并下载主页的示例 HTTPS 客户程序。
一个简单的非阻塞安全服务器 (Secure Daytime Server)。
集成的安全和非安全客户程序。
阻塞,还是非阻塞?这就是问题所在。无论在程序员的头脑中多么高贵……当然这不是莎士比亚,本文提出了任何程序员在编写 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. 模拟非阻塞的读操作
/* The checkConnection method returns the character read when
determining if a connection is open.
*/
y = checkConnection();
if(y <= 0) return y;
buffer.putChar((char ) y);
return rbc.read(buffer);
向模拟层写入数据
对于非阻塞通信,写操作只写入能够写的数据。发送缓冲区的大小和一次可以写入的数据多少有很大关系。缓冲区的大小可以通过调用 Socket 对象的 getSendBufferSize() 方法确定。在尝试非阻塞写操作时必须考虑到这个大小。如果尝试写入比缓冲块更大的数据,必须拆开放到多个非阻塞写操作中。太大的单个写操作可能被阻塞。
清单 5. 模拟非阻塞的写操作
int x, y = s.getSendBufferSize(), z = 0;
int expectedWrite;
byte [] p = buffer.array();
ByteBuffer buf = ByteBuffer.allocateDirect(y);
/* If there isn't any data to write, return, otherwise flush the stream */
if(buffer.remaining() == 0) return 0;
os.flush()
for(x = 0; x < p.length; x += y)
{
if(p.length - x < y)
{
buf.put(p, x, p.length - x);
expectedWrite = p.length - x;
}
else
{
buf.put(p, x, y);
expectedWrite = y;
}
/* Check the status of the socket to make sure it's still open */
if(!s.isConnected()) break;
/* Write the data to the stream, flushing immediately afterward */
buf.flip();
z = wbc.write(buf); os.flush();
if(z < expectedWrite) break;
buf.clear();
}
if(x > p.length) return p.length;
else if(x == 0) return -1;
else return x + z;
与读操作类似,首先要检查套接字是否仍然连接。但是如果把数据写入 WritableByteBuffer 对象,就像清单 5 那样,该对象将自动进行检查并在没有连接时抛出必要的异常。在这个动作之后开始写数据之前,流必须立即被清空,以保证发送缓冲区中有发送数据的空间。任何写操作都要这样做。发送到块中的数据与发送缓冲区的大小相同。执行清除操作可以保证发送缓冲不会溢出而导致写操作被阻塞。
因为假定写操作只能写入能够写的内容,这个过程还必须检查套接字保证它在每个数据块写入后仍然是打开的。如果在写入数据时套接字被关闭,则必须中止写操作并返回套接字关闭之前能够发送的数据量。
BufferedOutputReader 可用于模拟非阻塞写操作。如果试图写入超过缓冲区两倍长度的数据,则直接写入缓冲区整倍数长度的数据(缓冲余下的数据)。比如说,如果缓冲区的长度是 256 字节而需要写入 529 字节的数据,则该对象将清除当前缓冲区、发送 512 字节然后保存剩下的 17 字节。
对于非阻塞写而言,这并非我们所期望的。我们希望分次把数据写入同样大小的缓冲区中,并最终把全部数据都写完。如果发送的大块数据留下一些数据被缓冲,那么在所有数据被发送的时候,写操作就会被阻塞。
模拟层类模板
整个模拟层可以放到一个类中,以便更容易和应用程序集成。如果要这样做,我建议从 ByteChannel 派生这个类。这个类可以强制转换成单独的 ReadableByteChannel 和 WritableByteChannel 类。
清单 6 给出了从 ByteChannel 派生的模拟层类模板的一个例子。本文后面将一直使用这个类表示通过阻塞连接执行的非阻塞操作。
清单 6. 模拟层的类模板
public class nbChannel implements ByteChannel
{
Socket s;
InputStream is; OutputStream os;
ReadableByteChannel rbc;
WritableByteChannel wbc;
public nbChannel(Socket socket);
public int read(ByteBuffer dest);
public int write(ByteBuffer src);
public void close();
protected int checkConnection();
}
使用模拟层创建套接字
使用新建的模拟层创建套接字非常简单。只要像通常那样创建 Socket 对象,然后创建 nbChannel 对象就可以了,如清单 7 所示:
清单 7. 使用模拟层
Socket s = new Socket("www.ibm.com", 80);
nbChannel socketChannel = new nbChannel(s);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
创建传统的非阻塞服务器套接字
服务器端的非阻塞套接字和客户端上的没有很大差别。稍微麻烦一点的只是建立接受输入连接的套接字。套接字必须通过从服务器套接字通道派生一个阻塞的服务器套接字绑定到阻塞模式。清单 8 列出了需要做的步骤。
清单 8. 创建非阻塞的服务器套接字(SocketChannel)
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket ss = ssc.socket();
ss.bind(new InetSocketAddress(port));
SocketChannel sc = ssc.accept();
与客户机套接字通道相似,服务器套接字通道也必须打开而不是使用 new 操作符或者构造函数。在打开之后,必须派生服务器套接字对象以便把套接字通道绑定到一个端口。一旦套接字被绑定,服务器套接字对象就可以丢弃了。
通道使用 accept() 方法接收到来的连接并把它们转给套接字通道。一旦接收了到来的连接并转给套接字通道对象,通信就可以通过 read() 和 write() 方法开始进行了。
创建替代的非阻塞服务器套接字
实际上,并非真正的替代。因为服务器套接字通道必须使用服务器套接字对象绑定,为何不完全绕开服务器套接字通道而仅使用服务器套接字对象呢?不过这里的通信不使用 SocketChannel ,而要使用模拟层 nbChannel。
清单 9. 建立服务器套接字的另一种方法
ServerSocket ss = new ServerSocket(port);
Socket s = ss.accept();
nbChannel socketChannel = new nbChannel(s);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
创建 SSL 连接
创建SSL连接,我们要分别从客户端和服务器端考察。
从客户端
创建 SS L连接的传统方法涉及到使用套接字工厂和其他一些东西。我将不会详细讨论如何创建SSL连接,不过有一本很好的教程,“Secure your sockets with JSSE”(请参阅 参考资料),从中您可以了解到更多的信息。
创建 SSL 套接字的默认方法非常简单,只包括几个很短的步骤:
创建套接字工厂。
创建连接的套接字。
开始握手。
派生流。
通信。
清单 10 说明了这些步骤:
清单 10. 创建安全的客户机套接字
SSLSocketFactory sslFactory =
(SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
ssl.startHandshake();
InputStream is = ssl.getInputStream();
OutputStream os = ssl.getOutputStream();
默认方法不包括客户验证、用户证书和其他特定连接可能需要的东西。
从服务器端
建立SSL服务器连接的传统方法稍微麻烦一点,需要加上一些类型转换。因为这些超出了本文的范围,我将不再进一步介绍,而是说说支持SSL服务器连接的默认方法。
创建默认的 SSL 服务器套接字也包括几个很短的步骤:
创建服务器套接字工厂。
创建并绑定服务器套接字。
接受传入的连接。
开始握手。
派生流。
通信。
尽管看起来似乎与客户端的步骤相似,要注意这里去掉了很多安全选项,比如客户验证。
清单 11 说明这些步骤:
清单 11. 创建安全的服务器套接字
SSLServerSocketFactory sslssf =
(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
SSLSocket ssls = (SSLSocket)sslss.accept();
ssls.startHandshake();
InputStream is = ssls.getInputStream();
OutputStream os = ssls.getOutputStream();
创建安全的非阻塞连接
要精心实现安全的非阻塞连接,也需要分别从客户端和服务器端来看。
从客户端
在客户端建立安全的非阻塞连接非常简单:
创建并连接 Socket 对象。
把 Socket 对象添加到模拟层上。
通过模拟层通信。
清单 12 说明了这些步骤:
清单 12. 创建安全的客户机连接
/* Create the factory, then the secure socket */
SSLSocketFactory sslFactory =
(SSLSocketFactory)SSLSocketFactory.getDefault();
SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
/* Start the handshake. Should be done before deriving channels */
ssl.startHandshake();
/* Put it into the emulation layer and create separate channels */
nbChannel socketChannel = new nbChannel(ssl);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;
利用前面给出的 模拟层类 就可以实现非阻塞的安全连接。因为安全套接字通道不能使用 SocketChannel 类打开,而 Java API 中又没有完成这项工作的类,所以创建了一个模拟类。模拟类可以实现非阻塞通信,无论使用安全套接字连接还是非安全套接字连接。
列出的步骤包括默认的安全设置。对于更高级的安全性,比如用户证书和客户验证, 参考资料 部分提供了说明如何实现的文章。
从服务器端
在服务器端建立套接字需要对默认安全稍加设置。但是一旦套接字被接收和路由,设置必须与客户端的设置完全相同,如清单 13 所示:
清单 13. 创建安全的非阻塞服务器套接字
/* Create the factory, then the socket, and put it into listening mode */
SSLServerSocketFactory sslssf =
(SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
SSLSocket ssls = (SSLSocket)sslss.accept();
/* Start the handshake on the new socket */
ssls.startHandshake();
/* Put it into the emulation layer and create separate channels */
nbChannel socketChannel = new nbChannel(ssls);
ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
WritableByteChannel wbc = (WritableByteChannel)socketChannel;同样,要记住这些步骤使用的是默认安全设置。
集成安全的和非安全的客户机连接
多数 Internet 客户机应用程序,无论使用 Java 语言还是其他语言编写,都需要提供安全和非安全连接。Java Secure Socket Extensions 库使得这项工作非常容易,我最近在编写一个 HTTP 客户库时就使用了这种方法。
SSLSocket 类派生自 Socket。 您可能已经猜到我要怎么做了。所需要的只是该对象的一个 Socket 指针。如果套接字连接不使用SSL,则可以像通常那样创建套接字。如果要使用 SSL,就稍微麻烦一点,但此后的代码就很简单了。清单 14 给出了一个例子:
清单 14. 集成安全的和非安全的客户机连接
Socket s;
ReadableByteChannel rbc;
WritableByteChannel wbc;
nbChannel socketChannel;
if(!useSSL) s = new Socket(host, port);
else
{
SSLSocketFactory sslsf = SSLSocketFactory.getDefault();
SSLSocket ssls = (SSLSocket)SSLSocketFactory.createSocket(host, port);
ssls.startHandshake();
s = ssls;
}
socketChannel = new nbChannel(s);
rbc = (ReadableByteChannel)socketChannel;
wbc = (WritableByteChannel)socketChannel;
...
s.close();
创建通道之后,如果套接字使用了SSL,那么就是安全通信,否则就是普通通信。如果使用了 SSL,关闭套接字将导致握手中止。
这种设置的一种可能是使用两个单独的类。一个类负责处理通过套接字沿着与非安全套接字的连接进行的所有通信。一个单独的类应该负责创建安全的连接,包括安全连接的所有必要设置,无论是否是默认的。安全类应该直接插入通信类,只有在使用安全连接时被调用。
最简单的集成方法
本文提出的方法是我所知道的把 JSSE 和 NIO 集成到同一代码中以提供非阻塞安全通信的最简单方法。尽管还有其他方法,但是都需要准备实现这一过程的程序员花费大量的时间和精力。
一种可能是使用 Java Cryptography Extensions 在 NIO 上实现自己的 SSL 层。另一种方法是修改现有的称为 EspreSSL (以前称为 jSSL)的定制 SSL 层, 把它改到 NIO 库中。我建议只有在您有很充裕的时间时才使用这两种方法。
参考资料 部分的可下载 zip 文件提供了示例代码,帮助您实践本文所述的技术,其中包括:
nbChannel,清单 7 所介绍的模拟层的源代码。
连接到 Verisign's Web 站点并下载主页的示例 HTTPS 客户程序。
一个简单的非阻塞安全服务器 (Secure Daytime Server)。
集成的安全和非安全客户程序。
发表评论
-
Java虚拟机(JVM)参数配置说明
2011-10-14 16:50 995转自 http://lavasoft.blog.51cto.c ... -
基于事件的 NIO 多线程服务器
2011-10-14 16:24 1102转自 http://www.ibm.com/developer ... -
windows下批量删除svn文件夹
2011-06-10 16:02 1696Windows Registry Editor Versi ... -
Spring 3.x 全注解配置
2011-06-10 15:47 2137web.xml <listener> & ... -
Log4j配置文件XML方式和按包路径分开输出日志
2011-06-10 14:20 12561使用Log4j,则推荐用XML来配置属性参数,优点是打印输出更 ... -
基于Woodstox的StAX 2 解析XML
2011-06-08 17:56 9317StAX (Streaming API for XML)面向流 ... -
Spring 3.0.5 MVC 基于注解的拦截器
2011-05-23 17:28 3099org.springframework.web.servlet ... -
Spring 3.0.5 MVC 异常处理
2011-05-23 16:33 2398SimpleMappingExceptionResolver ... -
过滤掉非指定保留的html元素,保留元素间的内容和指定的html
2011-03-05 21:07 1479public static void main(Strin ... -
根据表单对象,为业务对象赋值
2010-09-08 15:22 1093package com.team.engine.util; ... -
在iBatis中加入c3p0数据库连接池
2009-06-13 16:32 4559我看论坛里有两个人写了在iBatis中加入c3p0,我也就不跟 ... -
enum的写法
2009-01-07 11:12 1387public static enum TTutorial ... -
Tomcat5.5.20 使用JDBC 配置Mysql 5.0.22数据库连接池
2008-12-21 01:17 3006这是我最早在csdn发的文章,2007年3月13日,很有纪念 ... -
Spring之IOC
2008-12-21 01:05 1133Spring的功能是 ... -
List的JavaExcel工具类(读,写)
2008-12-21 01:03 116207年9月的 读XML: import java.i ... -
List的JavaExcel工具类
2008-12-21 00:58 1503这也是大约在夏季,应该接近秋天了吧 支持office200 ... -
读,写 properties属性文件
2008-12-21 00:50 2147这个是07年,可能是在8月份写的。。。大约在夏季。。。 ... -
分~页~,自动生成表格
2008-12-21 00:36 1429这是07年11月发在csdn blog里的,隔得时间太长,都记 ... -
java正则式的应用,持续更新。。。
2008-12-20 23:55 1256String ss = "呵呵HAAhaha2 ... -
控制Word,Excel在浏览器中打开,还是下载
2008-12-20 23:41 4984<%@ page contentType=&quo ...
相关推荐
这种方法是通过配置 Java 的内置 SSL/TLS 库来实现的,适用于希望使用 Java 本身提供的安全套接字功能的用户。 ##### 1. 生成 Java 认证文件 为了确保 HTTPS 连接的安全性,首先需要生成一个 Java 认证文件,即 ...
通过启用Domain配置中的“ExalogicOptimizationsEnabled”标志,可以启用包括纯Java NIO套接字合成器、分散读取、集中写入、使用JSSE作为SSL提供者、无锁定请求管理器以及更加主动的自调优线程算法等一系列内核优化...
Java NIO(New I/O)提供了一种更高效的数据传输方式,通过选择器(Selector)和通道(Channel)实现非阻塞I/O,适合高并发、低延迟的网络应用。 七、Java网络编程实践 在“第十课 Java 网络编程”中,你将学习如何...
在本文中,我们将深入探讨Java网络通信系统的关键技术和实现方法,并结合提供的源代码和开题报告来理解其核心概念。 首先,我们要了解Java在网络通信中的基础——Java套接字(Sockets)。Java套接字是网络通信的...
Java的网络编程主要包括套接字(Sockets)、服务器套接字(ServerSockets)、URL、HTTP客户端、多线程以及NIO(非阻塞I/O)等核心概念。下面将详细阐述这些知识点。 1. **套接字(Sockets)** Java套接字是实现...
6. **NIO(非阻塞I/O)**:Java的NIO(New I/O)API提供了一种更高效的I/O模型,它支持非阻塞读写,适用于高并发的网络应用。NIO通道、缓冲区和选择器是其核心概念。 7. **网络安全**:网络安全是网络编程的重要...
面向对象的Java网络编程主要涉及如何使用Java API来实现网络通信,包括TCP/IP套接字、UDP套接字、HTTP协议以及NIO(非阻塞I/O)等技术。下面将详细介绍这些知识点。 1. **TCP/IP套接字**:Java提供了java.net....
例如,可以设计一种简单的文本协议,每条消息都以特定的分隔符(如换行符)开头和结尾,以便服务器和客户端能够正确地解析消息。还可以添加一些额外的信息,如发送者ID,以确保消息的可追踪性。 总的来说,`...
5. **IO流与NIO**: Java标准IO流用于读写网络数据,而NIO(Non-blocking I/O)提供了一种非阻塞的I/O模型,能更高效地处理大量并发连接。NIO引入了选择器(Selector)和通道(Channel)的概念,允许单个线程处理多个...
Java网络编程是Java开发中的重要领域,它涵盖了网络通信的所有基本概念和技术,包括TCP/IP协议、套接字编程、多线程以及数据的序列化与反序列化等。本学习资料将带你深入理解并掌握这些关键知识点。 1. **TCP/IP...
6. **NIO(非阻塞I/O)**:Java的NIO(New I/O)库提供了一种更高效的数据传输方式,尤其在处理大量并发连接时。它引入了选择器(Selector)和通道(Channel)的概念,允许单个线程管理多个网络连接。 7. **HTTP...
3. **网络安全**:涉及加密技术,如SSL/TLS协议,以及如何在Java中使用JSSE(Java Secure Socket Extension)实现安全套接字通信。 4. **网络I/O模型**:除了基本的阻塞I/O,Java还提供了非阻塞I/O(NIO)和异步I/O...
NIO引入了通道(Channel)和选择器(Selector),能够实现非阻塞I/O,这对于高并发的网络应用至关重要。 5. **网络协议的理解**:HTTP、FTP、SMTP等常见网络协议的理解是实现网络通信的关键。Java通过HttpURLConnection...
3. **套接字编程**:Java的Socket编程是网络编程的核心,包括TCP套接字和UDP套接字。TCP套接字提供面向连接的、可靠的、基于字节流的通信,而UDP套接字则提供无连接的、不可靠的、基于数据报的通信。 4. **多线程与...
其次,书中会详细介绍套接字(Socket)和服务器套接字(ServerSocket)的工作原理及使用方法。包括创建Socket对象来建立连接,发送和接收数据,以及关闭连接等基本操作。同时,还会讲解多线程技术在处理并发连接中的...
JAVA的`java.io`和`java.nio`包提供了丰富的流类,如`InputStream`和`OutputStream`用于字节流,`Reader`和`Writer`用于字符流,以及NIO(非阻塞I/O)提供的`Selector`和`Channel`,这些都极大地简化了网络数据的...
4. **NIO(非阻塞I/O)与AIO(异步I/O)**: Java NIO(New I/O)库提供了一种新的I/O模型,可以实现非阻塞的数据读写,提高了系统的并发性能。而AIO(Asynchronous I/O)进一步扩展了这一概念,引入了异步事件通知...