從I/O多路復(fù)用到Netty,還要跨過(guò)Java NIO包
上一篇文章我們深入了解了I/O多路復(fù)用的三種實(shí)現(xiàn)形式,select/poll/epoll。
那Netty是使用哪種實(shí)現(xiàn)的I/O多路復(fù)用呢?這個(gè)問(wèn)題,得從Java NIO包說(shuō)起。
Netty實(shí)際上也是一個(gè)封裝好的框架,它的網(wǎng)絡(luò)I/O本質(zhì)上還是使用了Java的NIO包(New IO,不是網(wǎng)絡(luò)I/O模型的NIO,Nonblocking IO)包。所以,從網(wǎng)絡(luò)I/O模型到Netty,我們還需要了解下Java NIO包。
本文預(yù)計(jì)閱讀時(shí)間 5 分鐘,將重點(diǎn)回答以下幾個(gè)問(wèn)題:
- 如何用Java NIO包實(shí)現(xiàn)一個(gè)服務(wù)端
- Java NIO包如何實(shí)現(xiàn)I/O多路復(fù)用模型
- 有了Java NIO包,為什么還要封裝一個(gè)Netty?
1.先來(lái)看一個(gè)Java NIO服務(wù)端的例子
上一篇文章我們已經(jīng)了解了I/O多路復(fù)用的實(shí)現(xiàn)形式。
就是多個(gè)的進(jìn)程的IO可以注冊(cè)到一個(gè)復(fù)用器(selector)上,然后用一個(gè)進(jìn)程調(diào)用select,select會(huì)監(jiān)聽(tīng)所有注冊(cè)進(jìn)來(lái)的IO。
NIO包做了對(duì)應(yīng)的實(shí)現(xiàn)。如下圖所示。

有一個(gè)統(tǒng)一的selector負(fù)責(zé)監(jiān)聽(tīng)所有的Channel。這些channel中只要有一個(gè)有IO動(dòng)作,就可以通過(guò)Selector.select()方法檢測(cè)到,并且使用selectedKeys得到這些有IO的channel,然后對(duì)它們調(diào)用相應(yīng)的IO操作。
我們來(lái)個(gè)簡(jiǎn)單的demo做一下演示。如何使用NIO中三個(gè)核心組件(Buffer緩沖區(qū)、Channel通道、Selector選擇器)來(lái)編寫(xiě)一個(gè)服務(wù)端程序。
- public class NioDemo {
- public static void main(String[] args) {
- try {
- //1.創(chuàng)建channel
- ServerSocketChannel socketChannel1 = ServerSocketChannel.open();
- //設(shè)置為非阻塞模式,默認(rèn)是阻塞的
- 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.創(chuàng)建selector,并將channel1和channel2進(jìn)行注冊(cè)。
- Selector selector = Selector.open();
- socketChannel1.register(selector, SelectionKey.OP_ACCEPT);
- socketChannel2.register(selector, SelectionKey.OP_ACCEPT);
- while (true) {
- //3.一直阻塞直到有至少有一個(gè)通道準(zhǔn)備就緒
- int readChannelCount = selector.select();
- Set<SelectionKey> selectionKeys = selector.selectedKeys();
- Iterator<SelectionKey> iterator = selectionKeys.iterator();
- //4.輪訓(xùn)已經(jīng)就緒的通道
- while (iterator.hasNext()) {
- SelectionKey key = iterator.next();
- iterator.remove();
- //5.判斷準(zhǔn)備就緒的事件類(lèi)型,并作相應(yīng)處理
- if (key.isAcceptable()) {
- // 創(chuàng)建新的連接,并且把連接注冊(cè)到selector上,并且聲明這個(gè)channel只對(duì)讀操作感興趣。
- 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();
- }
- }
- }
通過(guò)這個(gè)代碼示例,我們能清楚地了解如何用Java NIO包實(shí)現(xiàn)一個(gè)服務(wù)端:
- 1)創(chuàng)建channel1和channel2,分別監(jiān)聽(tīng)特定端口。
- 2)創(chuàng)建selector,并將channel1和channel2進(jìn)行注冊(cè)。
- 3)selector.select()一直阻塞,直到有至少有一個(gè)通道準(zhǔn)備就緒。
- 4)輪訓(xùn)已經(jīng)就緒的通道
- 5)并根據(jù)事件類(lèi)型做出相應(yīng)的響應(yīng)動(dòng)作。程序啟動(dòng)后,會(huì)一直阻塞在selector.select()。
通過(guò)瀏覽器調(diào)用localhost:8811 或者 localhost:8822就能觸發(fā)我們的服務(wù)端代碼了。
2.Java NIO包如何實(shí)現(xiàn)I/O多路復(fù)用模型
上文演示的Java NIO服務(wù)端已經(jīng)比較清楚地展示了使用NIO編寫(xiě)服務(wù)端程序的過(guò)程。
那這個(gè)過(guò)程中如何實(shí)現(xiàn)了I/O多路復(fù)用的呢?
我們得深入看下selector的實(shí)現(xiàn)。
- //2.創(chuàng)建selector,并將channel1和channel2進(jìn)行注冊(cè)。
- Selector selector = Selector.open();
從open這里開(kāi)始吧。

這里用了一個(gè)SelectorProvider來(lái)創(chuàng)建selector。
進(jìn)入SelectorProvider.provider(),看到具體的provider是由
sun.nio.ch.DefaultSelectorProvider創(chuàng)建的,對(duì)應(yīng)的方法是:

咦?原來(lái)不同的操作系統(tǒng)會(huì)提供不同的provider對(duì)象。這里包括了PollSelectorProvider、EPollSelectorProvide等。
名字是不是有點(diǎn)眼熟?
沒(méi)錯(cuò),跟我們上一篇文章分析過(guò)的I/O多路復(fù)用的不同實(shí)現(xiàn)方式poll/epoll有關(guān)。
我們選擇默認(rèn)的
sun.nio.ch.PollSelectorProvider往下看看。

OK,找到了實(shí)現(xiàn)類(lèi)PollSelectorImpl。
然后,通過(guò)以下調(diào)用:

找到最終的native方法poll0。

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

這種模型在Reactor中負(fù)責(zé)監(jiān)聽(tīng)端口、接收請(qǐng)求,如果是連接事件交給acceptor處理,如果是讀寫(xiě)事件和業(yè)務(wù)處理就交給handler處理,但始終只有一個(gè)線程執(zhí)行所有的事情。
為了提高性能,我們理所當(dāng)然相當(dāng)可以把事件處理交給線程池,那就可以演進(jìn)為 “單Reactor多線程” 設(shè)計(jì)模式。

這種模型和第一種模型的主要區(qū)別是把業(yè)務(wù)處理從之前的單一線程脫離出來(lái),換成線程池處理。Reactor線程只處理連接事件、讀寫(xiě)事件,所有業(yè)務(wù)處理都交給線程池,充分利用多核機(jī)器的資源,提高性能。
但是這仍然不夠!
我們可以發(fā)現(xiàn),一個(gè)Reactor線程承擔(dān)了所有的網(wǎng)絡(luò)事件,例如監(jiān)聽(tīng)和響應(yīng),高并發(fā)場(chǎng)景下單線程存在性能問(wèn)題。
為了充分利用多核能力,可以構(gòu)建兩個(gè) Reactor,主 Reactor 單獨(dú)監(jiān)聽(tīng)server socket,accept新連接,然后將建立的 SocketChannel 注冊(cè)給指定的從 Reactor,從Reactor再執(zhí)行事件的讀寫(xiě)、分發(fā),把業(yè)務(wù)處理就扔給worker線程池完成。這就演進(jìn)為 ”主從Reactor模式“ 設(shè)計(jì)模式。

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