Netty剖析之Netty線程模型

線程模型基本介紹

不同的線程模式,對程序的性能有很大影響,爲了搞清Netty線程模式,下面我們來系統的講解下各個線程模式。

目前存在的線程模型有:

  1. 傳統阻塞IO服務模型
  2. Reactor 模式,根據 Reactor 的數量和處理資源池線程的數量不同,有3種典型的實現:
    2.1 單Reactor單線程
    2.2 單Reactor多線程
    2.3 主從Reactor多線程
  3. Netty線程模式(Netty主要基於主從Reactor多線程模型做了一定的改進,其中主從Reactor多線程模型有多個 Reactor)

傳統阻塞IO服務模型

示意圖:
在這裏插入圖片描述

模型特點:

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

問題分析:

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

Reactor模式

針對傳統阻塞IO服務模型的2個缺點,產生了如下解決方案:

  1. 基於IO複用模型:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理

  2. 基於線程池複用線程資源:不必再爲每個連接創建線程,將連接完成後的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。

Reactor通常有如下幾種稱呼:

  1. 反應器模式
  2. 分發者模式(Dispatcher)
  3. 通知者模式(notifier)

IO複用結合線程池,就是Reactor模式基本設計思想,如下圖所示:
在這裏插入圖片描述
Reactor模型說明:

  • Reactor 模式,通過一個或多個輸入同時傳遞給服務處理器的模式(基於事件驅動)
  • 服務器端程序處理傳入的多個請求,並將它們同步分派到相應的處理線程, 因此Reactor模式也叫 Dispatcher模式
  • Reactor 模式使用IO複用監聽事件, 收到事件後,分發給某個線程(進程), 這點就是網絡服務器高併發處理關鍵

Reactor核心組成部分:

  1. Reactor:Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對IO事件做出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯繫人;
  2. Handlers:處理程序執行IO事件要完成的實際事件,類似於客戶想要與之交談的公司中的實際官員。Reactor 通過調度適當的處理程序來響應IO事件,處理程序執行非阻塞操作;

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

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

下面我將分別詳細來介紹

單Reactor單線程

示意圖:
在這裏插入圖片描述

工作原理:

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

優點:

  • 模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成

缺點:

  • 性能問題,只有一個線程,無法完全發揮多核 CPU 的性能。Handler 在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸
  • 可靠性問題,線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障

使用場景:客戶端的數量有限,業務處理非常快速,比如 Redis在業務處理的時間複雜度 O(1) 的情況

單Reactor多線程

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

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

優點:

  • 可以充分的利用多核cpu 的處理能力

缺點:

  • 多線程數據共享和訪問比較複雜, reactor 處理所有的事件的監聽和響應,在單線程運行, 在高併發場景容易出現性能瓶頸

主從Reactor多線程

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

  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

優點:

  • 父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成後續的業務處理
  • 父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據

缺點:

  • 編程複雜度較高

Reactor模式優點

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

Netty模型

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

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

Netty入門案例

需求:使用Netty實現客戶端可以向服務端發送消息,服務端並可以回覆消息給客戶端

Netty服務端:

public class NettyServer {

    public static void main(String[] args) throws Exception{
        /**
         * 分別創建bossGroup和workerGroup
         *
         * NioEventLoopGroup可以指定子線程數量,默認是cpu的核數*2
         * new NioEventLoopGroup(1)則只會創建一個子線程
         */
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(); // bossGroup用來處理與客戶端的連接請求
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(); // workerGroup用來處理客戶端業務

        try {
            // 創建服務端啓動對象,服務端是ServerBootstrap,客戶端是Bootstrap
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            // 配置啓動對象
            serverBootstrap.group(bossGroup, workerGroup) // 設置事件循環組
                    .channel(NioServerSocketChannel.class) // 設置通道實現類
                    .option(ChannelOption.SO_BACKLOG, 128) // 設置線程隊列的連接個數
                    .childOption(ChannelOption.SO_KEEPALIVE, true) // 設置保持活動連接狀態
                    .childHandler(new ChannelInitializer<SocketChannel>() { // 創建一個通道測試對象
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 給pipeline設置處理器
                            socketChannel.pipeline().addLast(new NettyServerHandler());
                        }
                    }); // 給線程組設置對應的管道處理器

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

            // 綁定端口
            ChannelFuture channelFuture = serverBootstrap.bind(8899).sync();
            // 關閉通道監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

自定義服務端通道處理handler:

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 數據讀取處理方法
     *
     * @param ctx 上下文對象
     * @param msg 客戶端發送的數據
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        System.out.println("server channelRead thread :" + Thread.currentThread().getName());

        Channel channel = ctx.channel();


        // 此處的ByteBuf是Nttey提供的,不是NIO中的ByteBuffer
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到來自[" + channel.remoteAddress() + "]的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 數據讀取完成處理方法
     * channelRead方法執行完後纔會執行此方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 數據讀取完成之後,我們向客戶端發送一個消息
        String msg = "hello client,msg is read!";
        // 消息編碼
        ByteBuf byteBuf = Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8);
        // 將數據寫入到緩存,並刷新 ,同時執行了兩個操作 write + flush
        ctx.writeAndFlush(byteBuf);
    }

    /**
     * 異常處理方法
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

Netty客戶端:

public class NettyClient {

    public static void main(String[] args) throws Exception {
        // 客戶端只需要創建一個事件循環組
        NioEventLoopGroup group = new NioEventLoopGroup();

        try {
            // 創建客戶端啓動對象,客戶端是Bootstrap,服務端是ServerBootstrap
            Bootstrap bootstrap = new Bootstrap();
            // 配置啓動對象
            bootstrap.group(group) // 設置事件循環組
                    .channel(NioSocketChannel.class) // 設置通道實現類
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 給pipeline設置處理器
                            socketChannel.pipeline().addLast(new NeetyClientHandler());
                        }
                    });

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

            // 連接服務端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8899).sync();
            // 關閉通道監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

自定義客戶端通道處理handler:

public class NeetyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 通道就緒處理方法
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // 通道就緒後,我們向服務端發送一個消息
        String msg = "hello server!";
        // 消息編碼
        ByteBuf byteBuf = Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8);
        // 將數據寫入到緩存,並刷新 ,同時執行了兩個操作 write + flush
        ctx.writeAndFlush(byteBuf);
    }

    /**
     * 數據讀取處理方法
     *
     * @param ctx 上下文對象
     * @param msg 客戶端發送的數據
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        Channel channel = ctx.channel();

        // 此處的ByteBuf是Nttey提供的,不是NIO中的ByteBuffer
        ByteBuf byteBuf = (ByteBuf) msg;
        System.out.println("收到來自[" + channel.remoteAddress() + "]的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 異常處理方法
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

測試:

netty server is ready....
收到來自[/127.0.0.1:7007]的消息:hello server!
netty client is ready....
收到來自[/127.0.0.1:8899]的消息:hello client,msg is read!

客戶端與服務端通信成功!

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