【Java NIO】網絡IO模型與Java中的NIO之間的聯繫

目錄

1 前言

2 五種網絡IO模型

2.1 阻塞IO模型(BIO)

2.2 非阻塞IO(NIO)

2.3 多路複用IO模型

2.4 異步IO(AIO)

2.5 信號驅動IO模型

3 Java NIO與IO的區別

3.1 面向流與面向緩衝區

3.2 阻塞與非阻塞IO

4 Java NIO與IO模型

4.1 Java NIO實現阻塞IO模型

4.2 Java NIO實現非阻塞IO模型

4.3 Java NIO實現多路複用IO模型


1 前言

學習Java NIO這個知識點花費了幾天時間,通過閱讀大量的譯文和博客文章,終於有點頭緒了,雖說不是深入瞭解,但好歹是明白BIO、NIO、AIO這幾種模型之間的區別,附帶着瞭解了Java NIO的相關類源碼,也算是有點收穫。這裏總結一下自己所瞭解的IO模型,以及它們在Java NIO中的應用。

2 五種網絡IO模型

以linux系統爲例,分別是阻塞IO模型(BIO)、非阻塞IO模型(NIO)、多路複用IO模型、異步IO模型(AIO)、信號驅動IO模型。對於一個network IO (這裏我們以read舉例),它會涉及到兩個系統對象,一個是調用這個IO的process (or thread),另一個就是系統內核(kernel)。當一個read操作發生時,它會經歷兩個階段:

  1. 等待數據在內核中準備就緒
  2. 將數據從內核拷貝到用戶進程中

2.1 阻塞IO模型(BIO)

當進程發起read請求後,產生了一條系統調用:recvfrom,之後進程便陷入了阻塞狀態直到內核return OK。內核中經歷了 數據未就緒——>數據準備就緒——>將數據從內核拷貝到進程空間中——>return ok 這個過程,所以,阻塞IO模型的特點就是在IO執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了。同理,進程發起的write請求也會阻塞,直到所有的數據都被寫入到內核中。

在阻塞IO模型下,socket通信的server同一時刻只能處理一個客戶端的連接請求,在多用戶連接的場景下會導致其他用戶連接等待,所以都會採用“一連接一線程”的方式來處理,server每accept一個connect,就會new一個線程去單獨處理這個連接。爲了避免過多的創建線程(線程會佔用系統資源,同時線程的切換代價也比較大),可以使用線程池去管理,複用已創建的線程資源,減少線程的創建個數。

2.2 非阻塞IO(NIO

當進程發起read請求後,若內核中數據未準備就緒,不會阻塞。內核會返回一個標誌,進程通過這個標誌可以知道數據未就緒,從而可以做其他事情,在後續操作中,進程可以不斷地發起read請求來判斷數據是否就緒。若內核中數據就緒,進程會阻塞在本次read請求操作上,直到將數據從內核複製到進程空間中並return OK。write請求也類似,當進程執行write操作時,操作會立刻返回,可以通過內核返回的標誌判斷數據是否完全寫入。

所以,該模型在等待數據就緒階段是非阻塞的,在數據從內核複製到進程中時是阻塞的。非阻塞的接口相比於阻塞型接口的顯著差異在於,在被調用之後立即返回。

在非阻塞IO模型中,可以只用一個線程來管理多個連接,即一個server線程在accept多個連接後,循環對這些連接執行read操作,若當前遍歷連接數據已就緒,則讀取數據,然後繼續遍歷。但這種模式決不被推薦,因爲,循環調用recv()將大幅度推高CPU 佔用率;此外,在這個方案中recv()更多的是起到檢測“操作是否完成”的作用,實際操作系統提供了更爲高效的檢測“操作是否完成“作用的接口,例如select()多路複用模式,可以一次檢測多個連接是否活躍。

2.3 多路複用IO模型

它的基本原理就是Linux中select/epoll這個function會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。從圖中可以看到,這次不需要我們主動執行read操作了,而是進程首先調用select函數,調用select之後進程會阻塞,直到內核中數據準備就緒返回標誌通知進程,此時進程從阻塞中回覆,直接執行read操作讀取數據,阻塞直到return OK。

多路複用IO模型在檢測連接數據是否就緒和數據複製過程中都會阻塞。

顯然,多路複用IO模型比非阻塞IO模型更適合一個線程管理多個連接的場景,因爲使用select函數來判斷數據是否準備就緒比主動執行recvfrom來輪訓具有更好的性能,判斷直接發生在內核空間,省去了大量的recvfrom系統調用。

注意

  1. 如果處理的連接數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。
  2. 該模型將數據探測和事件相應夾雜在一起,一旦某個連接的事件執行體龐大,則會大大推遲其它連接的數據探測和事件執行。

2.4 異步IO(AIO)

從圖中可以看到,當進程請求一個aio_read系統調用後就直接返回,接下來在內核中自動完成數據的準備的複製工作,之後內核給進程發送一個信號,通知它數據準備完畢,然後進程執行read操作讀取即可。

異步模型全程非阻塞,數據的準備工作都在內核中異步完成。

異步IO是真正非阻塞的,它不會對請求進程產生任何的阻塞,因此對高併發的網絡服務器實現至關重要。

2.5 信號驅動IO模型

信號驅動IO,調用sigaltion系統調用,當內核中IO數據就緒時以SIGIO信號通知請求進程,請求進程再把數據從內核讀入到用戶空間,這一步是阻塞的。

3 Java NIO與IO的區別

IO NIO
面向流 面向緩衝區
阻塞IO 非阻塞IO
無選擇器 有選擇器

 

3.1 面向流與面向緩衝區

 Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。

Java NIO的緩衝導向方法略有不同。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏尚未處理的數據。

3.2 阻塞與非阻塞IO

參考上面提到的阻塞IO模型與非阻塞IO模型。

Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了。

Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel)。

4 Java NIO與IO模型

Java NIO是阻塞IO模型、非阻塞IO模型和多路複用IO模型的組合體,可以單獨實現這三種模型中的任何一種,使用最多的是多路複用IO模型。使用的組件包括channel、buffer和selector,這裏不介紹了。下面簡單使用Java NIO代碼體現這三種模型的socket通信。

4.1 Java NIO實現阻塞IO模型

顯然,如同Java IO一樣,這裏socketChannel.read(buf)會阻塞直到內核空間中數據準備和複製完畢,然後將數據讀到緩衝區。

// client創建通道並建立連接
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
// 創建緩衝區並從通道中讀數據到緩衝區中
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);

socketChannel.close();

4.2 Java NIO實現非阻塞IO模型

通過socketChannel.configureBlocking(false);可以將通道設置爲非阻塞模式,該模式下read方法執行候會直接返回,通過返回的int值可以判斷是否有數據可讀(0)、讀到了多少字節數據(>0)、是否讀完了數據(-1)。

// client創建通道並建立連接
SocketChannel socketChannel = SocketChannel.open();
// #############這裏配置非阻塞模式###################
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));
// 創建緩衝區並從通道中讀數據到緩衝區中
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);        // bytesRead爲讀到的字節個數
byte[] bytes = new byte[Integer.MAX_VALUE];

// bytesRead = -1表示讀到了尾部
while (bytesRead != -1){
    if (bytesRead == 0){
        // 沒有數據可讀
        // do something else
    }

    // 從緩衝區中取出讀到的數據
    buf.flip();
    buf.get(bytes);
    buf.clear();
    bytesRead = socketChannel.read(buf);
}
// 打印結果
System.out.println(new String(bytes));

socketChannel.close();

4.3 Java NIO實現多路複用IO模型

通過selector選擇器,可以讓一個server線程管理大量建立的連接,閱讀下面代碼你就會發現設計的精妙之處。

public class Server implements Runnable{
    //1 多路複用器(管理所有的通道)
    private Selector seletor;
    //2 建立緩衝區
    private ByteBuffer readBuf = ByteBuffer.allocate(1024);
    private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
    public Server(int port){
        try {
            //1 打開路複用器
            this.seletor = Selector.open();
            //2 打開服務器通道
            ServerSocketChannel ssc = ServerSocketChannel.open();
            //3 設置服務器通道爲非阻塞模式
            ssc.configureBlocking(false);
            //4 綁定地址
            ssc.bind(new InetSocketAddress(port));
            //5 把服務器通道註冊到多路複用器上,並且監聽連接事件
            ssc.register(this.seletor, SelectionKey.OP_ACCEPT);

            System.out.println("Server start, port :" + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        while(true){
            try {
                //1 必須要讓多路複用器開始監聽
                this.seletor.select();
                //2 返回多路複用器已經選擇的結果集
                Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
                //3 進行遍歷
                while(keys.hasNext()){
                    //4 獲取一個選擇的元素
                    SelectionKey key = keys.next();
                    //5 直接從容器中移除就可以了
                    keys.remove();
                    //6 如果是有效的
                    if(key.isValid()){
                        //7 如果爲阻塞狀態
                        if(key.isAcceptable()){
                            this.accept(key);
                        }
                        //8 如果爲可讀狀態
                        if(key.isReadable()){
                            this.read(key);
                        }
                        //9 寫數據
                        if(key.isWritable()){
                            this.write(key); 
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void write(SelectionKey key){}

    private void read(SelectionKey key) {
        try {
            //1 清空緩衝區舊的數據
            this.readBuf.clear();
            //2 獲取之前註冊的socket通道對象
            SocketChannel sc = (SocketChannel) key.channel();
            //3 讀取數據
            int count = sc.read(this.readBuf);
            //4 如果沒有數據
            if(count == -1){
                key.channel().close();
                key.cancel();
                return;
            }
            //5 有數據則進行讀取 讀取之前需要進行復位方法(把position 和limit進行復位)
            this.readBuf.flip();
            //6 根據緩衝區的數據長度創建相應大小的byte數組,接收緩衝區的數據
            byte[] bytes = new byte[this.readBuf.remaining()];
            //7 接收緩衝區數據
            this.readBuf.get(bytes);
            //8 打印結果
            String body = new String(bytes).trim();
            System.out.println("Server : " + body);

            // 9..可以寫回給客戶端數據 
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void accept(SelectionKey key) {
        try {
            //1 獲取服務通道
            ServerSocketChannel ssc =  (ServerSocketChannel) key.channel();
            //2 執行阻塞方法
            SocketChannel sc = ssc.accept();
            //3 設置阻塞模式
            sc.configureBlocking(false);
            //4 註冊到多路複用器上,並設置讀取標識
            sc.register(this.seletor, SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new Thread(new Server(7788)).start();;
    }
}

參考:

http://ifeve.com/socket-channel/

http://ifeve.com/java-nio-vs-io/

https://www.cnblogs.com/barrywxx/p/8430790.html

https://blog.csdn.net/qq_34039868/article/details/105577401

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