從I/O多路復用到Netty,還要跨過Java NIO包
上一篇文章我們深入了解了I/O多路復用的三種實現形式,select/poll/epoll。
那Netty是使用哪種實現的I/O多路復用呢?這個問題,得從Java NIO包說起。
Netty實際上也是一個封裝好的框架,它的網絡I/O本質上還是使用了Java的NIO包(New IO,不是網絡I/O模型的NIO,Nonblocking IO)包。所以,從網絡I/O模型到Netty,我們還需要了解下Java NIO包。
本文預計閱讀時間 5 分鐘,將重點回答以下幾個問題:
- 如何用Java NIO包實現一個服務端
- Java NIO包如何實現I/O多路復用模型
- 有了Java NIO包,為什么還要封裝一個Netty?
1.先來看一個Java NIO服務端的例子
上一篇文章我們已經了解了I/O多路復用的實現形式。
就是多個的進程的IO可以注冊到一個復用器(selector)上,然后用一個進程調用select,select會監聽所有注冊進來的IO。
NIO包做了對應的實現。如下圖所示。

有一個統一的selector負責監聽所有的Channel。這些channel中只要有一個有IO動作,就可以通過Selector.select()方法檢測到,并且使用selectedKeys得到這些有IO的channel,然后對它們調用相應的IO操作。
我們來個簡單的demo做一下演示。如何使用NIO中三個核心組件(Buffer緩沖區、Channel通道、Selector選擇器)來編寫一個服務端程序。
- public class NioDemo {
- public static void main(String[] args) {
- try {
- //1.創建channel
- ServerSocketChannel socketChannel1 = ServerSocketChannel.open();
- //設置為非阻塞模式,默認是阻塞的
- socketChannel1.configureBlocking(false);
- socketChannel1.socket().bind(new InetSocketAddress("127.0.0.1", 8811));
- ServerSocketChannel socketChannel2 = ServerSocketChannel.open();
- socketChannel2.configureBlocking(false);
- socketChannel2.socket().bind(new InetSocketAddress("127.0.0.1", 8822));
- //2.創建selector,并將channel1和channel2進行注冊。
- Selector selector = Selector.open();
- socketChannel1.register(selector, SelectionKey.OP_ACCEPT);
- socketChannel2.register(selector, SelectionKey.OP_ACCEPT);
- while (true) {
- //3.一直阻塞直到有至少有一個通道準備就緒
- int readChannelCount = selector.select();
- Set<SelectionKey> selectionKeys = selector.selectedKeys();
- Iterator<SelectionKey> iterator = selectionKeys.iterator();
- //4.輪訓已經就緒的通道
- while (iterator.hasNext()) {
- SelectionKey key = iterator.next();
- iterator.remove();
- //5.判斷準備就緒的事件類型,并作相應處理
- if (key.isAcceptable()) {
- // 創建新的連接,并且把連接注冊到selector上,并且聲明這個channel只對讀操作感興趣。
- ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
- SocketChannel socketChannel = serverSocketChannel.accept();
- socketChannel.configureBlocking(false);
- socketChannel.register(selector, SelectionKey.OP_READ);
- }
- if (key.isReadable()) {
- SocketChannel socketChannel = (SocketChannel) key.channel();
- ByteBuffer readBuff = ByteBuffer.allocate(1024);
- socketChannel.read(readBuff);
- readBuff.flip();
- System.out.println("received : " + new String(readBuff.array()));
- socketChannel.close();
- }
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
通過這個代碼示例,我們能清楚地了解如何用Java NIO包實現一個服務端:
- 1)創建channel1和channel2,分別監聽特定端口。
- 2)創建selector,并將channel1和channel2進行注冊。
- 3)selector.select()一直阻塞,直到有至少有一個通道準備就緒。
- 4)輪訓已經就緒的通道
- 5)并根據事件類型做出相應的響應動作。程序啟動后,會一直阻塞在selector.select()。
通過瀏覽器調用localhost:8811 或者 localhost:8822就能觸發我們的服務端代碼了。
2.Java NIO包如何實現I/O多路復用模型
上文演示的Java NIO服務端已經比較清楚地展示了使用NIO編寫服務端程序的過程。
那這個過程中如何實現了I/O多路復用的呢?
我們得深入看下selector的實現。
- //2.創建selector,并將channel1和channel2進行注冊。
- Selector selector = Selector.open();
從open這里開始吧。

這里用了一個SelectorProvider來創建selector。
進入SelectorProvider.provider(),看到具體的provider是由
sun.nio.ch.DefaultSelectorProvider創建的,對應的方法是:

咦?原來不同的操作系統會提供不同的provider對象。這里包括了PollSelectorProvider、EPollSelectorProvide等。
名字是不是有點眼熟?
沒錯,跟我們上一篇文章分析過的I/O多路復用的不同實現方式poll/epoll有關。
我們選擇默認的
sun.nio.ch.PollSelectorProvider往下看看。

OK,找到了實現類PollSelectorImpl。
然后,通過以下調用:

找到最終的native方法poll0。

是不是仍然很眼熟?
沒錯!跟我們上一篇文章分析過的poll函數是一致的。
- int poll (struct pollfd *fds, unsigned int nfds, int timeout);
繞了這么久,到最后,還是找到了我們聊過I/O多路復用的 poll 實現。
至此,我們終于把Java NIO和 I/O多路復用模型串聯起來了。
Java NIO包使用selector,實現了I/O多路復用模型。
同時,在不同的操作系統中,會有不同的poll/epoll選擇。
3.為什么還需要Netty呢?
那既然已經有了NIO包了,我們可以自己手動編寫服務框架了,為什么還需要封裝一個Netty框架呢?有什么好處呢?
好處當然是有很多了!我們從一開始實現的demo說起。
3.1 設計模式的優化
我們的demo確實已經能夠工作了,但是還是有比較明顯的問題。第4步(輪詢已經就緒的通道)和第5步(對事件作相應處理)是在同一個線程中的,當事件處理比較耗時甚至阻塞時,整個流程就會阻塞了。
我們使用的實際上就是 “單Reactor單線程” 設計模式。

這種模型在Reactor中負責監聽端口、接收請求,如果是連接事件交給acceptor處理,如果是讀寫事件和業務處理就交給handler處理,但始終只有一個線程執行所有的事情。
為了提高性能,我們理所當然相當可以把事件處理交給線程池,那就可以演進為 “單Reactor多線程” 設計模式。

這種模型和第一種模型的主要區別是把業務處理從之前的單一線程脫離出來,換成線程池處理。Reactor線程只處理連接事件、讀寫事件,所有業務處理都交給線程池,充分利用多核機器的資源,提高性能。
但是這仍然不夠!
我們可以發現,一個Reactor線程承擔了所有的網絡事件,例如監聽和響應,高并發場景下單線程存在性能問題。
為了充分利用多核能力,可以構建兩個 Reactor,主 Reactor 單獨監聽server socket,accept新連接,然后將建立的 SocketChannel 注冊給指定的從 Reactor,從Reactor再執行事件的讀寫、分發,把業務處理就扔給worker線程池完成。這就演進為 ”主從Reactor模式“ 設計模式。

所以,如果有人直接幫我們 封裝好這樣的設計模式 ,是不是太好了?
沒錯,Netty就是這樣的“活雷鋒”!
Netty就使用了主從Reactor模式封裝了Java NIO包的使用,大大提高了性能。
3.2 其他優點 (以后的核心知識點)
除了封裝了高性能的設計模式外,Netty還有許多其他優點:
穩定性。 Netty 更加可靠穩定,修復和完善了 JDK NIO 較多已知問題,包括 select 空轉導致 CPU 消耗 100%、keep-alive 檢測等問題。
性能優化。對象池復用技術。Netty 通過復用對象,避免頻繁創建和銷毀帶來的開銷。零拷貝技術。 除了操作系統級別的零拷貝技術外,Netty 提供了面向用戶態的零拷貝技術,在 I/O 讀寫時直接使用 DirectBuffer,避免了數據在堆內存和堆外內存之間的拷貝。
便捷性。 Netty 提供了很多常用的工具,例如行解碼器、長度域解碼器等。如果我們使用JDK NIO包,那么這些常用工具都需要自己進行實現。
正是因為 Netty 做到了高性能、高穩定性、高易用性,完美彌補了 Java NIO 的不足,所以在我們在網絡編程時,首選Netty,而不是自己直接使用Java NIO。
回顧一下前幾章內容,到目前為止,我們從網絡I/O模型出發,一步步了解到了Netty的網絡I/O模型。
對于I/O多路復用、Java NIO包 和 Netty 的關系也有了全面的認識。
有了這些知識基礎,我們初步了解了Netty是什么,為什么使用Netty。
后面的文章,我們將逐步展開Netty框架的核心知識點,敬請期待。