Read book Netty in action(Chapter IX)--Bootstrapping

序言

在深入地学习了ChannelPipeline、ChannelHandler 和EventLoop 之后,你接下来的问题可能是:“如何将这些部分组织起来,成为一个可实际运行的应用程序呢?”
答案是?“引导”(Bootstrapping)。到目前为止,我们对这个术语的使用还比较含糊,现在已经到了精确定义它的时候了。简单来说,引导一个应用程序是指对它进行配置,并使它运行起来的过程—尽管该过程的具体细节可能并不如它的定义那样简单,尤其是对于一个网络应用程序来说。
和它对应用程序体系架构的做法②一致,Netty处理引导的方式使你的应用程序和网络层相隔离,无论它是客户端还是服务器。正如同你将要看到的,所有的框架组件都将会在后台结合在一起并且启用。引导是我们一直以来都在组装的完整拼图中缺失的那一块。当你把它放到正确的位置上时,你的Netty应用程序就完整了。
以前阅读spring源码的时候,Spring对功能的隔离,完美的调用方式,简单的使用都是我们应该追求的。如果说阅读过这些源码之后再去读任意一款框架(前提是已知其大概运行方式,或者看过官方文档)源码,那事情将会变得很简单。直到现在2023年了,在你决定深耕JAVA底层调用(非C端API调用师)的时候,我还是推荐去读Spring源码。很多人会用某一款框架,例如Mybatis,之后就认为自己懂了(甚至连Mybatis四大对象,调用过程一概不知),确实你对调用mybatis API或者其他框架也好,都非常熟练,但是如果换一个类似的框架,可能就一头雾水,重新学习了。我曾经反复说过这个问题,因为我知道重要性。我自己也是一直贯彻这个理念除非职业生涯结束。

Bootstrap 类

引导类的层次结构包括一个抽象的父类和两个具体的引导子类,这个层次在任何一款高性能高可用高稳定框架都是常见的,一个Abstractxxx,加上几个实现类,非常常见的。那bootstrap类就是ServerBootStrap和Bootstrap顾名思义,一个是服务端使用,一个是客户端使用。它们的本意是用来支撑不同的应用程序的功能的将有所裨益。也就是说,服务器致力于使用一个父Channel 来接受来自客户端的连接,并创建子Channel 以用于它们之间的通信;而客户端将最可能只需要一个单独的、没有父Channel 的Channel 来用于所有的网络交互。我们在前面学习的几个Netty 组件都参与了引导的过程,而且其中一些在客户端和服务器都有用到。两种应用程序类型之间通用的引导步骤由AbstractBootstrap 处理,而特定于客户端或者服务器的引导步骤则分别由Bootstrap 或ServerBootstrap 处理。这就是模板模式的典型体现,参照spring中的refresh方法,立马融汇贯通。

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
 // ... do sonmething
}

通过这个泛型,应该就能大概猜到一些东西,例如,它肯定是关联了两个引导,然后还关联了Channel

   public B channel(Class<? extends C> channelClass) {
        return channelFactory(new ReflectiveChannelFactory<C>(
                ObjectUtil.checkNotNull(channelClass, "channelClass")
        ));
    }

通过这个方法就可以看出来 比如:Server关联ServerChannel,然后Clinet关联一个属于Clinet的Channel。有时候如果有多个配置相同的Channel那可能会要创建多个BootStrap,所以它实现了是Cloneable,所以它可以实现浅拷贝(Light Copy)。这是共享状态的,幸运的是克隆的Channel生命周期都比较短暂,参考一个HTTP请求一个Channel。

引导客户端和无连接协议

Bootstrap 类被用于客户端或者使用了无连接协议的应用程序中。

Bootstrap group(EventLoopGroup) 设置 事件组
Bootstrap channel(Class<? extends C>)
Bootstrap channelFactory( ChannelFactory<? extends C>)
channel()方法指定了Channel的实现类。如果该实现类没提供默认的构造函数,可以通过调用channelFactory()方法来指定一个工厂类,它将会被bind()方法调用。一般直接指定Channel实现类。
Bootstrap localAddress(SocketAddress) 指定Channel 应该绑定到的本地地址。如果没有指定,则将由操作系统创建一个随机的地址。或者,也可以通过bind()或者connect()方法指定localAddress
Bootstrap option(ChannelOption option,T value) 类似于配置,设置ChannelOption,其将被应用到每个新创建的Channel 的ChannelConfig。这些选项将会通过bind()或者connect()方法设置到Channel,不管哪个先被调用。这个方法在Channel 已经被创建后再调用将不会有任何的效果。这个已经验证过,之前对netty不熟,网上找的案例,发现配置无效,请避开这个坑支持的ChannelOption 取决于使用的Channel 类型。
Bootstrap attr(Attribute key, T value) 指定新创建的Channel 的属性值。这些属性值是通过bind()或者connect()方法设置到Channel 的,具体取决于谁最先被调用。这个方法在Channel 被创建后将不会有任何的效果。
Bootstrap handler(ChannelHandler) 这个比较简单,设置将被添加到ChannelPipeline 以接收事件通知的ChannelHandler
Bootstrap clone() 创建一个当前Bootstrap 的克隆,其具有和原始的Bootstrap 相同的设置信息
Bootstrap remoteAddress(SocketAddress) 设置远程地址。或者,也可以通过connect()方法来指定它。
ChannelFuture connect() 连接到远程节点并返回一个ChannelFuture,其将会在连接操作完成后接收到通知
ChannelFuture bind() 绑定Channel 并返回一个ChannelFuture,其将会在绑定操作完成后接收到通知,在那之后必须调用Channel.connect()方法来建立连接。我们将将一步一步地讲解客户端的引导过程。我们也将讨论在选择可用的组件实现时保持兼容性的问题。

引导客户端

Bootstrap 类负责为客户端和使用无连接协议的应用程序创建Channel。BootStrap类将会在bind方法调用之后创造一个Channel,在这之后将会调用Connect方法以建立连接,在connect方法调用之后,将会创建一个新的Channel。

 public static void main(String[] args) {
        BootstrapClient client = new BootstrapClient();
        client.bootstrap();
    }
    public void bootstrap() {
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        // group
        bootstrap.group(eventExecutors).channel(NioSocketChannel.class).handler(new SimpleChannelInboundHandler<ByteBuf>() {
            @Override
            protected void channelRead0(
                    ChannelHandlerContext channelHandlerContext,
                    ByteBuf byteBuf) throws Exception {
                System.out.println("Received data");
            }
        });
        ChannelFuture future = bootstrap.connect(
                new InetSocketAddress("xxx", 80));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture)
                    throws Exception {
                if (channelFuture.isSuccess()) {
                    System.out.println("Connection established");
                } else {
                    System.err.println("Connection attempt failed");
                    channelFuture.cause().printStackTrace();
                }
            }
        });
    }

这是一个用例,之前也写过的。

Channel 和EventLoopGroup 的兼容性

Nio、Oio是不能混用的,混用是要出事情的。例如下面有个反面教材:

public static void main(String args[]) {
        InvalidBootstrapClient client = new InvalidBootstrapClient();
        client.bootstrap();
    }

  
    public void bootstrap() {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(group)
                .channel(OioSocketChannel.class)
                .handler(new SimpleChannelInboundHandler<ByteBuf>() {
                    @Override
                    protected void channelRead0(
                            ChannelHandlerContext channelHandlerContext,
                            ByteBuf byteBuf) throws Exception {
                        System.out.println("Received data");
                    }
                });
        ChannelFuture future = bootstrap.connect(
                new InetSocketAddress("xxx", 80));
        future.syncUninterruptibly();
    }

这个案例在于混用了Nio和Oio的问题,导致了报异常的错误。

引导服务器

我们将从ServerBootstrap API 的概要视图开始我们对服务器引导过程的概述。然后,我们将会学习引导服务器过程中所涉及的几个步骤,以及几个相关的主题,包含从一个ServerChannel 的子Channel 中引导一个客户端这样的特殊情况。

ServerBootstrap 类

ServerBootstrap 类的方法。

group 设置ServerBootstrap 要用的EventLoopGroup。这个EventLoopGroup将用于ServerChannel 和被接受的子Channel 的I/O 处理
channel 设置将要被实例化的ServerChannel 类
channelFactory 如果不能通过默认的构造函数①创建Channel,那么可以提供一个ChannelFactory
localAddress 指定ServerChannel 应该绑定到的本地地址。如果没有指定,则将由操作系统使用一个随机地址。或者,可以通过bind()方法来指定该localAddress
option 指定要应用到新创建的ServerChannel 的ChannelConfig 的ChannelOption。这些选项将会通过bind()方法设置到Channel。在bind()方法被调用之后,设置或者改变ChannelOption 都不会有任何的效果。所支持的ChannelOption 取决于所使用的Channel 类型。
childOption 指定当子Channel 被接受时,应用到子Channel 的ChannelConfig 的ChannelOption。所支持的ChannelOption 取决于所使用的Channel 的类型。参见正在使用的ChannelConfig 的API 文档
attr 指定ServerChannel 上的属性,属性将会通过bind()方法设置给Channel。在调用bind()方法之后改变它们将不会有任何的效果
childAttr 将属性设置给已经被接受的子Channel。接下来的调用将不会有任何的效果
handler 设置被添加到ServerChannel 的ChannelPipeline 中的ChannelHandler。更加常用的方法参见childHandler()
childHandler 设置将被添加到已被接受的子Channel 的ChannelPipeline 中的ChannelHandler。handler()方法和childHandler()方法之间的区别是:前者所添加的ChannelHandler 由接受子Channel 的ServerChannel 处理,而childHandler()方法所添加的ChannelHandler 将由已被接受的子Channel处理,其代表一个绑定到远程节点的套接字
clone 克隆一个设置和原始的ServerBootstrap 相同的ServerBootstrap
bind 绑定ServerChannel 并且返回一个ChannelFuture,其将会在绑定操作完成后收到通知(带着成功或者失败的结果)

引导服务器

childHandler()、childAttr()和childOption()这些调用支持特别用于服务器应用程序的操作。具体来说,ServerChannel 的实现负责创建子Channel,这些子Channel 代表了已被接受的连接。因此,负责引导ServerChannel 的ServerBootstrap 提供了这些方法,以简化将设置应用到已被接受的子Channel 的ChannelConfig 的任务。当bind方法被调用时,将会创建一个ServerChannel,并且ServerChannel管理了多个子Channel

public void server() {
        NioEventLoopGroup group = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(group);
        bootstrap.channel(NioServerSocketChannel.class)
                .childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
                    @Override
                    protected void channelRead0(ChannelHandlerContext ctx,
                                                ByteBuf byteBuf) throws Exception {
                        System.out.println("Received data");
                    }
                });
        ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture)
                    throws Exception {
                if (channelFuture.isSuccess()) {
                    System.out.println("Server bound");
                } else {
                    System.err.println("Bound attempt failed");
                    channelFuture.cause().printStackTrace();
                }
            }
        });
    }
从Channel 引导客户端

假设你的服务器正在处理一个客户端的请求,这个请求需要它充当第三方系统的客户端。当
一个应用程序(如一个代理服务器)必须要和组织现有的系统(如Web 服务或者数据库)集成时,就可能发生这种情况。在这种情况下,将需要从已经被接受的子Channel 中引导一个客户端Channel。
你可以创建新的Bootstrap 实例,但是这并不是最高效的解决方案,因为它将要求你为每个新创建的客户端Channel 定义另一个EventLoop。这会产生额外的线程,以及在已被接受的子Channel 和客户端Channel 之间交换数据时不可避免的上下文切换。
一个更好的解决方案是:通过将已被接受的子Channel 的EventLoop 传递给Bootstrap的group()方法来共享该EventLoop。因为分配给EventLoop 的所有Channel 都使用同一个线程,所以这避免了额外的线程创建,以及前面所提到的相关的上下文切换。这个共享的解决方案是:在bind方法被创建的时候,ServerBootStrap创建了一个新的ServerChannel,ServerChannel接受新的连接,并且创建子Channel来处理他,为已接受的连接创建子Channel,由子Channel创建的BootStrap实例将在connect调用时创建新的Channel,新的Channel连接到了远程节点,EventLoop在ServerChannel创建的子Channel和Channel的Connect方法创建的Channel之间共享。实现EventLoop 共享涉及通过调用group()方法来设置EventLoop

    public void bootstrap() {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(
                        new SimpleChannelInboundHandler<ByteBuf>() {
                            ChannelFuture connectFuture;
                            @Override
                            public void channelActive(ChannelHandlerContext ctx)
                                    throws Exception {
                                Bootstrap bootstrap = new Bootstrap();
                                bootstrap.channel(NioSocketChannel.class).handler(
                                        new SimpleChannelInboundHandler<ByteBuf>() {
                                            @Override
                                            protected void channelRead0(
                                                    ChannelHandlerContext ctx, ByteBuf in)
                                                    throws Exception {
                                                System.out.println("Received data");
                                            }
                                        });
                                bootstrap.group(ctx.channel().eventLoop());
                                connectFuture = bootstrap.connect(
                                        new InetSocketAddress("xxx", 80));
                            }

                            @Override
                            protected void channelRead0(
                                    ChannelHandlerContext channelHandlerContext,
                                    ByteBuf byteBuf) throws Exception {
                                if (connectFuture.isDone()) {
                                }
                            }
                        });
        ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture)
                    throws Exception {
                if (channelFuture.isSuccess()) {
                    System.out.println("Server bound");
                } else {
                    System.err.println("Bind attempt failed");
                    channelFuture.cause().printStackTrace();
                }
            }
        });
    }
在引导过程中添加多个ChannelHandler

我们都在引导的过程中调用了handler()或者childHandler()方法来添加单个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 中移除它自己。

public class BootstrapWithInitializer {
    public void bootstrap() throws InterruptedException {
        ServerBootstrap bootstrap = new ServerBootstrap();
        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));
    }
}
使用Netty 的ChannelOption 和属性

在每个Channel 创建时都手动配置它可能会变得相当乏味。幸运的是,你不必这样做。相反,你可以使用option()方法来将ChannelOption 应用到引导。你所提供的值将会被自动应用到引导所创建的所有Channel。可用的ChannelOption 包括了底层连接的详细信息,如keep-alive 或者超时属性以及缓冲区设置。Netty 应用程序通常与组织的专有软件集成在一起,而像Channel 这样的组件可能甚至会在正常的Netty 生命周期之外被使用。在某些常用的属性和数据不可用时,Netty 提供了AttributeMap 抽象(一个由Channel 和引导类提供的集合)以及AttributeKey(一个用于插入和获取属性值的泛型类)。使用这些工具,便可以安全地将任何类型的数据项与客户端和服务器Channel(包含ServerChannel 的子Channel)相关联了。例如,考虑一个用于跟踪用户和Channel 之间的关系的服务器应用程序。这可以通过将用户的ID 存储为Channel 的一个属性来完成。类似的技术可以被用来基于用户的ID 将消息路由给用户,或者关闭活动较少的Channel。

  public static void main(String[] args) {
        final AttributeKey<Integer> id = AttributeKey.newInstance("ID");
        Bootstrap bootstrap = new Bootstrap();
        NioEventLoopGroup group = new NioEventLoopGroup();
        bootstrap.group(group);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.handler(new SimpleChannelInboundHandler<ByteBuf>() {

            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                super.channelRead(ctx, msg);
            }

            @Override
            protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
                System.out.println("Received data");
            }

            @Override
            public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
                Integer idValue = ctx.channel().attr(id).get();
            }
        });
        bootstrap.option(ChannelOption.SO_KEEPALIVE,true)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
        bootstrap.attr(id, 123456);
        ChannelFuture future = bootstrap.connect(
                new InetSocketAddress("xxxxxxxx", 80));
        future.syncUninterruptibly();
    }
引导DatagramChannel

前面的引导代码示例使用的都是基于TCP 协议的SocketChannel,但是Bootstrap 类也可以被用于无连接的协议。为此,Netty 提供了各种DatagramChannel 的实现。唯一区别就是,不再调用connect()方法,而是只调用bind()方法.

 public void bootstrap(){
        Bootstrap bootstrap = new Bootstrap();
        OioEventLoopGroup oioEventLoopGroup = new OioEventLoopGroup();
        bootstrap.group(oioEventLoopGroup).channel(OioSocketChannel.class)
                .handler(
                        new SimpleChannelInboundHandler<DatagramPacket>() {
                            @Override
                            public void channelRead0(ChannelHandlerContext ctx,
                                                     DatagramPacket msg) throws Exception {
                                // Do something with the packet
                            }
                        }
                );
        ChannelFuture future = bootstrap.bind(new InetSocketAddress(0));
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture)
                    throws Exception {
                if (channelFuture.isSuccess()) {
                    System.out.println("Channel bound");
                } else {
                    System.err.println("Bind attempt failed");
                    channelFuture.cause().printStackTrace();
                }
            }
        });
    }
关闭

引导使你的应用程序启动并且运行起来,但是迟早你都需要优雅地将它关闭。当然,你也可以让JVM 在退出时处理好一切,但是这不符合优雅的定义,优雅是指干净地释放资源。关Netty应用程序并没有太多的魔法,但是还是有些事情需要记在心上。最重要的是,你需要关闭EventLoopGroup,它将处理任何挂起的事件和任务,并且随后释放所有活动的线程。这就是调用EventLoopGroup.shutdownGracefully()方法的作用。这个方法调用将会返回一个Future,这个Future 将在关闭完成时接收到通知。需要注意的是,shutdownGracefully()方法也是一个异步的操作,所以你需要阻塞等待直到它完成,或者向所返回的Future 注册一个监听器以在关闭完成时获得通知。

       Future<?> future1 = oioEventLoopGroup.shutdownGracefully();
        // block until the group has shutdown
        future1.syncUninterruptibly();

结束语

这次,我们学习了如何引导Netty 服务器和客户端应用程序,包括那些使用无连接协议的应用程序。我们也涵盖了一些特殊情况,包括在服务器应用程序中引导客户端Channel,以及使用ChannelInitializer 来处理引导过程中的多个ChannelHandler 的安装。你看到了如何设置Channel 的配置选项,以及如何使用属性来将信息附加到Channel。最后,你学习了如何优雅地关闭应用程序,以有序地释放所有的资源。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值