簡(jiǎn)介: 盡管 SSL 阻塞操作――當讀寫(xiě)數據的時(shí)候套接字的訪(fǎng)問(wèn)被阻塞――與對應的非阻塞方式相比提供了更好的 I/O錯誤通知,但是非阻塞操作允許調用的線(xiàn)程繼續運行。本文中,作者同時(shí)就客戶(hù)端和服務(wù)器端描述了如何使用Java Secure SocketExtensions (JSSE) 和 Java NIO (新I/O)庫創(chuàng )建非阻塞的安全連接,并且介紹了創(chuàng )建非阻塞套接字的傳統方法,以及使用JSSE 和 NIO 的一種可選的(必需的)方法。
阻塞,還是非阻塞?這就是問(wèn)題所在。無(wú)論在程序員的頭腦中多么高貴……當然這不是莎士比亞,本文提出了任何程序員在編寫(xiě) Internet 客戶(hù)程序時(shí)都應該考慮的一個(gè)重要問(wèn)題。通信操作應該是阻塞的還是非阻塞的?
許多程序員在使用 Java 語(yǔ)言編寫(xiě) Internet 客戶(hù)程序時(shí)并沒(méi)有考慮這個(gè)問(wèn)題,主要是因為在以前只有一種選擇――阻塞通信。但是現在Java 程序員有了新的選擇,因此我們編寫(xiě)的每個(gè)客戶(hù)程序也許都應該考慮一下。
非阻塞通信在 Java 2 SDK 的 1.4 版被引入 Java 語(yǔ)言。如果您曾經(jīng)使用該版本編過(guò)程序,可能會(huì )對新的 I/O 庫(NIO)留下了印象。在引入它之前,非阻塞通信只有在實(shí)現第三方庫的時(shí)候才能使用,而第三方庫常常會(huì )給應用程序引入缺陷。
NIO 庫包含了文件、管道以及客戶(hù)機和服務(wù)器套接字的非阻塞功能。庫中缺少的一個(gè)特性是安全的非阻塞套接字連接。在 NIO 或者 JSSE 庫中沒(méi)有建立安全的非阻塞通道類(lèi),但這并不意味著(zhù)不能使用安全的非阻塞通信。只不過(guò)稍微麻煩一點(diǎn)。
要完全領(lǐng)會(huì )本文,您需要熟悉:
如果需要關(guān)于這些技術(shù)的介紹,請參閱參考資料部分。
那么到底什么是阻塞和非阻塞通信呢?
阻塞通信意味著(zhù)通信方法在嘗試訪(fǎng)問(wèn)套接字或者讀寫(xiě)數據時(shí)阻塞了對套接字的訪(fǎng)問(wèn)。在 JDK 1.4 之前,繞過(guò)阻塞限制的方法是無(wú)限制地使用線(xiàn)程,但這樣常常會(huì )造成大量的線(xiàn)程開(kāi)銷(xiāo),對系統的性能和可伸縮性產(chǎn)生影響。java.nio包改變了這種狀況,允許服務(wù)器有效地使用 I/O 流,在合理的時(shí)間內處理所服務(wù)的客戶(hù)請求。
沒(méi)有非阻塞通信,這個(gè)過(guò)程就像我所喜歡說(shuō)的“為所欲為”那樣?;旧?,這個(gè)過(guò)程就是發(fā)送和讀取任何能夠發(fā)送/讀取的東西。如果沒(méi)有可以讀取的東西,它就中止讀操作,做其他的事情直到能夠讀取為止。當發(fā)送數據時(shí),該過(guò)程將試圖發(fā)送所有的數據,但返回實(shí)際發(fā)送出的內容??赡苁侨繑祿?、部分數據或者根本沒(méi)有發(fā)送數據。
阻塞與非阻塞相比確實(shí)有一些優(yōu)點(diǎn),特別是遇到錯誤控制問(wèn)題的時(shí)候。在阻塞套接字通信中,如果出現錯誤,該訪(fǎng)問(wèn)會(huì )自動(dòng)返回標志錯誤的代碼。錯誤可能是由于網(wǎng)絡(luò )超時(shí)、套接字關(guān)閉或者任何類(lèi)型的I/O 錯誤造成的。在非阻塞套接字通信中,該方法能夠處理的唯一錯誤是網(wǎng)絡(luò )超時(shí)。為了檢測使用非阻塞通信的網(wǎng)絡(luò )超時(shí),需要編寫(xiě)稍微多一點(diǎn)的代碼,以確定自從上一次收到數據以來(lái)已經(jīng)多長(cháng)時(shí)間了。
哪種方式更好取決于應用程序。如果使用的是同步通信,如果數據不必在讀取任何數據之前處理的話(huà),阻塞通信更好一些,而非阻塞通信則提供了處理任何已經(jīng)讀取的數據的機會(huì )。而異步通信,如IRC 和聊天客戶(hù)機則要求非阻塞通信以避免凍結套接字。
Java NIO 庫使用通道而非流。通道可同時(shí)用于阻塞和非阻塞通信,但創(chuàng )建時(shí)默認為非阻塞版本。但是所有的非阻塞通信都要通過(guò)一個(gè)名字中包含Channel的類(lèi)完成。在套接字通信中使用的類(lèi)是SocketChannel, 而創(chuàng )建該類(lèi)的對象的過(guò)程不同于典型的套接字所用的過(guò)程,如清單1 所示。
SocketChannel sc = SocketChannel.open(); |
必須聲明一個(gè)SocketChannel類(lèi)型的指針,但是不能使用new操作符創(chuàng )建對象。相反,必須調用SocketChannel類(lèi)的一個(gè)靜態(tài)方法打開(kāi)通道。打開(kāi)通道后,可以通過(guò)調用connect()方法與它連接。但是當該方法返回時(shí),套接字不一定是連接的。為了確保套接字已經(jīng)連接,必須接著(zhù)調用finishConnect() 。
當套接字連接之后,非阻塞通信就可以開(kāi)始使用SocketChannel類(lèi)的read() 和write()方法了。也可以把該對象強制轉換成單獨的ReadableByteChannel和WritableByteChannel對象。無(wú)論哪種方式,都要對數據使用Buffer對象。因為 NIO庫的使用超出了本文的范圍,我們不再對此進(jìn)一步討論。
當不再需要套接字時(shí),可以使用close() 方法將其關(guān)閉:
sc.close(); |
這樣就會(huì )同時(shí)關(guān)閉套接字連接和底層的通信通道。
上述方法比傳統的創(chuàng )建套接字連接的例程稍微麻煩一點(diǎn)。不過(guò),傳統的例程也能用于創(chuàng )建非阻塞套接字,不過(guò)需要增加幾個(gè)步驟以支持非阻塞通信。
SocketChannel 對象中的底層通信包括兩個(gè)Channel 類(lèi):ReadableByteChannel和WritableByteChannel。 這兩個(gè)類(lèi)可以分別從現有的InputStream和OutputStream 阻塞流中使用Channels類(lèi)的newChannel() 方法創(chuàng )建,如清單 2 所示:
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream()); |
Channels 類(lèi)也用于把通道轉換成流或者 reader 和 writer。這似乎是把通信切換到阻塞模式,但并非如此。如果試圖讀取從通道派生的流,讀方法將拋出IllegalBlockingModeException 異常。
相反方向的轉換也是如此。不能使用Channels 類(lèi)把流轉換成通道而指望進(jìn)行非阻塞通信。如果試圖讀從流派生的通道,讀仍然是阻塞的。但是像編程中的許多事情一樣,這一規則也有例外。
這種例外適合于實(shí)現SelectableChannel抽象類(lèi)的類(lèi)。SelectableChannel和它的派生類(lèi)能夠選擇使用阻塞或者非阻塞模式。SocketChannel就是這樣的一個(gè)派生類(lèi)。
但是,為了能夠在兩者之間來(lái)回切換,接口必須作為SelectableChannel 實(shí)現。對于套接字而言,為了實(shí)現這種能力必須使用SocketChannel而不是Socket 。
回顧一下,要創(chuàng )建套接字,首先必須像通常使用Socket 類(lèi)那樣創(chuàng )建一個(gè)套接字。套接字連接之后,使用清單2中的兩行代碼把流轉換成通道。
Socket s = new Socket("www.ibm.com", 80); |
如前所述,這樣并不能實(shí)現非阻塞套接字通信――所有的通信仍然在阻塞模式下。在這種情況下,非阻塞通信必須模擬實(shí)現。模擬層不需要多少代碼。讓我們來(lái)看一看。
模擬層在嘗試讀操作之前首先檢查數據的可用性。如果數據可讀則開(kāi)始讀。如果沒(méi)有數據可用,可能是因為套接字被關(guān)閉,則返回表示這種情況的代碼。在清單4 中要注意仍然使用了ReadableByteChannel 讀,盡管InputStream完全可以執行這個(gè)動(dòng)作。為什么這樣做呢?為了造成是 NIO 而不是模擬層執行通信的假象。此外,還可以使模擬層與其他通道更容易結合,比如向文件通道內寫(xiě)入數據。
/* The checkConnection method returns the character read when |
對于非阻塞通信,寫(xiě)操作只寫(xiě)入能夠寫(xiě)的數據。發(fā)送緩沖區的大小和一次可以寫(xiě)入的數據多少有很大關(guān)系。緩沖區的大小可以通過(guò)調用Socket對象的getSendBufferSize()方法確定。在嘗試非阻塞寫(xiě)操作時(shí)必須考慮到這個(gè)大小。如果嘗試寫(xiě)入比緩沖塊更大的數據,必須拆開(kāi)放到多個(gè)非阻塞寫(xiě)操作中。太大的單個(gè)寫(xiě)操作可能被阻塞。
int x, y = s.getSendBufferSize(), z = 0; |
與讀操作類(lèi)似,首先要檢查套接字是否仍然連接。但是如果把數據寫(xiě)入WritableByteBuffer 對象,就像清單 5那樣,該對象將自動(dòng)進(jìn)行檢查并在沒(méi)有連接時(shí)拋出必要的異常。在這個(gè)動(dòng)作之后開(kāi)始寫(xiě)數據之前,流必須立即被清空,以保證發(fā)送緩沖區中有發(fā)送數據的空間。任何寫(xiě)操作都要這樣做。發(fā)送到塊中的數據與發(fā)送緩沖區的大小相同。執行清除操作可以保證發(fā)送緩沖不會(huì )溢出而導致寫(xiě)操作被阻塞。
因為假定寫(xiě)操作只能寫(xiě)入能夠寫(xiě)的內容,這個(gè)過(guò)程還必須檢查套接字保證它在每個(gè)數據塊寫(xiě)入后仍然是打開(kāi)的。如果在寫(xiě)入數據時(shí)套接字被關(guān)閉,則必須中止寫(xiě)操作并返回套接字關(guān)閉之前能夠發(fā)送的數據量。
BufferedOutputReader可用于模擬非阻塞寫(xiě)操作。如果試圖寫(xiě)入超過(guò)緩沖區兩倍長(cháng)度的數據,則直接寫(xiě)入緩沖區整倍數長(cháng)度的數據(緩沖余下的數據)。比如說(shuō),如果緩沖區的長(cháng)度是256 字節而需要寫(xiě)入 529 字節的數據,則該對象將清除當前緩沖區、發(fā)送 512 字節然后保存剩下的 17 字節。
對于非阻塞寫(xiě)而言,這并非我們所期望的。我們希望分次把數據寫(xiě)入同樣大小的緩沖區中,并最終把全部數據都寫(xiě)完。如果發(fā)送的大塊數據留下一些數據被緩沖,那么在所有數據被發(fā)送的時(shí)候,寫(xiě)操作就會(huì )被阻塞。
整個(gè)模擬層可以放到一個(gè)類(lèi)中,以便更容易和應用程序集成。如果要這樣做,我建議從ByteChannel 派生這個(gè)類(lèi)。這個(gè)類(lèi)可以強制轉換成單獨的ReadableByteChannel和WritableByteChannel 類(lèi)。
清單 6 給出了從ByteChannel 派生的模擬層類(lèi)模板的一個(gè)例子。本文后面將一直使用這個(gè)類(lèi)表示通過(guò)阻塞連接執行的非阻塞操作。
public class nbChannel implements ByteChannel |
使用新建的模擬層創(chuàng )建套接字非常簡(jiǎn)單。只要像通常那樣創(chuàng )建Socket 對象,然后創(chuàng )建nbChannel對象就可以了,如清單 7 所示:
Socket s = new Socket("www.ibm.com", 80); |
服務(wù)器端的非阻塞套接字和客戶(hù)端上的沒(méi)有很大差別。稍微麻煩一點(diǎn)的只是建立接受輸入連接的套接字。套接字必須通過(guò)從服務(wù)器套接字通道派生一個(gè)阻塞的服務(wù)器套接字綁定到阻塞模式。清單8 列出了需要做的步驟。
ServerSocketChannel ssc = ServerSocketChannel.open(); |
與客戶(hù)機套接字通道相似,服務(wù)器套接字通道也必須打開(kāi)而不是使用new 操作符或者構造函數。在打開(kāi)之后,必須派生服務(wù)器套接字對象以便把套接字通道綁定到一個(gè)端口。一旦套接字被綁定,服務(wù)器套接字對象就可以丟棄了。
通道使用accept()方法接收到來(lái)的連接并把它們轉給套接字通道。一旦接收了到來(lái)的連接并轉給套接字通道對象,通信就可以通過(guò)read()和write() 方法開(kāi)始進(jìn)行了。
實(shí)際上,并非真正的替代。因為服務(wù)器套接字通道必須使用服務(wù)器套接字對象綁定,為何不完全繞開(kāi)服務(wù)器套接字通道而僅使用服務(wù)器套接字對象呢?不過(guò)這里的通信不使用SocketChannel ,而要使用模擬層nbChannel。
ServerSocket ss = new ServerSocket(port); |
創(chuàng )建SSL連接,我們要分別從客戶(hù)端和服務(wù)器端考察。
創(chuàng )建 SS L連接的傳統方法涉及到使用套接字工廠(chǎng)和其他一些東西。我將不會(huì )詳細討論如何創(chuàng )建SSL連接,不過(guò)有一本很好的教程,“Secure yoursockets with JSSE”(請參閱參考資料),從中您可以了解到更多的信息。
創(chuàng )建 SSL 套接字的默認方法非常簡(jiǎn)單,只包括幾個(gè)很短的步驟:
清單 10 說(shuō)明了這些步驟:
SSLSocketFactory sslFactory = |
默認方法不包括客戶(hù)驗證、用戶(hù)證書(shū)和其他特定連接可能需要的東西。
建立SSL服務(wù)器連接的傳統方法稍微麻煩一點(diǎn),需要加上一些類(lèi)型轉換。因為這些超出了本文的范圍,我將不再進(jìn)一步介紹,而是說(shuō)說(shuō)支持SSL服務(wù)器連接的默認方法。
創(chuàng )建默認的 SSL 服務(wù)器套接字也包括幾個(gè)很短的步驟:
盡管看起來(lái)似乎與客戶(hù)端的步驟相似,要注意這里去掉了很多安全選項,比如客戶(hù)驗證。
清單 11 說(shuō)明這些步驟:
SSLServerSocketFactory sslssf = |
要精心實(shí)現安全的非阻塞連接,也需要分別從客戶(hù)端和服務(wù)器端來(lái)看。
在客戶(hù)端建立安全的非阻塞連接非常簡(jiǎn)單:
Socket 對象。 Socket 對象添加到模擬層上。 清單 12 說(shuō)明了這些步驟:
/* Create the factory, then the secure socket */ |
利用前面給出的模擬層類(lèi)就可以實(shí)現非阻塞的安全連接。因為安全套接字通道不能使用SocketChannel類(lèi)打開(kāi),而 Java API中又沒(méi)有完成這項工作的類(lèi),所以創(chuàng )建了一個(gè)模擬類(lèi)。模擬類(lèi)可以實(shí)現非阻塞通信,無(wú)論使用安全套接字連接還是非安全套接字連接。
列出的步驟包括默認的安全設置。對于更高級的安全性,比如用戶(hù)證書(shū)和客戶(hù)驗證,參考資料部分提供了說(shuō)明如何實(shí)現的文章。
在服務(wù)器端建立套接字需要對默認安全稍加設置。但是一旦套接字被接收和路由,設置必須與客戶(hù)端的設置完全相同,如清單 13 所示:
/* Create the factory, then the socket, and put it into listening mode */ |
同樣,要記住這些步驟使用的是默認安全設置。
多數 Internet 客戶(hù)機應用程序,無(wú)論使用 Java 語(yǔ)言還是其他語(yǔ)言編寫(xiě),都需要提供安全和非安全連接。Java Secure SocketExtensions 庫使得這項工作非常容易,我最近在編寫(xiě)一個(gè) HTTP 客戶(hù)庫時(shí)就使用了這種方法。
SSLSocket 類(lèi)派生自Socket。 您可能已經(jīng)猜到我要怎么做了。所需要的只是該對象的一個(gè)Socket 指針。如果套接字連接不使用SSL,則可以像通常那樣創(chuàng )建套接字。如果要使用 SSL,就稍微麻煩一點(diǎn),但此后的代碼就很簡(jiǎn)單了。清單14 給出了一個(gè)例子:
Socket s; |
創(chuàng )建通道之后,如果套接字使用了SSL,那么就是安全通信,否則就是普通通信。如果使用了 SSL,關(guān)閉套接字將導致握手中止。
這種設置的一種可能是使用兩個(gè)單獨的類(lèi)。一個(gè)類(lèi)負責處理通過(guò)套接字沿著(zhù)與非安全套接字的連接進(jìn)行的所有通信。一個(gè)單獨的類(lèi)應該負責創(chuàng )建安全的連接,包括安全連接的所有必要設置,無(wú)論是否是默認的。安全類(lèi)應該直接插入通信類(lèi),只有在使用安全連接時(shí)被調用。
本文提出的方法是我所知道的把 JSSE 和 NIO 集成到同一代碼中以提供非阻塞安全通信的最簡(jiǎn)單方法。盡管還有其他方法,但是都需要準備實(shí)現這一過(guò)程的程序員花費大量的時(shí)間和精力。
一種可能是使用 Java Cryptography Extensions 在 NIO 上實(shí)現自己的 SSL 層。另一種方法是修改現有的稱(chēng)為EspreSSL (以前稱(chēng)為 jSSL)的定制 SSL 層, 把它改到 NIO 庫中。我建議只有在您有很充裕的時(shí)間時(shí)才使用這兩種方法。
參考資料部分的可下載 zip文件提供了示例代碼,幫助您實(shí)踐本文所述的技術(shù),其中包括:
聯(lián)系客服