Netty-架構設計及入門程序-3

一、原生 NIO 存在的問題

1、NIO 的類庫和 API 繁雜,使用麻煩:需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
2、需要具備其他的額外技能:要熟悉 Java 多線程編程,因爲 NIO 編程涉及到 Reactor 模式,你必須對多線程和網絡編程非常熟悉,才能編寫出高質量的 NIO 程序。
3、開發工作量和難度都非常大:例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常流的處理等等。
4、JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU 100%。直到 JDK 1.7版本該問題仍舊存在,沒有被根本解決。

二、Netty 官網說明

官網:https://netty.io/

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients

三、Netty 的優點

Netty 對 JDK 自帶的 NIO 的 API 進行了封裝,解決了上述問題。

1、設計優雅:適用於各種傳輸類型的統一 API 阻塞和非阻塞 Socket;基於靈活且可擴展的事件模型,可以清晰地分離關注點;高度可定製的線程模型 - 單線程,一個或多個線程池.

2、使用方便:詳細記錄的 Javadoc,用戶指南和示例;沒有其他依賴項,JDK 5(Netty 3.x)或 6(Netty 4.x)就足夠了。

3、高性能、吞吐量更高:延遲更低;減少資源消耗;最小化不必要的內存複製。

4、安全:完整的 SSL/TLS 和 StartTLS 支持。

5、社區活躍、不斷更新:社區活躍,版本迭代週期短,發現的 Bug 可以被及時修復,同時,更多的新功能會被加入

四、Netty 版本說明

1、netty 版本分爲 netty3.x 和 netty4.x、netty5.x
2、因爲 Netty5 出現重大 bug,已經被官網廢棄了,目前推薦使用的是 Netty4.x 的穩定版本
3、目前在官網可下載的版本 netty3.x netty4.0.x 和 netty4.1.x
4、netty下載地址: https://bintray.com/netty/downloads/netty/

Netty 高性能架構設計

一、線程模型基本介紹

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

2、目前存在的線程模型有:

  • 傳統阻塞 I/O 服務模型
  • Reactor 模式

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

  • 單 Reactor 單線程;
  • 單 Reactor 多線程;
  • 主從 Reactor 多線程

4、Netty 線程模式

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

二、傳統阻塞 I/O 服務模型

1、工作原理圖

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

2、模型特點

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

3、問題分析

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

三、Reactor 模式

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

基於I/O 複用模型

多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。

當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理Reactor 對應的叫法:

  • 反應器模式
  • 分發者模式(Dispatcher)
  • 通知者模式(notifier)

基於線程池複用線程資源:

不必再爲每個連接創建線程,將連接完成後的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務

 

2、I/O 複用結合線程池, Reactor 模式基本設計思想

 

對上圖說明:

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

3、Reactor 模式中核心組成

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

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

4、Reactor 模式分類

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

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

四、單 Reactor 單線程

原理圖,並使用 NIO 羣聊系統驗證

1、方案說明

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

結合實例:

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

2、方案優缺點分析

1、優點:

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

2、缺點:

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

3、使用場景:

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

五、單 Reactor 多線程

1、原理圖

Handler將具體的業務處理Worker線程池分層出去,並通過開闢新的線程去完成

2、小結

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

3、方案優缺點

1、優點:

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

2、缺點:

  • 多線程數據共享和訪問比較複雜, reactor 處理所有的事件的監聽和響應

  • 單線程運行, 在高併發場景容易出現性能瓶頸.

六、主從 Reactor 多線程

1、工作原理圖

多加了一層派發層並採用新開線程(Reactor子線程,SubReactor),分爲了3層,獨立開

針對單 Reactor 多線程模型中,Reactor 在單線程中運行,高併發場景下容易成爲性能瓶頸,可以讓 Reactor 在多線程中運行

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

3、Scalable IO in Java 對 Multiple Reactors 的原理圖解

4、方案優缺點

1、優點:

  • 父線程與子線程的數據交互簡單職責明確

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

2、缺點:

  • 編程複雜度較高

結合實例:

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

七、Reactor 模式小結

1、3種模式用生活案例來理解

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

2、Reactor 模式的優點

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

八、Netty 模型

1、工作原理-簡單版

Netty 主要基於主從 Reactors 多線程模型(如圖)做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor

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

2、工作原理-進階版

3、工作原理-詳細版

1、Netty 抽象出兩組線程池

  • BossGroup 專門負責接收客戶端的連接
  • WorkerGroup 專門負責網絡的讀寫

2、BossGroup 和 WorkerGroup 類型都是 NioEventLoopGroup

3、NioEventLoopGroup 相當於一個事件循環組, 這個組中含有多個事件循環 ,每一個事件循環是 NioEventLoop

4、NioEventLoop 表示一個不斷循環的執行處理任務的線程, 每個 NioEventLoop 都有一個selector , 用於監聽綁定在其上的 socket 的網絡通訊

5、NioEventLoopGroup 可以有多個線程, 即可以含有多個 NioEventLoop

6、每個 Boss NioEventLoop 循環執行的步驟有 3 步

  • 輪詢 accept 事件
  • 處理 accept 事件 , 與 client 建立連接 , 生成 NioScocketChannel , 並將其註冊到某個 worker NIOEventLoop 上的 selector
  • 處理任務隊列的任務 , 即 runAllTasks

7、每個 Worker NIOEventLoop 循環執行的步驟

  • 輪詢 read, write 事件
  • 處理 i/o 事件, 即 read , write 事件,在對應 NioScocketChannel 處理
  • 處理任務隊列的任務 , 即 runAllTasks

8、每個Worker NIOEventLoop 處理業務時,會使用pipeline(管道), pipeline 中包含了 channel , 即通過pipeline可以獲取到對應通道, 管道中維護了很多的 處理器

4、Netty實例-TCP 服務

 1、引入netty依賴

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.20.Final</version>
</dependency>

2、NettyServer服務端

package com.sun.netty.nettySimple;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        /*
        創建BossGroup 和 WorkerGroup
        說明:
            1、創建兩個線程組 BossGroup 和 WorkerGroup
            2、 BossGroup:只處理連接請求
                WorkerGroup: 處理和客戶端業務處理
            3、兩個線程組都是自旋
         */
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 創建服務器啓動對象,配置啓動參數
            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>() { // 創建一個通道初始化對象
                        // 給pipeline設置處理器
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            System.out.println("服務器端準備完畢...");
            // 服務器端綁定端口並同步處理,返回ChannelFuture對象,啓動服務端
            ChannelFuture channelFuture = serverBootstrap.bind(9998).sync();
            // 對關閉通道進行監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
View Code

3、NettyServerHandler服務端處理器

package com.sun.netty.nettySimple;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
        /*
        創建BossGroup 和 WorkerGroup
        說明:
            1、創建兩個線程組 BossGroup 和 WorkerGroup
            2、 BossGroup:只處理連接請求
                WorkerGroup: 處理和客戶端業務處理
            3、兩個線程組都是自旋
         */
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 創建服務器啓動對象,配置啓動參數
            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>() { // 創建一個通道初始化對象
                        // 給pipeline設置處理器
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            System.out.println("服務器端準備完畢...");
            // 服務器端綁定端口並同步處理,返回ChannelFuture對象,啓動服務端
            ChannelFuture channelFuture = serverBootstrap.bind(9998).sync();
            // 對關閉通道進行監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
View Code

4、NettyClient客戶端

package com.sun.netty.nettySimple;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
 * @Title: NettyClient
 * @Author sunguoqiang
 * @Package com.sun.netty.nettySimple
 * @Date 2022/12/29 16:28
 * @description:
 */
public class NettyClient {

    public static void main(String[] args) throws InterruptedException {
        // 客戶端需要一個事件循環組
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        try {
            //創建客戶端的啓動對象
            Bootstrap bootstrap = new Bootstrap();
            //設置啓動參數
            bootstrap.group(eventExecutors) //設置線程組
                    .channel(NioSocketChannel.class) //設置客戶端通道實現類
                    .handler(new ChannelInitializer<SocketChannel>() { //創建一個通道初始化對象
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyClientHandler()); //加入自己的處理器
                        }
                    });
            System.out.println("客戶端準備完畢...");
            // 指定客戶端連接的服務器地址
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9998).sync();
            // 對關閉通道進行監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            // 優雅關閉
            eventExecutors.shutdownGracefully();
        }
    }
}
View Code

5、NettyClient客戶端處理器

package com.sun.netty.nettySimple;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * @Title: NettyClientHandler
 * @Author sunguoqiang
 * @Package com.sun.netty.nettySimple
 * @Date 2022/12/29 16:47
 * @description:
 */
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    /**
     * 通道就緒就會觸發該方法
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client ctx:"+ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("我是客戶端...", StandardCharsets.UTF_8));
    }

    /**
     * 接受服務端返回的消息,當通道有讀取事件時就觸發
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf= (ByteBuf) msg;
        System.out.println("來自服務端的消息:"+buf.toString(StandardCharsets.UTF_8));
        System.out.println("服務端地址:"+ctx.channel().remoteAddress());
    }


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

運行結果:

 

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

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

2、用戶自定義定時任務

3、非當前 Reactor 線程調用 Channel 的各種方法

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

4、代碼

package com.sun.netty.nettySimple;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

import java.util.concurrent.TimeUnit;

/**
 * 服務端處理器,自定義handler需要繼承netty規定好的某個HandlerAdapter適配器才能生效
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 讀取數據事件(這裏可以讀取客戶端發送來的消息)
     * 1、ChannelHandlerContext ctx:上下文對象。含有管道pipeline、通道channel、地址等
     * 2、Object msg:客戶端發送來的數據
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("server ctx:" + ctx);
        //將msg轉爲ByteBuffer(這個ByteBuf和nio的ByteBuffer是有區別的)
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客戶端發送消息是:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客戶端地址爲:" + ctx.channel().remoteAddress());

        // 用戶程序自定義普通任務,該任務提交到taskQueue自定義異步任務
        ctx.channel().eventLoop().execute(()->{
            try {
                Thread.sleep(10*1000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("用戶自定義普通任務,taskQueue...",CharsetUtil.UTF_8));
            }catch (Exception e){
                e.printStackTrace();
            }
        });

        // 用戶自定義定時任務,該任務提交到scheduleTaskQueue中
        ctx.channel().eventLoop().schedule(()->{
            try {
                Thread.sleep(10*1000);
                ctx.writeAndFlush(Unpooled.copiedBuffer("用戶自定義定時任務,scheduleTaskQueue...",CharsetUtil.UTF_8));
            }catch (Exception e){
                e.printStackTrace();
            }
        },10, TimeUnit.SECONDS);
    }

    /**
     * 數據讀取完畢事件
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        /**
         * 1、writeAndFlush是 write+Flush方法的合併
         * 2、將數據寫入緩存並刷新
         * 3、對發送的數據進行編碼
         */
        ctx.writeAndFlush(Unpooled.copiedBuffer("我是服務器端...",CharsetUtil.UTF_8));
    }

    /**
     * 發生異常處理,發送異常事件,一般是關閉通道
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.channel().close();
    }
}
View Code

說明:

1、Netty 抽象出兩組線程池,

1、BossGroup 專門負責接收客戶端連接,
2、WorkerGroup 專門負責網絡讀寫操作。

2、NioEventLoop 表示一個不斷循環執行處理任務的線程,每個 NioEventLoop 都有一個 selector,用於監聽綁定在其上的 socket 網絡通道。

3、NioEventLoop 內部採用串行化設計,從消息的讀取->解碼->處理->編碼->發送,始終由 IO 線程 NioEventLoop負責

  1. NioEventLoopGroup 下包含多個 NioEventLoop
  2. 每個 NioEventLoop 中包含有一個 Selector,一個 taskQueue
  3. 每個 NioEventLoop 的 Selector 上可以註冊監聽多個 NioChannel
  4. 每個 NioChannel 只會綁定在唯一的 NioEventLoop 上
  5. 每個 NioChannel 都綁定有一個自己的 ChannelPipeline

九、異步模型

1、基本介紹

1、異步的概念和同步相對。

2、當一個異步過程調用發出後,調用者不能立刻得到結果。

3、實際處理這個調用的組件在完成後,通過狀態、通知和回調來通知調用者。

4、Netty 中的 I/O 操作是異步的,包括 Bind、Write、Connect 等操作會簡單的返回一個 ChannelFuture。

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

6、Netty 的異步模型是建立在 future 和 callback 的之上的。callback 就是回調。重點說 Future,它的核心思想是:

 假設一個方法 fun,計算過程可能非常耗時,等待 fun 返回顯然不合適。那麼可以在調用 fun 的時候,立馬返回一個 Future,後續可以通過 Future 去監控方法 fun 的處理過程(即 : Future-Listener 機制)

2、Future 說明

1、表示 異步的執行結果, 可以通過它提供的方法來檢測執行是否完成,比如檢索計算等等.

2、ChannelFuture 是一個接口 :*public interface ChannelFuture extends Future< Void >*我們可以添加監聽器,當監聽的事件發生時,就會通知到監聽器

3、工作原理示意圖

說明

1、在使用 Netty 進行編程時,攔截操作和轉換出入站數據只需要您提供 callback 或利用 future 即可。這使得鏈式操作簡單、高效, 並有利於編寫可重用的、通用的代碼。

2、Netty 框架的目標就是讓你的業務邏輯從網絡基礎應用編碼中分離出來、解脫出來

4、Future-Listener 機制

1、當 Future 對象剛剛創建時,處於非完成狀態,調用者可以通過返回的 ChannelFuture 來獲取操作執行的狀態,註冊監聽函數來執行完成後的操作。

2、常見有如下操作

1、通過 isDone 方法來判斷當前操作是否完成;
2、通過isSuccess 方法來判斷已完成的當前操作是否成功;
3、通過 getCause 方法來獲取已完成的當前操作失敗的原因;
4、通過 isCancelled 方法來判斷已完成的當前操作是否被取消;
5、通過 addListener 方法來註冊監聽器,當操作已完成(isDone 方法返回完成),將會通知指定的監聽器;如果Future 對象已完成,則通知指定的監聽器

代碼

綁定端口是異步操作,當綁定操作處理完,將會調用相應的監聽器處理邏輯,該位於客戶端代碼

ChannelFuture cf = bootstrap.connect("127.0.0.1", 6668).sync();

cf.addListener(new ChannelFutureListener() {
    public void operationComplete(ChannelFuture channelFuture) throws Exception {
        if (channelFuture.isSuccess()){
            System.out.println("監聽6668端口成功");
        }else {
            System.out.println("監聽6668端口失敗");
        }
    }
});

十、快速入門實例-HTTP 服務

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

2、Netty 服務器在 7000 端口監聽,瀏覽器發出請求 "http://localhost:7000/ "

3、服務器可以回覆消息給客戶端 "hello,我是阿昌的服務器 " , 並對特定請求資源進行過濾.

這裏我們用瀏覽器來作爲客戶端,所以就不需要寫客戶端的代碼

HttpServer

package com.sun.netty.http;

import com.sun.security.ntlm.Server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2023/1/4 10:55
 **/
public class HttpServer {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ServerInitializer());
            System.out.println("服務器已啓動...");
            ChannelFuture channelFuture = serverBootstrap.bind(9090).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            workGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}
View Code

ServerInitializer

package com.sun.netty.http;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2023/1/4 11:18
 **/
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline()
                .addLast("myHttpServerCodec", new HttpServerCodec())
                .addLast(new HttpServerHandler());
    }
}
View Code

HttpServerHandler

package com.sun.netty.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;

/**
 * @Author: sunguoqiang
 * @Description: SimpleChannelInboundHandler:他就是ChannelInboundHandlerAdapter的子類
 * HttpObject:表示客戶端和服務器端相互通訊的數據被封裝成HttpObject類型
 * @DateTime: 2023/1/4 11:24
 **/
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    // 當有讀取事件就會觸發該事件,讀取客戶端數據
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
        if (httpObject instanceof HttpRequest) {
            // 打印相關信息
            System.out.println("pipeline hashcode:" + channelHandlerContext.pipeline().hashCode());
            System.out.println("msg(httpObject)類型:" + httpObject.getClass());
            System.out.println("客戶端瀏覽器地址:" + channelHandlerContext.channel().remoteAddress());
            System.out.println("請求URI:" + ((HttpRequest) httpObject).getUri());
            // 過濾不響應uri請求
            String uri = ((HttpRequest) httpObject).getUri();
            if ("/favicon.ico".equals(uri)) {
                System.out.println("此[/favicon.ico]請求不處理.");
                return;
            }
            // 回覆信息給客戶端瀏覽器[http協議]
            ByteBuf byteBuf = Unpooled.copiedBuffer("你好瀏覽器,我是server...", CharsetUtil.UTF_8);
            // 構造http響應,即HttpResponse響應
            DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
            httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=utf-8");
            httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
            // 將構建好的Response返回
            channelHandlerContext.writeAndFlush(httpResponse);
        }
    }
}
View Code

測試

因爲http協議是短連接協議,所以每次請求完就會斷開連接,所以每次請求都會分配一個新的pipeline對象,與對應的channelSocket

 

 

 

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