NIO編程

傳統socket的編程實現

瞭解上面的demo存在問題,我們提出瞭解決方案:

NIO編程

關於NIO相關的文章網上也有很多,這裏不打算詳細深入分析,下面簡單描述一下NIO是如何解決以上三個問題的。

線程資源受限

NIO編程模型中,新來一個連接不再創建一個新的線程,而是可以把這條連接直接綁定到某個固定的線程,然後這條連接所有的讀寫都由這個線程來負責,那麼他是怎麼做到的?我們用一幅圖來對比一下IO與NIO

如上圖所示,IO模型中,一個連接來了,會創建一個線程,對應一個while死循環,死循環的目的就是不斷監測這條連接上是否有數據可以讀,大多數情況下,1w個連接裏面同一時刻只有少量的連接有數據可讀,因此,很多個while死循環都白白浪費掉了,因爲讀不出啥數據。

而在NIO模型中,他把這麼多while死循環變成一個死循環,這個死循環由一個線程控制,那麼他又是如何做到一個線程,一個while死循環就能監測1w個連接是否有數據可讀的呢?
這就是NIO模型中selector的作用,一條連接來了之後,現在不創建一個while死循環去監聽是否有數據可讀了,而是直接把這條連接註冊到selector上,然後,通過檢查這個selector,就可以批量監測出有數據可讀的連接,進而讀取數據,下面我再舉個非常簡單的生活中的例子說明IO與NIO的區別。

舉個列子:

在一家幼兒園裏,小朋友有上廁所的需求,小朋友都太小以至於你要問他要不要上廁所,他纔會告訴你。幼兒園一共有100個小朋友,有兩種方案可以解決小朋友上廁所的問題:

  1. 每個小朋友配一個老師。每個老師隔段時間詢問小朋友是否要上廁所,如果要上,就領他去廁所,100個小朋友就需要100個老師來詢問,並且每個小朋友上廁所的時候都需要一個老師領着他去上,這就是IO模型,一個連接對應一個線程。
  2. 所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然後每一時刻把所有要上廁所的小朋友批量領到廁所,這就是NIO模型,所有小朋友都註冊到同一個老師,對應的就是所有的連接都註冊到一個線程,然後批量輪詢。

這就是NIO模型解決線程資源受限的方案,實際開發過程中,我們會開多個線程,每個線程都管理着一批連接,相對於IO模型中一個線程管理一條連接,消耗的線程資源大幅減少。

線程切換效率低下

由於NIO模型中線程數量大大降低,線程切換效率因此也大幅度提高

IO讀寫以字節爲單位

NIO解決這個問題的方式是數據讀寫不再以字節爲單位,而是以字節塊爲單位。IO模型中,每次都是從操作系統底層一個字節一個字節地讀取數據,而NIO維護一個緩衝區,每次可以從這個緩衝區裏面讀取一塊的數據,
這就好比一盤美味的豆子放在你面前,你用筷子一個個夾(每次一個),肯定不如要勺子挖着吃(每次一批)效率來得高。

簡單講完了JDK NIO的解決方案之後,我們接下來使用NIO的方案替換掉IO的方案,我們先來看看,如果用JDK原生的NIO來實現服務端,該怎麼做


NIOServer.java 


/**
 * @author 閃電俠
 */
public class NIOServer {
    public static void main(String[] args) throws IOException {
        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        new Thread(() -> {
            try {
                // 對應IO編程中服務端啓動
                ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                listenerChannel.socket().bind(new InetSocketAddress(8000));
                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) 讀取數據以塊爲單位批量讀取
                                    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();


    }
}

相信大部分沒有接觸過NIO的同學應該會直接跳過代碼來到這一行:原來使用JDK原生NIO的API實現一個簡單的服務端通信程序是如此複雜!

複雜得我都沒耐心解釋這一坨代碼的執行邏輯(開個玩笑),我們還是先對照NIO來解釋一下幾個核心思路

  1. NIO模型中通常會有兩個線程,每個線程綁定一個輪詢器selector,在我們這個例子中serverSelector負責輪詢是否有新的連接,clientSelector負責輪詢連接是否有數據可讀
  2. 服務端監測到新的連接之後,不再創建一個新的線程,而是直接將新連接綁定到clientSelector上,這樣就不用IO模型中1w個while循環在死等,參見(1)
  3. clientSelector被一個while死循環包裹着,如果在某一時刻有多條連接有數據可讀,那麼通過 clientSelector.select(1)方法可以輪詢出來,進而批量處理,參見(2)
  4. 數據的讀寫以內存塊爲單位,參見(3)

其他的細節部分,我不願意多講,因爲實在是太複雜,你也不用對代碼的細節深究到底。總之,強烈不建議直接基於JDK原生NIO來進行網絡開發,下面是我總結的原因

1、JDK的NIO編程需要了解很多的概念,編程複雜,對NIO入門非常不友好,編程模型不友好,ByteBuffer的api簡直反人類
2、對NIO編程來說,一個比較合適的線程模型能充分發揮它的優勢,而JDK沒有給你實現,你需要自己實現,就連簡單的自定義協議拆包都要你自己實現
3、JDK的NIO底層由epoll實現,該實現飽受詬病的空輪訓bug會導致cpu飆升100%
4、項目龐大之後,自行實現的NIO很容易出現各類bug,維護成本較高,上面這一坨代碼我都不能保證沒有bug

正因爲如此,我客戶端代碼都懶得寫給你看了==!,你可以直接使用IOClient.javaNIOServer.java通信

JDK的NIO猶如帶刺的玫瑰,雖然美好,讓人嚮往,但是使用不當會讓你抓耳撓腮,痛不欲生,正因爲如此,Netty橫空出世!

 

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