java BIO、NIO學習

ahiru?發表於2019-02-14

在IO執行有兩個階段很重要:

  • 等待資料準備
  • 將資料從核心複製到程式中

BIO(Blocking I/O)阻塞I/O模型


java BIO、NIO學習


當使用者程式呼叫了recvfrom這個系統呼叫,核心就開始了io的第一個階段:等待資料準備。如果資料還沒準備好(比如還沒有收到一個完整的udp包),這時候核心要等待足夠的資料到來,而在使用者程式這邊,程式會被阻塞。當核心等到資料準備好,程式將資料從核心中拷貝到使用者空間,然後核心返回結果,使用者程式才解除block狀態,重新執行起來。

BIO在IO執行的兩個階段都被阻塞了。

示例程式碼:

 public static void server(){
        ServerSocket serverSocket = null;
        InputStream in = null;
        try
        {
            serverSocket = new ServerSocket(8080);
            int recvMsgSize = 0;
            byte[] recvBuf = new byte[1024];
            while(true){
                Socket clntSocket = serverSocket.accept();
                SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();
                System.out.println("Handling client at "+clientAddress);
                in = clntSocket.getInputStream();
                while((recvMsgSize=in.read(recvBuf))!=-1){
                    byte[] temp = new byte[recvMsgSize];
                    System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
                    System.out.println(new String(temp));
                }
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally{
            try{
                if(serverSocket!=null){
                    serverSocket.close();
                }
                if(in!=null){
                    in.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
複製程式碼

NIO(Non-Blocking I/O)非阻塞I/O模型

NIO是一種同步非阻塞的IO模型。同步指執行緒不斷輪詢IO事件是否就緒,非阻塞是指執行緒在等待IO的時候,可以做其他任務。同步的核心是Selector,Selector代替了執行緒本身輪詢IO事件,避免了阻塞同時減少了不必要的執行緒消耗;非阻塞的核心是通道和緩衝區,當IO事件就緒時,可以通過寫入緩衝區,保證IO的成功,無需執行緒阻塞式地等待。

java BIO、NIO學習

當使用者程式呼叫recvfrom時,系統不會阻塞使用者程式,如果資料還沒準備好,系統立刻返回一個ewouldblock錯誤給程式,使用者程式知道資料還沒準備好,使用者程式就可以去做其他事了。程式輪詢核心檢視資料是否準備好。直到資料準備好了,並且收到使用者程式的system call,程式複製資料包,然後返回。

NIO有三大核心部分:Channel(通道),Buffer(緩衝區),Selector(多路複用器)


java BIO、NIO學習

Channel

基本上,所有的IO在NIO中都從一個Channel開始。資料可以從Channel讀到Buffer中,也可以從Buffer寫到Channel。

Buffer

Buffer通過以下幾個變數來儲存這個資料的當前位置狀態:

capacity:緩衝區陣列的總長度
position:下一個要操作的資料元素的位置
limit:緩衝區陣列中不可操作的下一個元素的位置
mark:用於記錄當前position的前一個位置或者預設是-1

0 <= mark <= position <= limit <= capacity複製程式碼

java BIO、NIO學習

開始時Buffer的position為0,limit為capacity,程式寫入資料到緩衝區,position往後移。當Buffer寫入資料結束之後,呼叫flip()方法之後(此時limit=position,position=0),Buffer為輸出資料做好準備;當Buffer輸出資料結束之後,呼叫clear()方法(此時limit=capacity,position=0),clear()方法不是清空Buffer的資料,它僅僅將position置為0,將limit置為capacity,為再次向Buffer裝入資料做準備;

Buffer的使用:

  • 分配空間:ByteBuffer buf = ByteBuffer.allocate(1024);
  • 寫入資料到Buffer:int bytesRead = fileChannel.read(buf);
  • 呼叫flip()方法:buf.flip();
  • 從Buffer中讀取資料:buf.get();
  • 呼叫clear()方法或compact()方法

Buffer一些重要方法:

  • flip():把limit設定為position,position設定為0,為輸出資料做好準備
  • clear():把position設定為0,limit設定為capacity,為再次向Buffer裝入資料做準備
  • compact():將所有未讀的資料拷貝到Buffer起始處,把position設定到最後一個未讀元素的正後面,limit設定為capacity
  • mark():可以標記Buffer中的一個特定的position,之後可以通過reset()恢復到position的位置
  • rewind():將position設回為0,limit保持不變,這樣可以重讀Buffer中的所有資料。

示例程式碼:

public static void client(){
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        SocketChannel socketChannel = null;
        try
        {
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress("10.10.195.115",8080));
            if(socketChannel.finishConnect())
            {
                int i=0;
                while(true)
                {
                    TimeUnit.SECONDS.sleep(1);
                    String info = "I'm "+i+++"-th information from client";
                    buffer.clear();
                    buffer.put(info.getBytes());
                    buffer.flip();
                    while(buffer.hasRemaining()){
                        System.out.println(buffer);
                        socketChannel.write(buffer);
                    }
                }
            }
        }
        catch (IOException | InterruptedException e)
        {
            e.printStackTrace();
        }
        finally{
            try{
                if(socketChannel!=null){
                    socketChannel.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
複製程式碼

Selector

Channel和Selector配合使用,必須先將Channel註冊到Selector上,通過register()方法來實現。Channel.register()方法會返回一個SelectionKey物件。這個物件代表了註冊到該Selector的通道。

一旦向Selector註冊了一個或多個通道,就可以呼叫select()方法。

select()方法返回的int值表示有多少通道已經就緒。

參考資料:瘋狂Java講義

Java NIO

Java BIO/NIO/AIO學習


相關文章