写点什么

netty 系列之:Bootstrap,ServerBootstrap 和 netty 中的实现

作者:程序那些事
  • 2022 年 2 月 10 日
  • 本文字数:6848 字

    阅读完需:约 22 分钟

netty系列之:Bootstrap,ServerBootstrap和netty中的实现

简介

虽然 netty 很强大,但是使用 netty 来构建程序却是很简单,只需要掌握特定的 netty 套路就可以写出强大的 netty 程序。每个 netty 程序都需要一个 Bootstrap,什么是 Bootstrap 呢?Bootstrap 翻译成中文来说就是鞋拔子,在计算机世界中,Bootstrap 指的是引导程序,通过 Bootstrap 可以轻松构建和启动程序。


在 netty 中有两种 Bootstrap:客户端的 Bootstrap 和服务器端的 ServerBootstrap。两者有什么不同呢?netty 中这两种 Bootstrap 到底是怎么工作的呢? 一起来看看吧。

Bootstrap 和 ServerBootstrap 的联系

首先看一下 Bootstrap 和 ServerBootstrap 这两个类的继承关系,如下图所示:


<img src="https://img-blog.csdnimg.cn/4eda7812e7eb4825aa471bbc679e7cad.png" style="zoom:67%;" />


可以看到 Bootstrap 和 ServerBootstrap 都是继承自 AbstractBootstrap,而 AbstractBootstrap 则是实现了 Cloneable 接口。

AbstractBootstrap

有细心的同学可能会问了,上面图中还有一个 Channel,channel 跟 AbstractBootstrap 有什么关系呢?


我们来看下 AbstractBootstrap 的定义:


public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable
复制代码


AbstractBootstrap 接受两个泛型参数,一个是 B 继承自 AbstractBootstrap,一个是 C 继承自 Channel。


我们先来观察一下一个简单的 Bootstrap 启动需要哪些元素:


EventLoopGroup bossGroup = new NioEventLoopGroup();        EventLoopGroup workerGroup = new NioEventLoopGroup();        try {            ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup, workerGroup)                    .channel(NioServerSocketChannel.class)                    .childHandler(new ChannelInitializer<SocketChannel>() {                        @Override                        public void initChannel(SocketChannel ch) throws Exception {                            ch.pipeline().addLast(new FirstServerHandler());                        }                    })                    .option(ChannelOption.SO_BACKLOG, 128)                    .childOption(ChannelOption.SO_KEEPALIVE, true);
// 绑定端口并开始接收连接 ChannelFuture f = b.bind(port).sync(); // 等待server socket关闭 f.channel().closeFuture().sync();
复制代码


上面的代码是一个最基本也是最标准的 netty 服务器端的启动代码。可以看到和 Bootstrap 相关的元素有这样几个:


  1. EventLoopGroup,主要用来进行 channel 的注册和遍历。

  2. channel 或者 ChannelFactory,用来指定 Bootstrap 中使用的 channel 的类型。

  3. ChannelHandler,用来指定具体 channel 中消息的处理逻辑。

  4. ChannelOptions,表示使用的 channel 对应的属性信息。

  5. SocketAddress,bootstrap 启动是绑定的 ip 和端口信息。


目前看来和 Bootstrap 相关的就是这 5 个值,而 AbstractBootstrap 的构造函数中也就定义了这些属性的赋值:


    AbstractBootstrap(AbstractBootstrap<B, C> bootstrap) {        group = bootstrap.group;        channelFactory = bootstrap.channelFactory;        handler = bootstrap.handler;        localAddress = bootstrap.localAddress;        synchronized (bootstrap.options) {            options.putAll(bootstrap.options);        }        attrs.putAll(bootstrap.attrs);    }
复制代码


示例代码中的 group,channel,option 等方法实际上都是向这些属性中赋值,并没有做太多的业务操作。


注意,AbstractBootstrap 中只存在一个 group 属性,所以两个 group 属性是在 ServerBootstrap 中添加的扩展属性。


在 Bootstrap 中,channel 其实是有两种赋值方法,一种是直接传入 channel,另外一种方法是传入 ChannelFactory。两者的本质都是一样的,我们看下 channel 是怎么转换成为 ChannelFactory 的:


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


channelClass 被封装在一个 ReflectiveChannelFactory 中,最终还是设置的 channelFactory 属性。


AbstractBootstrap 中真正启动服务的方法就是 bind,bind 方法传入的是一个 SocketAddress,返回的是 ChannelFuture,很明显,bind 方法中会创建一个 channel。我们来看一下 bind 方法的具体实现:


   private ChannelFuture doBind(final SocketAddress localAddress) {        final ChannelFuture regFuture = initAndRegister();        final Channel channel = regFuture.channel();        if (regFuture.cause() != null) {            return regFuture;        }
if (regFuture.isDone()) { // At this point we know that the registration was complete and successful. ChannelPromise promise = channel.newPromise(); doBind0(regFuture, channel, localAddress, promise); return promise; } else { // Registration future is almost always fulfilled already, but just in case it's not. final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause = future.cause(); if (cause != null) { // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an // IllegalStateException once we try to access the EventLoop of the Channel. promise.setFailure(cause); } else { // Registration was successful, so set the correct executor to use. // See https://github.com/netty/netty/issues/2586 promise.registered();
doBind0(regFuture, channel, localAddress, promise); } } }); return promise; } }
复制代码


在 doBind 方法中,首先调用 initAndRegister 方法去初始化和注册一个 channel。


channel 是通过 channelFactory 的 newChannel 方法来创建的:


channel = channelFactory.newChannel();
复制代码


接着调用初始化 channel 的 init 方法。这个 init 方法在 AbstractBootstrap 中并没有实现,需要在具体的实现类中实现。


有了 channel 之后,通过调用 EventLoopGroup 的 register 方法将 channel 注册到 EventLoop 中,并将注册生成的 ChannelFuture 返回。


然后通过判断返回的 regFuture 的状态,来判断 channel 是否注册成功,如果注册成功,最后调用 doBind0 方法,完成最后的绑定工作:


    private static void doBind0(            final ChannelFuture regFuture, final Channel channel,            final SocketAddress localAddress, final ChannelPromise promise) {
// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up // the pipeline in its channelRegistered() implementation. channel.eventLoop().execute(new Runnable() { @Override public void run() { if (regFuture.isSuccess()) { channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } else { promise.setFailure(regFuture.cause()); } } }); }
复制代码


因为 eventLoop 本身是一个 Executor,所以可以执行一个具体的命令的,在它的 execute 方法中,传入了一个新的 Runnable 对象,在其中的 run 方法中执行了 channel.bind 方法,将 channel 跟 SocketAddress 进行绑定。


到此,Bootstrap 的 bind 方法执行完毕。


我们再来回顾一下 bind 方法的基本流程:


  1. 通过 ChannelFactory 创建一个 channel。

  2. 将 channel 注册到 Bootstrap 中的 EventLoopGroup 中。

  3. 如果 channel 注册成功,则调用 EventLoopGroup 的 execute 方法,将 channel 和 SocketAddress 进行绑定。


是不是很清晰?


讲完 AbstractBootstrap,接下来,我们再继续探讨一下 Bootstrap 和 ServerBootstrap。

Bootstrap 和 ServerBootstrap

首先来看下 Bootstrap,Bootstrap 主要使用在客户端使用,或者 UDP 协议中。


先来看下 Bootstrap 的定义:


public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> 
复制代码


Bootstrap 和 AbstractBootstrap 相比,主要多了一个属性和一个方法。


多的一个属性是 resolver:


private static final AddressResolverGroup<?> DEFAULT_RESOLVER = DefaultAddressResolverGroup.INSTANCE;
private volatile AddressResolverGroup<SocketAddress> resolver = (AddressResolverGroup<SocketAddress>) DEFAULT_RESOLVER;
复制代码


AddressResolverGroup 里面有一个 IdentityHashMap,它的 key 是 EventExecutor,value 是 AddressResolver:


    private final Map<EventExecutor, AddressResolver<T>> resolvers =            new IdentityHashMap<EventExecutor, AddressResolver<T>>();
复制代码


实际上 AddressResolverGroup 维护了一个 EventExecutor 和 AddressResolver 的映射关系。


AddressResolver 主要用来解析远程的 SocketAddress 的地址。因为远程的 SocketAddress 可能并不是一个 IP 地址,所以需要使用 AddressResolver 解析一下。


这里的 EventExecutor 实际上就是 channel 注册的 EventLoop。


另外 Bootstrap 作为一个客户端的应用,它需要连接到服务器端,所以 Bootstrap 类中多了一个 connect 到远程 SocketAddress 的方法:


    public ChannelFuture connect(SocketAddress remoteAddress) {        ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");        validate();        return doResolveAndConnect(remoteAddress, config.localAddress());    }
复制代码


connect 方法和 bind 方法的逻辑类似,只是多了一个 resolver 的 resolve 过程。


解析完毕之后,会调用 doConnect 方法,进行真正的连接:


    private static void doConnect(            final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {
// This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up // the pipeline in its channelRegistered() implementation. final Channel channel = connectPromise.channel(); channel.eventLoop().execute(new Runnable() { @Override public void run() { if (localAddress == null) { channel.connect(remoteAddress, connectPromise); } else { channel.connect(remoteAddress, localAddress, connectPromise); } connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } }); }
复制代码


可以看到 doConnect 方法和 doBind 方法很类似,都是通过当前 channel 注册的 eventLoop 来执行 channel 的 connect 或许 bind 方法。


再看一下 ServerBootstrap 的定义:


public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel>
复制代码


因为是 ServerBootstrap 用在服务器端,所以不选 Bootstrap 那样去解析 SocketAddress,所以没有 resolver 属性。


但是对应服务器端来说,可以使用 parent EventLoopGroup 来接受连接,然后使用 child EventLoopGroup 来执行具体的命令。所以在 ServerBootstrap 中多了一个 childGroup 和对应的 childHandler:


    private volatile EventLoopGroup childGroup;    private volatile ChannelHandler childHandler;
复制代码


因为 ServerBootstrap 有两个 group,所以 ServerBootstrap 包含一个含有两个 EventLoopGroup 的 group 方法:


    public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) 
复制代码


还记得 bind 方法需要实现的 init 方法吗? 我们看下 ServerBootstrap 中 init 的具体逻辑:


   void init(Channel channel) {        setChannelOptions(channel, newOptionsArray(), logger);        setAttributes(channel, newAttributesArray());
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup; final ChannelHandler currentChildHandler = childHandler; final Entry<ChannelOption<?>, Object>[] currentChildOptions = newOptionsArray(childOptions); final Entry<AttributeKey<?>, Object>[] currentChildAttrs = newAttributesArray(childAttrs);
p.addLast(new ChannelInitializer<Channel>() { @Override public void initChannel(final Channel ch) { final ChannelPipeline pipeline = ch.pipeline(); ChannelHandler handler = config.handler(); if (handler != null) { pipeline.addLast(handler); }
ch.eventLoop().execute(new Runnable() { @Override public void run() { pipeline.addLast(new ServerBootstrapAcceptor( ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); } }); } }); }
复制代码


首先是设置 channel 的一些属性,然后通过 channel.pipeline 方法获得 channel 对应的 pipeline,然后向 pipeline 中添加 channelHandler。


这些都是常规操作,我们要注意的是最后通过 channel 注册到的 eventLoop,将 ServerBootstrapAcceptor 加入到了 pipeline 中。


很明显 ServerBootstrapAcceptor 本身应该是一个 ChannelHandler,它的主要作用就是用来接受连接:


    private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter
复制代码


我们来看一下它的 channelRead 方法:


        public void channelRead(ChannelHandlerContext ctx, Object msg) {            final Channel child = (Channel) msg;
child.pipeline().addLast(childHandler);
setChannelOptions(child, childOptions, logger); setAttributes(child, childAttrs);
try { childGroup.register(child).addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { forceClose(child, future.cause()); } } }); } catch (Throwable t) { forceClose(child, t); } }
复制代码


因为 server 端接受的是客户端 channel 的 connect 操作,所以对应的 channelRead 中的对象实际上是一个 channel。这里把这个接受到的 channel 称作 child。通过给这个 child channel 添加 childHandler,childOptions 和 childAttrs,一个能够处理 child channel 请求的逻辑就形成了。


最后将 child channel 注册到 childGroup 中,至此整个 ServerBootstrapAcceptor 接受 channel 的任务就完成了。


这里最妙的部分就是将客户端的 channel 通过 server 端的 channel 传到 server 端,然后在 server 端为 child channel 配备 handler 进行具体的业务处理,非常巧妙。

总结

通过具体分析 AbstractBootstrap,Bootstrap 和 ServerBootstrap 的结构和实现逻辑,相信大家对 netty 服务的启动流程有了大概的认识,后面我们会详细讲解 netty 中的 channel 和非常重要的 eventLoop。


本文已收录于 http://www.flydean.com/03-1-netty-boots…-serverbootstrap/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 18 小时前阅读数: 18
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:Bootstrap,ServerBootstrap和netty中的实现