BIO、NIO、多路複用IO、AIO

風鈴峰頂發表於2020-10-19

BIO(Blocking IO):阻塞IO(有的資料稱同步阻塞IO),面向Stream。
NIO(Non-Blocking IO):非阻塞IO(有的資料稱同步非阻塞IO),面向Channel。
多路複用IO(Multiplexing IO):也稱事件驅動IO(event driven IO)或者Reactor模式(反應器模式)。
AIO(Asynchronous IO,也稱NIO 2.0):非同步IO(有的資料稱非同步非阻塞IO,也稱Proactor模式(主動器模式)),面向Channel。

有的資料稱同步才有阻塞、非阻塞之分,非同步一定同時非阻塞,沒有非同步阻塞的操作。

NIO核心元件:Buffer(緩衝區)、Channel(通道)、Selector(選擇器)。

NIO提供了BIO中Socket和ServerSocket對應的SocketChannel和ServerSocketChannel,這兩種Channel都支援阻塞和非阻塞兩種模式。
Selector在單個執行緒中處理多個Channel。
Selector中可以輪詢ServerSocketChannel是否有連線請求,輪詢SocketChannel是否有IO請求。
通過Channel將資料IO進Buffer,IO的同時可以進行其他操作。Channel是雙向的,可讀可寫。Stream是單向的,讀或者寫。
NIO的IO之間是同步的,IO之間可以非阻塞地進行其他操作,所以NIO是非同步非阻塞IO。

public class BIOServer {
   public static void main(String[] args) throws IOException {
       // TODO 服務端處理客戶端連線請求
       ServerSocket serverSocket = new ServerSocket(3333);
       // 接收到客戶端連線請求之後為每個客戶端建立一個新的執行緒進行鏈路處理
       new Thread(() -> {
           while (true) {
               try {
                   // 阻塞方法獲取新的連線
                   Socket socket = serverSocket.accept();
                   // 每一個新的連線都建立一個執行緒,負責讀取資料
                   new Thread(() -> {
                       try {
                           int len;
                           byte[] data = new byte[1024];
                           InputStream inputStream = socket.getInputStream();
                           // 按位元組流方式讀取資料
                           while ((len = inputStream.read(data)) != -1) {
                               System.out.println(new String(data, 0, len));
                           }
                       } catch (IOException e) {
                       }
                   }).start();
               } catch (IOException e) {
               }
           }
       }).start();
   }
}
public class NIOServer {
   public static void main(String[] args) throws IOException {
       // 1. serverSelector負責輪詢是否有新的連線,服務端監測到新的連線之後,不再建立一個新的執行緒,
       // 而是直接將新連線繫結到clientSelector上,這樣就不用 IO 模型中 1w 個 while 迴圈在死等
       Selector serverSelector = Selector.open();
       // 2. clientSelector負責輪詢連線是否有資料可讀
       Selector clientSelector = Selector.open();
       new Thread(() -> {
           try {
               // 對應IO程式設計中服務端啟動
               ServerSocketChannel listenerChannel = ServerSocketChannel.open();
               listenerChannel.socket().bind(new InetSocketAddress(3333));
               listenerChannel.configureBlocking(false);
               listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
               while (true) {
                   // 監測是否有新的連線,這裡的1指的是阻塞的時間為 1ms
                   if (serverSelector.select(1) > 0) {
                       Set<SelectionKey> set = serverSelector.selectedKeys();
                       Iterator<SelectionKey> keyIterator = set.iterator();
                       while (keyIterator.hasNext()) {
                           SelectionKey key = keyIterator.next();
                           if (key.isAcceptable()) {
                               try {
                                   // (1)
                                   // 每來一個新連線,不需要建立一個執行緒,而是直接註冊到clientSelector
                                   SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                   clientChannel.configureBlocking(false);
                                   clientChannel.register(clientSelector, SelectionKey.OP_READ);
                               } finally {
                                   keyIterator.remove();
                               }
                           }
                       }
                   }
               }
           } catch (IOException ignored) {
           }
       }).start();
       new Thread(() -> {
           try {
               while (true) {
                   // (2) 批量輪詢是否有哪些連線有資料可讀,這裡的1指的是阻塞的時間為 1ms
                   if (clientSelector.select(1) > 0) {
                       Set<SelectionKey> set = clientSelector.selectedKeys();
                       Iterator<SelectionKey> keyIterator = set.iterator();
                       while (keyIterator.hasNext()) {
                           SelectionKey key = keyIterator.next();
                           if (key.isReadable()) {
                               try {
                                   SocketChannel clientChannel = (SocketChannel) key.channel();
                                   ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                   // (3) 面向 Buffer
                                   clientChannel.read(byteBuffer);
                                   byteBuffer.flip();
                                   System.out.println(
                                           Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());
                               } finally {
                                   keyIterator.remove();
                                   key.interestOps(SelectionKey.OP_READ);
                               }
                           }
                       }
                   }
               }
           } catch (IOException ignored) {
           }
       }).start();
   }
}

Netty是NIO的,Vert.x基於Netty。
Netty重寫了JDK中NIO的Buffer、Channel。Windows上對AIO支援優秀,Linux的AIO底層和NIO一樣是使用epoll,效能無顯著優勢,編碼還更復雜,所以Netty使用的是NIO。

NIO從JDK1.4開始,AIO從JDK1.7開始。

IO分兩個階段,對於Input而言,先是資料進入作業系統核心(等待資料:wait for data),然後是將資料從作業系統核心拷貝到使用者程式(拷貝資料:copy data from kernel to user)

核心:硬體上的第一層軟體擴充,負責管理程式、記憶體、檔案、網路、裝置驅動。

阻塞IO和非阻塞IO的差異在於等待資料階段是否阻塞。阻塞IO在等待資料階段是阻塞的;讀資料時,資料沒有到達,會阻塞。非阻塞IO在等待資料階段是非阻塞的;讀資料時,資料沒有到達,直接返回未到達標記,可以做其他的事情,下一次再來檢視。非阻塞IO的讀資料中,會輪詢資料是否可讀,如JDK的NIO:while(selectionKey.isReadable()){}

SelectionKey:

/**
     * Tests whether this key's channel is ready for reading.
     *
     * <p> An invocation of this method of the form {@code k.isReadable()}
     * behaves in exactly the same way as the expression
     *
     * <blockquote><pre>{@code
     * k.readyOps() & OP_READ != 0
     * }</pre></blockquote>
     *
     * <p> If this key's channel does not support read operations then this
     * method always returns {@code false}.  </p>
     *
     * @return  {@code true} if, and only if,
                {@code readyOps() & OP_READ} is nonzero
     *
     * @throws  CancelledKeyException
     *          If this key has been cancelled
     */
    public final boolean isReadable() {
        return (readyOps() & OP_READ) != 0;
    }

    /**
     * Tests whether this key's channel is ready for writing.
     *
     * <p> An invocation of this method of the form {@code k.isWritable()}
     * behaves in exactly the same way as the expression
     *
     * <blockquote><pre>{@code
     * k.readyOps() & OP_WRITE != 0
     * }</pre></blockquote>
     *
     * <p> If this key's channel does not support write operations then this
     * method always returns {@code false}.  </p>
     *
     * @return  {@code true} if, and only if,
     *          {@code readyOps() & OP_WRITE} is nonzero
     *
     * @throws  CancelledKeyException
     *          If this key has been cancelled
     */
    public final boolean isWritable() {
        return (readyOps() & OP_WRITE) != 0;
    }

    /**
     * Tests whether this key's channel has either finished, or failed to
     * finish, its socket-connection operation.
     *
     * <p> An invocation of this method of the form {@code k.isConnectable()}
     * behaves in exactly the same way as the expression
     *
     * <blockquote><pre>{@code
     * k.readyOps() & OP_CONNECT != 0
     * }</pre></blockquote>
     *
     * <p> If this key's channel does not support socket-connect operations
     * then this method always returns {@code false}.  </p>
     *
     * @return  {@code true} if, and only if,
     *          {@code readyOps() & OP_CONNECT} is nonzero
     *
     * @throws  CancelledKeyException
     *          If this key has been cancelled
     */
    public final boolean isConnectable() {
        return (readyOps() & OP_CONNECT) != 0;
    }

    /**
     * Tests whether this key's channel is ready to accept a new socket
     * connection.
     *
     * <p> An invocation of this method of the form {@code k.isAcceptable()}
     * behaves in exactly the same way as the expression
     *
     * <blockquote><pre>{@code
     * k.readyOps() & OP_ACCEPT != 0
     * }</pre></blockquote>
     *
     * <p> If this key's channel does not support socket-accept operations then
     * this method always returns {@code false}.  </p>
     *
     * @return  {@code true} if, and only if,
     *          {@code readyOps() & OP_ACCEPT} is nonzero
     *
     * @throws  CancelledKeyException
     *          If this key has been cancelled
     */
    public final boolean isAcceptable() {
        return (readyOps() & OP_ACCEPT) != 0;
    }

同步IO在拷貝資料階段阻塞,所以IO只能順序進行。非同步IO發起IO請求後,不阻塞,直接返回,核心等待資料結束後,Proactor(前攝器,包含3個執行緒)將資料分發給使用者程式,給使用者程式一個訊號,通知IO結束。

多路複用IO在等待資料階段,可以在一個執行緒中通過select函式監控多個檔案描述符的IO是否就緒,直接返回結果。監控時是阻塞的,可以設定監控時長,即阻塞時長;拷貝資料階段阻塞,但可通過多執行緒分離IO,也有資料從這個角度稱多路複用IO是非同步阻塞IO,但這裡的阻塞和阻塞IO的阻塞是不相同的,這裡的非同步和非同步IO的非同步是不相同的。

https://blog.csdn.net/m0_38109046/article/details/89449305
https://www.cnblogs.com/cainingning/p/9556642.html
https://www.cnblogs.com/straybirds/p/9479158.html

epoll是Linux特有。
select是POSIX(Portable Operating System Interface,可移植作業系統介面)的規定。
select、poll、epoll是同步的。
Linux的select、poll、epoll:
https://www.cnblogs.com/cainingning/p/9556642.html
https://segmentfault.com/a/1190000003063859
https://www.cnblogs.com/aspirant/p/9166944.html

相關文章