從I/O多路復用到Netty,還要跨過Java NIO包
本文是Netty系列第4篇
上一篇文章我們深入了解了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框架的核心知識點,敬請期待。
都看到最後了,原創不易,點個關注,點個贊吧~
文章持續更新,可以微信搜索「阿丸筆記 」第一時間閱讀,回復【筆記】獲取Canal、MySQL、HBase、JAVA實戰筆記,回復【資料】獲取一線大廠面試資料。
知識碎片重新梳理,構建Java知識圖譜:github.com/saigu/JavaK…(歷史文章查閱非常方便)
