Netty源码分析(1)——Bootstrap

开始之前,还是先贴一段netty客户端的经典使用姿势,如下:

    try {
            // 通过无参构造函数,新建一个Bootstrap实例
            Bootstrap b = new Bootstrap();
            // 设置EventLoop线程组
            b.group(new NioEventLoopGroup())
            // 设置具体使用的Channel子类
             .channel(NioSocketChannel.class)
            // 设置tcp的参数
             .option(ChannelOption.TCP_NODELAY, true)
            // 设置数据处理的handler
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                    p.addLast(new EchoClientHandler());
                 }
             });
            // 建立与服务的的连接
            ChannelFuture f = b.connect(HOST, PORT).sync();
            // 等待channel关闭
            f.channel().closeFuture().sync();
        } finally {
            // 关闭EventLoop线程组
            group.shutdownGracefully();
        }

从上述代码可以看出,netty客户端的使用很方便,由于客户端需要配置的参数较多,所以Bootstrap提供了一个无参构造函数,而具体的参数配置,则通过build模式进行各自独立配置。

group配置的就是用来执行I/O操作的线程池,每个线程以EventLoop的形式存在,配置代码如下:

public B group(EventLoopGroup group) {
    if (group == null) {
        throw new NullPointerException("group");
    }
    if (this.group != null) {
        throw new IllegalStateException("group set already");
    }
    this.group = group;
    return (B) this;
}

做了一些非null的校验,如果传入的group为null,以及如果已经被设置过,则都会抛出异常,校验通过之后,则进行属性赋值。

对于channel的配置,会根据传入的class,构建一个工厂类,然后赋值给channelFactory这个属性,赋值过程与group赋值过程类似。

而工厂类,则会根据需要,通过反射的机制,得到一个channel具体子类的实例,用于建立连接。

public B channel(Class<? extends C> channelClass) {
    if (channelClass == null) {
        throw new NullPointerException("channelClass");
    }
    return channelFactory(new BootstrapChannelFactory<C>(channelClass));
}

public B channelFactory(ChannelFactory<? extends C> channelFactory) {
    if (channelFactory == null) {
        throw new NullPointerException("channelFactory");
    }
    if (this.channelFactory != null) {
        throw new IllegalStateException("channelFactory set already");
    }

    this.channelFactory = channelFactory;
    return (B) this;
}

option就是配置tcp相关的参数,就是一系列的key-value对。

handler配置的就是客户端想要对I/O过程中的数据的处理逻辑,就是一系列的ChannelHandler构成的列表。

Bootstrap在经过上述一系列配置后,各项准备工作已经就绪,接下来重点分析其connect过程。

public ChannelFuture connect(String inetHost, int inetPort) {
    return connect(new InetSocketAddress(inetHost, inetPort));
}

public ChannelFuture connect(SocketAddress remoteAddress) {
    if (remoteAddress == null) {
        throw new NullPointerException("remoteAddress");
    }
    validate();
    return doConnect(remoteAddress, localAddress());
}

connect的调用链如上,最终会通过调用doConnect方法进行connect操作,在doConnect之前,会调用validate方法进行校验操作,主要是对group、channelFactory、handler这三个必须配置进行校验,看是否做了配置,代码如下:

public Bootstrap validate() {
    super.validate();
    if (handler() == null) {
        throw new IllegalStateException("handler not set");
    }
    return this;
}

public B validate() {
    if (group == null) {
        throw new IllegalStateException("group not set");
    }
    if (channelFactory == null) {
        throw new IllegalStateException("channel or channelFactory not set");
    }
    return (B) this;
}

在上述校验通过后,继续看connect真正发生的地方,也就是doConnect方法里做了什么。

private ChannelFuture doConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    /**
     * 新建 NioSocketChannel , 并将该channel 注册到一个 eventLoop 上去
     * 返回的 ChannelFuture 子类为 DefaultChannelPromise
     * 这步的操作都是本机操作,还不涉及到网络操作
     */
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }

    final ChannelPromise promise = channel.newPromise();

    /**
     * 根据注册时,返回的channelFuture的状态来决定,进行connect操作
     * 这里都是完全异步的,如果注册还没有完成,则会监听注册的状态,在注册完成时,才会进行connect操作
     */
    if (regFuture.isDone()) {
        doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
    } else {
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                doConnect0(regFuture, channel, remoteAddress, localAddress, promise);
            }
        });
    }
    return promise;
}

这段代码很短,是进行connect操作的入口,主要分为两大步骤,register和connect操作。

1、register就是将新建的NioSocketChannel注册到NioEventLoop上,最终就是将socket注册到一个selector上,
2、connect则是建立与远程服务器的网络连接,我的理解,就是与远程主机完成了3次握手的操作。

从上述这段代码可以看出,register和connect操作都是异步操作,如调用initAndRegister方法后,返回的是一个regFutrure,这是一个异步调用的返回结果,接着对其做了判断,如果其中的异常不为null,说明register过程出错,则直接就讲regFutrure作为整个doConnect方法的返回值,也就是结束了connect操作。判断如下:

if (regFuture.cause() != null) {
        return regFuture;
    }

如果regFutrure没有异常,则会再判断是否已经done,如果done,则直接调用doConnect0方法进行connect操作,如果没有完成,则在regFutrure上添加了一个ChannelFutureListener,主要逻辑就是在regFuture完成时,调用doConnect0方法,这里需要注意下,在这之前,通过如下代码:

final ChannelPromise promise = channel.newPromise();

新建了一个ChannelPromise的实例promise,并作为doConnect0的第五个入参,然后doConnect方法的返回值也是这个promise。

从这个过程可以看出,register和connect过程都是异步操作,后面具体分析里面的过程时,会更加明确这一点。

这里简单说明下ChannelFuture和ChannelPromise,这两个都是异步执行的方法结果,其中ChannelPromise是ChannelFuture的子类,ChannelFuture只能获取结果,不能对异步返回结果进行操作,而ChannelPromise对其进行了扩展,可以对异步返回结果进行设置等操作。

接下里,先来看register的过程,主要逻辑在initAndRegister方法中,代码如下:

final ChannelFuture initAndRegister() {
    // 1、通过channelFactory新建一个NioSocketChannel的实例
    final Channel channel = channelFactory().newChannel();
    try {
        // 2、初始化channel
        init(channel);
    } catch (Throwable t) {
        channel.unsafe().closeForcibly();
        return channel.newFailedFuture(t);
    }
    // 3、对channel进行注册
    ChannelFuture regFuture = group().register(channel);
    if (regFuture.cause() != null) {
        if (channel.isRegistered()) {
            channel.close();
        } else {
            channel.unsafe().closeForcibly();
        }
    }
    return regFuture;
}
1、根据Bootstrap的配置,用channelFactory,利用反射新建一个NioSocketChannel的实例;
2、对新建好的NioSocketChannel的实例进行初始化操作,代码如下:
void init(Channel channel) throws Exception {
    ChannelPipeline p = channel.pipeline();
    p.addLast(handler());

    final Map<ChannelOption<?>, Object> options = options();
    synchronized (options) {
        for (Entry<ChannelOption<?>, Object> e: options.entrySet()) {
            try {
                if (!channel.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
                    logger.warn("Unknown channel option: " + e);
                }
            } catch (Throwable t) {
                logger.warn("Failed to set a channel option: " + channel, t);
            }
        }
    }

    final Map<AttributeKey<?>, Object> attrs = attrs();
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
            channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
        }
    }
}

初始化的逻辑很明确,

a、把配置的handler添加到channel的pipeline中,用于后续数据处理;
b、把配置的tcp的参数添加到channel的配置中;
c、把配置的属性添加到channel的属性中。
3、上述初始化完成之后,则进行channel的注册过程,代码如下:
ChannelFuture regFuture = group().register(channel);

其中group方法返回的就是Bootstrap在初始时传入的NioEventLoopGroup对象,其调用register方法,最终是调用了SingleThreadEventLoop的register方法,如下:

public ChannelFuture register(Channel channel) {
    return register(channel, new DefaultChannelPromise(channel, this));
}

在register的入参中新建了一个DefaultChannelPromise实例,该实例也就是该register方法后面会返回的ChannelFuture。

public ChannelFuture register(final Channel channel, final ChannelPromise promise) {
    if (channel == null) {
        throw new NullPointerException("channel");
    }
    if (promise == null) {
        throw new NullPointerException("promise");
    }

    channel.unsafe().register(this, promise);
    return promise;
}

在调用了unsafte的register方法后,就会把这个promise返回。接下来继续看register里做了什么。

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    // 1
    if (eventLoop == null) {
        throw new NullPointerException("eventLoop");
    }
    // 2
    if (isRegistered()) {
        promise.setFailure(new IllegalStateException("registered to an event loop already"));
        return;
    }
    // 3
    if (!isCompatible(eventLoop)) {
        promise.setFailure(
                new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
        return;
    }
    // 4
    AbstractChannel.this.eventLoop = eventLoop;
    // 5
    if (eventLoop.inEventLoop()) {
        register0(promise);
    } else {
        try {
            eventLoop.execute(new OneTimeTask() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
            // 6
            logger.warn(
                    "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                    AbstractChannel.this, t);
            closeForcibly();
            closeFuture.setClosed();
            safeSetFailure(promise, t);
        }
    }
}
1、判断eventloop是否为null,如果为null,则抛出异常;
2、判断该channel是否已经被注册过,一个channel只能被注册一次,不能注册到多个eventloop上,所以如果注册,则抛出异常;
3、兼容性判断,看eventloop是否是一个NioEventLoop的实例;

protected boolean isCompatible(EventLoop loop) {
return loop instanceof NioEventLoop;
}

4、将eventloop赋值给channel的eventloop属性;
5、这里就是register实现异步的地方,判断当前执行现场是否就是eventloop的线程,如果是,则进行调用register0方法,如果不是,则将register0方法封装成一个task,提交给eventloop来执行,注意register0的入参就是前面传入的promise,所以可以根据这个promise来判断register是否完成,以及是否成功等;
6、如果注册出现异常,则会做一些收尾工作,如关闭channel,promise中设置fail标志等。

所以到这里,用户执行的register的动作已经完成了,但真正的register操作还没有发生,被提交给eventloop去异步执行了,但是会返回一个ChannelFuture的子类实例promise,可以用来检测注册的完成情况。

再继续跟进到register0里,看看做了什么操作。

private void register0(ChannelPromise promise) {
    try {
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        // 1
        doRegister();
        // 2
        registered = true;
        // 3
        safeSetSuccess(promise);
        // 4
        pipeline.fireChannelRegistered();
        if (isActive()) {
            // 5
            pipeline.fireChannelActive();
        }
    } catch (Throwable t) {
        closeForcibly();
        closeFuture.setClosed();
        safeSetFailure(promise, t);
    }
}
1、doRegister方法,进行真正的注册,实质就是调用了java nio中的channel的register方法,将其注册到selector上去,这里需要注意的是,注册时,register的第二个参数是设置感兴趣的操作,这里设置的是0,说明没有设置任何感兴趣的操作,这里只是简单的完成了注册的动作,对于感兴趣的操作的设置是在fireChannelActive中设置的,后续会分析到。
protected void doRegister() throws Exception {
    boolean selected = false;
    for (;;) {
        try {
            selectionKey = javaChannel().register(eventLoop().selector, 0, this);
            return;
        } catch (CancelledKeyException e) {
            if (!selected) {
                eventLoop().selectNow();
                selected = true;
            } else {
                throw e;
            }
        }
    }
}
2、注册成功后,会将channel的registered标志设置为true;
3、在promise中设置注册成功的标识,这样在前面doConnect方法中,监听注册情况的那个listener就可以开始进行doConnect0方法的执行了;
4、上述动作完成之后,就会调用pipeline的fireChannelRegistered方法,在pipeline的ChannelHandler链中,依次处理channel注册完成的操作;
5、接着判断该channel是否处于活跃状态,也就是该channel中包含的ch是否处于open和connect的状态,一般情况下,第3步结束后才触发了doConnect0操作,所以一般这里的判断都是false,也就是不会触发fireChannelActive操作,但也不是绝对。
public boolean isActive() {
    SocketChannel ch = javaChannel();
    return ch.isOpen() && ch.isConnected();
}

上述就是注册的过程,在注册完成之后,就会触发connect操作,来继续看下doConnect0方法中做了什么。

private static void doConnect0(
        final ChannelFuture regFuture, final Channel channel,
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
    channel.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
            if (regFuture.isSuccess()) {
                if (localAddress == null) {
                    channel.connect(remoteAddress, promise);
                } else {
                    channel.connect(remoteAddress, localAddress, promise);
                }
                promise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

这里做的更直白,直接封装了一个task,然后提交给eventloop去异步执行,然后就返回了。

前面我们分析过,在register完成之后,才会触发doConnect0操作,当这里封装的任务开始执行时,首先就是对regFuture的状态进行判断,看是成功还是失败,成功了,才会继续执行connect操作,调用channel的connect操作,就是调用的该channel的pipeline的connect操作,对于connect是一个用户发起的动作,所以是一个outbound的操作,outbound操作都是从tail开始,传递到head,所以真正的操作是在head的connect方法中。

public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
    return pipeline.connect(remoteAddress, promise);
}

public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
    return tail.connect(remoteAddress, promise);
}

而head的connect方法中,则是直接调用了unsafe的connect方法,继续向下看。

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

unsafe的connect的方法如下:

public void connect(
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
    // 1、设置channel不能取消标志,并判断channel是否处于open状态
    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }
    try {
        // 2、如果connectPromise不为null,则说明已经做过连接,则这里就要抛出异常了
        if (connectPromise != null) {
            throw new IllegalStateException("connection attempt already made");
        }
        // 3、判断当前channel是否活跃,也就是要open和connect都完成,才是true,才开始连接,所以wasActive应该是false
        boolean wasActive = isActive();
        // 4、进行连接操作,下面会详细分析里面内容
        if (doConnect(remoteAddress, localAddress)) {
            // 5、连接成功,则会触发channelActive事件,以及将selectionKey的监听事件中加入read事件
            fulfillConnectPromise(promise, wasActive);
        } else {
            // 6、如果当前没有连接成功
            connectPromise = promise;
            requestedRemoteAddress = remoteAddress;

            // Schedule connect timeout.
            int connectTimeoutMillis = config().getConnectTimeoutMillis();
            if (connectTimeoutMillis > 0) {
                /**
                 * 如果超时时间大于0, 则设置一个定时任务, 在超时时间时, 检查连接是否成功,
                 * 如果还没有连接上, 则会抛出连接超时异常, 这是netty自己做的一个超时检查任务
                 */
                connectTimeoutFuture = eventLoop().schedule(new OneTimeTask() {
                    @Override
                    public void run() {
                        ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                        ConnectTimeoutException cause =
                                new ConnectTimeoutException("connection timed out: " + remoteAddress);
                        if (connectPromise != null && connectPromise.tryFailure(cause)) {
                            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) {
        if (t instanceof ConnectException) {
            Throwable newT = new ConnectException(t.getMessage() + ": " + remoteAddress);
            newT.setStackTrace(t.getStackTrace());
            t = newT;
        }
        promise.tryFailure(t);
        closeIfClosed();
    }
}
4、进行连接操作的具体逻辑如下,如果连接成功了,则返回true,由于channel是非阻塞的,所以暂时没有连接成功的,会注册OP_CONNECT事件,等待其连接成功。
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    if (localAddress != null) {
        javaChannel().socket().bind(localAddress);
    }
    boolean success = false;
    try {
        boolean connected = javaChannel().connect(remoteAddress);
        if (!connected) {
            selectionKey().interestOps(SelectionKey.OP_CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
            doClose();
        }
    }
}
5、如果连接当场就成功了,则会fullFillConnectPromise操作,如下:
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    if (promise == null) {
        return;
    }
    // 1、尝试设置连接成功的标识,让用户调用的地方,知道连接成功了
    boolean promiseSet = promise.trySuccess();
    // 2、如果在连接之前不处于活跃状态,而现在处于活跃状态了,则触发fireChannelActive操作
    if (!wasActive && isActive()) {
        pipeline().fireChannelActive();
    }
    // 3、如果第2步设置失败,则会关闭channel
    if (!promiseSet) {
        close(voidPromise());
    }
}
6、在当前没有连接成功的情况下,netty对这种异步连接的操作,做了一个连接超时的检测,就是设置了一个定时任务,用来判断连接是否成功。
if (connectTimeoutMillis > 0) {
    /**
     * 如果超时时间大于0, 则设置一个定时任务, 在超时时间时, 检查连接是否成功,
     * 如果还没有连接上, 则会抛出连接超时异常, 这是netty自己做的一个超时检查任务
     */
    connectTimeoutFuture = eventLoop().schedule(new OneTimeTask() {
        @Override
        public void run() {
            ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
            ConnectTimeoutException cause =
                    new ConnectTimeoutException("connection timed out: " + remoteAddress);
            if (connectPromise != null && connectPromise.tryFailure(cause)) {
                close(voidPromise());
            }
        }
    }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}

在设置netty的客户端时,有一个参数 CONNECT_TIMEOUT_MILLIS 就是用来设置这里的超时时间的,如果设置过小,而网络较差时,则有可能会出现 ConnectTimeoutException 的异常,可以根据网络环境,适当设置该参数的大小。

在channel被注册的selector上去时,并没有设置OP_READ事件,那在哪里设置的哪?

在第5步中,讲到了fireChannelActive这个操作,来继续看看里面做了什么。

public ChannelPipeline fireChannelActive() {
    head.fireChannelActive();

    if (channel.config().isAutoRead()) {
        channel.read();
    }

    return this;
}

可以看到,先会调用pipeline上的所有ChannelHandler的channelActive方法。

然后会判断是否自动读,该配置默认是true,所以会进入channel的read方法,继续看下去。

public Channel read() {
    pipeline.read();
    return this;
}

直接调用了pipeline的read方法,由于是一个outbound事件,所以最终是调用head的read方法,进入看看。

public void read(ChannelHandlerContext ctx) {
    unsafe.beginRead();
}


public void beginRead() {
    // Channel.read() or ChannelHandlerContext.read() was called
    readPending = true;
    super.beginRead();
}
public void beginRead() {
    if (!isActive()) {
        return;
    }
    try {
        doBeginRead();
    } catch (final Exception e) {
        invokeLater(new OneTimeTask() {
            @Override
            public void run() {
                pipeline.fireExceptionCaught(e);
            }
        });
        close(voidPromise());
    }
}

经过一层层的调用,最终是调用了doBeginRead方法,实现如下:

protected void doBeginRead() throws Exception {
    if (inputShutdown) {
        return;
    }

    final SelectionKey selectionKey = this.selectionKey;
    if (!selectionKey.isValid()) {
        return;
    }

    final int interestOps = selectionKey.interestOps();
    if ((interestOps & readInterestOp) == 0) {
        selectionKey.interestOps(interestOps | readInterestOp);
    }
}

这里找到了设置感兴趣事件的地方。

selectionKey.interestOps(interestOps | readInterestOp);

至此,netty客户端建立与服务端的连接操作就结束了。

总结如下:

1、netty中,处处都是异步操作,这样就能理解为什么netty中到处都ChannelFuture了;
2、连接过程中,主要分为两大步骤,先register,且register成功,再connect;
3、真正的register和connect操作,都是封装成一个task,提交给eventloop去执行的;
4、connect timeout的检测是netty自己实现的一套机制,通过一个定时任务来检测实现的;
5、对于OP_READ事件,在channel已经建立连接好,触发channelActive操作时,才会添加到事件检测中的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值