天天看點

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

本文原題“從實踐角度重新了解BIO和NIO”,原文由Object分享,為了更好的内容表現力,收錄時有改動。

1、引言

這段時間自己在看一些Java中BIO和NIO之類的東西,也看了很多部落格,發現各種關于NIO的理論概念說的天花亂墜頭頭是道,可以說是非常的完整,但是整個看下來之後,發現自己對NIO還是一知半解、一臉蒙逼的狀态(請原諒我太笨)。

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

基于以上原因,就有了寫本文的想法。本文不會提到很多Java NIO和Java BIO的理論概念(需要的話請參見本文的“相關文章”一節),而是站在編碼實踐的角度,通過代碼執行個體,總結了我自己對于Java NIO的見解。有了代碼實踐的過程後再重新回頭看理論概念,會有一個不一樣的了解視角,希望能助你吃透它們!

術語約定:本文所說的BIO即Java程式員常說的經典阻塞式IO,NIO是指Java 1.4版加入的NIO(即異步IO)。

(本文同步釋出于:http://www.52im.net/thread-2846-1-1.html)

2、關于作者

本文作者:Object

個人部落格:http://blog.objectspace.cn/

3、相關文章

本文為了避免過多的闡述Java NIO、BIO的概念性内容,因而盡量少的提及相關理論知識,如果你對Java NIO、BIO的理論知識本來就了解不多,建議還是先讀一讀即時通訊網整理一下文章,将有助于你更好地了解本文。

《少啰嗦!一分鐘帶你讀懂Java的NIO和經典IO的差別》(* 推薦)

《史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!》(* 推薦)

《高性能網絡程式設計(五):一文讀懂高性能網絡程式設計中的I/O模型》

《高性能網絡程式設計(六):一文讀懂高性能網絡程式設計中的線程模型》

4、先用經典的BIO來實作一個簡易的單線程網絡通信程式

要講明白BIO和NIO,首先我們應該自己實作一個簡易的伺服器,不用太複雜,單線程即可。

4.1 為什麼使用單線程作為示範

因為在單線程環境下可以很好地對比出BIO和NIO的一個差別,當然我也會示範在實際環境中BIO的所謂一個請求對應一個線程的狀況。

4.2 服務端代碼

public class Server {

        public static void main(String[] args) {

                byte[] buffer = new byte[1024];

                try{

                        ServerSocket serverSocket = newServerSocket(8080);

                        System.out.println("伺服器已啟動并監聽8080端口");

                        while(true) {

                                System.out.println();

                                System.out.println("伺服器正在等待連接配接...");

                                Socket socket = serverSocket.accept();

                                System.out.println("伺服器已接收到連接配接請求...");

                                System.out.println();

                                System.out.println("伺服器正在等待資料...");

                                socket.getInputStream().read(buffer);

                                System.out.println("伺服器已經接收到資料");

                                System.out.println();

                                String content = newString(buffer);

                                System.out.println("接收到的資料:"+ content);

                        }

                } catch(IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                }

        }

}

4.3 用戶端代碼

public class Consumer {

        public static void main(String[] args) {

                try{

                        Socket socket = newSocket("127.0.0.1",8080);

                        socket.getOutputStream().write("向伺服器發資料".getBytes());

                        socket.close();

                } catch(IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                }

        }

}

4.4 代碼解析

我們首先建立了一個服務端類,在類中實作執行個體化了一個SocketServer并綁定了8080端口。之後調用accept方法來接收連接配接請求,并且調用read方法來接收用戶端發送的資料。最後将接收到的資料列印。

完成了服務端的設計後,我們來實作一個用戶端,首先執行個體化Socket對象,并且綁定ip為127.0.0.1(本機),端口号為8080,調用write方法向伺服器發送資料。

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

4.5 運作結果

當我們啟動伺服器,但用戶端還沒有向伺服器發起連接配接時,控制台結果如下:

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

當用戶端啟動并向伺服器發送資料後,控制台結果如下:

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

4.6 結論

從上面的運作結果,首先我們至少可以看到,在伺服器啟動後,用戶端還沒有連接配接伺服器時,伺服器由于調用了accept方法,将一直阻塞,直到有用戶端請求連接配接伺服器。

5、對用戶端功能進行擴充

在上節中,我們實作的用戶端的邏輯主要是:建立Socket –> 連接配接伺服器 –> 發送資料,我們的資料是在連接配接伺服器之後就立即發送的,現在我們來對用戶端進行一次擴充,當我們連接配接伺服器後,不立即發送資料,而是等待控制台手動輸入資料後,再發送給服務端。(注意:本節中,服務端代碼保持不變)

5.1 改進後的代碼

public class Consumer {

        public static void main(String[] args) {

                try{

                        Socket socket = newSocket("127.0.0.1",8080);

                        String message = null;

                        Scanner sc = newScanner(System.in);

                        message = sc.next();

                        socket.getOutputStream().write(message.getBytes());

                        socket.close();

                        sc.close();

                } catch(IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                }

        }

}

5.2 測試

當服務端啟動,用戶端還沒有請求連接配接伺服器時,控制台結果如下:

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

當服務端啟動,用戶端連接配接服務端,但沒有發送資料時,控制台結果如下:

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

當服務端啟動,用戶端連接配接服務端,并且發送資料時,控制台結果如下:

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

5.3 結論

從上面的運作結果中我們可以看到,伺服器端在啟動後:

1)首先需要等待用戶端的連接配接請求(第一次阻塞);

2)如果沒有用戶端連接配接,服務端将一直阻塞等待;

3)然後當用戶端連接配接後,伺服器會等待用戶端發送資料(第二次阻塞);

4)如果用戶端沒有發送資料,那麼服務端将會一直阻塞等待用戶端發送資料。

服務端從啟動到收到用戶端資料的這個過程,将會有兩次阻塞的過程:

1)第一次在等待連接配接時阻塞;

2)第二次在等待資料時阻塞。

BIO會産生兩次阻塞,這就是BIO的非常重要的一個特點。

6、BIO

6.1 在單線程條件下BIO的弱點

在上兩節中,我們用經典的Java BIO實作了一個簡易的網絡通信程式,這個簡易的程式是以單線程運作的。

其實我們不難看出:當我們的伺服器接收到一個連接配接後,并且沒有接收到用戶端發送的資料時,是會阻塞在read()方法中的,那麼此時如果再來一個用戶端的請求,服務端是無法進行響應的。換言之:在不考慮多線程的情況下,BIO是無法處理多個用戶端請求的。

6.2 BIO如何處理并發

在上面的伺服器實作中,我們實作的是單線程版的BIO伺服器,不難看出,單線程版的BIO并不能處理多個用戶端的請求,那麼如何能使BIO處理多個用戶端請求呢。

其實不難想到:我們隻需要在每一個連接配接請求到來時,建立一個線程去執行這個連接配接請求,就可以在BIO中處理多個用戶端請求了,這也就是為什麼BIO的其中一條概念是伺服器實作模式為一個連接配接一個線程,即用戶端有連接配接請求時伺服器端就需要啟動一個線程進行處理。

6.3 多線程BIO伺服器簡易實作

public class Server {

        public static void main(String[] args) {

                byte[] buffer = newbyte[1024];

                try{

                        ServerSocket serverSocket = newServerSocket(8080);

                        System.out.println("伺服器已啟動并監聽8080端口");

                        while(true) {

                                System.out.println();

                                System.out.println("伺服器正在等待連接配接...");

                                Socket socket = serverSocket.accept();

                                newThread(newRunnable() {

                                        @Override

                                        publicvoidrun() {

                                                System.out.println("伺服器已接收到連接配接請求...");

                                                System.out.println();

                                                System.out.println("伺服器正在等待資料...");

                                                try{

                                                        socket.getInputStream().read(buffer);

                                                } catch(IOException e) {

                                                        // TODO Auto-generated catch block

                                                        e.printStackTrace();

                                                }

                                                System.out.println("伺服器已經接收到資料");

                                                System.out.println();

                                                String content = newString(buffer);

                                                System.out.println("接收到的資料:"+ content);

                                        }

                                }).start();

                        }

                } catch(IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                }

        }

}

6.4 運作結果 

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料
Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

很明顯,現在我們的伺服器的狀态就是一個線程對應一個請求,換言之,伺服器為每一個連接配接請求都建立了一個線程來處理。

6.5 多線程BIO伺服器的弊端

多線程BIO伺服器雖然解決了單線程BIO無法處理并發的弱點,但是也帶來一個問題:如果有大量的請求連接配接到我們的伺服器上,但是卻不發送消息,那麼我們的伺服器也會為這些不發送消息的請求建立一個單獨的線程,那麼如果連接配接數少還好,連接配接數一多就會對服務端造成極大的壓力。

是以:如果這種不活躍的線程比較多,我們應該采取單線程的一個解決方案,但是單線程又無法處理并發,這就陷入了一種很沖突的狀态,于是就有了NIO。

7、NIO

題外話:如果你對Java的NIO理論知識了解的太少,建議優先讀一下這兩篇文章,《少啰嗦!一分鐘帶你讀懂Java的NIO和經典IO的差別》、《史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!》。

7.1 NIO的引入

我們先來看看單線程模式下BIO伺服器的代碼,其實NIO需要解決的最根本的問題就是存在于BIO中的兩個阻塞,分别是等待連接配接時的阻塞和等待資料時的阻塞。

public class Server {

        public static void main(String[] args) {

                byte[] buffer = new byte[1024];

                try{

                        ServerSocket serverSocket = newServerSocket(8080);

                        System.out.println("伺服器已啟動并監聽8080端口");

                        while(true) {

                                System.out.println();

                                System.out.println("伺服器正在等待連接配接...");

                                //阻塞1:等待連接配接時阻塞

                                Socket socket = serverSocket.accept();

                                System.out.println("伺服器已接收到連接配接請求...");

                                System.out.println();

                                System.out.println("伺服器正在等待資料...");

                                //阻塞2:等待資料時阻塞

                                socket.getInputStream().read(buffer);

                                System.out.println("伺服器已經接收到資料");

                                System.out.println();

                                String content = new String(buffer);

                                System.out.println("接收到的資料:"+ content);

                        }

                } catch(IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                }

        }

}

我們需要再老調重談的一點是,如果單線程伺服器在等待資料時阻塞,那麼第二個連接配接請求到來時,伺服器是無法響應的。如果是多線程伺服器,那麼又會有為大量空閑請求産生新線程進而造成線程占用系統資源,線程浪費的情況。

那麼我們的問題就轉移到,如何讓單線程伺服器在等待用戶端資料到來時,依舊可以接收新的用戶端連接配接請求。

7.2 模拟NIO解決方案

如果要解決上文中提到的單線程伺服器接收資料時阻塞,而無法接收新請求的問題,那麼其實可以讓伺服器在等待資料時不進入阻塞狀态,問題不就迎刃而解了嗎?

【第一種解決方案(等待連接配接時和等待資料時不阻塞)】:

public class Server {

        public static void main(String[] args) throws InterruptedException {

                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                try{

                        //Java為非阻塞設定的類

                        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

                        serverSocketChannel.bind(newInetSocketAddress(8080));

                        //設定為非阻塞

                        serverSocketChannel.configureBlocking(false);

                        while(true) {

                                SocketChannel socketChannel = serverSocketChannel.accept();

                                if(socketChannel==null) {

                                        //表示沒人連接配接

                                        System.out.println("正在等待用戶端請求連接配接...");

                                        Thread.sleep(5000);

                                }else{

                                        System.out.println("目前接收到用戶端請求連接配接...");

                                }

                                if(socketChannel!=null) {

                    //設定為非阻塞

                                        socketChannel.configureBlocking(false);

                                        byteBuffer.flip();//切換模式  寫-->讀

                                        int effective = socketChannel.read(byteBuffer);

                                        if(effective!=0) {

                                                String content = Charset.forName("utf-8").decode(byteBuffer).toString();

                                                System.out.println(content);

                                        }else{

                                                System.out.println("目前未收到用戶端消息");

                                        }

                                }

                        }

                } catch(IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                }

        }

}

運作結果: 

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

代碼解析:

不難看出,在這種解決方案下,雖然在接收用戶端消息時不會阻塞,但是又開始重新接收伺服器請求,使用者根本來不及輸入消息,伺服器就轉向接收别的用戶端請求了,換言之,伺服器弄丢了目前用戶端的請求。

【解決方案二(緩存Socket,輪詢資料是否準備好)】:

public class Server {

        public static void main(String[] args) throws InterruptedException {

                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                List<SocketChannel> socketList = newArrayList<SocketChannel>();

                try{

                        //Java為非阻塞設定的類

                        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

                        serverSocketChannel.bind(newInetSocketAddress(8080));

                        //設定為非阻塞

                        serverSocketChannel.configureBlocking(false);

                        while(true) {

                                SocketChannel socketChannel = serverSocketChannel.accept();

                                if(socketChannel==null) {

                                        //表示沒人連接配接

                                        System.out.println("正在等待用戶端請求連接配接...");

                                        Thread.sleep(5000);

                                }else{

                                        System.out.println("目前接收到用戶端請求連接配接...");

                                        socketList.add(socketChannel);

                                }

                                for(SocketChannel socket:socketList) {

                                        socket.configureBlocking(false);

                                        int effective = socket.read(byteBuffer);

                                        if(effective!=0) {

                                                byteBuffer.flip();//切換模式  寫-->讀

                                                String content = Charset.forName("UTF-8").decode(byteBuffer).toString();

                                                System.out.println("接收到消息:"+content);

                                                byteBuffer.clear();

                                        }else{

                                                System.out.println("目前未收到用戶端消息");

                                        }

                                }

                        }

                } catch(IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                }

        }

}

運作結果:

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料
Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

代碼解析:

在解決方案一中,我們采用了非阻塞方式,但是發現一旦非阻塞,等待用戶端發送消息時就不會再阻塞了,而是直接重新去擷取新用戶端的連接配接請求,這就會造成用戶端連接配接丢失。

而在解決方案二中,我們将連接配接存儲在一個list集合中,每次等待用戶端消息時都去輪詢,看看消息是否準備好,如果準備好則直接列印消息。

可以看到,從頭到尾我們一直沒有開啟第二個線程,而是一直采用單線程來處理多個用戶端的連接配接,這樣的一個模式可以很完美地解決BIO在單線程模式下無法處理多用戶端請求的問題,并且解決了非阻塞狀态下連接配接丢失的問題。

7.3 存在的問題(解決方案二)

從剛才的運作結果中其實可以看出,消息沒有丢失,程式也沒有阻塞。

但是,在接收消息的方式上可能有些許不妥,我們采用了一個輪詢的方式來接收消息,每次都輪詢所有的連接配接,看消息是否準備好,測試用例中隻是三個連接配接,是以看不出什麼問題來,但是我們假設有1000萬連接配接,甚至更多,采用這種輪詢的方式效率是極低的。

另外,1000萬連接配接中,我們可能隻會有100萬會有消息,剩下的900萬并不會發送任何消息,那麼這些連接配接程式依舊要每次都去輪詢,這顯然是不合适的。

7.4 真實NIO中如何解決

在真實NIO中,并不會在Java層上來進行一個輪詢,而是将輪詢的這個步驟交給我們的作業系統來進行,他将輪詢的那部分代碼改為作業系統級别的系統調用(select函數,在linux環境中為epoll),在作業系統級别上調用select函數,主動地去感覺有資料的socket。

這方面的知識,建議詳讀以下文章:

《高性能網絡程式設計(五):一文讀懂高性能網絡程式設計中的I/O模型》

《高性能網絡程式設計(六):一文讀懂高性能網絡程式設計中的線程模型》

8、關于使用select/epoll和直接在應用層做輪詢的差別

我們在之前實作了一個使用Java做多個用戶端連接配接輪詢的邏輯,但是在真正的NIO源碼中其實并不是這麼實作的,NIO使用了作業系統底層的輪詢系統調用 select/epoll(windows:select,linux:epoll),那麼為什麼不直接實作而要去調用系統來做輪詢呢?

8.1 select底層邏輯

Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!1、引言2、關于作者3、相關文章4、先用經典的BIO來實作一個簡易的單線程網絡通信程式5、對用戶端功能進行擴充6、BIO7、NIO8、關于使用select/epoll和直接在應用層做輪詢的差別9、Java中BIO和NIO的概念總結10、本文小結附錄:更多NIO、網絡程式設計方面的資料

假設有A、B、C、D、E五個連接配接同時連接配接伺服器,那麼根據我們上文中的設計,程式将會周遊這五個連接配接,輪詢每個連接配接,擷取各自資料準備情況,那麼和我們自己寫的程式有什麼差別呢?

首先:我們寫的Java程式其本質在輪詢每個Socket的時候也需要去調用系統函數,那麼輪詢一次調用一次,會造成不必要的上下文切換開銷。

而:Select會将五個請求從使用者态空間全量複制一份到核心态空間,在核心态空間來判斷每個請求是否準備好資料,完全避免頻繁的上下文切換。是以效率是比我們直接在應用層寫輪詢要高的。

如果:select沒有查詢到到有資料的請求,那麼将會一直阻塞(是的,select是一個阻塞函數)。如果有一個或者多個請求已經準備好資料了,那麼select将會先将有資料的檔案描述符置位,然後select傳回。傳回後通過周遊檢視哪個請求有資料。

select的缺點:

1)底層存儲依賴bitmap,處理的請求是有上限的,為1024;

2)檔案描述符是會置位的,是以如果當被置位的檔案描述符需要重新使用時,是需要重新賦空值的;

3)fd(檔案描述符)從使用者态拷貝到核心态仍然有一筆開銷;

4)select傳回後還要再次周遊,來獲知是哪一個請求有資料。

8.2 poll函數底層邏輯

poll的工作原理和select很像,先來看一段poll内部使用的一個結構體。

struct pollfd{

    int fd;

    short events;

    short revents;

}

poll同樣會将所有的請求拷貝到核心态,和select一樣,poll同樣是一個阻塞函數,當一個或多個請求有資料的時候,也同樣會進行置位,但是它置位的是結構體pollfd中的events或者revents置位,而不是對fd本身進行置位,是以在下一次使用的時候不需要再進行重新賦空值的操作。poll内部存儲不依賴bitmap,而是使用pollfd數組的這樣一個資料結構,數組的大小肯定是大于1024的。解決了select 1、2兩點的缺點。

8.3 epoll函數底層邏輯

epoll是最新的一種多路IO複用的函數。這裡隻說說它的特點。

epoll和上述兩個函數最大的不同是,它的fd是共享在使用者态和核心态之間的,是以可以不必進行從使用者态到核心态的一個拷貝,這樣可以節約系統資源。

另外,在select和poll中,如果某個請求的資料已經準備好,它們會将所有的請求都傳回,供程式去周遊檢視哪個請求存在資料,但是epoll隻會傳回存在資料的請求,這是因為epoll在發現某個請求存在資料時,首先會進行一個重排操作,将所有有資料的fd放到最前面的位置,然後傳回(傳回值為存在資料請求的個數N),那麼我們的上層程式就可以不必将所有請求都輪詢,而是直接周遊epoll傳回的前N個請求,這些請求都是有資料的請求。

以上有關高性能線程、網絡IO模型的知識,可以詳讀以下幾篇:

《高性能網絡程式設計(五):一文讀懂高性能網絡程式設計中的I/O模型》

《高性能網絡程式設計(六):一文讀懂高性能網絡程式設計中的線程模型》

9、Java中BIO和NIO的概念總結

通常一些文章都是在開頭放上概念,但是我這次選擇将概念放在結尾,因為通過上面的實操,相信大家對Java中BIO和NIO都有了自己的一些了解,這時候再來看概念應該會更好了解一些了。

先來個例子了解一下概念,以銀行取款為例:

1)同步 : 自己親自出馬持銀行卡到銀行取錢(使用同步IO時,Java自己處理IO讀寫);

3)異步 : 委托一小弟拿銀行卡到銀行取錢,然後給你(使用異步IO時,Java将IO讀寫委托給OS處理,需要将資料緩沖區位址和大小傳給OS(銀行卡和密碼),OS需要支援異步IO操作API);

3)阻塞 : ATM排隊取款,你隻能等待(使用阻塞IO時,Java調用會一直阻塞到讀寫完成才傳回);

4)非阻塞 : 櫃台取款,取個号,然後坐在椅子上做其它事,等号廣播會通知你辦理,沒到号你就不能去,你可以不斷問大堂經理排到了沒有,大堂經理如果說還沒到你就不能去(使用非阻塞IO時,如果不能讀寫Java調用會馬上傳回,當IO事件分發器會通知可讀寫時再繼續進行讀寫,不斷循環直到讀寫完成)。

Java對BIO、NIO的支援:

1)Java BIO (blocking I/O):同步并阻塞,伺服器實作模式為一個連接配接一個線程,即用戶端有連接配接請求時伺服器端就需要啟動一個線程進行處理,如果這個連接配接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善;

2)Java NIO (non-blocking I/O): 同步非阻塞,伺服器實作模式為一個請求一個線程,即用戶端發送的連接配接請求都會注冊到多路複用器上,多路複用器輪詢到連接配接有I/O請求時才啟動一個線程進行處理。

BIO、NIO适用場景分析:

1)BIO方式: 适用于連接配接數目比較小且固定的架構,這種方式對伺服器資源要求比較高,并發局限于應用中,JDK1.4以前的唯一選擇,但程式直覺簡單易了解;

2)NIO方式: 适用于連接配接數目多且連接配接比較短(輕操作)的架構,比如聊天伺服器,并發局限于應用中,程式設計比較複雜,JDK1.4開始支援。

10、本文小結

本文介紹了一些關于JavaBIO和NIO從自己實操的角度上的一些了解,我個人認為這樣去了解BIO和NIO會比光看概念會有更深的了解,也希望各位同學可以自己去敲一遍,通過程式的運作結果得出自己對JavaBIO和NIO的了解。

附錄:更多NIO、網絡程式設計方面的資料

[1] NIO異步網絡程式設計資料:

《Java新一代網絡程式設計模型AIO原理及Linux系統AIO介紹》

《有關“為何選擇Netty”的11個疑問及解答》

《開源NIO架構八卦——到底是先有MINA還是先有Netty?》

《選Netty還是Mina:深入研究與對比(一)》

《選Netty還是Mina:深入研究與對比(二)》

《NIO架構入門(一):服務端基于Netty4的UDP雙向通信Demo示範》

《NIO架構入門(二):服務端基于MINA2的UDP雙向通信Demo示範》

《NIO架構入門(三):iOS與MINA2、Netty4的跨平台UDP雙向通信實戰》

《NIO架構入門(四):Android與MINA2、Netty4的跨平台UDP雙向通信實戰》

《Netty 4.x學習(一):ByteBuf詳解》

《Netty 4.x學習(二):Channel和Pipeline詳解》

《Netty 4.x學習(三):線程模型詳解》

《Apache Mina架構進階篇(一):IoFilter詳解》

《Apache Mina架構進階篇(二):IoHandler詳解》

《MINA2 線程原理總結(含簡單測試執行個體)》

《Apache MINA2.0 開發指南(中文版)[附件下載下傳]》

《MINA、Netty的源代碼(線上閱讀版)已整理釋出》

《解決MINA資料傳輸中TCP的粘包、缺包問題(有源碼)》

《解決Mina中多個同類型Filter執行個體共存的問題》

《實踐總結:Netty3.x更新Netty4.x遇到的那些坑(線程篇)》

《實踐總結:Netty3.x VS Netty4.x的線程模型》

《詳解Netty的安全性:原理介紹、代碼示範(上篇)》

《詳解Netty的安全性:原理介紹、代碼示範(下篇)》

《詳解Netty的優雅退出機制和原理》

《NIO架構詳解:Netty的高性能之道》

《Twitter:如何使用Netty 4來減少JVM的GC開銷(譯文)》

《絕對幹貨:基于Netty實作海量接入的推送服務技術要點》

《Netty幹貨分享:京東京麥的生産級TCP網關技術實踐總結》

《新手入門:目前為止最透徹的的Netty高性能原理和架構架構解析》

《寫給初學者:Java高性能NIO架構Netty的學習方法和進階政策》

《少啰嗦!一分鐘帶你讀懂Java的NIO和經典IO的差別》

《史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!》

《手把手教你用Netty實作網絡通信程式的心跳機制、斷線重連機制》

《Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!》

>> 更多同類文章 ……

[2] 網絡程式設計基礎資料:

《TCP/IP詳解 - 第11章·UDP:使用者資料報協定》

《TCP/IP詳解 - 第17章·TCP:傳輸控制協定》

《TCP/IP詳解 - 第18章·TCP連接配接的建立與終止》

《TCP/IP詳解 - 第21章·TCP的逾時與重傳》

《技術往事:改變世界的TCP/IP協定(珍貴多圖、手機慎點)》

《通俗易懂-深入了解TCP協定(上):理論基礎》

《通俗易懂-深入了解TCP協定(下):RTT、滑動視窗、擁塞處理》

《理論經典:TCP協定的3次握手與4次揮手過程詳解》

《理論聯系實際:Wireshark抓包分析TCP 3次握手、4次揮手過程》

《計算機網絡通訊協定關系圖(中文珍藏版)》

《UDP中一個包的大小最大能多大?》

《P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介》

《P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解》

《P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解》

《通俗易懂:快速了解P2P技術中的NAT穿透原理》

《高性能網絡程式設計(一):單台伺服器并發TCP連接配接數到底可以有多少》

《高性能網絡程式設計(二):上一個10年,著名的C10K并發連接配接問題》

《高性能網絡程式設計(三):下一個10年,是時候考慮C10M并發問題了》

《高性能網絡程式設計(四):從C10K到C10M高性能網絡應用的理論探索》

《高性能網絡程式設計(五):一文讀懂高性能網絡程式設計中的I/O模型》

《高性能網絡程式設計(六):一文讀懂高性能網絡程式設計中的線程模型》

《Java的BIO和NIO很難懂?跟着代碼示例,重新了解它們!》

《不為人知的網絡程式設計(一):淺析TCP協定中的疑難雜症(上篇)》

《不為人知的網絡程式設計(二):淺析TCP協定中的疑難雜症(下篇)》

《不為人知的網絡程式設計(三):關閉TCP連接配接時為什麼會TIME_WAIT、CLOSE_WAIT》

《不為人知的網絡程式設計(四):深入研究分析TCP的異常關閉》

《不為人知的網絡程式設計(五):UDP的連接配接性和負載均衡》

《不為人知的網絡程式設計(六):深入地了解UDP協定并用好它》

《不為人知的網絡程式設計(七):如何讓不可靠的UDP變的可靠?》

《不為人知的網絡程式設計(八):從資料傳輸層深度解密HTTP》

《不為人知的網絡程式設計(九):理論聯系實際,全方位深入了解DNS》

《網絡程式設計懶人入門(一):快速了解網絡通信協定(上篇)》

《網絡程式設計懶人入門(二):快速了解網絡通信協定(下篇)》

《網絡程式設計懶人入門(三):快速了解TCP協定一篇就夠》

《網絡程式設計懶人入門(四):快速了解TCP和UDP的差異》

《網絡程式設計懶人入門(五):快速了解為什麼說UDP有時比TCP更有優勢》

《網絡程式設計懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門》

《網絡程式設計懶人入門(七):深入淺出,全面了解HTTP協定》

《網絡程式設計懶人入門(八):手把手教你寫基于TCP的Socket長連接配接》

《網絡程式設計懶人入門(九):通俗講解,有了IP位址,為何還要用MAC位址?》

《網絡程式設計懶人入門(十):一泡尿的時間,快速讀懂QUIC協定》

《技術掃盲:新一代基于UDP的低延時網絡傳輸層協定——QUIC詳解》

《讓網際網路更快:新一代QUIC協定在騰訊的技術實踐分享》

《現代移動端網絡短連接配接的優化手段總結:請求速度、弱網适應、安全保障》

《聊聊iOS中網絡程式設計長連接配接的那些事》

《移動端IM開發者必讀(一):通俗易懂,了解移動網絡的“弱”和“慢”》

《移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結》

《IPv6技術詳解:基本概念、應用現狀、技術實踐(上篇)》

《IPv6技術詳解:基本概念、應用現狀、技術實踐(下篇)》

《從HTTP/0.9到HTTP/2:一文讀懂HTTP協定的曆史演變和設計思路》

《腦殘式網絡程式設計入門(一):跟着動畫來學TCP三向交握和四次揮手》

《腦殘式網絡程式設計入門(二):我們在讀寫Socket時,究竟在讀寫什麼?》

《腦殘式網絡程式設計入門(三):HTTP協定必知必會的一些知識》

《腦殘式網絡程式設計入門(四):快速了解HTTP/2的伺服器推送(Server Push)》

《腦殘式網絡程式設計入門(五):每天都在用的Ping指令,它到底是什麼?》

《腦殘式網絡程式設計入門(六):什麼是公網IP和内網IP?NAT轉換又是什麼鬼?》

《以網遊服務端的網絡接入層設計為例,了解實時通信的技術挑戰》

《邁向高階:優秀Android程式員必知必會的網絡基礎》

《全面了解移動端DNS域名劫持等雜症:技術原理、問題根源、解決方案等》

《美圖App的移動端DNS優化實踐:HTTPS請求耗時減小近半》

《Android程式員必知必會的網絡通信傳輸層協定——UDP和TCP》

《IM開發者的零基礎通信技術入門(一):通信交換技術的百年發展史(上)》

《IM開發者的零基礎通信技術入門(二):通信交換技術的百年發展史(下)》

《IM開發者的零基礎通信技術入門(三):國人通信方式的百年變遷》

《IM開發者的零基礎通信技術入門(四):手機的演進,史上最全移動終端發展史》

《IM開發者的零基礎通信技術入門(五):1G到5G,30年移動通信技術演進史》

《IM開發者的零基礎通信技術入門(六):移動終端的接頭人——“基站”技術》

《IM開發者的零基礎通信技術入門(七):移動終端的千裡馬——“電磁波”》

《IM開發者的零基礎通信技術入門(八):零基礎,史上最強“天線”原理掃盲》

《IM開發者的零基礎通信技術入門(九):無線通信網絡的中樞——“核心網”》

《IM開發者的零基礎通信技術入門(十):零基礎,史上最強5G技術掃盲》

《IM開發者的零基礎通信技術入門(十一):為什麼WiFi信号差?一文即懂!》

《IM開發者的零基礎通信技術入門(十二):上網卡頓?網絡掉線?一文即懂!》

《IM開發者的零基礎通信技術入門(十三):為什麼手機信号差?一文即懂!》

《IM開發者的零基礎通信技術入門(十四):高鐵上無線上網有多難?一文即懂!》

《IM開發者的零基礎通信技術入門(十五):了解定位技術,一篇就夠》

《百度APP移動端網絡深度優化實踐分享(一):DNS優化篇》

《百度APP移動端網絡深度優化實踐分享(二):網絡連接配接優化篇》

《百度APP移動端網絡深度優化實踐分享(三):移動端弱網優化篇》

《技術大牛陳碩的分享:由淺入深,網絡程式設計學習經驗幹貨總結》

《可能會搞砸你的面試:你知道一個TCP連接配接上能發起多少個HTTP請求嗎?》

《知乎技術分享:知乎千萬級并發的高性能長連接配接網關技術實踐》

>> 更多同類文章 ……

(本文同步釋出于:http://www.52im.net/thread-2846-1-1.html)

繼續閱讀