Netty學習筆記(二)

  • 原生 NIO 存在的問題:
    • NIO 的類庫和 API 繁雜,使用麻煩:需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
    • 需要具備其他的額外技能:要熟悉 Java 多線程編程,因為 NIO 編程涉及到 Reactor 模式,你必須對多線程和網絡編程非常熟悉,才能編寫出高質量的 NIO 程序。
    • 開發工作量和難度都非常大:例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常流的處理等等。
    • JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU100%。直到 JDK1.7 版本該問題仍舊存在,沒有被根本解決。
  • Netty的優點:Netty 對 JDK 自帶的 NIO 的 API 進行了封裝,解決了上述問題。
    • 設計優雅:適用于各種傳輸類型的統一 API 阻塞和非阻塞 Socket;基于靈活且可擴展的事件模型,可以清晰地分離關注點;高度可定制的線程模型-單線程,一個或多個線程池。
    • 使用方便:詳細記錄的 Javadoc,用戶指南和示例;沒有其他依賴項,JDK5(Netty3.x)或 6(Netty4.x)就足夠了。
    • 高性能、吞吐量更高:延遲更低;減少資源消耗;最小化不必要的內存復制。
    • 安全:完整的 SSL/TLS 和 StartTLS 支持。
    • 社區活躍、不斷更新:社區活躍,版本迭代周期短,發現的 Bug 可以被及時修復,同時更多的新功能會被加入。
  • 目前存在的線程模型有:傳統阻塞I/O服務模型Reactor模式
傳統阻塞I/O服務模型
  • 傳統阻塞I/O服務模型的特點:
    • 采用阻塞 IO 的模式獲取輸入的數據;
    • 每個連接都需要獨立的線程完成數據的輸入,業務處理,數據返回。
  • 上圖反應的問題分析:
    • 當并發數很大,就會創建大量的線程,占用很大的系統資源;
    • 連接創建后,若當前線程暫時沒有數據可讀,則該線程會阻塞在 read 操作,造成線程資源浪費。
  • 針對傳統阻塞 I/O 服務模型的缺點,給出以下解決方案:
    • 基于 I/O 復用模型:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象等待,無需阻塞等待所有連接。當某個連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理。
    • 基于線程池復用線程資源:不必再為每個連接創建線程,將連接完成后的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。
  • Reactor模式有3鐘叫法:①反應器模式;②分發者模式(Dispatcher);③通知者模式(notifier)
Reactor模式
  • 對上圖的說明:
    • Reactor模式:通過一個或多個輸入同時傳遞給服務處理器的模式(基于事件驅動)。
    • 服務器端程序處理傳入的多個請求,并將它們同步分派到相應的處理線程,因此 Reactor 模式也叫Dispatcher模式
    • Reactor 模式使用 IO 復用監聽事件,收到事件后,分發給某個線程(進程),這點就是網絡服務器高并發的處理關鍵。
  • 根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現:①單Reactor單線程;②單Reactor多線程;③主從Reactor多線程。Netty 主要基于主從Reactor多線程模型做了一定的改進,其中主從 Reactor 多線程模型有多個 Reactor。
單Reactor單線程
  • 對上圖的說明:
    • select是前面 I/O 復用模型介紹的標準網絡編程 API,可以實現應用程序通過一個阻塞對象監聽多路連接請求;
    • Reactor 對象通過 Select 監控客戶端請求事件,收到事件后Dispatch分發給處理進程;
    • 若是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然后創建一個 Handler 對象處理連接完成后的后續業務處理;
    • 若不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應
      Handler,會完成 Read → 業務處理 → Send 的完整業務流程。
    • 優點:模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成;
    • 缺點:性能問題,只有一個線程,無法完全發揮多核CPU的性能。Handler在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸。可靠性問題:線程意外終止,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障。
    • 使用場景:客戶端的數量有限,業務處理非常快速,比如 Redis 在業務處理方面的時間復雜度為 O(1) 的情況。
單Reactor多線程
  • 對上圖的說明:
    • Reactor 對象通過 Select 監控客戶端請求事件,收到事件后,通過 Dispatch 進行分發;
    • 若建立連接請求,則由 Acceptor 通過 accept 處理連接請求,然后創建一個 Handler 對象處理完成連接后的各種事件;
    • 若不是連接請求,則由 Reactor 分發調用連接對應的 handler 來處理;
    • handler 只負責響應事件,不做具體的業務處理,通過 read 讀取數據后,會分發給后面的 worker 線程池的某個線程處理業務;
    • worker 線程池會分配獨立的線程完成真正的業務,并將結果返回給 handler;
    • handler 收到響應后,通過 send 將結果返回給 client。
    • 優點:可以充分的利用多核 cpu 的處理能力;
    • 缺點:多線程數據共享和訪問比較復雜,Reactor 處理所有的事件的監聽和響應,在單線程中運行,在高并發場景時運行容易出現性能瓶頸。
主從Reactor多線程
  • 對上圖的說明:
    • Reactor 主線程 MainReactor 對象通過 select 監聽連接事件,收到事件后,通過 Acceptor 處理連接事件;
    • 當 Acceptor 處理連接事件后,MainReactor 將連接分配給 SubReactor;
    • Subreactor 將連接加入到連接隊列進行監聽,并創建 handler 進行各種事件處理,當有新事件發生時,Subreactor 就會調用對應的 handler 處理;
    • handler 通過 read 讀取數據,分發給后面的 worker 線程處理,worker 線程池分配獨立的 worker 線程進行業務處理,并返回結果;
    • handler 收到響應的結果后,再通過 send 將結果返回給 client;
    • Reactor 主線程可以對應多個 Reactor 子線程,即 MainRecator 可以關聯多個 SubReactor。
    • 優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成后續的業務處理。父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據。
    • 缺點:編程復雜度較高。
    • 應用場景:這種模型在許多項目中廣泛使用,包括 Nginx 主從 Reactor 多進程模型,Memcached 主從多線程,Netty 主從多線程模型的支持。
  • Reactor模式的優點:①響應快,不必為單個同步時間所阻塞,雖然 Reactor 本身依然是同步的;②可以最大程度地避免復雜的多線程及同步問題、多線程/進程的切換帶來的開銷;③擴展性好,可以方便的通過增加 Reactor 實例個數來充分利用 CPU 資源;④復用性好,Reactor 模型本身與具體事件處理邏輯無關,具有很高的復用性。
  • 對上圖的說明:
    • BossGroup 線程維護 Selector,只關注 Accecpt;
    • 當接收到 Accept 事件,獲取到對應的 SocketChannel,封裝成 NIOScoketChannel,并注冊到 Worker 線程(事件循環)進行維護;
    • 當 Worker 線程監聽到 Selector 中注冊的通道發生自己感興趣的事件后,就進行處理(由 handler來完成)。注意: handler 已經加入到通道。
  • 對上圖的說明:
    • Netty 抽象出兩組線程池:BossGroup 專門負責接收客戶端的連接,WorkerGroup 專門負責網絡的讀寫;
    • BossGroup 和 WorkerGroup 類型都是 NioEventLoopGroup;
    • NioEventLoopGroup 相當于一個事件循環組,這個組中含有多個事件循環,每一個事件循環是 NioEventLoop;
    • NioEventLoop 表示一個不斷循環的執行處理任務的線程,每個 NioEventLoop 都有一個 Selector,用于監聽綁定在其上的 socket 的網絡通訊;
    • NioEventLoopGroup 可以有多個線程,即可以含有多個 NioEventLoop;
    • 每個 BossNioEventLoop 循環執行的步驟有 3 步:
      • 輪詢 accept 事件;
      • 處理 accept 事件,與 client 建立連接,生成 NioScocketChannel,并將其注冊到某個 worker NIOEventLoop 上的 Selector;
      • 處理任務隊列的任務,即 runAllTasks;
    • 每個 Worker NIOEventLoop 循環執行的步驟:
      • 輪詢 read,write 事件;
      • 處理 I/O 事件,即 read,write 事件,在對應 NioScocketChannel 處理;
      • 處理任務隊列的任務,即 runAllTasks;
    • 每個 Worker NIOEventLoop 處理業務時,會使用 pipeline(管道),pipeline 中包含了 channel,即通過 pipeline 可以獲取到對應通道,管道中維護了很多的處理器。
  • Netty入門案例:TCP通信。
  • 導入maven依賴:
<dependency>
    <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
    <version>4.1.54.Final</version>
</dependency>
  • NettyServer.java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
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 Exception {
        //創建BossGroup 和 WorkerGroup
        //說明
        //1、創建兩個線程組 bossGroup 和 workerGroup
        //2、bossGroup 只是處理連接請求,真正的客戶端業務處理是會交給 workerGroup 完成
        //3、兩個都是無限循環
        //4、bossGroup 和 workerGroup 含有的子線程(NioEventLoop)的個數:默認值為 cpu核數 * 2
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8個
        try {
            //創建服務器端的啟動對象,配置參數
            ServerBootstrap bootstrap = new ServerBootstrap();
            //使用鏈式編程來進行設置
            bootstrap.group(bossGroup, workerGroup) //設置兩個線程組
                    .channel(NioServerSocketChannel.class) //使用 NioSocketChannel 作為服務器的通道實現
                    .option(ChannelOption.SO_BACKLOG, 128) //設置線程隊列等待連接的個數
                    .childOption(ChannelOption.SO_KEEPALIVE, true) //設置保持活動連接狀態
                    //.handler(null) // 該 handler對應 bossGroup , childHandler 對應 workerGroup
                    .childHandler(new ChannelInitializer<SocketChannel>() { //創建一個通道初始化對象(匿名對象)
                        //給 pipeline 設置處理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //可以使用一個集合管理 SocketChannel,在推送消息時,可以將業務加入到各個channel 對應的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue 中
                            System.out.println("客戶socketChannel hashcode=" + ch.hashCode());
                            ch.pipeline().addLast(new NettyServerHandler()); //給 workerGroup 的 EventLoop 對應的管道設置處理器
                        }
                    });
            System.out.println(".....服務器 is ready...");
            //綁定一個端口并且同步處理,生成了一個 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 失敗");
                    }
                }
            });
            //對關閉通道進行監聽
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
  • NettyServerHandler.java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
 * 說明:自定義一個Handler 需要繼承 netty 規定好的某個HandlerAdapter(規范)
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    //讀取數據實際(這里我們可以讀取客戶端發送的消息)
    /**
     * 1、ChannelHandlerContext ctx:上下文對象,含有管道 pipeline,通道channel,地址
     * 2、Object msg:客戶端發送的數據,默認是 Object
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服務器讀取線程:" + Thread.currentThread().getName() + ",channel=" + ctx.channel());
        System.out.println("server ctx:" + ctx);
        System.out.println("看看channel 和 pipeline的關系");
        Channel channel = ctx.channel();
        //本質是一個雙向鏈接, 出站入站
        ctx.pipeline();
        //將 msg 轉成一個 ByteBuf
        //ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer。
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客戶端發送消息是:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客戶端地址:" + channel.remoteAddress());
    }
    //數據讀取完畢
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //writeAndFlush:write + flush
        //將數據寫入到緩存,并刷新
        //一般需要對發送的數據進行編碼
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客戶端~(>^ω^<)喵1", CharsetUtil.UTF_8));
    }
    //發生異常時,需要關閉通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}
  • NettyClient.java
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClient {
    public static void main(String[] args) throws Exception {
        //客戶端需要一個事件循環組
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //創建客戶端啟動對象
            //注意客戶端使用的不是 ServerBootstrap 而是 Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            //設置相關參數
            bootstrap.group(group) //設置線程組
                    .channel(NioSocketChannel.class) // 設置客戶端通道的實現類(反射)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler()); //加入自定義的處理器
                        }
                    });
            System.out.println("...客戶端 is ok...");
            //啟動客戶端去連接服務器端
            //關于ChannelFuture 涉及到netty的異步模型
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            //給關閉通道進行監聽
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}
  • NettyClientHandler.java
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;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    //當通道就緒就會觸發該方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client:" + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,server: (>^ω^<)喵", CharsetUtil.UTF_8));
    }
    //當通道有讀取事件時,會觸發
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("服務器回復的消息:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("服務器的地址:" + ctx.channel().remoteAddress());
    }
    //發生異常時,需要關閉通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
  • 任務隊列中的 Task 有 3 種典型使用場景:
    • 用戶程序自定義的普通任務;
    • 用戶自定義定時任務;
    • 非當前 Reactor 線程調用 Channel 的各種方法:例如在推送系統的業務線程里面,根據用戶的標識,找到對應的 Channel 引用,然后調用 Write 類方法向該用戶推送消息,就會進入到這種場景,最終的 Write 會提交到任務隊列中后被異步消費。
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
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:客戶端發送的數據,默認是 Object
     * 假設這里有一個非常耗時長的業務 -> 異步執行 -> 提交該channel 對應的 NIOEventLoop 的 taskQueue中
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服務器讀取線程:" + Thread.currentThread().getName() + ",channel=" + ctx.channel());
        System.out.println("server ctx:" + ctx);
        System.out.println("看看channel 和 pipeline的關系");
        Channel channel = ctx.channel();
        //本質是一個雙向鏈接, 出站入站
        ctx.pipeline();
        //將 msg 轉成一個 ByteBuf
        //ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer。
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客戶端發送消息是:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客戶端地址:" + channel.remoteAddress());
        //解決方案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 : 用戶自定義定時任務 -> 該任務是提交到 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);
        System.out.println("go on ...");
    }
    //數據讀取完畢
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //writeAndFlush:write + flush
        //將數據寫入到緩存,并刷新
        //一般需要對發送的數據進行編碼
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客戶端~(>^ω^<)喵1", CharsetUtil.UTF_8));
    }
    //發生異常時,需要關閉通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}
  • 每個 NioEventLoop 中包含有一個 Selector,一個 taskQueue;每個 NioEventLoop 的 Selector 上可以注冊監聽多個 NioChannel;每個 NioChannel 只會綁定在唯一的 NioEventLoop 上;每個 NioChannel 都綁定有一個自己的 ChannelPipeline。
  • 異步模型:當一個異步過程調用發出后,調用者不能立刻得到結果。實際處理這個調用的組件在完成后,通過狀態、通知和回調來通知調用者。
  • Netty 中的 I/O 操作是異步的,包括 Bind、Write、Connect 等操作會簡單的返回一個 ChannelFuture。調用者并不能立刻獲得結果,而是通過Future-Listener機制,用戶可以方便地主動獲取或者通過通知機制獲得 IO 操作結果。
  • Netty 的異步模型是建立在 future 和 callback 的之上的。callback 就是回調。重點說 Future,它的核心思想是:假設一個方法 fun,計算過程可能非常耗時,等待 fun 返回顯然不合適。那么可以在調用 fun 的時候,立馬返回一個 Future,后續可以通過 Future 去監控方法 fun 的處理過程(即:Future-Listener 機制)。
  • Future說明:表示異步的執行結果,可以通過它提供的方法來檢測執行是否完成,比如檢索計算等等。ChannelFuture是一個接口:public interface ChannelFuture extends Future<Void>,可以添加自己的監聽器,當監聽的事件發生時,就會通知到監聽器。
工作原理示意圖
  • Future-Listener機制:當 Future 對象剛剛創建時,處于非完成狀態,調用者可以通過返回的 ChannelFuture 來獲取操作執行的狀態,注冊監聽函數來執行完成后的操作。常見的操作如下:
    • 通過isDone方法來判斷當前操作是否完成;
    • 通過isSuccess方法來判斷已完成的當前操作是否成功;
    • 通過getCause方法來獲取已完成的當前操作失敗的原因;
    • 通過isCancelled方法來判斷已完成的當前操作是否被取消;
    • 通過addListener方法來注冊監聽器,當操作已完成(isDone方法返回完成),將會通知指定的監聽器;若 Future 對象已完成,則通知指定的監聽器。
  • Netty入門案例:HTTP服務。
  • TestServer.java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
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(23333).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
  • TestServerInitializer.java
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
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~~~~");
    }
}
  • TestHttpServerHandler.java
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;
import java.net.URI;
/**
 * 1、SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter 的抽象子類
 * 2、HttpObject 客戶端和服務器端相互通訊的數據被封裝成 HttpObject
 */
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    //讀取客戶端數據
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        System.out.println("對應的channel=" + ctx.channel() + ",pipeline=" + ctx
                .pipeline() + ",通過pipeline獲取channel" + ctx.pipeline().channel());
        System.out.println("當前ctx的handler=" + ctx.handler());
        //判斷 msg 是不是 http request請求
        if (msg instanceof HttpRequest) {
            System.out.println("ctx 類型=" + ctx.getClass());
            System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + ",TestHttpServerHandler hash=" + this.hashCode());
            System.out.println("msg 類型=" + msg.getClass());
            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協議]
            ByteBuf content = Unpooled.copiedBuffer("hello,我是服務器", CharsetUtil.UTF_8);
            //構造一個http的響應,即 http response
            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);
        }
    }
}
  • Netty 中 Bootstrap 類是客戶端程序的啟動引導類,ServerBootstrap 是服務端啟動引導類。常見的方法有:
    • public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup):該方法用于服務器端,用來設置兩個 EventLoop。
    • public B group(EventLoopGroup group):該方法用于客戶端,用來設置一個 EventLoop。
    • public B channel(Class<? extends C> channelClass):該方法用來設置一個服務器端的通道實現。
    • public <T> B option(ChannelOption<T> option, T value):用來給 ServerChannel 添加配置。
    • public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value):用來給接收到的通道添加配置。
    • public ServerBootstrap childHandler(ChannelHandler childHandler):該方法用來設置業務處理類(自定義的handler)。
    • public ChannelFuture bind(int inetPort):該方法用于服務器端,用來設置占用的端口號。
    • public ChannelFuture connect(String inetHost, int inetPort):該方法用于客戶端,用來連接服務器端。
  • Netty 中所有的 IO 操作都是異步的,不能立刻得知消息是否被正確處理,但可以過一會等它執行完成或者直接注冊一個監聽(通過 Future 和 ChannelFutures來實現),當操作執行成功或失敗時會自動觸發注冊的監聽事件。常見的方法有:
    • Channel channel():返回當前正在進行 IO 操作的通道。
    • ChannelFuture sync():等待異步操作執行完畢。
  • Channel是Netty 網絡通信的組件,能夠用于執行網絡 I/O 操作。
  • 通過 Channel 可獲得當前網絡連接的通道的狀態。
  • 通過 Channel 可獲得網絡連接的配置參數(例如接收緩沖區大小)。
  • Channel 提供異步的網絡 I/O 操作(如建立連接,讀寫,綁定端口),異步調用意味著任何 I/O 調用都將立即返回,并且不保證在調用結束時所請求的 I/O 操作已完成。調用立即返回一個 ChannelFuture 實例,通過注冊監聽器到 ChannelFuture 上,可以 I/O 操作成功、失敗或取消時回調通知調用方。Channel 支持關聯 I/O 操作與對應的處理程序。不同協議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應,常用的 Channel 類型:
    • NioSocketChannel:異步的客戶端 TCP Socket 連接。
    • NioServerSocketChannel:異步的服務器端 TCP Socket 連接。
    • NioDatagramChannel:異步的 UDP 連接。
    • NioSctpChannel:異步的客戶端 Sctp 連接。
    • NioSctpServerChannel:異步的 Sctp 服務器端連接,這些通道涵蓋了 UDP 和 TCP 網絡 IO 以及文件 IO。
  • Netty 基于 Selector 對象實現 I/O 多路復用,通過 Selector 一個線程可以監聽多個連接的 Channel 事件。
  • 當向一個 Selector 中注冊 Channel 后,Selector 內部的機制就可以自動不斷地查詢(Select)這些注冊的 Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel。
  • ChannelHandler 是一個接口,用于處理 I/O 事件或攔截 I/O 操作,并將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程序。
  • ChannelHandler 本身并沒有提供很多方法,因為這個接口有許多的方法需要實現,方便使用期間,可以繼承它的子類。
ChannelHandler 及其實現類
  • 我們經常需要自定義一個 Handler 類去繼承 ChannelInboundHandlerAdapter,然后通過重寫相應方法實現業務邏輯,接下來看看一般都需要重寫哪些方法:
  • ChannelPipeline 是一個 Handler 的集合,它負責處理和攔截 inbound 或者 outbound 的事件和操作,相當于一個貫穿 Netty 的鏈。(也可以這樣理解:ChannelPipeline 是保存 ChannelHandler 的 List,用于處理或攔截 Channel 的入站事件和出站操作)
  • ChannelPipeline 實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel 中各個的 ChannelHandler 如何相互交互。
  • 在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應,它們的組成關系如下:
  • ChannelPipeline 常用的相關方法:ChannelPipeline addFirst(ChannelHandler... handlers):把一個業務處理類(handler)添加到鏈中的第一個位置;ChannelPipeline addLast(ChannelHandler... handlers):把一個業務處理類(handler)添加到鏈中的最后一個位置。
  • ChannelHandlerContext 中包含一個具體的事件處理器 ChannelHandler,同時 ChannelHandlerContext 中也綁定了對應的 pipeline 和 Channel 的信息,方便對 ChannelHandler 進行調用。常用的方法如下:
    • ChannelFuture close():關閉通道。
    • ChannelOutboundInvoker flush():刷新。
    • ChannelFuture writeAndFlush(Object msg):將數據寫到ChannelPipeline 中當前 ChannelHandler 的下一個 ChannelHandler 開始處理(出站)。
  • Netty 在創建 Channel 實例后,一般都需要設置ChannelOption參數。
    ChannelOption 參數如下:
    • ChannelOption.SO_BACKLOG:對應 TCP/IP 協議 listen 函數中的 backlog 參數,用來初始化服務器可連接隊列大小。因為服務端處理客戶端連接請求是順序處理的,所以同一時間只能處理一個客戶端連接,多個客戶端來的時候,服務端將不能處理的客戶端連接請求放在隊列中等待處理,backlog 參數指定了隊列的大小。
    • ChannelOption.SO_KEEPALIVE:一直保持連接活動狀態。
  • EventLoopGroup是一組 EventLoop 的抽象,Netty 為了更好地利用多核 CPU 資源,一般會有多個 EventLoop 同時工作,每個 EventLoop 維護著一個 Selector 實例。
  • EventLoopGroup提供 next 接口,可以從組里面按照一定規則獲取其中一個 EventLoop 來處理任務。在 Netty 服務器端編程中,一般都需要提供兩個 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup。常用的方法如下:
    • public NioEventLoopGroup():構造方法。
    • public Future<?> shutdownGracefully():斷開連接,關閉線程。
  • 通常一個服務端口即一個 ServerSocketChannel 對應一個 Selector 和一個 EventLoop 線程。BossEventLoop 負責接收客戶端的連接并將 SocketChannel 交給 WorkerEventLoopGroup 來進行 IO 處理,如下圖所示:
  • Netty 提供一個專門用來操作緩沖區(即 Netty 的數據容器)的工具類,常用的方法:public static ByteBuf copiedBuffer(CharSequence string, Charset charset),類似于 NIO 中的 ByteBuffer 但有區別。
  • NettyByteBuf01.java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class NettyByteBuf01 {
    public static void main(String[] args) {
        //創建一個ByteBuf
        //1、創建 ByteBuf 對象,該對象包含一個數組,是一個byte[10]
        //2、在netty的buffer中,不需要使用flip進行反轉,其底層維護了 readerIndex 和 writerIndex
        //3、通過 readerIndex、writerIndex 和 capacity,將buffer分成三個區域
        //0 到 readerIndex:表示已經讀取的區域
        //readerIndex 到 writerIndex:表示可讀的區域
        //writerIndex 到 capacity:表示可寫的區域
        ByteBuf buffer = Unpooled.buffer(10);
        for (int i = 0; i < 10; i++) {
            buffer.writeByte(i);
        }
        System.out.println("capacity=" + buffer.capacity());//10
        /*for (int i = 0; i < buffer.capacity(); i++) {
            System.out.println(buffer.getByte(i));
        }*/
        for (int i = 0; i < buffer.capacity(); i++) {
            System.out.println(buffer.readByte());
        }
        System.out.println("執行完畢");
    }
}
  • NettyByteBuf02.java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.charset.StandardCharsets;
public class NettyByteBuf02 {
    public static void main(String[] args) {
        //創建ByteBuf
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", StandardCharsets.UTF_8);
        if (byteBuf.hasArray()) { // true
            byte[] content = byteBuf.array();
            //將 content 轉成字符串
            System.out.println(new String(content, StandardCharsets.UTF_8));
            System.out.println("byteBuf=" + byteBuf);
            System.out.println(byteBuf.arrayOffset()); // 0
            System.out.println(byteBuf.readerIndex()); // 0
            System.out.println(byteBuf.writerIndex()); // 12
            System.out.println(byteBuf.capacity()); // 64
            //System.out.println(byteBuf.readByte()); //注意:若讀取一個,則下面可讀取的字節數將減1
            System.out.println(byteBuf.getByte(0)); // 104
            int len = byteBuf.readableBytes(); //返回可讀的字節數 :12
            System.out.println("len=" + len);
            //使用for取出各個字節,注意:不會改變readerIndex的值
            for (int i = 0; i < len; i++) {
                System.out.print((char) byteBuf.getByte(i));
            }
            System.out.println();
            //按照某個范圍讀取
            System.out.println(byteBuf.getCharSequence(0, 4, StandardCharsets.UTF_8));
            System.out.println(byteBuf.getCharSequence(4, 6, StandardCharsets.UTF_8));
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容