Netty之Bootstrap客户端的启动流程

934 阅读5分钟

首先来看下官方的example中echo的例子

// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
    Bootstrap b = new Bootstrap();
    b.group(group)
     .channel(NioSocketChannel.class)
     .option(ChannelOption.TCP_NODELAY, true)
     .handler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch) throws Exception {
             ChannelPipeline p = ch.pipeline();
             if (sslCtx != null) {
                 p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
             }
             //p.addLast(new LoggingHandler(LogLevel.INFO));
             p.addLast(new EchoClientHandler());
         }
     });

    // Start the client.
    ChannelFuture f = b.connect(HOST, PORT).sync();

    // Wait until the connection is closed.
    f.channel().closeFuture().sync();
  1. 配置客户端的EventLoopGroup.
  2. 设置channel类型为NioSocketChannel.
  3. 设置channelOption.
  4. 设置ChannelHandler,进行初始化Channel,往pipeline中添加EchoClientHandler处理器.
  5. 调用connect建立socket的连接.
  6. 等待客户端关闭.

NioSocketChannel的初始化

1.主要初始化三个重要变量,第一个是Channel唯一标识ChannelId, 第二个是UNSA否Unsafe实现类NioByteUnsafe, 第三个ChannelPipeline实现类DefaultChannelPipeline

public NioSocketChannel(SocketChannel socket) {
    this(null, socket);
}

public NioSocketChannel(Channel parent, SocketChannel socket) {
    super(parent, socket);
    config = new NioSocketChannelConfig(this, socket.socket());
}

protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
    super(parent, ch, SelectionKey.OP_READ);
}

protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);
    this.ch = ch;
    this.readInterestOp = readInterestOp;
    try {
        ch.configureBlocking(false);
    } catch (IOException e) {
        try {
            ch.close();
        } catch (IOException e2) {
            logger.warn(
                        "Failed to close a partially initialized socket.", e2);
        }

        throw new ChannelException("Failed to enter non-blocking mode.", e);
    }
}
protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}

客户端connect远程服务地址

BootStrap#connect

  1. 校验handler不能为空。
  2. 调用doResolveAndConnect进行连接。
public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
    ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
    validate();
    return doResolveAndConnect(remoteAddress, localAddress);
}

BootStrap#doResolveAndConnect

  1. 调用initAndRegister进行Channle的初始化和注册(这在Server启动是一样的流程,再这里就不赘述)
  2. 获取ChannelFuture的Channel。\
    • 判断是否完成注册,然后判断是否成功,不成功则直接返回,成功则直接调用doResolveAndConnect0进行连接操作\
    • 如果没有完成注册, 则将Channel包装成PendingRegistrationPromise(继承DefaultChannelPromise)对象,并在ChannelFuture添加监听器,一旦绑定完成,设置PendingRegistrationPromise的registed属性值为true,接着调用doResolveAndConnect0.
private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();

    if (regFuture.isDone()) {
        if (!regFuture.isSuccess()) {
            return regFuture;
        }
        return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
    } 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 {
                // Directly obtain the cause and do a null check so we only need one volatile read in case of a
                // failure.
                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();
                    doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

BootStrap#doResolveAndConnect0

  1. 创建一个地址解析器AddressResolver。
  2. 如果接卸器不支持解析远程网络地址或则远程网络地址已经解析过,则直接调用doConnect进行远程网络连接。
  3. 直接解析远程网路地址,如果解析完成,则有异常,则关闭Channel, 成功则调用 doConnect进行连接。
private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
                                           final SocketAddress localAddress, final ChannelPromise promise) {
    try {
        final EventLoop eventLoop = channel.eventLoop();
        AddressResolver<SocketAddress> resolver;
        try {
            resolver = this.resolver.getResolver(eventLoop);
        } catch (Throwable cause) {
            channel.close();
            return promise.setFailure(cause);
        }

        if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
            // Resolver has no idea about what to do with the specified remote address or it's resolved already.
            doConnect(remoteAddress, localAddress, promise);
            return promise;
        }

        final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress);

        if (resolveFuture.isDone()) {
            final Throwable resolveFailureCause = resolveFuture.cause();

            if (resolveFailureCause != null) {
                // Failed to resolve immediately
                channel.close();
                promise.setFailure(resolveFailureCause);
            } else {
                // Succeeded to resolve immediately; cached? (or did a blocking lookup)
                doConnect(resolveFuture.getNow(), localAddress, promise);
            }
            return promise;
        }

        // Wait until the name resolution is finished.
        resolveFuture.addListener(new FutureListener<SocketAddress>() {
            @Override
            public void operationComplete(Future<SocketAddress> future) throws Exception {
                if (future.cause() != null) {
                    channel.close();
                    promise.setFailure(future.cause());
                } else {
                    doConnect(future.getNow(), localAddress, promise);
                }
            }
        });
    } catch (Throwable cause) {
        promise.tryFailure(cause);
    }
    return promise;
}

BootStrap#doConnect

  1. 获取ChannelPromise中Channel对象.
  2. 获取Channel中的eventloop执行封装Runnalbe匿名内部类,里面逻辑会判断localAddress是否为空,为空,则调用channel的connect,否则调用Channel的connect相比上一步会多传入一个localAddress参数。
  3. 在ChannelPromise中添加监听器,监听出现异常关系channel.
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);
        }
    });
}

接下来就是看Channel的connect过程,这里Channel就是例子设置的NioSocketChannel。 NioSocketChannel#connect

  1. 可以看到Channel中connect调用实际是委托给ChannelPipeline的connect执行connect进行建立连接。
@Override
public ChannelFuture connect(SocketAddress remoteAddress) {
    return pipeline.connect(remoteAddress);
}

DefaultChannelPipeline#connect

  1. 从ChannelPipeline中是从ChannelHandlerContext中tail开启传播,一直会找到head, 由于tail是找下一个ChannelContext, 由于例子没有添加handler,所以最终会调用head节点ChanelHanderContext的connect进行连接。
@Override
public final ChannelFuture connect(SocketAddress remoteAddress) {
    return tail.connect(remoteAddress);
}

HeadContext#connect

  1. HeadContext里面实际是用unsafe(实现类是NioByteUnsafe)的connect进行远程连接

@Override
public void connect(
        ChannelHandlerContext ctx,
        SocketAddress remoteAddress, SocketAddress localAddress,
        ChannelPromise promise) {
    unsafe.connect(remoteAddress, localAddress, promise);
}

AbstractNioUnsafe#connect

  1. 校验异常.
  2. 调用doConnect进行远程地址,如果成功,则调用fulfillConnectPromise进行实则ChannelPromise为success, 进行传播channelActive事件,如果connect失败,则进行超时或异常的检验.
public final void connect(
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }
    try {
        if (connectPromise != null) {
            // Already a connect in process.
            throw new ConnectionPendingException();
        }

        boolean wasActive = isActive();
        if (doConnect(remoteAddress, localAddress)) {
            fulfillConnectPromise(promise, wasActive);
        } else {
            connectPromise = promise;
            requestedRemoteAddress = remoteAddress;

            // Schedule connect timeout.
            int connectTimeoutMillis = config().getConnectTimeoutMillis();
            if (connectTimeoutMillis > 0) {
                connectTimeoutFuture = eventLoop().schedule(new Runnable() {
                    @Override
                    public void run() {
                        ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                        if (connectPromise != null && !connectPromise.isDone()
                                && connectPromise.tryFailure(new ConnectTimeoutException(
                                        "connection timed out: " + remoteAddress))) {
                            close(voidPromise());
                        }
                    }
                }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
            }

            promise.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (future.isCancelled()) {
                        if (connectTimeoutFuture != null) {
                            connectTimeoutFuture.cancel(false);
                        }
                        connectPromise = null;
                        close(voidPromise());
                    }
                }
            });
        }
    } catch (Throwable t) {
        promise.tryFailure(annotateConnectException(t, remoteAddress));
        closeIfClosed();
    }
}

NioSocketChannel#doConnect

  1. 判断localAddress不为空,则绑定本地地址
  2. 嗲用SOcketUtils的connect方法进行远程连接,
  3. 设置Selectionkey关注事件增加SelectKey.OP_READ.
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    if (localAddress != null) {
        doBind0(localAddress);
    }

    boolean success = false;
    try {
        boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
        if (!connected) {
            selectionKey().interestOps(SelectionKey.OP_CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
            doClose();
        }
    }
}

SOcketUtils#connect 1,实际就是调用JDK的SocketChannel的connect进行连接远程。

public static boolean connect(final SocketChannel socketChannel, final SocketAddress remoteAddress)
        throws IOException {
    try {
        return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
            @Override
            public Boolean run() throws IOException {
                return socketChannel.connect(remoteAddress);
            }
        });
    } catch (PrivilegedActionException e) {
        throw (IOException) e.getCause();
    }
}

总结
本文主要是对客户端连接远程服务端地址流程,首先第一步还是跟服务端一样,开始记性初始化Channel,只是客户端的Channel类型是NioSocketChnnel,并并注册SocketChannel到多路复用器Selector中,开始设置感兴趣事件为0,这样就防止channel建立连接完成禁止读写操作,然后是HandlerContext中进行connect事件传播,最有找到HeadContext的connect,由HeadContext利用netty中unsafe中connect,其底层是调用Jdk的SocketChannel的connect进行连接远程连接,并设置Seleckey的增加感兴趣事件SelectionKey.OP_READ,至此Channel建立完成。