天天看點

java bio到nio的演進之路

目錄

    • BIO、NIO介紹
    • BIO和NIO的差別
    • NIO和AIO的差別

BIO、NIO介紹

BIO:(Blocking I/O) 同步阻塞I/O模式,資料的讀取寫入必須阻塞在一個線程内等待其完成。

NIO:(New I/O) 同步非阻塞I/O模式,資料的讀取寫入如果資料還沒準備好可以處理别的請求

我們先來看下最傳統的BIO模型

public class ServerSocketDemo2 {

    public static void main(String[] args) {
        ServerSocket serverSocket=null;
        try {
            //localhost: 8080
            serverSocket=new ServerSocket(8080);
            while(true) {
                // 核心代碼1,監聽用戶端連接配接(阻塞1-連接配接阻塞)
                Socket socket = serverSocket.accept();
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));//輸入流
                // 核心代碼2,被阻塞了(阻塞2-流-讀取資料阻塞)
                String clientStr = bufferedReader.readLine();
                System.out.println("接收到用戶端的資訊:" + clientStr);
                bufferedReader.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(serverSocket!=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
           

我們可以看到阻塞的地方有兩個

1、等待連接配接

2、讀取資料

在這個模型下首先要解決的問題是,并發情況下隻能一個一個的處理請求,這個我們肯定是不能接受的,于是有了下面的模型

public class ServerSocketDemo {

    static ExecutorService executorService= Executors.newFixedThreadPool(20);

    public static void main(String[] args) {
        ServerSocket serverSocket=null;
        try {
            //localhost: 8080
            serverSocket=new ServerSocket(8080);
            
            while(true) {
                // 核心代碼1,監聽用戶端連接配接(阻塞1-連接配接阻塞)
                Socket socket = serverSocket.accept(); 
                System.out.println(socket.getPort());
                executorService.execute(()->{
                    // 異步
                    try {
                        BufferedReader bufferedReader = new BufferedReader(
                                new InputStreamReader(socket.getInputStream()));//輸入流
                        // 核心代碼2,被阻塞了(阻塞2-流-讀取資料阻塞)
                        String clientStr = bufferedReader.readLine(); //讀取用戶端的一行資料
                        System.out.println("接收到用戶端的資訊:" + clientStr);
                        
                        bufferedReader.close();
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(serverSocket!=null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
           

上面的模型我們使用了線程池技術來解決請求隻能一個一個請求的問題,可以并發處理多個請求,如tomcat7之前就是采用的線程池技術來解決并發問題,但是面臨的瓶頸是線程池的線程數是有限的,當并發過大時采用線程池模型的性能就不夠強大了,于是NIO模型誕生了。

public class NIOServerDemo2 {

    private int port = 8080;

    //輪詢器 Selector
    private Selector selector;
    //緩沖區 Buffer 等候區
    private ByteBuffer buffer = ByteBuffer.allocate(1024);

    //初始化
    public NIOServerDemo2(int port){
        try {
            this.port = port;
            ServerSocketChannel server = ServerSocketChannel.open();
            // 綁定ip端口
            server.bind(new InetSocketAddress(this.port));
            //BIO 更新版本 NIO,為了相容BIO,NIO模型預設是采用阻塞式,設定為非阻塞
            server.configureBlocking(false);

            //打開輪訓器
            selector = Selector.open();

            // 把ServerSocketChannel注冊到輪訓器
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void listen(){
        System.out.println("listen on " + this.port + ".");
        try {
            // 輪詢主線程
            while (true){
                // 核心代碼1,輪訓器,阻塞,直到有事件注冊到selector
                selector.select();
                // 每次都拿到所有的号子,判斷号子狀态
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iter = keys.iterator();
                //不斷地疊代,就叫輪詢
                //同步展現在這裡,因為每次隻能拿一個key,每次隻能處理一種狀态
                while (iter.hasNext()){
                    SelectionKey key = iter.next();
                    iter.remove();
                    //每一個key代表一種狀态
                    //每一個号對應一個業務
                    //核心代碼2,資料就緒、資料可讀、資料可寫 等等等等
                    process(key);
                }
                
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //具體辦業務的方法,坐班櫃員
    //每一次輪詢就是調用一次process方法,而每一次調用,隻能幹一件事
    //在同一時間點,隻能幹一件事
    private void process(SelectionKey key) throws IOException {
        //針對于每一種狀态給一個反應
        if(key.isAcceptable()){
            System.out.println("就緒了");
            // 狀态1:就緒了,就把狀态改為可讀,下次輪訓進來就可以讀了
            ServerSocketChannel server = (ServerSocketChannel)key.channel();
            //這個方法展現非阻塞,不管你資料有沒有準備好
            //你給我一個狀态和回報
            SocketChannel channel = server.accept();
            //一定一定要記得設定為非阻塞
            channel.configureBlocking(false);
            //當資料準備就緒的時候,将狀态改為可讀
            key = channel.register(selector,SelectionKey.OP_READ);

        }
        else if(key.isReadable()){
            System.out.println("開始讀取資料");
            // 狀态2:可讀,可以讀取資料了,可以選中把号子改成可寫,這樣就可以實作對話了
            //key.channel 從多路複用器中拿到用戶端的引用
            SocketChannel channel = (SocketChannel)key.channel();
            int len = channel.read(buffer);
            if(len > 0){
                buffer.flip();
                String content = new String(buffer.array(),0,len);
                key = channel.register(selector,SelectionKey.OP_WRITE);
                //在key上攜帶一個附件,一會再寫出去
                key.attach(content);
                System.out.println("讀取内容:" + content);
            }
        }
        else if(key.isWritable()){
            // 狀态3:可以寫,那麼就可以寫給連接配接進來的用戶端發消息了
            SocketChannel channel = (SocketChannel)key.channel();

            String content = (String)key.attachment();
            channel.write(ByteBuffer.wrap(("輸出:" + content).getBytes()));
            channel.close();
        }
    }

    public static void main(String[] args) {
        new NIOServerDemo2(8080).listen();
    }
}
           

用戶端給服務端發送資料

public class BIOClient {

	public static void main(String[] args) throws UnknownHostException, IOException {

		//要和誰進行通信,伺服器IP、伺服器的端口
		//一台機器的端口号是有限
		Socket client = new Socket("localhost", 8080);

		//輸出 O  write();
		//不管是用戶端還是服務端,都有可能write和read

		OutputStream os = client.getOutputStream();

		//生成一個随機的ID
		String name = UUID.randomUUID().toString();
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("用戶端發送資料1:" + name);
		//傳說中的101011010
		os.write(("用戶端1:"+name).getBytes());
		os.close();
		client.close();

		
	}
	
}
           

由上可見NIO使用了一個**多路複用器(Selector)**來處理每個請求的連接配接、寫資料、讀資料等操作,當一個請求連接配接進來但是資料沒有發送完成時Selector可以去處理别的請求,而不需要等待請求一處理完再去處理其他請求,很好的解決傳統BIO模型的問題

NIO相對于BIO解決的問題有:

1、BIO每個線程在獲得一個連接配接時必須阻塞等待讀取資料完成才能處理其他請求,而NIO在資料未準備完成時即可處理其他請求

2、BIO未使用線程池時處理請求必須一個一個處理,使用線程池後存線上程開銷問題。NIO一個線程即可同時處理多個請求,解決了線程池的問題同時解決了性能問題

BIO和NIO的差別

BIO和NIO差別

1、BIO處理資料是阻塞的(讀取資料)

2、NIO處理資料是非阻塞的(資料還不能讀取時可以處理别的請求)

這就是BIO稱為同步阻塞IO而NIO稱為同步非阻塞IO的原因

NIO和AIO的差別

1、NIO是java工作線程在Selector輪詢自己檢查資料是否準備完成了

2、AIO是由作業系統來通知工作線程資料已經準備完成了

NIO和AIO的同步和異步就展現在是由誰通知資料準備完成

擴充:

1、NIO的代碼操作太過于複雜,是以netty出現了,netty是基于nio的封裝

2、AIO是基于作業系統來通知的,是以作業系統的性能決定了IO的性能,在Linux系統上,AIO的底層實作仍使用EPOLL,沒有很好實作AIO,是以在性能上沒有明顯的優勢3

3、目前的主流仍然是NIO,AIO在linux系統上還是不夠成熟,存在一些缺陷