阻塞,還是非阻塞?這就是問題所在。無論在程序員的頭腦中多么高貴……當然這不是莎士比亞,本文提出了任何程序員在編寫 Internet 客戶程序時都應該考慮的一個重要問題。通信操作應該是阻塞的還是非阻塞的?
許多程序員在使用 Java 語言編寫 Internet 客戶程序時并沒有考慮這個問題,主要是因為在以前只有一種選擇――阻塞通信。但是現(xiàn)在
Java 程序員有了新的選擇,因此我們編寫的每個客戶程序也許都應該考慮一下。
非阻塞通信在 Java 2 SDK 的 1.4 版被引入 Java 語言。如果您曾經(jīng)使用該版本編過程序,可能會對新的 I/O 庫(NIO)留下了印象。在引入它之前,非阻塞通信只有在實現(xiàn)第三方庫的時候才能使用,而第三方庫常常會給應用程序引入缺陷。
NIO 庫包含了文件、管道以及客戶機和服務器套接字的非阻塞功能。庫中缺少的一個特性是安全的非阻塞套接字連接。在 NIO 或者 JSSE 庫中沒有建立安全的非阻塞通道類,但這并不意味著不能使用安全的非阻塞通信。只不過稍微麻煩一點。
要完全領會本文,您需要熟悉:
- Java 套接字通信的概念。您也應該實際編寫過應用程序。而且不只是打開連接、讀取一行然后退出的簡單應用程序,應該是實現(xiàn) POP3 或
HTTP 之類協(xié)議的客戶機或通信庫這樣的程序。
- SSL 基本概念和加密之類的概念。基本上就是知道如何設置一個安全連接(但不必擔心 JSSE ――這就是關于它的一個“緊急教程”)。
- NIO
庫。
- 在您選擇的平臺上安裝 Java 2 SDK 1.4
或以后的版本。(我是在 Windows 98 上使用 1.4.1_01 版。)
如果需要關于這些技術的介紹,請參閱
參考資料部分。
那么到底什么是阻塞和非阻塞通信呢?
阻塞和非阻塞通信
阻塞通信意味著通信方法在嘗試訪問套接字或者讀寫數(shù)據(jù)時阻塞了對套接字的訪問。在 JDK 1.4 之前,繞過阻塞限制的方法是無限制地使用線程,但這樣常常會造成大量的線程開銷,對系統(tǒng)的性能和可伸縮性產(chǎn)生影響。java.nio
包改變了這種狀況,允許服務器有效地使用 I/O 流,在合理的時間內(nèi)處理所服務的客戶請求。
沒
有非阻塞通信,這個過程就像我所喜歡說的“為所欲為”那樣。基本上,這個過程就是發(fā)送和讀取任何能夠發(fā)送/讀取的東西。如果沒有可以讀取的東西,它就中止
讀操作,做其他的事情直到能夠讀取為止。當發(fā)送數(shù)據(jù)時,該過程將試圖發(fā)送所有的數(shù)據(jù),但返回實際發(fā)送出的內(nèi)容。可能是全部數(shù)據(jù)、部分數(shù)據(jù)或者根本沒有發(fā)送
數(shù)據(jù)。
阻塞與非阻塞相比確實有一些優(yōu)點,特別是遇到錯誤控制問題的時候。在阻塞套接字通信中,如果出現(xiàn)錯誤,該訪問會自動返回標志錯誤的代碼。錯誤可能是由于網(wǎng)絡超時、套接字關閉或者任何類型的
I/O 錯誤造成的。在非阻塞套接字通信中,該方法能夠處理的唯一錯誤是網(wǎng)絡超時。為了檢測使用非阻塞通信的網(wǎng)絡超時,需要編寫稍微多一點的代碼,以確定自從上一次收到數(shù)據(jù)以來已經(jīng)多長時間了。
哪種方式更好取決于應用程序。如果使用的是同步通信,如果數(shù)據(jù)不必在讀取任何數(shù)據(jù)之前處理的話,阻塞通信更好一些,而非阻塞通信則提供了處理任何已經(jīng)讀取的數(shù)據(jù)的機會。而異步通信,如
IRC 和聊天客戶機則要求非阻塞通信以避免凍結(jié)套接字。
創(chuàng)建傳統(tǒng)的非阻塞客戶機套接字
Java NIO 庫使用通道而非流。通道可同時用于阻塞和非阻塞通信,但創(chuàng)建時默認為非阻塞版本。但是所有的非阻塞通信都要通過一個名字中包含
Channel
的類完成。在套接字通信中使用的類是
SocketChannel,
而創(chuàng)建該類的對象的過程不同于典型的套接字所用的過程,如清單
1 所示。
清單 1. 創(chuàng)建并連接 SocketChannel 對象
SocketChannel sc = SocketChannel.open(); sc.connect("www.ibm.com",80); sc.finishConnect();
|
必須聲明一個
SocketChannel
類型的指針,但是不能使用
new
操作符創(chuàng)建對象。相反,必須調(diào)用
SocketChannel
類的一個靜態(tài)方法打開通道。打開通道后,可以通過調(diào)用
connect()
方法與它連接。但是當該方法返回時,套接字不一定是連接的。為了確保套接字已經(jīng)連接,必須接著調(diào)用
finishConnect()
。
當套接字連接之后,非阻塞通信就可以開始使用
SocketChannel
類的
read()
和
write()
方法了。也可以把該對象強制轉(zhuǎn)換成單獨的
ReadableByteChannel
和
WritableByteChannel
對象。無論哪種方式,都要對數(shù)據(jù)使用
Buffer
對象。因為 NIO
庫的使用超出了本文的范圍,我們不再對此進一步討論。
當不再需要套接字時,可以使用
close()
方法將其關閉:
這樣就會同時關閉套接字連接和底層的通信通道。
創(chuàng)建替代的非阻塞的客戶機套接字
上述方法比傳統(tǒng)的創(chuàng)建套接字連接的例程稍微麻煩一點。不過,傳統(tǒng)的例程也能用于創(chuàng)建非阻塞套接字,不過需要增加幾個步驟以支持非阻塞通信。
SocketChannel
對象中的底層通信包括兩個
Channel
類:
ReadableByteChannel
和
WritableByteChannel。
這兩個類可以分別從現(xiàn)有的
InputStream
和
OutputStream
阻塞流中使用
Channels
類的
newChannel()
方法創(chuàng)建,如清單 2 所示:
清單 2. 從流中派生通道
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());
|
Channels
類也用于把通道轉(zhuǎn)換成流或者 reader 和 writer。這似乎是把通信切換到阻塞模式,但并非如此。如果試圖讀取從通道派生的流,讀方法將拋出
IllegalBlockingModeException
異常。
相反方向的轉(zhuǎn)換也是如此。不能使用
Channels
類把流轉(zhuǎn)換成通道而指望進行非阻塞通信。如果試圖讀從流派生的通道,讀仍然是阻塞的。但是像編程中的許多事情一樣,這一規(guī)則也有例外。
這種例外適合于實現(xiàn)
SelectableChannel
抽象類的類。
SelectableChannel
和它的派生類能夠選擇使用阻塞或者非阻塞模式。
SocketChannel
就是這樣的一個派生類。
但是,為了能夠在兩者之間來回切換,接口必須作為
SelectableChannel
實現(xiàn)。對于套接字而言,為了實現(xiàn)這種能力必須使用
SocketChannel
而不是
Socket
。
回顧一下,要創(chuàng)建套接字,首先必須像通常使用
Socket
類那樣創(chuàng)建一個套接字。套接字連接之后,使用
清單
2中的兩行代碼把流轉(zhuǎn)換成通道。
清單 3. 創(chuàng)建套接字的另一種方法
Socket s = new Socket("www.ibm.com", 80); ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());
|
如前所述,這樣并不能實現(xiàn)非阻塞套接字通信――所有的通信仍然在阻塞模式下。在這種情況下,非阻塞通信必須模擬實現(xiàn)。模擬層不需要多少代碼。讓我們來看一看。
從模擬層讀數(shù)據(jù)
模擬層在嘗試讀操作之前首先檢查數(shù)據(jù)的可用性。如果數(shù)據(jù)可讀則開始讀。如果沒有數(shù)據(jù)可用,可能是因為套接字被關閉,則返回表示這種情況的代碼。在清單
4 中要注意仍然使用了
ReadableByteChannel
讀,盡管
InputStream
完全可以執(zhí)行這個動作。為什么這樣做呢?為了造成是 NIO 而不是模擬層執(zhí)行通信的假象。此外,還可以使模擬層與其他通道更容易結(jié)合,比如向文件通道內(nèi)寫入數(shù)據(jù)。
清單 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);
|
向模擬層寫入數(shù)據(jù)
對于非阻塞通信,寫操作只寫入能夠?qū)懙臄?shù)據(jù)。發(fā)送緩沖區(qū)的大小和一次可以寫入的數(shù)據(jù)多少有很大關系。緩沖區(qū)的大小可以通過調(diào)用
Socket
對象的
getSendBufferSize()
方法確定。在嘗試非阻塞寫操作時必須考慮到這個大小。如果嘗試寫入比緩沖塊更大的數(shù)據(jù),必須拆開放到多個非阻塞寫操作中。太大的單個寫操作可能被阻塞。
清單 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;
|
與讀操作類似,首先要檢查套接字是否仍然連接。但是如果把數(shù)據(jù)寫入
WritableByteBuffer
對象,就像清單 5
那樣,該對象將自動進行檢查并在沒有連接時拋出必要的異常。在這個動作之后開始寫數(shù)據(jù)之前,流必須立即被清空,以保證發(fā)送緩沖區(qū)中有發(fā)送數(shù)據(jù)的空間。任何
寫操作都要這樣做。發(fā)送到塊中的數(shù)據(jù)與發(fā)送緩沖區(qū)的大小相同。執(zhí)行清除操作可以保證發(fā)送緩沖不會溢出而導致寫操作被阻塞。
因為假定寫操作只能寫入能夠?qū)懙膬?nèi)容,這個過程還必須檢查套接字保證它在每個數(shù)據(jù)塊寫入后仍然是打開的。如果在寫入數(shù)據(jù)時套接字被關閉,則必須中止寫操作并返回套接字關閉之前能夠發(fā)送的數(shù)據(jù)量。
BufferedOutputReader
可用于模擬非阻塞寫操作。如果試圖寫入超過緩沖區(qū)兩倍長度的數(shù)據(jù),則直接寫入緩沖區(qū)整倍數(shù)長度的數(shù)據(jù)(緩沖余下的數(shù)據(jù))。比如說,如果緩沖區(qū)的長度是
256 字節(jié)而需要寫入 529 字節(jié)的數(shù)據(jù),則該對象將清除當前緩沖區(qū)、發(fā)送 512 字節(jié)然后保存剩下的 17 字節(jié)。
對于非阻塞寫而言,這并非我們所期望的。我們希望分次把數(shù)據(jù)寫入同樣大小的緩沖區(qū)中,并最終把全部數(shù)據(jù)都寫完。如果發(fā)送的大塊數(shù)據(jù)留下一些數(shù)據(jù)被緩沖,那么在所有數(shù)據(jù)被發(fā)送的時候,寫操作就會被阻塞。
模擬層類模板
整個模擬層可以放到一個類中,以便更容易和應用程序集成。如果要這樣做,我建議從
ByteChannel
派生這個類。這個類可以強制轉(zhuǎn)換成單獨的
ReadableByteChannel
和
WritableByteChannel
類。
清單 6 給出了從
ByteChannel
派生的模擬層類模板的一個例子。本文后面將一直使用這個類表示通過阻塞連接執(zhí)行的非阻塞操作。
清單 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(); }
|
使用模擬層創(chuàng)建套接字
使用新建的模擬層創(chuàng)建套接字非常簡單。只要像通常那樣創(chuàng)建
Socket
對象,然后創(chuàng)建
nbChannel
對象就可以了,如清單 7 所示:
清單 7. 使用模擬層
Socket s = new Socket("www.ibm.com", 80); nbChannel socketChannel = new nbChannel(s); ReadableByteChannel rbc = (ReadableByteChannel)socketChannel; WritableByteChannel wbc = (WritableByteChannel)socketChannel;
|
創(chuàng)建傳統(tǒng)的非阻塞服務器套接字
服務器端的非阻塞套接字和客戶端上的沒有很大差別。稍微麻煩一點的只是建立接受輸入連接的套接字。套接字必須通過從服務器套接字通道派生一個阻塞的服務器套接字綁定到阻塞模式。清單
8 列出了需要做的步驟。
清單 8. 創(chuàng)建非阻塞的服務器套接字(SocketChannel)
ServerSocketChannel ssc = ServerSocketChannel.open(); ServerSocket ss = ssc.socket(); ss.bind(new InetSocketAddress(port)); SocketChannel sc = ssc.accept();
|
與客戶機套接字通道相似,服務器套接字通道也必須打開而不是使用
new
操作符或者構(gòu)造函數(shù)。在打開之后,必須派生服務器套接字對象以便把套接字通道綁定到一個端口。一旦套接字被綁定,服務器套接字對象就可以丟棄了。
通道使用
accept()
方法接收到來的連接并把它們轉(zhuǎn)給套接字通道。一旦接收了到來的連接并轉(zhuǎn)給套接字通道對象,通信就可以通過
read()
和
write()
方法開始進行了。
創(chuàng)建替代的非阻塞服務器套接字
實際上,并非真正的替代。因為服務器套接字通道必須使用服務器套接字對象綁定,為何不完全繞開服務器套接字通道而僅使用服務器套接字對象呢?不過這里的通信不使用
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;
|
創(chuàng)建 SSL 連接
創(chuàng)建SSL連接,我們要分別從客戶端和服務器端考察。
從客戶端
創(chuàng)建 SS L連接的傳統(tǒng)方法涉及到使用套接字工廠和其他一些東西。我將不會詳細討論如何創(chuàng)建SSL連接,不過有一本很好的教程,“Secure your
sockets with JSSE”(請參閱
參考資料),從中您可以了解到更多的信息。
創(chuàng)建 SSL 套接字的默認方法非常簡單,只包括幾個很短的步驟:
- 創(chuàng)建套接字工廠。
- 創(chuàng)建連接的套接字。
- 開始握手。
- 派生流。
- 通信。
清單 10 說明了這些步驟:
清單 10. 創(chuàng)建安全的客戶機套接字
SSLSocketFactory sslFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port); ssl.startHandshake(); InputStream is = ssl.getInputStream(); OutputStream os = ssl.getOutputStream();
|
默認方法不包括客戶驗證、用戶證書和其他特定連接可能需要的東西。
從服務器端
建立SSL服務器連接的傳統(tǒng)方法稍微麻煩一點,需要加上一些類型轉(zhuǎn)換。因為這些超出了本文的范圍,我將不再進一步介紹,而是說說支持SSL服務器連接的默認方法。
創(chuàng)建默認的 SSL 服務器套接字也包括幾個很短的步驟:
- 創(chuàng)建服務器套接字工廠。
- 創(chuàng)建并綁定服務器套接字。
- 接受傳入的連接。
- 開始握手。
- 派生流。
- 通信。
盡管看起來似乎與客戶端的步驟相似,要注意這里去掉了很多安全選項,比如客戶驗證。
清單 11 說明這些步驟:
清單 11. 創(chuàng)建安全的服務器套接字
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();
|
創(chuàng)建安全的非阻塞連接
要精心實現(xiàn)安全的非阻塞連接,也需要分別從客戶端和服務器端來看。
從客戶端
在客戶端建立安全的非阻塞連接非常簡單:
- 創(chuàng)建并連接
Socket
對象。
- 把
Socket
對象添加到模擬層上。
- 通過模擬層通信。
清單 12 說明了這些步驟:
清單 12. 創(chuàng)建安全的客戶機連接
/* 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;
|
利用前面給出的
模擬層類
就可以實現(xiàn)非阻塞的安全連接。因為安全套接字通道不能使用
SocketChannel
類打開,而 Java API
中又沒有完成這項工作的類,所以創(chuàng)建了一個模擬類。模擬類可以實現(xiàn)非阻塞通信,無論使用安全套接字連接還是非安全套接字連接。
列出的步驟包括默認的安全設置。對于更高級的安全性,比如用戶證書和客戶驗證,
參考資料
部分提供了說明如何實現(xiàn)的文章。
從服務器端
在服務器端建立套接字需要對默認安全稍加設置。但是一旦套接字被接收和路由,設置必須與客戶端的設置完全相同,如清單 13 所示:
清單 13. 創(chuàng)建安全的非阻塞服務器套接字
/* 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;
|
同樣,要記住這些步驟使用的是默認安全設置。
集成安全的和非安全的客戶機連接
多數(shù) Internet 客戶機應用程序,無論使用 Java 語言還是其他語言編寫,都需要提供安全和非安全連接。Java Secure Socket
Extensions 庫使得這項工作非常容易,我最近在編寫一個 HTTP 客戶庫時就使用了這種方法。
SSLSocket
類派生自
Socket。
您可能已經(jīng)猜到我要怎么做了。所需要的只是該對象的一個
Socket
指針。如果套接字連接不使用SSL,則可以像通常那樣創(chuàng)建套接字。如果要使用 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();
|
創(chuàng)建通道之后,如果套接字使用了SSL,那么就是安全通信,否則就是普通通信。如果使用了 SSL,關閉套接字將導致握手中止。
這種設置的一種可能是使用兩個單獨的類。一個類負責處理通過套接字沿著與非安全套接字的連接進行的所有通信。一個單獨的類應該負責創(chuàng)建安全的連接,包括安全連接的所有必要設置,無論是否是默認的。安全類應該直接插入通信類,只有在使用安全連接時被調(diào)用。
最簡單的集成方法
本文提出的方法是我所知道的把 JSSE 和 NIO 集成到同一代碼中以提供非阻塞安全通信的最簡單方法。盡管還有其他方法,但是都需要準備實現(xiàn)這一過程的程序員花費大量的時間和精力。
一種可能是使用 Java Cryptography Extensions 在 NIO 上實現(xiàn)自己的 SSL 層。另一種方法是修改現(xiàn)有的稱為
EspreSSL (以前稱為 jSSL)的定制 SSL 層, 把它改到 NIO 庫中。我建議只有在您有很充裕的時間時才使用這兩種方法。
參考資料
部分的可下載 zip
文件提供了示例代碼,幫助您實踐本文所述的技術,其中包括:
- nbChannel,清單 7 所介紹的模擬層的源代碼。
- 連接到 Verisign's Web 站點并下載主頁的示例 HTTPS 客戶程序。
- 一個簡單的非阻塞安全服務器 (Secure Daytime Server)。
- 集成的安全和非安全客戶程序。
參考資料
關于作者
|
|
|
Kenneth Ballard 目前在內(nèi)布拉斯加州珀魯州立大學學習計算機科學。他還是校內(nèi)的學生報紙
Peru
State Times 的職業(yè)作者。他從愛荷華州 Creston 的 Southwestern Community College
獲得了計算機編程 Associate of Science 學位,在那里作為 PC 技術人員參加了一個工作效率研究項目。他的研究范圍包括
Java 技術、C++、COBOL、Visual Basic 和網(wǎng)絡。可以通過
kenneth.ballard@ptk.org
與 Kenneth 聯(lián)系。
|
posted on 2007-01-19 00:02
苦笑枯 閱讀(330)
評論(0) 編輯 收藏 所屬分類:
Java