第 5 章 Netty 高性能架構設計

5.1 線程模型基本介紹

  1. 不同的線程模式,對程序的性能有很大影響,爲了搞清 Netty 線程模式,瞭解下 各個線程模式,最後看看 Netty 線程模型有什麼優越性.

  2. 目前存在的線程模型有:傳統阻塞 I/O 服務模型,Reactor 模式

  3. 根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現

  • 單 Reactor 單線程;
  • 單 Reactor 多線程;
  • 主從 Reactor 多線程
  1. Netty 線程模式(Netty 主要基於主從 Reactor 多線程模型做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor)

5.2 傳統阻塞 I/O 服務模型

在這裏插入圖片描述
工作原理圖

  1. 黃色的框表示對象, 藍色的框表示線程
  2. 白色的框表示方法(API)

模型特點

  1. 採用阻塞 IO 模式獲取輸入的數據
  2. 每個連接都需要獨立的線程完成數據的輸入,業務處理,數據返回

問題分析

  1. 當併發數很大,就會創建大量的線程,佔用很大系統資源
  2. 連接創建後,如果當前線程暫時沒有數據可讀,該線程會阻塞在 read方法的操作,造成線程資源浪費

5.3 Reactor 模式

5.3.1針對傳統阻塞 I/O 服務模型的 2 個缺點,解決的基礎方案:

  1. 基於 I/O 複用模型:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理 ----解決BIO的read方法阻塞問題
    Reactor 對應的叫法: 1. 反應器模式 2. 分發者模式(Dispatcher) 3. 通知者模式(notifier)

  2. 基於線程池複用線程資源:不必再爲每個連接創建線程,將連接完成後的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。----解決BIO的一個Client一個Thread
    在這裏插入圖片描述

5.3.2 I/O 複用結合線程池,就是 Reactor 模式基本設計思想,如圖
在這裏插入圖片描述
說明:

  1. Reactor 模式,通過一個或多個輸入同時傳遞給服務處理器的模式(基於事件驅動)

  2. 服務器端程序處理傳入的多個請求,並將它們同步分派到相應的處理線程, 因此 Reactor 模式也叫 Dispatcher模式,上圖中的ServiceHandler 就類似 Reactor 反應器

  3. Reactor 模式使用 IO 複用監聽事件, 收到事件後,分發給某個線程(進程), 這點就是網絡服務器高併發處理關鍵

5.3.3 Reactor 模式中 核心組成:

  1. Reactor:Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對 IO 事件做出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯繫人;

  2. Handlers:處理程序執行 I/O 事件要完成的實際事件,類似於客戶想要與之交談的公司中的實際官員。Reactor通過調度適當的處理程序來響應 I/O 事件,處理程序執行非阻塞操作。

5.3.4 Reactor 模式分類:

根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現

  1. 單 Reactor 單線程
  2. 單 Reactor 多線程
  3. 主從 Reactor 多線程

5.4 單 Reactor 單線程

原理圖,並使用 NIO 羣聊系統驗證,之前的羣聊系統就是單 Reactor 單線程模型
在這裏插入圖片描述
5.4.1方案說明:

  1. Select 是前面 I/O 複用模型介紹的標準網絡編程 API,可以實現應用程序通過一個阻塞對象監聽多路連接請求
  2. Reactor 對象通過 Select 監控客戶端請求事件,收到事件後通過 Dispatch 進行分發
  3. 如果是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然後創建一個Handler 對象處理連接完成後的後續業務處理
  4. 如果不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應
  5. Handler 會完成 Read→業務處理→Send 的完整業務流程

結合實例:服務器端用一個線程通過多路複用搞定所有的 IO 操作(包括連接,讀、寫等),編碼簡單,清晰明瞭,但是如果客戶端連接數量較多,將無法支撐,前面的 NIO 案例就屬於這種模型

5.4.2方案優缺點分析:

  1. 優點:模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成
  2. 缺點:性能問題,只有一個線程,無法完全發揮多核 CPU 的性能。Handler 在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸
  3. 缺點:可靠性問題,線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障
  4. 使用場景:客戶端的數量有限,業務處理非常快速,比如 Redis 在業務處理的時間複雜度O(1) 的情況

5.5 單 Reactor 多線程

5.5.1 原理圖
在這裏插入圖片描述
5.5.2 對上圖的小結

  1. Reactor 對象通過 select 監控客戶端請求事件, 收到事件後,通過 dispatch 進行分發
  2. 如果建立連接請求, 則右 Acceptor 通過accept 處理連接請求, 然後創建一個 Handler 對象處理完成連接後的各種事件
  3. 如果不是連接請求,則由 reactor 分發調用連接對應的 handler 來處理
  4. handler 只負責響應事件,不做具體的業務處理, 通過 read 讀取數據後,會分發給後面的 worker 線程池的某個線程處理業務
  5. worker 線程池會分配獨立線程完成真正的業務,並將結果返回給 handler
  6. handler 收到響應後,通過 send 將結果返回給 client

5.5.3方案優缺點分析:

  1. 優點:可以充分的利用多核 cpu 的處理能力
  2. 缺點:多線程數據共享和訪問比較複雜, reactor 處理所有的事件的監聽和響應,在單線程運行, 在高併發場景容易出現性能瓶頸.

5.6 主從 Reactor 多線程

5.6.1 工作原理圖
針對單 Reactor 多線程模型中,Reactor 在單線程中運行,高併發場景下容易成爲性能瓶頸,可以讓 Reactor 在多線程中運行
在這裏插入圖片描述
注意:這裏的Reactor子線程以下部分是有多個的,即多個從 SubReactor ,開發要麼加緩存,要麼架構分層;

5.6.2上圖的方案說明

  1. Reactor 主線程 MainReactor 對象通過 select 監聽連接事件, 收到事件後,通過 Acceptor 處理連接事件
  2. 當 Acceptor 處理連接事件後,MainReactor 將連接分配給 SubReactor(多個)
  3. subreactor 將連接加入到連接隊列進行監聽,並創建 handler 進行各種事件處理
  4. 當有新事件發生時, subreactor 就會調用對應的 handler 處理
  5. handler 通過 read 讀取數據,分發給後面的 worker 線程處理
  6. worker 線程池分配獨立的 worker 線程進行業務處理,並返回結果
  7. handler 收到響應的結果後,再通過 send 將結果返回給 client
  8. Reactor 主線程可以對應多個 Reactor 子線程, 即 MainRecator 可以關聯多個 SubReactor

5.6.3 Scalable IO in Java 對 Multiple Reactors 的原理圖解:
在這裏插入圖片描述
5.6.4方案優缺點說明:

  1. 優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成後續的業務處理。
  2. 優點:父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據。
  3. 缺點:編程複雜度較高

結合實例
這種模型在許多項目中廣泛使用,包括 Nginx 主從 Reactor 多進程模型,Memcached 主從多線程,Netty 主從多線程模型的支持

5.7 Reactor 模式小結

5.7.13 種模式用生活案例來理解

  1. 單 Reactor 單線程,前臺接待員和服務員是同一個人,全程爲顧客服
  2. 單 Reactor 多線程,1 個前臺接待員,多個服務員,接待員只負責接待
  3. 主從 Reactor 多線程,多個前臺接待員,多個服務生

5.7.2 Reactor 模式具有如下的優點:

  1. 響應快,不必爲單個同步時間所阻塞,雖然 Reactor 本身依然是同步的
  2. 可以最大程度的避免複雜的多線程及同步問題,並且避免了多線程/進程的切換開銷
  3. 擴展性好,可以方便的通過增加 Reactor 實例個數來充分利用 CPU 資源
  4. 複用性好,Reactor 模型本身與具體事件處理邏輯無關,具有很高的複用性

5.8 Netty 模型

5.8.1 工作原理示意圖 1-簡單版
Netty 主要基於主從 Reactors 多線程模型(如上圖)做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor
在這裏插入圖片描述

5.8.2 對上圖說明

  1. BossGroup 線程維護 Selector , 只關注 Accecpt事件
  2. 當接收到 Accept 事件,獲取到對應的 SocketChannel, 封裝成 NIOScoketChannel 並註冊到 Worker 線程(事件循環), 並進行維護
  3. 當 Worker 線程監聽到 selector 中通道發生自己感興趣的事件後,就進行處理(就由handler), 注意 handler 之前就已經加入到通道

5.8.3工作原理示意圖 2-進階版
Netty 主要基於主從 Reactors 多線程模型(如圖)做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor
在這裏插入圖片描述

5.8.4工作原理示意圖-詳細版
在這裏插入圖片描述
5.8.5對上圖的說明小結

  1. Netty 抽象出兩組線程池 BossGroup 專門負責接收客戶端的連接, WorkerGroup 專門負責網絡的讀寫
  2. BossGroup 和 WorkerGroup 類型都是 NioEventLoopGroup,圖中NioEventGroup應該是NioEventLoopGroup
  3. NioEventLoopGroup 相當於一個事件循環組, 這個組中含有多個事件循環 ,每一個事件循環是 NioEventLoop
  4. NioEventLoop 表示一個不斷循環的執行處理任務的線程, 每個 NioEventLoop 都有一個 selector , 用於監聽綁定在其上的 socket 的網絡通訊
  5. NioEventLoopGroup 可以有多個線程, 即可以含有多個 NioEventLoop
  6. 每個 Boss NioEventLoop 循環執行的步驟有 3 步
    1 輪詢 accept 事件
    2 處理 accept 事件 , 與 client 建立連接 , 生成 NioScocketChannel , 並將其註冊到某個 worker NIOEventLoop 上的 selector,至於是註冊到哪個selector上這個看算法的實現
    3 處理任務隊列的任務 , 即 runAllTasks
  7. 每個 Worker NIOEventLoop 循環執行的步驟
    1 輪詢 read, write 事件
    2 處理 i/o 事件, 即 read , write 事件,在對應 NioScocketChannel 處理
    3 處理任務隊列的任務 , 即 runAllTasks
  8. 每個Worker NIOEventLoop 處理業務時,會使用pipeline(管道), pipeline 中包含了 channel , 即通過pipeline可以獲取到對應通道, 管道中維護了很多的 處理器

在這裏插入圖片描述
在這裏插入圖片描述
5.8.6 Netty 快速入門實例-TCP 服務

實例要求:使用 IDEA 創建 Netty 項目

  1. Netty 服務器在 6668 端口監聽,客戶端能發送消息給服務器 “hello, 服務器~”
  2. 服務器可以回覆消息給客戶端 “hello, 客戶端~”,接收消息的工作是由 channel 關聯的Pipeline 的 ChannelHandler(現有+自定義) 來處理,Channel 和 Pipeline 是互相包含的
  3. 目的:對 Netty 線程模型 有一個初步認識, 便於理解 Netty 模型理論
    …略…

5.8.7任務隊列中的 Task 有 3 種典型使用場景

1) 用戶程序自定義的普通任務

//比如這裏我們有一個非常耗時長的業務-> 異步執行 -> 提交該channel 對應的
        //NIOEventLoop 的 taskQueue中,  taskQueue自定義任務第49節視頻,debug可以看到是提交到taskQueue中

        //解決方案1 用戶程序自定義的普通任務
        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客戶端~(>^ω^<)喵2", CharsetUtil.UTF_8));
                    System.out.println("channel code=" + ctx.channel().hashCode());
                } catch (Exception ex) {
                    System.out.println("發生異常" + ex.getMessage());
                }
            }
        });
        ctx.channel().eventLoop().execute(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客戶端~(>^ω^<)喵3", CharsetUtil.UTF_8));
                    System.out.println("channel code=" + ctx.channel().hashCode());
                } catch (Exception ex) {
                    System.out.println("發生異常" + ex.getMessage());
                }
            }
        });
        //上面如果啓動2個任務,會順序執行,要花10秒才執行完,因爲執行的是同一個線程的,2個任務都會提交到taskQueue中;

在這裏插入圖片描述
2) 用戶自定義定時任務

//解決方案2 : 用戶自定義定時任務 ---》 該任務是提交到 scheduleTaskQueue中
        ctx.channel().eventLoop().schedule(new Runnable() {
            @Override
            public void run() {

                try {
                    Thread.sleep(5 * 1000);
                    ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客戶端~(>^ω^<)喵4", CharsetUtil.UTF_8));
                    System.out.println("channel code=" + ctx.channel().hashCode());
                } catch (Exception ex) {
                    System.out.println("發生異常" + ex.getMessage());
                }
            }
        }, 5, TimeUnit.SECONDS);

在這裏插入圖片描述
3) 非當前 Reactor 線程調用 Channel 的各種方法

例如在推送系統的業務線程裏面,根據用戶的標識,找到對應的 Channel 引用,然後調用 Write 類方法向該用戶推送消息,就會進入到這種場景。最終的 Write 會提交到任務隊列中後被異步消費
思路:用集合存放SocketChannel,根據用戶標識存放;

@Override
 protected void initChannel(SocketChannel ch) throws Exception {
  System.out.println("客戶socketChannel hashcode=" + ch.hashCode());
  //可以使用一個集合管理 SocketChannel, 再推送消息時,
  // 可以將業務加入到各個channel 對應的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
    ch.pipeline().addLast(new NettyServerHandler());
  }
});

在這裏插入圖片描述

5.9 異步模型

5.9.1 基本介紹

  1. 異步的概念和同步相對。當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的組件在完成後,通過狀態、通知和回調來通知調用者。

  2. Netty 中的 I/O 操作是異步的,包括 Bind、Write、Connect 等操作會簡單的返回一個 ChannelFuture。
    在這裏插入圖片描述

  3. 調用者並不能立刻獲得結果,而是通過 Future-Listener 機制,用戶可以方便的主動獲取或者通過通知機制獲得IO 操作結果

  4. Netty 的異步模型是建立在 future 和 callback 的之上的。callback 就是回調。重點說 Future,它的核心思想是:假設一個方法 fun,計算過程可能非常耗時,等待 fun 返回顯然不合適。那麼可以在調用 fun 的時候,立馬返回一個 Future,後續可以通過 Future 去監控方法 fun 的處理過程(即 : Future-Listener 機制)

5.9.2 Future 說明

  1. 表示 異步的執行結果, 可以通過它提供的方法來檢測執行是否完成,比如檢索計算等等.
  2. ChannelFuture 是一個接口 : public interface ChannelFuture extends Future<Void>我們可以添 加監聽器,當監聽的事件發生時,就會通知到監聽器. 案例說明
//綁定一個端口並且同步, 生成了一個 ChannelFuture 對象
            //啓動服務器(並綁定端口)
            ChannelFuture cf = bootstrap.bind(6668).sync();

            //給cf 註冊監聽器,監控我們關心的事件
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (cf.isSuccess()) {
                        System.out.println("監聽端口 6668 成功");
                    } else {
                        System.out.println("監聽端口 6668 失敗");
                    }
                }
            });

5.9.3 工作原理示意圖
在這裏插入圖片描述
在這裏插入圖片描述
說明:

  1. 在使用 Netty 進行編程時,攔截操作和轉換出入站數據只需要您提供 callback 或利用 future 即可。這使得鏈式操作簡單、高效, 並有利於編寫可重用的、通用的代碼。
  2. Netty 框架的目標就是讓你的業務邏輯從網絡基礎應用編碼中分離出來、解脫出來

5.9.4 Future-Listener 機制

  1. 當 Future 對象剛剛創建時,處於非完成狀態,調用者可以通過返回的 ChannelFuture 來獲取操作執行的狀態,註冊監聽函數來執行完成後的操作。
  2. 常見有如下操作
     通過 isDone 方法來判斷當前操作是否完成;
     通過 isSuccess 方法來判斷已完成的當前操作是否成功;
     通過 getCause 方法來獲取已完成的當前操作失敗的原因;
     通過 isCancelled 方法來判斷已完成的當前操作是否被取消;
     通過 addListener 方法來註冊監聽器,當操作已完成(isDone 方法返回完成),將會通知指定的監聽器;如果Future 對象已完成,則通知指定的監聽器

5.10 快速入門實例-HTTP 服務

  1. 實例要求:使用 IDEA 創建 Netty 項目
  2. Netty 服務器在 6668 端口監聽,瀏覽器發出請求 "http://localhost:6668/ "
  3. 服務器可以回覆消息給客戶端 "Hello! 我是服務器 5 " , 並對特定請求資源進行過濾.
  4. 目的:Netty 可以做 Http 服務開發,並且理解 Handler 實例和客戶端及其請求的關係.
  5. 代碼演示
public class TestServer {
    public static void main(String[] args) throws Exception {

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

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new TestServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(6668).sync();
            
            channelFuture.channel().closeFuture().sync();

        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

public class TestServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {

        //向管道加入處理器

        //得到管道
        ChannelPipeline pipeline = ch.pipeline();

        //加入一個netty 提供的httpServerCodec codec =>[coder - decoder] 編解碼器
        //HttpServerCodec 說明
        //1. HttpServerCodec 是netty 提供的處理http的 編-解碼器
        pipeline.addLast("MyHttpServerCodec",new HttpServerCodec());
        //2. 增加一個自定義的handler
        pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());

        System.out.println("ok~~~~");
    }
}

/*
說明
1. SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter
2. HttpObject 客戶端和服務器端相互通訊的數據被封裝成 HttpObject
 */
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    //channelRead0 讀取客戶端數據,當有讀取事件發生的時候就會調用此方法
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {


        //每個瀏覽器都有單獨的 pipeline 和 handler 進行處理,每次刷新瀏覽器又會產生新的 pipeline 和 handler,因爲http請求是一次性的
        System.out.println("對應的channel=" + ctx.channel() + " pipeline=" + ctx.pipeline() + " 通過pipeline獲取channel" + ctx.pipeline().channel());

        System.out.println("當前ctx的handler=" + ctx.handler());//TestHttpServerHandler

        //判斷 msg 是不是 httpRequest請求
        if(msg instanceof HttpRequest) {

            System.out.println("ctx 類型="+ctx.getClass());//DefaultChannelHandlerContext

            System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode());

            System.out.println("msg 類型=" + msg.getClass());//DefaultHttpRequest
            System.out.println("客戶端地址" + ctx.channel().remoteAddress());

            //獲取到
            HttpRequest httpRequest = (HttpRequest) msg;
            //獲取uri, 過濾指定的資源
            URI uri = new URI(httpRequest.uri());
            if("/favicon.ico".equals(uri.getPath())) {
                System.out.println("請求了 favicon.ico, 不做響應");
                return;
            }
            //回覆信息給瀏覽器 [http協議] 回覆需要符合http協議

            ByteBuf content = Unpooled.copiedBuffer("hello, 我是服務器", CharsetUtil.UTF_8);

            //構造一個http的相應,即 httpResponse
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);

            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());

            //將構建好 response返回
            ctx.writeAndFlush(response);
        }
    }
}

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