天天看點

java io bio nio aio 詳解

BIO、NIO、AIO的差別:

BIO就是基于Thread per Request的傳統server/client實作模式,

NIO通常采用Reactor模式,

AIO通常采用Proactor模式,

AIO簡化了程式的編寫,stream的讀取和寫入都有OS來完成,不需要像NIO那樣子周遊Selector。Windows基于IOCP實作AIO,Linux隻有eppoll模拟實作了AIO。

Java7之前的JDK隻支援NIO和BIO,從7開始支援AIO。

4種通信方式:TCP/IP+BIO, TCP/IP+NIO, UDP/IP+BIO, UDP/IP+NIO。

一、 Reactor and Proactor

IO讀寫時,多路複用機制都會依賴對一個事件多路分離器,負責把源事件的IO 事件分離出來,分别到相應的read/write事件分離器。涉及到事件分離器的兩種模式分别就是 Reactor和Proactor,Reactor是基于同步IO的,Proactor是基于異步IO的。

關于同步和異步IO

Io的兩個重要步驟:發起IO請求,和實際的IO操作。在unix網絡程式設計的定義裡異步和非異步概念的差別就是實際的IO操作是否阻塞。如果不是就是異步,如果是就是同步。

而阻塞和非阻塞的差別在于發起IO請求的時候是否會阻塞,如果會就是阻塞,不會就是非阻塞。

本人了解能力有限,想了個例子來輔助自己了解:

小明想要買一本<深入java虛拟機>的書,以下幾個場景可以來了解這幾種io模式:

1. 如果小明每天都去書店問售貨員說有沒有這本書,如果沒有就回去繼續等待,等下次再過來文。(阻塞)

2. 如果小明告訴售貨員想買一本<深入java虛拟機>的書,那麼就在家裡等着做其他事情去了,如果書到了售貨員就通知小明,小明再自己過去取。

3. 如果小明告售貨員想買一本<深入java虛拟機>的書,然後告訴售貨員到了幫他送到某某地方去,就做其他事情去了。小明就不管了,等書到了,售貨員就幫他送到那個地方了。

售貨員可以認為是作業系統的一個服務,而小明是一個使用者程序。不知道是否有誤,如果有誤請大家拍磚指出,謝謝。

可以看出2,3的效率明顯要比1高。但是1最簡單,而2,3需要一些協作。充分證明了團隊合作的力量。

在Reactor模式中,事件分離者等待某個事件或者可應用或個操作的狀态發生(比如檔案描述符可讀寫,或者是socket可讀寫),事件分離者就把這個事件傳給事先注冊的事件處理函數或者回調函數,由後者來做實際的讀寫操作。

在Proactor模式中,事件處理者(或者代由事件分離者發起)直接發起一個異步讀寫操作(相當于請求),而實際的工作是由作業系統來完成的。發起時,需要提供的參數包括用于存放讀到資料的緩存區,讀的資料大小,或者用于存放外發資料的緩存區,以及這個請求完後的回調函數等資訊。事件分離者得知了這個請求,它默默等待這個請求的完成,然後轉發完成事件給相應的事件處理者或者回調。舉例來說,在Windows上事件處理者投遞了一個異步IO操作(稱有 overlapped的技術),事件分離者等IOCompletion事件完成. 這種異步模式的典型實作是基于作業系統底層異步API的,是以我們可稱之為“系統級别”的或者“真正意義上”的異步,因為具體的讀寫是由作業系統代勞的。

舉個例子,将有助于了解Reactor與Proactor二者的差異,以讀操作為例(類操作類似)。

在Reactor中實作讀:

- 注冊讀就緒事件和相應的事件處理器

- 事件分離器等待事件

- 事件到來,激活分離器,分離器調用事件對應的處理器。

- 事件處理器完成實際的讀操作,處理讀到的資料,注冊新的事件,然後返還控制權。

與如下Proactor(真異步)中的讀過程比較:

- 處理器發起異步讀操作(注意:作業系統必須支援異步IO)。在這種情況下,處理器無視IO就緒事件,它關注的是完成事件。

- 事件分離器等待操作完成事件

- 在分離器等待過程中,作業系統利用并行的核心線程執行實際的讀操作,并将結果資料存入使用者自定義緩沖區,最後通知事件分離器讀操作完成。

- 事件分離器呼喚處理器。

- 事件處理器處理使用者自定義緩沖區中的資料,然後啟動一個新的異步操作,并将控制權傳回事件分離器。

可以看出,兩個模式的相同點,都是對某個IO事件的事件通知(即告訴某個子產品,這個IO操作可以進行或已經完成)。在結構

上,兩者也有相同點:demultiplexor負責送出IO操作(異步)、查詢裝置是否可操作(同步),然後當條件滿足時,就回調handler;

不同點在于,異步情況下(Proactor),當回調handler時,表示IO操作已經完成;同步情況下(Reactor),回調handler時,表示

IO裝置可以進行某個操作(can read or can write),handler這個時候開始送出操作。

二、BIO、NIO、AIO

NIO通常采用Reactor模式,AIO通常采用Proactor模式。AIO簡化了程式的編寫,stream的讀取和寫入都有OS來完成,不需要像NIO那樣子周遊Selector。Windows基于IOCP實作AIO,Linux隻有eppoll模拟實作了AIO。

Java7之前的JDK隻支援NIO和BIO,從7開始支援AIO。

4種通信方式:TCP/IP+BIO, TCP/IP+NIO, UDP/IP+BIO, UDP/IP+NIO。

(1)TCP/IP+BIO

Socket和ServerSocket實作,ServerSocket實作Server端端口監聽,Socket用于建立網絡IO連接配接。不适用于處理多個請求 1.生成Socket會消耗過多的本地資源。2. Socket連接配接的建立一般比較慢。

BIO情況下,能支援的連接配接數有限,一般都采取accept擷取Socket以後采用一個thread來處理,one connection one thread。無論連接配接是否有真正資料請求,都需要獨占一個thread。

    可以通過設立Socket池來一定程度上解決問題,但是使用池需要注意的問題是:1. 競争等待比較多。 2. 需要控制好逾時時間。Socket和ServerSocket實作,ServerSocket實作Server端端口監聽,Socket用于建立網絡IO連接配接。

伺服器,使用ServerSocket監聽指定的端口,端口可以随意指定(由于1024以下的端口通常屬于保留端口,在一些作業系統中不可以随意使用,是以建議使用大于1024的端口),等待客戶連接配接請求,客戶連接配接後,會話産生;在完成會話後,關閉連接配接。

package test;  
  
import java.io.BufferedReader;  
import java.io.IOException;  
import java.io.InputStreamReader;  
import java.net.ServerSocket;  
import java.net.Socket;  
  
public class Server {  
    private Socket socket;  
    private ServerSocket ss;  
  
    public Server() throws IOException {  
        ss = new ServerSocket(7777);  
        while (true) {  
            socket = ss.accept();  
            BufferedReader br = new BufferedReader(new InputStreamReader(socket  
                    .getInputStream()));  
            System.out.println("you input is : " + br.readLine());  
        }  
    }  
  
    public static void main(String[] args) {  
        try {  
            new Server();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}  
           

用戶端,使用Socket對網絡上某一個伺服器的某一個端口發出連接配接請求,一旦連接配接成功,打開會話;會話完成後,關閉Socket。用戶端不需要指定打開的端口,通常臨時的、動态的配置設定一個1024以上的端口。

package test;  
    
  import java.io.BufferedRear;  
  import java.io.IOException;  
  import java.io.InputStreamReader;  
  import java.io.PrintWriter;  
  import java.net.Socket;  
  import java.net.UnknownHostException;  
    
 public class Client {  
     Socket client;  
     PrintWriter pw;  
     public Client() throws UnknownHostException, IOException {  
         client=new Socket("Socket伺服器IP",7777);  
         pw=new PrintWriter(client.getOutputStream());  
         BufferedReader br=new BufferedReader(new InputStreamReader(System.in));  
         pw.write(br.readLine());  
         pw.close();  
         br.close();  
     }  
     public static void main(String[] args) {  
         try {  
             new Client();  
         } catch (UnknownHostException e) {  
             e.printStackTrace();  
         } catch (IOException e) {  
             e.printStackTrace();  
	     }  
     }  
 }  
           

不适用于處理多個請求 1.生成Socket會消耗過多的本地資源。2. Socket連接配接的建立一般比較慢。

BIO情況下,能支援的連接配接數有限,一般都采取accept擷取Socket以後采用一個thread來處理,one connection one thread。無論連接配接是否有真正資料請求,都需要獨占一個thread。

可以通過設立Socket池來一定程度上解決問題,

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.PrintWriter;

import java.net.ServerSocket;

import java.net.Socket;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

 

public class BIOPoolServer {

ExecutorService pool = null;

    public  BIOPoolServer(){

    try {

ServerSocket server = new ServerSocket(29001);

pool = Executors.newFixedThreadPool(1);

while(true){

pool.execute(new Handler(server.accept()));

}

} catch (IOException e) {

e.printStackTrace();

}finally{

pool.shutdown();

}

    }

    

    class Handler implements Runnable{

    Socket socket;

    public Handler(Socket socket){

    this.socket = socket;

    }

public void run() {

try {

BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

PrintWriter out = new PrintWriter(socket.getOutputStream(),true);

                String msg = in.readLine();

                System.out.println("The client send the msg : "+msg);

    out.println("The server has received!");

} catch (IOException e) {

e.printStackTrace();

}

}

    }

    public static void main(String[] args) {

new BIOPoolServer();

}

}
           

但是使用池需要注意的問題是:1. 競争等待比較多。 2. 需要控制好逾時時間。

(2)TCP/IP+NIO

使用Channel(SocketChannel和ServerSocketChannel)和Selector。

Server端通常由一個thread來監聽connect事件,另外多個thread來監聽讀寫事件。這樣做的好處是這些連接配接隻有在真是請求的時候才會建立thread來處理,one request one thread。這種方式在server端需要支援大量連接配接但這些連接配接同時發送請求的峰值不會很多的時候十分有效。

server端:

 

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.IntBuffer;

import java.nio.channels.SelectionKey;

import java.nio.channels.Selector;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.nio.channels.spi.SelectorProvider;

import java.util.Iterator;

import java.util.Set;

 

public class NIOServer {
ServerSocketChannel channel = null;
   public NIOServer(){
  try {

  openChannel();
  waitForConnection();

} catch (IOException e) {

e.printStackTrace();

}

   }
   private void openChannel() throws IOException{
channel = ServerSocketChannel.open();
//綁定監聽端口
channel.socket().bind(new InetSocketAddress(29000));
//設定為非阻塞形式
channel.configureBlocking(false);
  }
   private void waitForConnection() throws IOException{  
  Selector acceptSelector = SelectorProvider.provider().openSelector();
  channel.register(acceptSelector, SelectionKey.OP_ACCEPT);
  int keyAdded = 0;
  while((keyAdded=acceptSelector.select())>0){

// 某客戶已經準備好可以進行I/O操作了,擷取其ready鍵集合
  Set readKeys = acceptSelector.selectedKeys();
  Iterator iter = readKeys.iterator();
  while(iter.hasNext()){
  SelectionKey sk = (SelectionKey)iter.next();
  iter.remove();
  if(sk.isAcceptable()){
  ServerSocketChannel server = (ServerSocketChannel) sk.channel();
  SocketChannel socket = server.accept();
  ByteBuffer _buffer = ByteBuffer.allocate(8);  
  IntBuffer _intBuffer = _buffer.asIntBuffer(); 
   _buffer.clear();  

   socket.read(_buffer);
       int result = _intBuffer.get(0) + _intBuffer.get(1);  
       _buffer.flip();  
       _buffer.clear();  
       _intBuffer.put(0, result);  
       socket.write(_buffer);

  }
  }
  }

   }
   public static void main(String[] args) {

 new NIOServer();

}

}

 
client端:

 

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.IntBuffer;

import java.nio.channels.SocketChannel;
public class NIOClient {
 public void start(int first, int second){
SocketChannel channel = null;
try {

InetSocketAddress socketAddress = new InetSocketAddress("localhost", 29000); 
channel = SocketChannel.open(socketAddress);
channel.configureBlocking(false);
ByteBuffer _buffer = ByteBuffer.allocate(8); 
IntBuffer _intBuffer = _buffer.asIntBuffer();
_buffer.clear();  
_intBuffer.put(0, first);  
_intBuffer.put(1, second);  
channel.write(_buffer); 
System.out.println("發送加法請求 " + first + "+" + second);
_buffer.clear();  

    channel.read(_buffer); 
    int result = _intBuffer.get(0); 
    System.out.println("運算結果:"+result);
} catch (IOException e) {
e.printStackTrace();

}finally {  

        if (channel != null) {  

            try {  

            channel.close();  

            } catch (IOException e) {  

            }  

        }  

    }    

 }

 public static void main(String[] args) {

new NIOClient().start(3, 23);

}

}
           

使用Channel(SocketChannel和ServerSocketChannel)和Selector。

Server端通常由一個thread來監聽connect事件,另外多個thread來監聽讀寫事件。這樣做的好處是這些連接配接隻有在真是請求的時候才會建立thread來處理,one request one thread。這種方式在server端需要支援大量連接配接但這些連接配接同時發送請求的峰值不會很多的時候十分有效。

(3)UDP/IP+BIO

DatagramSocket和DatagramPacket。DatagramSocket負責監聽端口以及讀寫資料,DatagramPacket作為資料流對象進行傳輸。

UDP/IP是無連接配接的,無法進行雙向通信,除非雙方都成為UDP Server。

(4)UDP/IP+NIO

通過DatagramChannel和ByteBuffer實作。DatagramChannel負責端口監聽及讀寫。ByteBuffer負責資料流傳輸。

如果要将消息發送到多台機器,如果為每個目标機器都建立一個連接配接的話,會有很大的網絡流量壓力。這時候可以使用基于UDP/IP的Multicast協定傳輸,Java中可以通過MulticastSocket和DatagramPacket來實作。

Multicast一般多用于多台機器的狀态同步,比如JGroups。SRM, URGCP都是Multicast的實作方式。eBay就采用SRM來實作将資料從主資料庫同步到各個搜尋節點機器。