hk2000c技術專欄

          技術源于哲學,哲學來源于生活 關心生活,關注健康,關心他人

            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理 ::
            111 隨筆 :: 1 文章 :: 28 評論 :: 0 Trackbacks

          2003 年 11 月 24 日

          盡管 SSL 阻塞操作――當讀寫數據的時候套接字的訪問被阻塞――與對應的非阻塞方式相比提供了更好的 I/O 錯誤通知,但是非阻塞操作允許調用的線程繼續運行。本文中,作者同時就客戶端和服務器端描述了如何使用Java Secure Socket Extensions (JSSE) 和 Java NIO (新 I/O)庫創建非阻塞的安全連接,并且介紹了創建非阻塞套接字的傳統方法,以及使用JSSE 和 NIO 的一種可選的(必需的)方法。

          阻塞,還是非阻塞?這就是問題所在。無論在程序員的頭腦中多么高貴……當然這不是莎士比亞,本文提出了任何程序員在編寫 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() 方法了。也可以把該對象強制轉換成單獨的 ReadableByteChannelWritableByteChannel 對象。無論哪種方式,都要對數據使用 Buffer 對象。因為 NIO 庫的使用超出了本文的范圍,我們不再對此進一步討論。

          當不再需要套接字時,可以使用 close() 方法將其關閉:

          sc.close();
                      

          這樣就會同時關閉套接字連接和底層的通信通道。





          回頁首


          創建替代的非阻塞的客戶機套接字

          上述方法比傳統的創建套接字連接的例程稍微麻煩一點。不過,傳統的例程也能用于創建非阻塞套接字,不過需要增加幾個步驟以支持非阻塞通信。

          SocketChannel 對象中的底層通信包括兩個 Channel 類: ReadableByteChannelWritableByteChannel。 這兩個類可以分別從現有的 InputStreamOutputStream 阻塞流中使用 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 派生這個類。這個類可以強制轉換成單獨的 ReadableByteChannelWritableByteChannel 類。

          清單 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 套接字的默認方法非常簡單,只包括幾個很短的步驟:

          1. 創建套接字工廠。
          2. 創建連接的套接字。
          3. 開始握手。
          4. 派生流。
          5. 通信。

          清單 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 服務器套接字也包括幾個很短的步驟:

          1. 創建服務器套接字工廠。
          2. 創建并綁定服務器套接字。
          3. 接受傳入的連接。
          4. 開始握手。
          5. 派生流。
          6. 通信。

          盡管看起來似乎與客戶端的步驟相似,要注意這里去掉了很多安全選項,比如客戶驗證。

          清單 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();
                      





          回頁首


          創建安全的非阻塞連接

          要精心實現安全的非阻塞連接,也需要分別從客戶端和服務器端來看。

          從客戶端

          在客戶端建立安全的非阻塞連接非常簡單:

          1. 創建并連接 Socket 對象。
          2. Socket 對象添加到模擬層上。
          3. 通過模擬層通信。

          清單 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,關閉套接字將導致握手中止。

          這種設置的一種可能是使用兩個單獨的類。一個類負責處理通過套接字沿著與非安全套接字的連接進行的所有通信。一個單獨的類應該負責創建安全的連接,包括安全連接的所有必要設置,無論是否是默認的。安全類應該直接插入通信類,只有在使用安全連接時被調用。

          posted on 2007-12-31 05:07 hk2000c 閱讀(1523) 評論(0)  編輯  收藏 所屬分類: Java 技術
          主站蜘蛛池模板: 台中县| 普陀区| 仙桃市| 巨鹿县| 玉林市| 墨脱县| 静宁县| 通辽市| 三都| 辽中县| 调兵山市| 应用必备| 建昌县| 双鸭山市| 泸溪县| 肥城市| 怀化市| 五常市| 甘谷县| 银川市| 六盘水市| 洞口县| 正蓝旗| 洛隆县| 鞍山市| 象州县| 托克托县| 长岛县| 普兰店市| 祁东县| 德钦县| 新和县| 寿阳县| 祁门县| 嘉鱼县| 四会市| 永定县| 醴陵市| 桐城市| 内丘县| 同心县|