Netty——Bootstrap類

Bootstrap類

引導類的層次結構包括一個抽象的父類和兩個具體的引導子類:
在這裏插入圖片描述
相對於將具體的引導類分別看作用於服務器和客戶端的引導來說,記住它們的本意是用來支撐不同的應用程序的功能的將有所裨益。也就是說,服務器致力於使用一個父 Channel 來接受來自客戶端的連接,並創建子 Channel 以用於它們之間的通信;而客戶端將最可能只需要一個 單獨的、沒有父 Channel 的 Channel 來用於所有的網絡交互。(正如同我們將要看到的,這也適用於無連接的傳輸協議,如 UDP,因爲它們並不是每個連接都需要一個單獨的 Channel。)

爲什麼引導類是 Cloneable 的?
你有時可能會需要創建多個具有類似配置或者完全相同配置的Channel。爲了支持這種模式而又不需要爲每個Channel都創建並配置一個新的引導類實例,AbstractBootstrap被標記爲了 Cloneable。在一個已經配置完成的引導類實例上調用clone()方法將返回另一個可以立即使用的引 導類實例。

注意,這種方式只會創建引導類實例的EventLoopGroup的一個淺拷貝,所以,後者將在所有克隆的Channel實例之間共享。這是可以接受的,因爲通常這些克隆的Channel的生命週期都很短暫,一 個典型的場景是——創建一個Channel以進行一次HTTP請求。

AbstractBootstrap 類的完整聲明是: public abstract class AbstractBootstrap <B extends AbstractBootstrap<B,C>,C extends Channel>
在這個簽名中,子類型 B 是其父類型的一個類型參數,因此可以返回到運行時實例的引用以
支持方法的鏈式調用(也就是所謂的流式語法)。

其子類的聲明如下:

public class Bootstrap extends AbstracBootstrap<Bootstrap,Channel>

public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap,ServerChannel>

引導客戶端和無連接協議

Bootstrap 類被用於客戶端或者使用了無連接協議的應用程序中。
在這裏插入圖片描述
在這裏插入圖片描述

引導客戶端

Bootstrap 類負責爲客戶端和使用無連接協議的應用程序創建 Channel:
在這裏插入圖片描述

public static void main(String[] args) {
        EventLoopGroup group = new NioEventLoopGroup();
        //創建一個Bootstrap類的實例以創建和連接新的客戶端Channel
        Bootstrap bootstrap = new Bootstrap();
        //設置EventLoopGroup提供用於處理Channel事件的EventLoop
        bootstrap.group(group)
                .channel(NioSocketChannel.class)//指定要使用的Channel實現
                .handler(new SimpleChannelInboundHandler<ByteBuf>() {//設置用於Channel事件和數據的ChannelInboundHandler
                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                        System.out.println("Received data");
                    }
                });
        //連接到遠程主機
        ChannelFuture future = bootstrap.connect(new InetSocketAddress("localhost", 8080));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    System.out.println("Connection established");
                } else {
                    System.out.println("Connection attempt failed");
                    future.cause().printStackTrace();
                }
            }
        });
    }

Channel和EventLoopGroup的兼容性

對於 NIO 以及 OIO 傳輸兩者來說,都有相關的 EventLoopGroup 和 Channel 實現:
在這裏插入圖片描述
必須保持這種兼容性,不能混用具有不同前綴的組件,如 NioEventLoopGroup 和 OioSocketChannel。

注意:在引導的過程中,在調用 bind()或者 connect()方法之前,必須調用以下方法來設置所需的組件:

  • group();
  • channel()或者 channelFactory();
  • handler()。

如果不這樣做,則將會導致 IllegalStateException。對 handler()方法的調用尤其重要,因爲它需要配置好 ChannelPipeline。

引導服務器

ServerBootstrap類的方法:
在這裏插入圖片描述
ServerChannel 的實現負責創建子 Channel,這些子 Channel 代表了已被接受的連接。因此,負責引導 ServerChannel 的 ServerBootstrap 提供了這些方法,以簡化將設置應用到 已被接受的子 Channel 的 ChannelConfig 的任務。

下圖展示了 ServerBootstrap 在 bind()方法被調用時創建了一個 ServerChannel, 並且該 ServerChannel 管理了多個子 Channel:
在這裏插入圖片描述

public static void main(String[] args) {
        NioEventLoopGroup group = new NioEventLoopGroup();
        //創建ServerBootstrap
        ServerBootstrap bootstrap = new ServerBootstrap();
        //設置EventLoopGroup,其提供了用於處理Channel事件的EventLoop
        bootstrap.group(group)
                .channel(NioServerSocketChannel.class)
                .childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                        System.out.println("Reveived data");
                    }
                });
        //通過配置好的ServerBootstrap的實例綁定該Channel
        ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    System.out.println("Server bound");
                } else {
                    System.out.println("Bound attempt failed");
                    future.cause().printStackTrace();
                }
            }
        });
    }

從Channel引導客戶端

假設你的服務器正在處理一個客戶端的請求,這個請求需要它充當第三方系統的客戶端。當 一個應用程序(如一個代理服務器)必須要和組織現有的系統(如 Web 服務或者數據庫)集成 時,就可能發生這種情況。在這種情況下,將需要從已經被接受的子 Channel 中引導一個客戶 端 Channel。

可以按照上面所描述的方式創建新的 Bootstrap 實例,但是這並不是最高效的解決方案,因爲它將要求你爲每個新創建的客戶端 Channel 定義另一個 EventLoop。這會產生 額外的線程,以及在已被接受的子 Channel 和客戶端 Channel 之間交換數據時不可避免的上 下文切換。

一個更好的解決方案是:通過將已被接受的子 Channel 的 EventLoop 傳遞給 Bootstrap 的 group()方法來共享該 EventLoop。因爲分配給 EventLoop 的所有 Channel 都使用同一 個線程,所以這避免了額外的線程創建,以及前面所提到的相關的上下文切換,如下圖所示:
在這裏插入圖片描述

實現 EventLoop 共享涉及通過調用 group()方法來設置 EventLoop:

public static void main(String[] args) {
        //創建ServerBootstrap以創建ServerSocketChannel,並綁定它
        ServerBootstrap bootstrap = new ServerBootstrap();
        //設置EventLoopGroup,其將提供用以處理Channel事件的EventLoop
        bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(//設置用於處理已被接收的子Channel的I/O和數據的ChannelInboundHandler
                        new SimpleChannelInboundHandler<ByteBuf>() {
                            ChannelFuture connectFuture;
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                //創建一個Bootstrap類的實例以連接到遠程主機
                                Bootstrap bootstrap = new Bootstrap();
                                //指定Channel的實現,爲入站I/O設置ChannelInboundHandler
                                bootstrap.channel(NioSocketChannel.class).handler(
                                        new SimpleChannelInboundHandler<ByteBuf>() {
                                            @Override
                                            protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                                                System.out.println("Received data");
                                            }
                                        });
                                //使用與分配給已被接受的子Channel相同的EventLoop
                                bootstrap.group(ctx.channel().eventLoop());
                                //連接到遠程端點
                                connectFuture = bootstrap.connect(
                                        new InetSocketAddress("localhost", 80));
                            }

                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                                if (connectFuture.isDone()) {
                                    //當連接完成時,執行一些數據操作(如代理)
                                }
                            }
                        });
        //通過配置好的ServerBootstrap綁定該ServerSocketChannel
        ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    System.out.println("Server bound");
                } else {
                    System.out.println("Bind attempt failed");
                    future.cause().printStackTrace();
                }
            }
        });
    }

在引導過程中添加多個ChannelHandler

在引導的過程中調用了 handler()或者 child-
Handler()方法來添加單個的 ChannelHandler。這對於簡單的應用程序來說可能已經足夠了,但是它不能滿足更加複雜的需求。例如,一個必須要支持多種協議的應用程序將會有很多的 ChannelHandler,而不會是一個龐大而又笨重的類。

正如你經常所看到的一樣,你可以根據需要,通過在 ChannelPipeline 中將它們鏈接在一起來 部署儘可能多的 ChannelHandler。但是,如果在引導的過程中你只能設置一個 ChannelHandler, 那麼你應該怎麼做到這一點呢?
正是針對於這個用例,Netty 提供了一個特殊的 ChannelInboundHandlerAdapter 子類:public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter

它定義了下面的方法:
protected abstract void initChannel(C ch) throws Exception;

這個方法提供了一種將多個 ChannelHandler 添加到一個 ChannelPipeline 中的簡便 方法。你只需要簡單地向 Bootstrap 或 ServerBootstrap 的實例提供你的 ChannelInitializer 實現即可,並且一旦 Channel 被註冊到了它的 EventLoop 之後,就會調用你的 initChannel()版本。在該方法返回之後,ChannelInitializer 的實例將會從 ChannelPipeline 中移除它自己。

下面代碼定義了ChannelInitializerImpl 類 , 並 通 過 ServerBootstrap 的 childHandler()方法註冊它。你可以看到,這個看似複雜的操作實際上是相當簡單直接的:

public class ChannelInitializerDemo {
    public static void main(String[] args) throws InterruptedException {
        //創建ServerBootstrap以創建和綁定新的Channel
        ServerBootstrap bootstrap = new ServerBootstrap();
        //設置EventLoopGroup,其將提供用以處理Channel事件的EventLoop
        bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializerImpl());
        ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
        future.sync();
    }
}

final class ChannelInitializerImpl extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new HttpClientCodec());
        pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
    }
}

如果你的應用程序使用了多個 ChannelHandler,請定義你自己的 ChannelInitializer 實現來將它們安裝到 ChannelPipeline 中。

使用Netty的ChannelOption和屬性

在每個 Channel 創建時都手動配置它可能會變得相當乏味。幸運的是,你不必這樣做。相 反,你可以使用 option()方法來將 ChannelOption 應用到引導。你所提供的值將會被自動 應用到引導所創建的所有 Channel。可用的 ChannelOption 包括了底層連接的詳細信息,如 keep-alive 或者超時屬性以及緩衝區設置。

Netty 應用程序通常與組織的專有軟件集成在一起,而像 Channel 這樣的組件可能甚至會在 正常的 Netty 生命週期之外被使用。在某些常用的屬性和數據不可用時,Netty 提供了 AttributeMap 抽象(一個由 Channel 和引導類提供的集合)以及 AttributeKey(一 個用於插入和獲取屬性值的泛型類)。使用這些工具,便可以安全地將任何類型的數據項與客戶 端和服務器 Channel(包含 ServerChannel 的子 Channel)相關聯了。

例如,考慮一個用於跟蹤用戶和 Channel 之間的關係的服務器應用程序。這可以通過將用 戶的 ID 存儲爲 Channel 的一個屬性來完成。類似的技術可以被用來基於用戶的 ID 將消息路由 給用戶,或者關閉活動較少的 Channel。

下面的代碼展示了可以如何使用 ChannelOption 來配置 Channel,以及如果使用屬性 來存儲整型值:

public static void main(String[] args) {
        //創建一個AttributeKey以標識該屬性
        final AttributeKey<Integer> id = new AttributeKey<Integer>("ID");
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new SimpleChannelInboundHandler<ByteBuf>() {
                    @Override
                    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
                        //使用 AttributeKey 檢索 屬性以及它的值
                        Integer idValue = ctx.channel().attr(id).get();
                        //do something with the idValue
                    }

                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                        System.out.println("Received data");
                    }
                });
        // 設置 ChannelOption, 其將在 connect()或者 bind()方法被調用時 被設置到已經創建的 Channel 上
        bootstrap.option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
        //存儲該id屬性
        bootstrap.attr(id, 123456);
        ChannelFuture future = bootstrap.connect(new InetSocketAddress("localhost", 80));
        future.syncUninterruptibly();
    }

引導DatagramChannel

前面的引導代碼示例使用的都是基於 TCP 協議的 SocketChannel,但是 Bootstrap 類 也可以被用於無連接的協議。爲此,Netty 提供了各種 DatagramChannel 的實現。唯一區別就 是,不再調用 connect()方法,而是隻調用 bind()方法:

public static void main(String[] args) {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(new OioEventLoopGroup())
                .channel(OioDatagramChannel.class)
                .handler(
                        new SimpleChannelInboundHandler<DatagramPacket>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
                                //Do somethind with the packet       
                            }
                        });
        //調用bind方法,因爲該協議是無連接的
        ChannelFuture future = bootstrap.bind(new InetSocketAddress(0));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess())
                    System.out.println("Channel bound");
                else {
                    System.out.println("Bind attempt failed");
                    future.cause().printStackTrace();
                }
            }
        });
    }

關閉

引導使你的應用程序啓動並且運行起來,但是遲早你都需要優雅地將它關閉。當然,你也可 以讓 JVM 在退出時處理好一切,但是這不符合優雅的定義,優雅是指乾淨地釋放資源。關閉 Netty 應用程序並沒有太多的魔法,但是還是有些事情需要記在心上。

最重要的是,你需要關閉 EventLoopGroup,它將處理任何掛起的事件和任務,並且隨後 釋放所有活動的線程。這就是調用EventLoopGroup.shutdownGracefully()方法的作用。 這個方法調用將會返回一個 Future,這個 Future 將在關閉完成時接收到通知。需要注意的是, shutdownGracefully()方法也是一個異步的操作,所以你需要阻塞等待直到它完成,或者向 所返回的 Future 註冊一個監聽器以在關閉完成時獲得通知。

//優雅的關閉
EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(NioSocketChannel.class);
        ...
        // shutdownGracefully()方法將釋放 所有的資源,並且關閉所有的當 前正在使用中的 Channel
        Future<?> future = group.shutdownGracefully();
        //block until the group hai shutdown
        future.syncUninterruptibly();

或者,你也可以在調用 EventLoopGroup.shutdownGracefully()方法之前,顯式地 在所有活動的 Channel 上調用 Channel.close()方法。但是在任何情況下,都請記得關閉 EventLoopGroup 本身。

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