3萬字加50張圖,帶你深度解析 Netty 架構(gòu)與原理(下)

篇幅限制,上文請見:3萬字加50張圖,帶你深度解析 Netty 架構(gòu)與原理(上)

2. Netty 的架構(gòu)與原理

2.1. 為什么要制造 Netty

既然 Java 提供了 NIO,為什么還要制造一個 Netty,主要原因是 Java NIO 有以下幾個缺點:

1)Java NIO 的類庫和 API 龐大繁雜,使用起來很麻煩,開發(fā)工作量大。

2)使用 Java NIO,程序員需要具備高超的 Java 多線程編碼技能,以及非常熟悉網(wǎng)絡(luò)編程,比如要處理斷連重連、網(wǎng)絡(luò)閃斷、半包讀寫、失敗緩存、網(wǎng)絡(luò)擁塞和異常流處理等一系列棘手的工作。

3)Java NIO 存在 Bug,例如 Epoll Bug 會導致 Selector 空輪訓,極大耗費 CPU 資源。

Netty 對于 JDK 自帶的 NIO 的 API 進行了封裝,解決了上述問題,提高了 IO 程序的開發(fā)效率和可靠性,同時 Netty:

1)設(shè)計優(yōu)雅,提供阻塞和非阻塞的 Socket;提供靈活可拓展的事件模型;提供高度可定制的線程模型。

2)具備更高的性能和更大的吞吐量,使用零拷貝技術(shù)最小化不必要的內(nèi)存復(fù)制,減少資源的消耗。

3)提供安全傳輸特性。

4)支持多種主流協(xié)議;預(yù)置多種編解碼功能,支持用戶開發(fā)私有協(xié)議。

**注:所謂支持 TCP、UDP、HTTP、WebSocket 等協(xié)議,就是說 Netty 提供了相關(guān)的編程類和接口,因此本文后面主要對基于 Netty 的 TCP Server/Client 開發(fā)案例進行講解,以展示 Netty 的核心原理,對于其他協(xié)議 Server/Client 開發(fā)不再給出示例,幫助讀者提升內(nèi)力而非教授花招是我寫作的出發(fā)點 :-) **

下圖為 Netty 官網(wǎng)給出的 Netty 架構(gòu)圖。

我們從其中的幾個關(guān)鍵詞就能看出 Netty 的強大之處:零拷貝、可拓展事件模型;支持 TCP、UDP、HTTP、WebSocket 等協(xié)議;提供安全傳輸、壓縮、大文件傳輸、編解碼支持等等。

2.2. 幾種 Reactor 線程模式

傳統(tǒng)的 BIO 服務(wù)端編程采用“每線程每連接”的處理模型,弊端很明顯,就是面對大量的客戶端并發(fā)連接時,服務(wù)端的資源壓力很大;并且線程的利用率很低,如果當前線程沒有數(shù)據(jù)可讀,它會阻塞在 read 操作上。這個模型的基本形態(tài)如下圖所示(圖片來源于網(wǎng)絡(luò))。

BIO 服務(wù)端編程采用的是 Reactor 模式(也叫做 Dispatcher 模式,分派模式),Reactor 模式有兩個要義:

1)基于 IO 多路復(fù)用技術(shù),多個連接共用一個多路復(fù)用器,應(yīng)用程序的線程無需阻塞等待所有連接,只需阻塞等待多路復(fù)用器即可。當某個連接上有新數(shù)據(jù)可以處理時,應(yīng)用程序的線程從阻塞狀態(tài)返回,開始處理這個連接上的業(yè)務(wù)。

2)基于線程池技術(shù)復(fù)用線程資源,不必為每個連接創(chuàng)建專用的線程,應(yīng)用程序?qū)⑦B接上的業(yè)務(wù)處理任務(wù)分配給線程池中的線程進行處理,一個線程可以處理多個連接的業(yè)務(wù)。

下圖反應(yīng)了 Reactor 模式的基本形態(tài)(圖片來源于網(wǎng)絡(luò)):

Reactor 模式有兩個核心組成部分:

1)Reactor(圖中的 ServiceHandler):Reactor 在一個單獨的線程中運行,負責監(jiān)聽和分發(fā)事件,分發(fā)給適當?shù)奶幚砭€程來對 IO 事件做出反應(yīng)。

2)Handlers(圖中的 EventHandler):處理線程執(zhí)行處理方法來響應(yīng) I/O 事件,處理線程執(zhí)行的是非阻塞操作。

Reactor 模式就是實現(xiàn)網(wǎng)絡(luò) IO 程序高并發(fā)特性的關(guān)鍵。它又可以分為單 Reactor 單線程模式、單 Reactor 多線程模式、主從 Reactor 多線程模式。

2.2.1. 單 Reactor 單線程模式

單 Reactor 單線程模式的基本形態(tài)如下(圖片來源于網(wǎng)絡(luò)):

這種模式的基本工作流程為:

1)Reactor 通過 select 監(jiān)聽客戶端請求事件,收到事件之后通過 dispatch 進行分發(fā)

2)如果事件是建立連接的請求事件,則由 Acceptor 通過 accept 處理連接請求,然后創(chuàng)建一個 Handler 對象處理連接建立后的后續(xù)業(yè)務(wù)處理。

3)如果事件不是建立連接的請求事件,則由 Reactor 對象分發(fā)給連接對應(yīng)的 Handler 處理。

4)Handler 會完成 read-->業(yè)務(wù)處理-->send 的完整處理流程。

這種模式的優(yōu)點是:模型簡單,沒有多線程、進程通信、競爭的問題,一個線程完成所有的事件響應(yīng)和業(yè)務(wù)處理。當然缺點也很明顯:

1)存在性能問題,只有一個線程,無法完全發(fā)揮多核 CPU 的性能。Handler 在處理某個連接上的業(yè)務(wù)時,整個進程無法處理其他連接事件,很容易導致性能瓶頸。

2)存在可靠性問題,若線程意外終止,或者進入死循環(huán),會導致整個系統(tǒng)通信模塊不可用,不能接收和處理外部消息,造成節(jié)點故障。

單 Reactor 單線程模式使用場景為:客戶端的數(shù)量有限,業(yè)務(wù)處理非常快速,比如 Redis 在業(yè)務(wù)處理的時間復(fù)雜度為 O(1)的情況。

2.2.2. 單 Reactor 多線程模式

單 Reactor 單線程模式的基本形態(tài)如下(圖片來源于網(wǎng)絡(luò)):

這種模式的基本工作流程為:

1)Reactor 對象通過 select 監(jiān)聽客戶端請求事件,收到事件后通過 dispatch 進行分發(fā)。

2)如果事件是建立連接的請求事件,則由 Acceptor 通過 accept 處理連接請求,然后創(chuàng)建一個 Handler 對象處理連接建立后的后續(xù)業(yè)務(wù)處理。

3)如果事件不是建立連接的請求事件,則由 Reactor 對象分發(fā)給連接對應(yīng)的 Handler 處理。Handler 只負責響應(yīng)事件,不做具體的業(yè)務(wù)處理,Handler 通過 read 讀取到請求數(shù)據(jù)后,會分發(fā)給后面的 Worker 線程池來處理業(yè)務(wù)請求。

4)Worker 線程池會分配獨立線程來完成真正的業(yè)務(wù)處理,并將處理結(jié)果返回給 Handler。Handler 通過 send 向客戶端發(fā)送響應(yīng)數(shù)據(jù)。

這種模式的優(yōu)點是可以充分的利用多核 cpu 的處理能力,缺點是多線程數(shù)據(jù)共享和控制比較復(fù)雜,Reactor 處理所有的事件的監(jiān)聽和響應(yīng),在單線程中運行,面對高并發(fā)場景還是容易出現(xiàn)性能瓶頸。

2.2.3. 主從 Reactor 多線程模式

主從 Reactor 多線程模式的基本形態(tài)如下(第一章圖片來源于網(wǎng)絡(luò),第二章圖片是 JUC 作者 Doug Lea 老師在《Scalable IO in Java》中給出的示意圖,兩張圖表達的含義一樣):

針對單 Reactor 多線程模型中,Reactor 在單個線程中運行,面對高并發(fā)的場景易成為性能瓶頸的缺陷,主從 Reactor 多線程模式讓 Reactor 在多個線程中運行(分成 MainReactor 線程與 SubReactor 線程)。這種模式的基本工作流程為:

1)Reactor 主線程 MainReactor 對象通過 select 監(jiān)聽客戶端連接事件,收到事件后,通過 Acceptor 處理客戶端連接事件。

2)當 Acceptor 處理完客戶端連接事件之后(與客戶端建立好 Socket 連接),MainReactor 將連接分配給 SubReactor。(即:MainReactor 只負責監(jiān)聽客戶端連接請求,和客戶端建立連接之后將連接交由 SubReactor 監(jiān)聽后面的 IO 事件。)

3)SubReactor 將連接加入到自己的連接隊列進行監(jiān)聽,并創(chuàng)建 Handler 對各種事件進行處理。

4)當連接上有新事件發(fā)生的時候,SubReactor 就會調(diào)用對應(yīng)的 Handler 處理。

5)Handler 通過 read 從連接上讀取請求數(shù)據(jù),將請求數(shù)據(jù)分發(fā)給 Worker 線程池進行業(yè)務(wù)處理。

6)Worker 線程池會分配獨立線程來完成真正的業(yè)務(wù)處理,并將處理結(jié)果返回給 Handler。Handler 通過 send 向客戶端發(fā)送響應(yīng)數(shù)據(jù)。

7)一個 MainReactor 可以對應(yīng)多個 SubReactor,即一個 MainReactor 線程可以對應(yīng)多個 SubReactor 線程。

這種模式的優(yōu)點是:

1)MainReactor 線程與 SubReactor 線程的數(shù)據(jù)交互簡單職責明確,MainReactor 線程只需要接收新連接,SubReactor 線程完成后續(xù)的業(yè)務(wù)處理。

2)MainReactor 線程與 SubReactor 線程的數(shù)據(jù)交互簡單, MainReactor 線程只需要把新連接傳給 SubReactor 線程,SubReactor 線程無需返回數(shù)據(jù)。

3)多個 SubReactor 線程能夠應(yīng)對更高的并發(fā)請求。

這種模式的缺點是編程復(fù)雜度較高。但是由于其優(yōu)點明顯,在許多項目中被廣泛使用,包括 Nginx、Memcached、Netty 等。

這種模式也被叫做服務(wù)器的 1+M+N 線程模式,即使用該模式開發(fā)的服務(wù)器包含一個(或多個,1 只是表示相對較少)連接建立線程+M 個 IO 線程+N 個業(yè)務(wù)處理線程。這是業(yè)界成熟的服務(wù)器程序設(shè)計模式。

2.3. Netty 的模樣

Netty 的設(shè)計主要基于主從 Reactor 多線程模式,并做了一定的改進。本節(jié)將使用一種漸進式的描述方式展示 Netty 的模樣,即先給出 Netty 的簡單版本,然后逐漸豐富其細節(jié),直至展示出 Netty 的全貌。

簡單版本的 Netty 的模樣如下:

關(guān)于這張圖,作以下幾點說明:

1)BossGroup 線程維護 Selector,ServerSocketChannel 注冊到這個 Selector 上,只關(guān)注連接建立請求事件(相當于主 Reactor)。

2)當接收到來自客戶端的連接建立請求事件的時候,通過 ServerSocketChannel.accept 方法獲得對應(yīng)的 SocketChannel,并封裝成 NioSocketChannel 注冊到 WorkerGroup 線程中的 Selector,每個 Selector 運行在一個線程中(相當于從 Reactor)。

3)當 WorkerGroup 線程中的 Selector 監(jiān)聽到自己感興趣的 IO 事件后,就調(diào)用 Handler 進行處理。

我們給這簡單版的 Netty 添加一些細節(jié):

關(guān)于這張圖,作以下幾點說明:

1)有兩組線程池:BossGroup 和 WorkerGroup,BossGroup 中的線程(可以有多個,圖中只畫了一個)專門負責和客戶端建立連接,WorkerGroup 中的線程專門負責處理連接上的讀寫。

2)BossGroup 和 WorkerGroup 含有多個不斷循環(huán)的執(zhí)行事件處理的線程,每個線程都包含一個 Selector,用于監(jiān)聽注冊在其上的 Channel。

3)每個 BossGroup 中的線程循環(huán)執(zhí)行以下三個步驟:

3.1)輪訓注冊在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

3.2)處理 accept 事件,與客戶端建立連接,生成一個 NioSocketChannel,并將其注冊到 WorkerGroup 中某個線程上的 Selector 上

3.3)再去以此循環(huán)處理任務(wù)隊列中的下一個事件

4)每個 WorkerGroup 中的線程循環(huán)執(zhí)行以下三個步驟:

4.1)輪訓注冊在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

4.2)在對應(yīng)的 NioSocketChannel 上處理 read/write 事件

4.3)再去以此循環(huán)處理任務(wù)隊列中的下一個事件

我們再來看下終極版的 Netty 的模樣,如下圖所示(圖片來源于網(wǎng)絡(luò)):

關(guān)于這張圖,作以下幾點說明:

1)Netty 抽象出兩組線程池:BossGroup 和 WorkerGroup,也可以叫做 BossNioEventLoopGroup 和 WorkerNioEventLoopGroup。每個線程池中都有 NioEventLoop 線程。BossGroup 中的線程專門負責和客戶端建立連接,WorkerGroup 中的線程專門負責處理連接上的讀寫。BossGroup 和 WorkerGroup 的類型都是 NioEventLoopGroup。

2)NioEventLoopGroup 相當于一個事件循環(huán)組,這個組中含有多個事件循環(huán),每個事件循環(huán)就是一個 NioEventLoop。

3)NioEventLoop 表示一個不斷循環(huán)的執(zhí)行事件處理的線程,每個 NioEventLoop 都包含一個 Selector,用于監(jiān)聽注冊在其上的 Socket 網(wǎng)絡(luò)連接(Channel)。

4)NioEventLoopGroup 可以含有多個線程,即可以含有多個 NioEventLoop。

5)每個 BossNioEventLoop 中循環(huán)執(zhí)行以下三個步驟:

5.1)select:輪訓注冊在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)

5.2)processSelectedKeys:處理 accept 事件,與客戶端建立連接,生成一個 NioSocketChannel,并將其注冊到某個 WorkerNioEventLoop 上的 Selector 上

5.3)runAllTasks:再去以此循環(huán)處理任務(wù)隊列中的其他任務(wù)

6)每個 WorkerNioEventLoop 中循環(huán)執(zhí)行以下三個步驟:

6.1)select:輪訓注冊在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)

6.2)processSelectedKeys:在對應(yīng)的 NioSocketChannel 上處理 read/write 事件

6.3)runAllTasks:再去以此循環(huán)處理任務(wù)隊列中的其他任務(wù)

7)在以上兩個processSelectedKeys步驟中,會使用 Pipeline(管道),Pipeline 中引用了 Channel,即通過 Pipeline 可以獲取到對應(yīng)的 Channel,Pipeline 中維護了很多的處理器(攔截處理器、過濾處理器、自定義處理器等)。這里暫時不詳細展開講解 Pipeline。

2.4. 基于 Netty 的 TCP Server/Client 案例

下面我們寫點代碼來加深理解 Netty 的模樣。下面兩段代碼分別是基于 Netty 的 TCP Server 和 TCP Client。

服務(wù)端代碼為:

/**
 * 需要的依賴:
 * <dependency>
 * <groupId>io.netty</groupId>
 * <artifactId>netty-all</artifactId>
 * <version>4.1.52.Final</version>
 * </dependency>
 */
public static void main(String[] args) throws InterruptedException {

    // 創(chuàng)建 BossGroup 和 WorkerGroup
    // 1\. bossGroup 只處理連接請求
    // 2\. 業(yè)務(wù)處理由 workerGroup 來完成
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    try {
        // 創(chuàng)建服務(wù)器端的啟動對象
        ServerBootstrap bootstrap = new ServerBootstrap();
        // 配置參數(shù)
        bootstrap
                // 設(shè)置線程組
                .group(bossGroup, workerGroup)
                // 說明服務(wù)器端通道的實現(xiàn)類(便于 Netty 做反射處理)
                .channel(NioServerSocketChannel.class)
                // 設(shè)置等待連接的隊列的容量(當客戶端連接請求速率大
             // 于 NioServerSocketChannel 接收速率的時候,會使用
                // 該隊列做緩沖)
                // option()方法用于給服務(wù)端的 ServerSocketChannel
                // 添加配置
                .option(ChannelOption.SO_BACKLOG, 128)
                // 設(shè)置連接保活
                // childOption()方法用于給服務(wù)端 ServerSocketChannel
                // 接收到的 SocketChannel 添加配置
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                // handler()方法用于給 BossGroup 設(shè)置業(yè)務(wù)處理器
                // childHandler()方法用于給 WorkerGroup 設(shè)置業(yè)務(wù)處理器
                .childHandler(
                        // 創(chuàng)建一個通道初始化對象
                        new ChannelInitializer<SocketChannel>() {
                            // 向 Pipeline 添加業(yè)務(wù)處理器
                            @Override
                            protected void initChannel(
                                    SocketChannel socketChannel
                            ) throws Exception {
                                socketChannel.pipeline().addLast(
                                        new NettyServerHandler()
                                );

                                // 可以繼續(xù)調(diào)用 socketChannel.pipeline().addLast()
                                // 添加更多 Handler
                            }
                        }
                );

        System.out.println("server is ready...");

        // 綁定端口,啟動服務(wù)器,生成一個 channelFuture 對象,
        // ChannelFuture 涉及到 Netty 的異步模型,后面展開講
        ChannelFuture channelFuture = bootstrap.bind(8080).sync();
        // 對通道關(guān)閉進行監(jiān)聽
        channelFuture.channel().closeFuture().sync();
    } finally {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

/**
 * 自定義一個 Handler,需要繼承 Netty 規(guī)定好的某個 HandlerAdapter(規(guī)范)
 * InboundHandler 用于處理數(shù)據(jù)流入本端(服務(wù)端)的 IO 事件
 * InboundHandler 用于處理數(shù)據(jù)流出本端(服務(wù)端)的 IO 事件
 */
static class NettyServerHandler extends ChannelInboundHandlerAdapter {
    /**
     * 當通道有數(shù)據(jù)可讀時執(zhí)行
     *
     * @param ctx 上下文對象,可以從中取得相關(guān)聯(lián)的 Pipeline、Channel、客戶端地址等
     * @param msg 客戶端發(fā)送的數(shù)據(jù)
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        // 接收客戶端發(fā)來的數(shù)據(jù)

        System.out.println("client address: "
                + ctx.channel().remoteAddress());

        // ByteBuf 是 Netty 提供的類,比 NIO 的 ByteBuffer 性能更高
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("data from client: "
                + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 數(shù)據(jù)讀取完畢后執(zhí)行
     *
     * @param ctx 上下文對象
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx)
            throws Exception {
        // 發(fā)送響應(yīng)給客戶端
        ctx.writeAndFlush(
                // Unpooled 類是 Netty 提供的專門操作緩沖區(qū)的工具
                // 類,copiedBuffer 方法返回的 ByteBuf 對象類似于
                // NIO 中的 ByteBuffer,但性能更高
                Unpooled.copiedBuffer(
                        "hello client! i have got your data.",
                        CharsetUtil.UTF_8
                )
        );
    }

    /**
     * 發(fā)生異常時執(zhí)行
     *
     * @param ctx   上下文對象
     * @param cause 異常對象
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        // 關(guān)閉與客戶端的 Socket 連接
        ctx.channel().close();
    }
}

客戶端端代碼為:

/**
 * 需要的依賴:
 * <dependency>
 * <groupId>io.netty</groupId>
 * <artifactId>netty-all</artifactId>
 * <version>4.1.52.Final</version>
 * </dependency>
 */
public static void main(String[] args) throws InterruptedException {

    // 客戶端只需要一個事件循環(huán)組,可以看做 BossGroup
    EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

    try {
        // 創(chuàng)建客戶端的啟動對象
        Bootstrap bootstrap = new Bootstrap();
        // 配置參數(shù)
        bootstrap
                // 設(shè)置線程組
                .group(eventLoopGroup)
                // 說明客戶端通道的實現(xiàn)類(便于 Netty 做反射處理)
                .channel(NioSocketChannel.class)
                // handler()方法用于給 BossGroup 設(shè)置業(yè)務(wù)處理器
                .handler(
                        // 創(chuàng)建一個通道初始化對象
                        new ChannelInitializer<SocketChannel>() {
                            // 向 Pipeline 添加業(yè)務(wù)處理器
                            @Override
                            protected void initChannel(
                                    SocketChannel socketChannel
                            ) throws Exception {
                                socketChannel.pipeline().addLast(
                                        new NettyClientHandler()
                                );

                                // 可以繼續(xù)調(diào)用 socketChannel.pipeline().addLast()
                                // 添加更多 Handler
                            }
                        }
                );

        System.out.println("client is ready...");

        // 啟動客戶端去連接服務(wù)器端,ChannelFuture 涉及到 Netty 的異步模型,后面展開講
        ChannelFuture channelFuture = bootstrap.connect(
                "127.0.0.1",
                8080).sync();
        // 對通道關(guān)閉進行監(jiān)聽
        channelFuture.channel().closeFuture().sync();
    } finally {
        eventLoopGroup.shutdownGracefully();
    }
}

/**
 * 自定義一個 Handler,需要繼承 Netty 規(guī)定好的某個 HandlerAdapter(規(guī)范)
 * InboundHandler 用于處理數(shù)據(jù)流入本端(客戶端)的 IO 事件
 * InboundHandler 用于處理數(shù)據(jù)流出本端(客戶端)的 IO 事件
 */
static class NettyClientHandler extends ChannelInboundHandlerAdapter {
    /**
     * 通道就緒時執(zhí)行
     *
     * @param ctx 上下文對象
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx)
            throws Exception {
        // 向服務(wù)器發(fā)送數(shù)據(jù)
        ctx.writeAndFlush(
                // Unpooled 類是 Netty 提供的專門操作緩沖區(qū)的工具
                // 類,copiedBuffer 方法返回的 ByteBuf 對象類似于
                // NIO 中的 ByteBuffer,但性能更高
                Unpooled.copiedBuffer(
                        "hello server!",
                        CharsetUtil.UTF_8
                )
        );
    }

    /**
     * 當通道有數(shù)據(jù)可讀時執(zhí)行
     *
     * @param ctx 上下文對象
     * @param msg 服務(wù)器端發(fā)送的數(shù)據(jù)
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        // 接收服務(wù)器端發(fā)來的數(shù)據(jù)

        System.out.println("server address: "
                + ctx.channel().remoteAddress());

        // ByteBuf 是 Netty 提供的類,比 NIO 的 ByteBuffer 性能更高
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("data from server: "
                + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 發(fā)生異常時執(zhí)行
     *
     * @param ctx   上下文對象
     * @param cause 異常對象
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        // 關(guān)閉與服務(wù)器端的 Socket 連接
        ctx.channel().close();
    }
}

什么?你覺得使用 Netty 編程難度和工作量更大了?不會吧不會吧,你要知道,你通過這么兩段簡短的代碼得到了一個基于主從 Reactor 多線程模式的服務(wù)器,一個高吞吐量和并發(fā)量的服務(wù)器,一個異步處理服務(wù)器……你還要怎樣?

對上面的兩段代碼,作以下簡單說明:

1)Bootstrap 和 ServerBootstrap 分別是客戶端和服務(wù)器端的引導類,一個 Netty 應(yīng)用程序通常由一個引導類開始,主要是用來配置整個 Netty 程序、設(shè)置業(yè)務(wù)處理類(Handler)、綁定端口、發(fā)起連接等。

2)客戶端創(chuàng)建一個 NioSocketChannel 作為客戶端通道,去連接服務(wù)器。

3)服務(wù)端首先創(chuàng)建一個 NioServerSocketChannel 作為服務(wù)器端通道,每當接收一個客戶端連接就產(chǎn)生一個 NioSocketChannel 應(yīng)對該客戶端。

4)使用 Channel 構(gòu)建網(wǎng)絡(luò) IO 程序的時候,不同的協(xié)議、不同的阻塞類型和 Netty 中不同的 Channel 對應(yīng),常用的 Channel 有:

  • NioSocketChannel:非阻塞的 TCP 客戶端 Channel(本案例的客戶端使用的 Channel)
  • NioServerSocketChannel:非阻塞的 TCP 服務(wù)器端 Channel(本案例的服務(wù)器端使用的 Channel)
  • NioDatagramChannel:非阻塞的 UDP Channel
  • NioSctpChannel:非阻塞的 SCTP 客戶端 Channel
  • NioSctpServerChannel:非阻塞的 SCTP 服務(wù)器端 Channel......

啟動服務(wù)端和客戶端代碼,調(diào)試以上的服務(wù)端代碼,發(fā)現(xiàn):

1)默認情況下 BossGroup 和 WorkerGroup 都包含 16 個線程(NioEventLoop),這是因為我的 PC 是 8 核的 NioEventLoop 的數(shù)量=coreNum*2。這 16 個線程相當于主 Reactor。

其實創(chuàng)建 BossGroup 和 WorkerGroup 的時候可以指定 NioEventLoop 數(shù)量,如下:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(16);

這樣就能更好地分配線程資源。

2)每一個 NioEventLoop 包含如下的屬性(比如自己的 Selector、任務(wù)隊列、執(zhí)行器等):

3)將代碼斷在服務(wù)端的 NettyServerHandler.channelRead 上:

可以看到 ctx 中包含的屬性如下:

可以看到:

  • 當前 ChannelHandlerContext ctx 是位于 ChannelHandlerContext 責任鏈中的一環(huán),可以看到其 next、prev 屬性
  • 當前 ChannelHandlerContext ctx 包含一個 Handler
  • 當前 ChannelHandlerContext ctx 包含一個 Pipeline
  • Pipeline 本質(zhì)上是一個雙向循環(huán)列表,可以看到其 tail、head 屬性
  • Pipeline 中包含一個 Channel,Channel 中又包含了該 Pipeline,兩者互相引用……

從下一節(jié)開始,我將深入剖析以上兩段代碼,向讀者展示 Netty 的更多細節(jié)。

2.5. Netty 的 Handler 組件

無論是服務(wù)端代碼中自定義的 NettyServerHandler 還是客戶端代碼中自定義的 NettyClientHandler,都繼承于 ChannelInboundHandlerAdapter,ChannelInboundHandlerAdapter 又繼承于 ChannelHandlerAdapter,ChannelHandlerAdapter 又實現(xiàn)了 ChannelHandler:

public class ChannelInboundHandlerAdapter 
    extends ChannelHandlerAdapter 
    implements ChannelInboundHandler {
    ......

public abstract class ChannelHandlerAdapter 
    implements ChannelHandler {
    ......

因此無論是服務(wù)端代碼中自定義的 NettyServerHandler 還是客戶端代碼中自定義的 NettyClientHandler,都可以統(tǒng)稱為 ChannelHandler。

Netty 中的 ChannelHandler 的作用是,在當前 ChannelHandler 中處理 IO 事件,并將其傳遞給 ChannelPipeline 中下一個 ChannelHandler 處理,因此多個 ChannelHandler 形成一個責任鏈,責任鏈位于 ChannelPipeline 中。

數(shù)據(jù)在基于 Netty 的服務(wù)器或客戶端中的處理流程是:讀取數(shù)據(jù)-->解碼數(shù)據(jù)-->處理數(shù)據(jù)-->編碼數(shù)據(jù)-->發(fā)送數(shù)據(jù)。其中的每個過程都用得到 ChannelHandler 責任鏈。

Netty 中的 ChannelHandler 體系如下(第一張圖來源于網(wǎng)絡(luò)):

其中:

  • ChannelInboundHandler 用于處理入站 IO 事件
  • ChannelOutboundHandler 用于處理出站 IO 事件
  • ChannelInboundHandlerAdapter 用于處理入站 IO 事件
  • ChannelOutboundHandlerAdapter 用于處理出站 IO 事件

ChannelPipeline 提供了 ChannelHandler 鏈的容器。以客戶端應(yīng)用程序為例,如果事件的方向是從客戶端到服務(wù)器的,我們稱事件是出站的,那么客戶端發(fā)送給服務(wù)器的數(shù)據(jù)會通過 Pipeline 中的一系列 ChannelOutboundHandler 進行處理;如果事件的方向是從服務(wù)器到客戶端的,我們稱事件是入站的,那么服務(wù)器發(fā)送給客戶端的數(shù)據(jù)會通過 Pipeline 中的一系列 ChannelInboundHandler 進行處理。

無論是服務(wù)端代碼中自定義的 NettyServerHandler 還是客戶端代碼中自定義的 NettyClientHandler,都繼承于 ChannelInboundHandlerAdapter,ChannelInboundHandlerAdapter 提供的方法如下:

從方法名字可以看出,它們在不同的事件發(fā)生后被觸發(fā),例如注冊 Channel 時執(zhí)行 channelRegistred()、添加 ChannelHandler 時執(zhí)行 handlerAdded()、收到入站數(shù)據(jù)時執(zhí)行 channelRead()、入站數(shù)據(jù)讀取完畢后執(zhí)行 channelReadComplete()等等。

2.6. Netty 的 Pipeline 組件

上一節(jié)說到,Netty 的 ChannelPipeline,它維護了一個 ChannelHandler 責任鏈,負責攔截或者處理 inbound(入站)和 outbound(出站)的事件和操作。這一節(jié)給出更深層次的描述。

ChannelPipeline 實現(xiàn)了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel 中各個 ChannelHandler 如何相互交互。

每個 Netty Channel 包含了一個 ChannelPipeline(其實 Channel 和 ChannelPipeline 互相引用),而 ChannelPipeline 又維護了一個由 ChannelHandlerContext 構(gòu)成的雙向循環(huán)列表,其中的每一個 ChannelHandlerContext 都包含一個 ChannelHandler。(前文描述的時候為了簡便,直接說 ChannelPipeline 包含了一個 ChannelHandler 責任鏈,這里給出完整的細節(jié)。)

如下圖所示(圖片來源于網(wǎng)絡(luò)):

還記得下面這張圖嗎?這是上文中基于 Netty 的 Server 程序的調(diào)試截圖,可以從中看到 ChannelHandlerContext 中包含了哪些成分:

ChannelHandlerContext 除了包含 ChannelHandler 之外,還關(guān)聯(lián)了對應(yīng)的 Channel 和 Pipeline。可以這么來講:ChannelHandlerContext、ChannelHandler、Channel、ChannelPipeline 這幾個組件之間互相引用,互為各自的屬性,你中有我、我中有你。

在處理入站事件的時候,入站事件及數(shù)據(jù)會從 Pipeline 中的雙向鏈表的頭 ChannelHandlerContext 流向尾 ChannelHandlerContext,并依次在其中每個 ChannelInboundHandler(例如解碼 Handler)中得到處理;出站事件及數(shù)據(jù)會從 Pipeline 中的雙向鏈表的尾 ChannelHandlerContext 流向頭 ChannelHandlerContext,并依次在其中每個 ChannelOutboundHandler(例如編碼 Handler)中得到處理。

2.7. Netty 的 EventLoopGroup 組件

在基于 Netty 的 TCP Server 代碼中,包含了兩個 EventLoopGroup——bossGroup 和 workerGroup,EventLoopGroup 是一組 EventLoop 的抽象。

追蹤 Netty 的 EventLoop 的繼承鏈,可以發(fā)現(xiàn) EventLoop 最終繼承于 JUC Executor,因此 EventLoop 本質(zhì)就是一個 JUC Executor,即線程,JUC Executor 的源碼為:

public interface Executor {
    /**
     * Executes the given command at some time in the future.
     */
    void execute(Runnable command);
}

Netty 為了更好地利用多核 CPU 的性能,一般會有多個 EventLoop 同時工作,每個 EventLoop 維護著一個 Selector 實例,Selector 實例監(jiān)聽注冊其上的 Channel 的 IO 事件。

EventLoopGroup 含有一個 next 方法,它的作用是按照一定規(guī)則從 Group 中選取一個 EventLoop 處理 IO 事件。

在服務(wù)端,通常 Boss EventLoopGroup 只包含一個 Boss EventLoop(單線程),該 EventLoop 維護者一個注冊了 ServerSocketChannel 的 Selector 實例。該 EventLoop 不斷輪詢 Selector 得到 OP_ACCEPT 事件(客戶端連接事件),然后將接收到的 SocketChannel 交給 Worker EventLoopGroup,Worker EventLoopGroup 會通過 next()方法選取一個 Worker EventLoop 并將這個 SocketChannel 注冊到其中的 Selector 上,由這個 Worker EventLoop 負責該 SocketChannel 上后續(xù)的 IO 事件處理。整個過程如下圖所示:

2.8. Netty 的 TaskQueue

在 Netty 的每一個 NioEventLoop 中都有一個 TaskQueue,設(shè)計它的目的是在任務(wù)提交的速度大于線程的處理速度的時候起到緩沖作用。或者用于異步地處理 Selector 監(jiān)聽到的 IO 事件。

Netty 中的任務(wù)隊列有三種使用場景:

1)處理用戶程序的自定義普通任務(wù)的時候

2)處理用戶程序的自定義定時任務(wù)的時候

3)非當前 Reactor 線程調(diào)用當前 Channel 的各種方法的時候。

對于第一種場景,舉個例子,2.4 節(jié)的基于 Netty 編寫的服務(wù)端的 Handler 中,假如 channelRead 方法中執(zhí)行的過程很耗時,那么以下的阻塞式處理方式無疑會降低當前 NioEventLoop 的并發(fā)度:

/**
 * 當通道有數(shù)據(jù)可讀時執(zhí)行
 *
 * @param ctx 上下文對象
 * @param msg 客戶端發(fā)送的數(shù)據(jù)
 * @throws Exception
 */
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
        throws Exception {
    // 借助休眠模擬耗時操作
    Thread.sleep(LONG_TIME);

    ByteBuf byteBuf = (ByteBuf) msg;
    System.out.println("data from client: "
            + byteBuf.toString(CharsetUtil.UTF_8));
}

改進方法就是借助任務(wù)隊列,代碼如下:

/**
 * 當通道有數(shù)據(jù)可讀時執(zhí)行
 *
 * @param ctx 上下文對象
 * @param msg 客戶端發(fā)送的數(shù)據(jù)
 * @throws Exception
 */
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
        throws Exception {
    // 假如這里的處理非常耗時,那么就需要借助任務(wù)隊列異步執(zhí)行

    final Object finalMsg = msg;

    // 通過 ctx.channel().eventLoop().execute()將耗時
    // 操作放入任務(wù)隊列異步執(zhí)行
    ctx.channel().eventLoop().execute(new Runnable() {
        public void run() {
            // 借助休眠模擬耗時操作
            try {
                Thread.sleep(LONG_TIME);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            ByteBuf byteBuf = (ByteBuf) finalMsg;
            System.out.println("data from client: "
                    + byteBuf.toString(CharsetUtil.UTF_8));
        }
    });

    // 可以繼續(xù)調(diào)用 ctx.channel().eventLoop().execute()
    // 將更多操作放入隊列

    System.out.println("return right now.");
}

斷點跟蹤這個函數(shù)的執(zhí)行,可以發(fā)現(xiàn)該耗時任務(wù)確實被放入的當前 NioEventLoop 的 taskQueue 中了。

對于第二種場景,舉個例子,2.4 節(jié)的基于 Netty 編寫的服務(wù)端的 Handler 中,假如 channelRead 方法中執(zhí)行的過程并不需要立即執(zhí)行,而是要定時執(zhí)行,那么代碼可以這樣寫:

/**
 * 當通道有數(shù)據(jù)可讀時執(zhí)行
 *
 * @param ctx 上下文對象
 * @param msg 客戶端發(fā)送的數(shù)據(jù)
 * @throws Exception
 */
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
        throws Exception {

    final Object finalMsg = msg;

    // 通過 ctx.channel().eventLoop().schedule()將操作
    // 放入任務(wù)隊列定時執(zhí)行(5min 之后才進行處理)
    ctx.channel().eventLoop().schedule(new Runnable() {
        public void run() {

            ByteBuf byteBuf = (ByteBuf) finalMsg;
            System.out.println("data from client: "
                    + byteBuf.toString(CharsetUtil.UTF_8));
        }
    }, 5, TimeUnit.MINUTES);

    // 可以繼續(xù)調(diào)用 ctx.channel().eventLoop().schedule()
    // 將更多操作放入隊列

    System.out.println("return right now.");
}

斷點跟蹤這個函數(shù)的執(zhí)行,可以發(fā)現(xiàn)該定時任務(wù)確實被放入的當前 NioEventLoop 的 scheduleTasjQueue 中了。

對于第三種場景,舉個例子,比如在基于 Netty 構(gòu)建的推送系統(tǒng)的業(yè)務(wù)線程中,要根據(jù)用戶標識,找到對應(yīng)的 SocketChannel 引用,然后調(diào)用 write 方法向該用戶推送消息,這時候就會將這一 write 任務(wù)放在任務(wù)隊列中,write 任務(wù)最終被異步消費。這種情形是對前兩種情形的應(yīng)用,且涉及的業(yè)務(wù)內(nèi)容太多,不再給出示例代碼,讀者有興趣可以自行完成,這里給出以下提示:

2.9. Netty 的 Future 和 Promise

Netty對使用者提供的多數(shù) IO 接口(即 Netty Channel 中的 IO 方法)是異步的(即都立即返回一個 Netty Future,而 IO 過程異步進行),因此,調(diào)用者調(diào)用 IO 操作后是不能直接拿到調(diào)用結(jié)果的。要想得到 IO 操作結(jié)果,可以借助 Netty 的 Future(上面代碼中的 ChannelFuture 就繼承了 Netty Future,Netty Future 又繼承了 JUC Future)查詢執(zhí)行狀態(tài)、等待執(zhí)行結(jié)果、獲取執(zhí)行結(jié)果等,使用過 JUC Future 接口的同學會非常熟悉這個機制,這里不再展開描述了。也可以通過 Netty Future 的 addListener()添加一個回調(diào)方法來異步處理 IO 結(jié)果,如下:

// 啟動客戶端去連接服務(wù)器端
// 由于 bootstrap.connect()是一個異步操作,因此用.sync()等待
// 這個異步操作完成
final ChannelFuture channelFuture = bootstrap.connect(
        "127.0.0.1",
        8080).sync();

channelFuture.addListener(new ChannelFutureListener() {
    /**
     * 回調(diào)方法,上面的 bootstrap.connect()操作執(zhí)行完之后觸發(fā)
     */
    public void operationComplete(ChannelFuture future)
            throws Exception {
        if (channelFuture.isSuccess()) {
            System.out.println("client has connected to server!");
            // TODO 其他處理
        } else {
            System.out.println("connect to serverfail!");
            // TODO 其他處理
        }
    }
});

Netty Future 提供的接口有:

注:會有一些資料給出這樣的描述:“Netty 中所有的 IO 操作都是異步的”,這顯然是錯誤的。Netty 基于 Java NIO,Java NIO 是同步非阻塞 IO。Netty 基于 Java NIO 做了封裝,向使用者提供了異步特性的接口,因此本文說 Netty對使用者提供的多數(shù) IO 接口(即 Netty Channel 中的 IO 方法)是異步的。例如在 io.netty.channel.ChannelOutboundInvoker(Netty Channel 的 IO 方法多繼承于此)提供的多數(shù) IO 接口都返回 Netty Future:

Promise 是可寫的 Future,F(xiàn)uture 自身并沒有寫操作相關(guān)的接口,Netty 通過 Promise 對 Future 進行擴展,用于設(shè)置 IO 操作的結(jié)果。Future 繼承了 Future,相關(guān)的接口定義如下圖所示,相比于上圖 Future 的接口,它多出了一些 setXXX 方法:

Netty 發(fā)起 IO 寫操作的時候,會創(chuàng)建一個新的 Promise 對象,例如調(diào)用 ChannelHandlerContext 的 write(Object object)方法時,會創(chuàng)建一個新的 ChannelPromise,相關(guān)代碼如下:

@Override
public ChannelFuture write(Object msg) {
    return write(msg, newPromise());
}
......
@Override
public ChannelPromise newPromise() {
    return new DefaultChannelPromise(channel(), executor());
}
......

當 IO 操作發(fā)生異常或者完成時,通過 Promise.setSuccess()或者 Promise.setFailure()設(shè)置結(jié)果,并通知所有 Listener。關(guān)于 Netty 的 Future/Promise 的工作原理,我將在下一篇文章中進行源碼級的解析。

看完三件事??

如果你覺得這篇內(nèi)容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉(zhuǎn)發(fā),有你們的 『點贊和評論』,才是我創(chuàng)造的動力。

  2. 關(guān)注公眾號 『 做一個柔情的程序猿 』,不定期分享原創(chuàng)知識。

  3. 同時可以期待后續(xù)文章ing??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內(nèi)容