耐心無止境 成功一瞬間

          BlogJava 聯系 聚合 管理
            31 Posts :: 5 Stories :: 25 Comments :: 0 Trackbacks

          NIO的使用
          (轉自starchu1981)

          導讀
           J2SE1.4以上版本中發布了全新的I/O類庫。本文將通過一些實例來簡單介紹NIO庫提供的一些新特性:非阻塞I/O,字符轉換,緩沖以及通道。

          一. 介紹NIO
          NIO包(java.nio.*)引入了四個關鍵的抽象數據類型,它們共同解決傳統的I/O類中的一些問題。
          1. Buffer:它是包含數據且用于讀寫的線形表結構。其中還提供了一個特殊類用于內存映射文件的I/O操作。
          2. Charset:它提供Unicode字符串影射到字節序列以及逆影射的操作。
          3. Channels:包含socket,file和pipe三種管道,它實際上是雙向交流的通道。
          4. Selector:它將多元異步I/O操作集中到一個或多個線程中(它可以被看成是Unix中select()函數或Win32中WaitForSingleEvent()函數的面向對象版本)。
          二. 回顧傳統
          在介紹NIO之前,有必要了解傳統的I/O操作的方式。以網絡應用為例,傳統方式需要監聽一個ServerSocket,接受請求的連接為其提供服務(服務通常包括了處理請求并發送響應)圖一是服務器的生命周期圖,其中標有粗黑線條的部分表明會發生I/O阻塞。
           
             圖一
          可以分析創建服務器的每個具體步驟。首先創建ServerSocket
           ServerSocket server=new ServerSocket(10000);
          然后接受新的連接請求
           Socket newConnection=server.accept();
          對于accept方法的調用將造成阻塞,直到ServerSocket接受到一個連接請求為止。一旦連接請求被接受,服務器可以讀客戶socket中的請求。
          InputStream in = newConnection.getInputStream();
          InputStreamReader reader = new InputStreamReader(in);
          BufferedReader buffer = new BufferedReader(reader);
          Request request = new Request();
          while(!request.isComplete()) {
            String line = buffer.readLine();
            request.addLine(line);
          }
          這樣的操作有兩個問題,首先BufferedReader類的readLine()方法在其緩沖區未滿時會造成線程阻塞,只有一定數據填滿了緩沖區或者客戶關閉了套接字,方法才會返回。其次,它回產生大量的垃圾,BufferedReader創建了緩沖區來從客戶套接字讀入數據,但是同樣創建了一些字符串存儲這些數據。雖然BufferedReader內部提供了StringBuffer處理這一問題,但是所有的String很快變成了垃圾需要回收。
          同樣的問題在發送響應代碼中也存在
          Response response = request.generateResponse();
          OutputStream out = newConnection.getOutputStream();
          InputStream in = response.getInputStream();
          int ch;
          while(-1 != (ch = in.read())) {
            out.write(ch);
          }
          newConnection.close();
          類似的,讀寫操作被阻塞而且向流中一次寫入一個字符會造成效率低下,所以應該使用緩沖區,但是一旦使用緩沖,流又會產生更多的垃圾。
          傳統的解決方法
           通常在Java中處理阻塞I/O要用到線程(大量的線程)。一般是實現一個線程池用來處理請求,如圖二
           
                 圖二
          線程使得服務器可以處理多個連接,但是它們也同樣引發了許多問題。每個線程擁有自己的棧空間并且占用一些CPU時間,耗費很大,而且很多時間是浪費在阻塞的I/O操作上,沒有有效的利用CPU。
          三. 新I/O
          1. Buffer
          傳統的I/O不斷的浪費對象資源(通常是String)。新I/O通過使用Buffer讀寫數據避免了資源浪費。Buffer對象是線性的,有序的數據集合,它根據其類別只包含唯一的數據類型。
          java.nio.Buffer 類描述 
          java.nio.ByteBuffer 包含字節類型。 可以從ReadableByteChannel中讀在    WritableByteChannel中寫 
          java.nio.MappedByteBuffer 包含字節類型,直接在內存某一區域映射 
          java.nio.CharBuffer 包含字符類型,不能寫入通道 
          java.nio.DoubleBuffer 包含double類型,不能寫入通道 
          java.nio.FloatBuffer 包含float類型 
          java.nio.IntBuffer 包含int類型 
          java.nio.LongBuffer 包含long類型 
          java.nio.ShortBuffer 包含short類型 
          可以通過調用allocate(int capacity)方法或者allocateDirect(int capacity)方法分配一個Buffer。特別的,你可以創建MappedBytesBuffer通過調用FileChannel.map(int mode,long position,int size)。直接(direct)buffer在內存中分配一段連續的塊并使用本地訪問方法讀寫數據。非直接(nondirect)buffer通過使用Java中的數組訪問代碼讀寫數據。有時候必須使用非直接緩沖例如使用任何的wrap方法(如ByteBuffer.wrap(byte[]))在Java數組基礎上創建buffer。
          2. 字符編碼
          向ByteBuffer中存放數據涉及到兩個問題:字節的順序和字符轉換。ByteBuffer內部通過ByteOrder類處理了字節順序問題,但是并沒有處理字符轉換。事實上,ByteBuffer沒有提供方法讀寫String。
           Java.nio.charset.Charset處理了字符轉換問題。它通過構造CharsetEncoder和CharsetDecoder將字符序列轉換成字節和逆轉換。
          3. 通道(Channel)
          你可能注意到現有的java.io類中沒有一個能夠讀寫Buffer類型,所以NIO中提供了Channel類來讀寫Buffer。通道可以認為是一種連接,可以是到特定設備,程序或者是網絡的連接。通道的類等級結構圖如下
           
              圖三
           圖中ReadableByteChannel和WritableByteChannel分別用于讀寫。
          GatheringByteChannel可以從使用一次將多個Buffer中的數據寫入通道,相反的,ScatteringByteChannel則可以一次將數據從通道讀入多個Buffer中。你還可以設置通道使其為阻塞或非阻塞I/O操作服務。
          為了使通道能夠同傳統I/O類相容,Channel類提供了靜態方法創建Stream或Reader
          4. Selector
          在過去的阻塞I/O中,我們一般知道什么時候可以向stream中讀或寫,因為方法調用直到stream準備好時返回。但是使用非阻塞通道,我們需要一些方法來知道什么時候通道準備好了。在NIO包中,設計Selector就是為了這個目的。SelectableChannel可以注冊特定的事件,而不是在事件發生時通知應用,通道跟蹤事件。然后,當應用調用Selector上的任意一個selection方法時,它查看注冊了的通道看是否有任何感興趣的事件發生。圖四是selector和兩個已注冊的通道的例子
           

                  圖四
          并不是所有的通道都支持所有的操作。SelectionKey類定義了所有可能的操作位,將要用兩次。首先,當應用調用SelectableChannel.register(Selector sel,int op)方法注冊通道時,它將所需操作作為第二個參數傳遞到方法中。然后,一旦SelectionKey被選中了,SelectionKey的readyOps()方法返回所有通道支持操作的數位的和。SelectableChannel的validOps方法返回每個通道允許的操作。注冊通道不支持的操作將引發IllegalArgumentException異常。下表列出了SelectableChannel子類所支持的操作。
            
          ServerSocketChannel OP_ACCEPT 
          SocketChannel OP_CONNECT, OP_READ, OP_WRITE 
          DatagramChannel OP_READ, OP_WRITE 
          Pipe.SourceChannel OP_READ 
          Pipe.SinkChannel OP_WRITE 

          四. 舉例說明
          1. 簡單網頁內容下載
          這個例子非常簡單,類SocketChannelReader使用SocketChannel來下載特定網頁的HTML內容。
          package examples.nio;

          import java.nio.ByteBuffer;
          import java.nio.channels.SocketChannel;
          import java.nio.charset.Charset;
          import java.net.InetSocketAddress;
          import java.io.IOException;

          public class SocketChannelReader{
             
              private Charset charset=Charset.forName("UTF-8");//創建UTF-8字符集
              private SocketChannel channel;

              public void getHTMLContent(){
              try{
                 connect();
                 sendRequest();
                 readResponse();
              }catch(IOException e){
                 System.err.println(e.toString());
              }finally{
               if(channel!=null){
               try{
                channel.close();
            }catch(IOException e){}
               }
           }
              }
              private void connect()throws IOException{//連接到CSDN
           InetSocketAddress socketAddress=
               new InetSocketAddress("www.csdn.net",80);
           channel=SocketChannel.open(socketAddress);
           //使用工廠方法open創建一個channel并將它連接到指定地址上
           //相當與SocketChannel.open().connect(socketAddress);調用
          }

          private void sendRequest()throws IOException{
           channel.write(charset.encode("GET "
                  +"/document"
                  +"\r\n\r\n"));//發送GET請求到CSDN的文檔中心
           //使用channel.write方法,它需要CharByte類型的參數,使用
           //Charset.encode(String)方法轉換字符串。
              }

              private void readResponse()throws IOException{//讀取應答
           ByteBuffer buffer=ByteBuffer.allocate(1024);//創建1024字節的緩沖
           while(channel.read(buffer)!=-1){
               buffer.flip();//flip方法在讀緩沖區字節操作之前調用。
               System.out.println(charset.decode(buffer));
          //使用Charset.decode方法將字節轉換為字符串
               buffer.clear();//清空緩沖
           }
              }

              public static void main(String [] args){
           new SocketChannelReader().getHTMLContent();
              }
          2. 簡單的加法服務器和客戶機
          服務器代碼
          package examples.nio;

          import java.nio.ByteBuffer;
          import java.nio.IntBuffer;
          import java.nio.channels.ServerSocketChannel;
          import java.nio.channels.SocketChannel;
          import java.net.InetSocketAddress;
          import java.io.IOException;

          /**
           * SumServer.java
           *
           *
           * Created: Thu Nov 06 11:41:52 2003
           *
           * @author starchu1981
           * @version 1.0
           */
          public class SumServer {

              private ByteBuffer _buffer=ByteBuffer.allocate(8);
              private IntBuffer _intBuffer=_buffer.asIntBuffer();
              private SocketChannel _clientChannel=null;
              private ServerSocketChannel _serverChannel=null;

              public void start(){
           try{
               openChannel();
               waitForConnection();
           }catch(IOException e){
               System.err.println(e.toString());
           }
              }

              private void openChannel()throws IOException{
           _serverChannel=ServerSocketChannel.open();
           _serverChannel.socket().bind(new InetSocketAddress(10000));
           System.out.println("服務器通道已經打開");
              }

              private void waitForConnection()throws IOException{
           while(true){
               _clientChannel=_serverChannel.accept();
               if(_clientChannel!=null){
          System.out.println("新的連接加入");
          processRequest();
          _clientChannel.close();
               }
           }
              }

              private void processRequest()throws IOException{
           _buffer.clear();
           _clientChannel.read(_buffer);
           int result=_intBuffer.get(0)+_intBuffer.get(1);
           _buffer.flip();
           _buffer.clear();
           _intBuffer.put(0,result);
           _clientChannel.write(_buffer);
              }

              public static void main(String [] args){
           new SumServer().start();
              }
          } // SumServer
          客戶代碼
          package examples.nio;

          import java.nio.ByteBuffer;
          import java.nio.IntBuffer;
          import java.nio.channels.SocketChannel;
          import java.net.InetSocketAddress;
          import java.io.IOException;

          /**
           * SumClient.java
           *
           *
           * Created: Thu Nov 06 11:26:06 2003
           *
           * @author starchu1981
           * @version 1.0
           */
          public class SumClient {

              private ByteBuffer _buffer=ByteBuffer.allocate(8);
              private IntBuffer _intBuffer;
              private SocketChannel _channel;

              public SumClient() {
                _intBuffer=_buffer.asIntBuffer();
              } // SumClient constructor
             
              public int getSum(int first,int second){
           int result=0;
           try{
               _channel=connect();
               sendSumRequest(first,second);
               result=receiveResponse();
           }catch(IOException e){System.err.println(e.toString());
           }finally{
               if(_channel!=null){
            try{
                _channel.close();
            }catch(IOException e){}
               }
           }
           return result;
              }

              private SocketChannel connect()throws IOException{
           InetSocketAddress socketAddress=
               new InetSocketAddress("localhost",10000);
           return SocketChannel.open(socketAddress);
              }
             
              private void sendSumRequest(int first,int second)throws IOException{
           _buffer.clear();
           _intBuffer.put(0,first);
           _intBuffer.put(1,second);
           _channel.write(_buffer);
           System.out.println("發送加法請求 "+first+"+"+second);
              }
             
              private int receiveResponse()throws IOException{
           _buffer.clear();
           _channel.read(_buffer);
           return _intBuffer.get(0);
              }

              public static void main(String [] args){
           SumClient sumClient=new SumClient();
           System.out.println("加法結果為 :"+sumClient.getSum(100,324));
              }
          } // SumClient
          3. 非阻塞的加法服務器
          首先在openChannel方法中加入語句
           _serverChannel.configureBlocking(false);//設置成為非阻塞模式

          重寫WaitForConnection方法的代碼如下,使用非阻塞方式
          private void waitForConnection()throws IOException{
           Selector acceptSelector = SelectorProvider.provider().openSelector(); 

           /*在服務器套接字上注冊selector并設置為接受accept方法的通知。
           這就告訴Selector,套接字想要在accept操作發生時被放在ready表
           上,因此,允許多元非阻塞I/O發生。*/
           SelectionKey acceptKey = ssc.register(acceptSelector,
                     SelectionKey.OP_ACCEPT);
           int keysAdded = 0;
           
           /*select方法在任何上面注冊了的操作發生時返回*/
           while ((keysAdded = acceptSelector.select()) > 0) {
               // 某客戶已經準備好可以進行I/O操作了,獲取其ready鍵集合
               Set readyKeys = acceptSelector.selectedKeys();
               Iterator i = readyKeys.iterator();

               // 遍歷ready鍵集合,并處理加法請求
               while (i.hasNext()) {
            SelectionKey sk = (SelectionKey)i.next();
            i.remove();
            ServerSocketChannel nextReady =
                (ServerSocketChannel)sk.channel();
            // 接受加法請求并處理它
            _clientSocket = nextReady.accept().socket();
             processRequest();
             _clientSocket.close();
               }
            }
              }

          posted on 2005-07-21 17:49 Joshua Yan 閱讀(999) 評論(0)  編輯  收藏

          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          主站蜘蛛池模板: 德令哈市| 眉山市| 静海县| 屏东市| 汪清县| 林周县| 清原| 万州区| 乌拉特中旗| 深水埗区| 平江县| 晋江市| 绿春县| 平潭县| 阿坝县| 江孜县| 克东县| 嘉善县| 柳州市| 涞源县| 阳原县| 湘西| 仁布县| 罗定市| 榕江县| 和平县| 那坡县| 兴山县| 洞头县| 永清县| 桃园市| 廊坊市| 黑龙江省| 蒙自县| 霍邱县| 武冈市| 故城县| 龙海市| 宝丰县| 留坝县| 靖远县|