Java NIO 與 IO之間的區別

概述

Java NIO提供了與標準IO不同的IO工作方式: 
  • Channels and Buffers(通道和緩衝區):標準的IO基於字節流和字符流進行操作的,而NIO是基於通道(Channel)和緩衝區(Buffer)進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。
  • Asynchronous IO(異步IO):Java NIO可以讓你異步的使用IO,例如:當線程從通道讀取數據到緩衝區時,線程還是可以進行其他事情。當數據被寫入到緩衝區時,線程可以繼續處理它。從緩衝區寫入通道也類似。
  • Selectors(選擇器):Java NIO引入了選擇器的概念,選擇器用於監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個的線程可以監聽多個數據通道。

使用場景

NIO

  • 優勢在於一個線程管理多個通道;但是數據的處理將會變得複雜;
  • 如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,採用這種;

傳統的IO

  • 適用於一個線程管理一個通道的情況;因爲其中的流數據的讀取是阻塞的;
  • 如果需要管理同時打開不太多的連接,這些連接會發送大量的數據;

NIO vs IO區別

NIO vs IO之間的理念上面的區別(NIO將阻塞交給了後臺線程執行)
  • IO是面向流的,NIO是面向緩衝區的
    • Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方;
    • NIO則能前後移動流中的數據,因爲是面向緩衝區的
  • IO流是阻塞的,NIO流是不阻塞的
    • Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了
    • Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取NIO可讓您只使用一個(或幾個)單線程管理多個通道(網絡連接或文件),但付出的代價是解析數據可能會比從一個阻塞流中讀取數據更復雜。 
    • 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。
  • 選擇器
    • Java NIO的選擇器允許一個單獨的線程來監視多個輸入通道,你可以註冊多個通道使用一個選擇器,然後使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道。 


Java NIO 由以下幾個核心部分組成: 


  • Channels
  • Buffers
  • Selectors

基本上,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。這裏有個圖示: 

Channel


Channel的實現: (涵蓋了UDP 和 TCP 網絡IO,以及文件IO)

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

讀數據:

  • int bytesRead = inChannel.read(buf);

寫數據:

  • int bytesWritten = inChannel.write(buf);  
還有部分的使用,如配置Channel爲阻塞或者非阻塞模式,以及如何註冊到Selector上面去,參考Selector部分;

Buffer

Buffer實現: (byte,  char、short, int, long, float, double )

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

Buffer使用

讀數據
  • flip()方法
    • 將Buffer從寫模式切換到讀模式
    • 調用flip()方法會將position設回0,並將limit設置成之前position的值。
    • buf.flip();
  •  (char) buf.get()
    • 讀取數據
  • Buffer.rewind()
    • 將position設回0,所以你可以重讀Buffer中的所有數據
    • limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte、char等)
  • Buffer.mark()方法,可以標記Buffer中的一個特定position。之後可以通過調用
  • Buffer.reset()方法,恢復到Buffer.mark()標記時的position
  • 一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。
  • clear()方法會:
    • 清空整個緩衝區。
    • position將被設回0,limit被設置成 capacity的值
  • compact()方法:
    • 只會清除已經讀過的數據;任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。
    • 將position設到最後一個未讀元素正後面,limit被設置成 capacity的值
寫數據
  • buf.put(127);  

Buffer的三個屬性

  • capacity:含義與模式無關;Buffer的一個固定的大小值;Buffer滿了需要將其清空才能再寫;
    • ByteBuffer.allocate(48);該buffer的capacity爲48byte
    • CharBuffer.allocate(1024);該buffer的capacity爲1024個char 
  • position:含義取決於Buffer處在讀模式還是寫模式(初始值爲0,寫或者讀操作的當前位置)
    • 寫數據時,初始的position值爲0;其值最大可爲capacity-1
    • 將Buffer從寫模式切換到讀模式,position會被重置爲0
  • limit:含義取決於Buffer處在讀模式還是寫模式(寫limit=capacity;讀limit等於最多可以讀取到的數據)
    • 寫模式下,limit等於Buffer的capacity
    • 切換Buffer到讀模式時, limit表示你最多能讀到多少數據;

Selector

概述

    Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用Selector就會很方便。例如,在一個聊天服務器中。
    要使用Selector,得向Selector註冊Channel,然後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件的例子有如新連接進來,數據接收等。 

使用

  1. 創建:Selector selector = Selector.open();  
  2. 註冊通道:
    • channel.configureBlocking(false);  
      • //與Selector一起使用時,Channel必須處於非阻塞模式
      • //這意味着不能將FileChannel與Selector一起使用,因爲FileChannel不能切換到非阻塞模式(而套接字通道都可以)
    • SelectionKey key = channel.register(selector, Selectionkey.OP_READ); 
      • //第二個參數表明Selector監聽Channel時對什麼事件感興趣
      • //SelectionKey.OP_CONNECT  SelectionKey.OP_ACCEPT  SelectionKey.OP_READ SelectionKey.OP_WRITE
      • //可以用或操作符將多個興趣組合一起
    • SelectionKey
      • 包含了interest集合 、ready集合 、Channel 、Selector 、附加的對象(可選)
      • int interestSet = key.interestOps();可以進行類似interestSet & SelectionKey.OP_CONNECT的判斷
  3. 使用:
    • select():阻塞到至少有一個通道在你註冊的事件上就緒了
    • selectNow():不會阻塞,不管什麼通道就緒都立刻返回
    • selectedKeys():訪問“已選擇鍵集(selected key set)”中的就緒通道
    • close():使用完selector需要用其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey實例無效

  1. Set selectedKeys = selector.selectedKeys();  
  2. Iterator keyIterator = selectedKeys.iterator();  
  3. while(keyIterator.hasNext()) {  
  4.     SelectionKey key = keyIterator.next();  
  5.     if(key.isAcceptable()) {  
  6.         // a connection was accepted by a ServerSocketChannel.  
  7.     } else if (key.isConnectable()) {  
  8.         // a connection was established with a remote server.  
  9.     } else if (key.isReadable()) {  
  10.         // a channel is ready for reading  
  11.     } else if (key.isWritable()) {  
  12.         // a channel is ready for writing  
  13.     } 
  14.     keyIterator.remove();//注意這裏必須手動remove;表明該selectkey我已經處理過了;
  15. }


Java測試關鍵代碼

  1. RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt""rw");  
  2. FileChannel inChannel = aFile.getChannel();  //從一個InputStream outputstream中獲取channel
  3.   
  4. //create buffer with capacity of 48 bytes  
  5. ByteBuffer buf = ByteBuffer.allocate(48);  
  6.   
  7. int bytesRead = inChannel.read(buf); //read into buffer.  
  8. while (bytesRead != -1) {  
  9.   
  10.   buf.flip();  //make buffer ready for read  
  11.   
  12.   while(buf.hasRemaining()){  
  13.       System.out.print((char) buf.get()); // read 1 byte at a time  
  14.   }  
  15.   
  16.   buf.clear(); //make buffer ready for writing  
  17.   bytesRead = inChannel.read(buf);  
  18. }  
  19. aFile.close();  


文件通道

  1. RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt""rw");  
  2. FileChannel inChannel = aFile.getChannel();  
讀數據
  1. ByteBuffer buf = ByteBuffer.allocate(48);  
  2. int bytesRead = inChannel.read(buf);  
寫數據
  1. String newData = "New String to write to file..." + System.currentTimeMillis();   
  2. ByteBuffer buf = ByteBuffer.allocate(48);  
  3. buf.clear();  
  4. buf.put(newData.getBytes());  
  5. buf.flip();  
  6. while(buf.hasRemaining()) {  
  7.     channel.write(buf);  

Socket 通道

  1. SocketChannel socketChannel = SocketChannel.open();  
  2. socketChannel.connect(new InetSocketAddress("http://jenkov.com"80));  
讀數據
  1. ByteBuffer buf = ByteBuffer.allocate(48);  
  2. int bytesRead = socketChannel.read(buf);  
寫數據
  1. String newData = "New String to write to file..." + System.currentTimeMillis();  
  2. ByteBuffer buf = ByteBuffer.allocate(48);  
  3. buf.clear();  
  4. buf.put(newData.getBytes());  
  5. buf.flip();  
  6. while(buf.hasRemaining()) {  
  7.     socketChannel.write(buf);  
  8. }  

ServerSocket 通道

  1. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  
  2. serverSocketChannel.socket().bind(new InetSocketAddress(9999));  
  3. while(true){  
  4.     SocketChannel socketChannel =  
  5.             serverSocketChannel.accept();  
  6.     //do something with socketChannel...  

Datagram 通道(channel的讀寫操作與前面的有差異)

  1. DatagramChannel channel = DatagramChannel.open();  
  2. channel.socket().bind(new InetSocketAddress(9999));  
讀數據
  1. ByteBuffer buf = ByteBuffer.allocate(48);  
  2. buf.clear();  
  3. channel.receive(buf);
  4. //receive()方法會將接收到的數據包內容複製到指定的Buffer. 如果Buffer容不下收到的數據,多出的數據將被丟棄。 
寫數據
  1. String newData = "New String to write to file..." + System.currentTimeMillis();  
  2. ByteBuffer buf = ByteBuffer.allocate(48);  
  3. buf.clear();  
  4. buf.put(newData.getBytes());  
  5. buf.flip();  
  6. int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com"80));  















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章