BIO、NIO、AIO等IO模式詳解(圖文、代碼示例解說)

1.BIO(blocking I/O)

BIO是一個傳統的IO,是一個阻塞的IO,當然都是這麼說,那麼堵塞在哪裏呢,我們通過代碼示例給大家解說

我先用程序模擬一個客戶端連接服務端程序,建立一個socket連接來監聽客戶端,然後監聽到了以後用getInputStream進行獲取流然後死循環監聽客戶端的請求數據。

/**
 * 用BIO方式讓客戶端連接程序,監聽服務端
 *
 * @Author df
 * @Date 2020/4/12 15:24
 * @Version 1.0
 */
public class BioServer {
    public static void main(String[] args) {
         try (ServerSocket serverSocket = new ServerSocket(8888)) {
            System.out.println("BIOServer has started,Listening on port" + serverSocket.getLocalSocketAddress());
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
                // BIO阻塞原因:getInputStream阻塞一直佔用當前線程資源,不讓當前線程做其他事情
                Scanner input = new Scanner(clientSocket.getInputStream());
                //針對每個socket,不斷的進行數據交互
                while (true) {
                    String request = input.nextLine();
                    if ("quit".equals(request)) {
                        break;
                    }
                    System.out.println(String.format("From %s : %s", clientSocket.getRemoteSocketAddress(), request));
                    String response = "From BIOServer Hello " + request + ".\n";
                    clientSocket.getOutputStream().write(response.getBytes());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

咱們來啓動一下main方法,啓動完成,監聽本地的8888端口

接下來就用cmd命令telnet來連接服務,輸入如下命令回車,如下第二張圖就連接上了

輸入字符敲回車,服務端都能收到了

然後打開第二個cmd窗口,同樣輸入telnet localhost 8888然後回車,你發現控制檯沒有再打印出第二個連接信息了,輸入任何字符也都沒有輸出了,那麼爲什麼呢?

當第一個線程請求以後,socket建立連接打印信息,一直循環等待,等第二個線程需要進來的時候就不能進行訪問了,因爲一直被第一個線程getInputStream堵塞着。也就是說阻塞一直佔用當前線程資源,不讓當前線程做其他事情。

那麼這樣阻塞也不是辦法啊,那我難道只能進行一次連接麼,這樣肯定是不行的,那需要解決這個問題啊!

既然BIO通過獲取getInputStream進行阻塞,那麼可以不可以用多線程的方式解決這個問題呢,讓客戶端能夠多個連接呢。也給大家準備了線程池改良版的代碼示例

/**
 * 線程池版改良BIO阻塞問題
 *
 * @Author df
 * @Date 2020/4/12 16:09
 * @Version 1.0
 */
public class BioServerThreadPool {

    Map<Socket, String> map;

    public static void main(String[] args) {
        // 創建3個線程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        RequestHandler requestHeader = new RequestHandler();
        try (ServerSocket serverSocket = new ServerSocket(8888)) {
            System.out.println("BIO thread Server has started,Listening on port" + serverSocket.getLocalSocketAddress());
            while (true) {
                Socket clientSocket = serverSocket.accept();
                // 線程提交
                executor.submit(new ClientHandler(clientSocket, requestHeader));
                System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
            }
        } catch (
                IOException e) {
            e.printStackTrace();
        }

    }
/**
 * @Author df
 * @Date 2020/4/12 20:52
 * @Version 1.0
 */
public class ClientHandler implements Runnable {
    private static RequestHandler requestHandler;
    private static Socket clientSocket;

    public ClientHandler(Socket clientSocket, RequestHandler requestHandler) {
        this.requestHandler = requestHandler;
        this.clientSocket = clientSocket;
    }

    @Override
    public void run() {
        try (Scanner input = new Scanner(clientSocket.getInputStream())) {
            while (true) {
                String request = input.nextLine();
                if ("quit".equals(request)) {
                    break;
                }
                System.out.println(String.format("From %s : %s", clientSocket.getRemoteSocketAddress(), request));
                String response = "From BIOServer Hello " + request + ".\n";
                clientSocket.getOutputStream().write(response.getBytes());
            }
        } catch (Exception e) {

        }
    }
}
public class RequestHandler {
    public String handle(String request) {
        return "From Server Hello " + request + ".\n";
    }
}

咱們來啓動一下,打開cmd同樣輸入telnet localhost 8888然後回車,發現可以連接,並且也正常通信的

那麼再打開一個cmd,輸入telnet localhost 8888然後回車,看是否能連接呢?發現第二個也能連接並且正常通信

那麼打開第三個呢,能否連接呢?發現也是能連接上的,並且正常通信

那麼第四個呢?發現可以連接,但是無法進行任何輸出操作,爲什麼?

這時候就把請求操作都放到等待隊列裏了,所以你能有連接操作,但是已經被其他三個線程阻塞掉了,不能再有線程處理它了,那麼這個時候咱們在其他三個已經連接成功的輸入quit回車將其連接退出,又會發生什麼呢?你會發現第四個線程立馬把之前阻塞時候進行輸入的字符全部輸出,並且可以進行正常的通信了

其實tomcat中就是用這種BIO的方式進行請求連接,防止阻塞用線程池方式,默認線程池設置200個。所以多線程是解決阻塞的一種方式。

但是啊,在高併發場景下我有10000個人請求,如果選用線程池,就算開1000個線程池,9999個放入了等待隊列裏,而且等待隊列也不是源源不斷的存儲的,再說了讓用戶等待這種設計是不行的,而且多線程切換的開銷也大,所以這個是jdk的BIO留下的坑,那麼jdk會解決,所以JDK1.4以後出現了NIO

那麼再說NIO之前一定要知道爲什麼IO是阻塞的呢?所以就說一下IO的阻塞流程!

1.1 BIO阻塞的過程!

其實IO是否阻塞和java代碼沒有直接關係,IO的本質(針對java而言):應用程序和操作系統內核進行數據交互。

爲什麼這麼說呢?假如我們要讀取硬盤的gupao.txt文件一定是要inputStream讀取,但是我們的java程序一定能直接讀取磁盤麼,答案是否的也是沒有權限的,需要操作系統幫助我們讀取,我們java程序是進程也可以叫做用戶空間,用戶空間是程序員可以操作的,可以直接對硬盤操作的就是內核空間。下圖在進行講解一下

以讀操作爲例說一下過程:

  1. java程序發出讀操作,內核空間收到請求通過磁盤控制器轉化對磁盤進行操作。
  2. 操作以後返回數據通過DMA方式將數據放入內核空間的緩衝區裏面。
  3. Java read是從緩衝區拿數據進行返回的。

所以走到這裏應該明白阻塞不阻塞的與我們程序沒有關係,我們也不會無緣無故的阻塞,阻塞不阻塞和內核空間纔有關係。

但是JDK設計者想好了該如何解決,因爲以前的BIO是我發起了read請求以後一直等待內核空間讀取放入緩衝然後用戶空間纔讀取,內核空間沒有操作完它就會一直等待,但是NIO就是我發起了read請求以後我就不管了我還可以做其他的事情,直到你把數據讀取完返回給我,我再操作,這樣就是非阻塞的了

把以上的敘述講解完大家就應該理解了,接下來說NIO

2.NIO(non-blocking I/O)

那麼這張圖就是同步非阻塞也就是NIO,首先application就是用戶空間,kernel就是內核空間。

1.用戶通過read,在這裏就是recvfrom(java也要調用c語言去讀取的)去讀取,內核空間接收到了進行操作,然後用戶空間不需要等待直接可以去幹別的事情,然後再來詢問內核空間處理好了麼,沒有的話用戶空間還可以幹別的,直到在繼續詢問內核空間發現內核空間告訴它自己處理好了(內核空間把數據拷貝到緩衝區)那麼進行返回,結束

那麼NIO方式的代碼示例我也準備好了,給大家演示一下

public class NIOServer {

    public static void main(String[] args) {
        try {
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            // 設置不阻塞
            serverChannel.configureBlocking(false);
            serverChannel.bind(new InetSocketAddress(9999));
            System.out.println("NIO NIOServer has started ,listening on port:" + serverChannel.getLocalAddress());

            Selector selector = Selector.open();

            // 每個客戶端來了之後,就把客戶端註冊到selector選擇器上,默認狀態就是ACCEPT
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            RequestHandler requestHandler = new RequestHandler();
            // 輪詢,服務端不斷輪詢,等待客戶端的連接
            while (true) {
                int select = selector.select();
                if (select == 0) {
                    continue;
                }
                // 如果selector有的話,那麼就取出對應的channel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 判斷SelectionKey中的channel狀態如何
                    if (key.isAcceptable()) {
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        SocketChannel clientChannel = channel.accept();
                        // 客戶端的channel來源打出來
                        System.out.println("Connection from " + clientChannel.getRemoteAddress());
                        // 將客戶端的也設置爲非阻塞
                        clientChannel.configureBlocking(false);
                        // 將channel的狀態設置爲read
                        clientChannel.register(selector, SelectionKey.OP_READ);
                    }
                    // 接下來輪詢到的時候發現狀態是readable
                    if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        // 數據的交互是以buffer爲中間橋樑的
                        channel.read(buffer);
                        // 用buffer取數據也用buffer返回數據
                        String request = new String(buffer.array()).trim();
                        buffer.clear();
                        System.out.println(String.format("From %s : %s", channel.getRemoteAddress(), request));
                        String response = requestHandler.handle(request);
                        channel.write(ByteBuffer.wrap(response.getBytes()));
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

你們自行用之前的方法測試把,打開5,7八個都不會阻塞

3.AIO(NIO.2) (Asynchronous I/O) 

異步非阻塞,服務器實現模式爲一個有效請求一個線程,客戶端I/O請求都是由OS先完成了再通知服務器應用啓動線程進行處理。

1.當應用空間要讀取的時候發送給內核空間,內核空間不管有沒有處理直接返回,然後等到內核空間處理完畢直接發送信號(deliver signal)把數據傳送給用戶空間。

4.多路複用IO

1.每一次的連接讀取也好寫操作也好都先不進行連接,都先存儲也可以說是登記到這邊,不會直接分配線程去調度資源,直到真的要操作input或Out的時候我纔去分配線程做這些操作呢。

1.有操作進行連接都會通過select進行註冊,輪詢監控發現有需要讀取或者寫操作再去分配線程,那麼之後的操作可以使用同步非阻塞或者異步非阻塞或者阻塞IO都是可以的。

以上就是全部內容,以上筆記記錄學習來源咕泡教育-Jack老師的公開課學習整理!

 

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