1、Netty客户端创建流程分析

Netty为了向使用者屏蔽NlO通信的底层细节,在和用户交互的边界做了封装,目的就是为了减少用户开发工作量,降低开发难度。 Bootstrap是 Socket客户端创建工具类,用户通过 Bootstrap可以方便地创建 Netty的客户端并发起异步TCP连接操作。

1.1、Netty客户端创建时序图

java 实现netty 客户端 netty做客户端_Bootstrap

1.2、Netty客户端创建流程分析

步骤1:用户线程创建 Bootstrap实例,通过APl设置创建客户端相关的参数,异步发起客户端连接。
步骤2:创建处理客户端连接、IO读写的 Reactor线程组 NioEventLoopGroup。可以通过构造函数指定IO线程的个数,默认为CPU内核数的2倍;
步骤3:通过 Bootstrap的 ChannelFactory和用户指定的 Channel类型创建用于客户端连接的 NioSocketChannel,它的功能类似于 JDK NIO类库提供的 SocketChannel
步骤4:创建默认的 ChannelHandlerPipeline,用于调度和执行网络事件;
步骤5:异步发起TCP连接,判断连接是否成功。如果成功,则直接将 NioSocketChannel注册到多路复用器上,监听读操作位,用于数据报读取和消息发送:如果没有立即连接成功,则注册连接监听位到多路复用器,等待连接结果;
步骤6:注册对应的网络监听状态位到多路复用器;
步骤7:由多路复用器在1O现场中轮询各 Channel,处理连接结果;
步骤8:如果连接成功,设置 Future结果,发送连接成功事件,触发 ChannelPipeline执行;
步骤9:由 ChannelPipeline调度执行系统和用户的 Channelhandler,执行业务逻辑。

2、Netty客户端创建源码分析

2.1、客户端连接辅助类 Bootstrap

Bootstrap是 Netty提供的客户端连接工具类,主要用于简化客户端的创建,下面我们对它的主要API进行讲解。
设置I/O线程组:在前面的章节我们介绍过,非阻塞IO的特点就是一个多路复用器可以同时处理成百上干条链路,这就意味着使用NIO模式一个线程可以处理多个TCP连接。考虑到O线程的处理性能,大多数NO框架都采用线程池的方式处理IO读写, Netty也不例外。客户端相对于服务端,只需要一个处理IO读写的线程组即可,因为 Bootstrap提供了设置IO线程组的接口,代码如下。

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;
}

由于Netty的NlO线程组默认采用 EventLoopGroup接口,因此线程组参数使用EventLoopGroup。TCP参数设置接口:无论是异步NIO,还是同步BIO,创建客户端套接字的时候通常都会设置连接参数,例如接收和发送缓冲区大小、连接超时时间等。 Bootstrap也提供了客户端TCP参数设置接口,代码如下。

public <T> B option(ChannelOption<T> option, T value) {
    if (option == null) {
        throw new NullPointerException("option");
    }
    if (value == null) {
        synchronized (options) {
            options.remove(option);
        }
    } else {
        synchronized (options) {
            options.put(option, value);
        }
    }
    return (B) this;
}

Netty提供的主要TCP参数如下。
(1) SO_TIMEOUT:控制读取操作将阻塞多少毫秒。如果返回值为0,计时器就被禁止了,该线程将无限期阻塞;
(2) SO_SNDBUF:套接字使用的发送缓冲区大小;
(3) SO_RCVBUF:套接字使用的接收缓冲区大小;
(4) SO_REUSEADDR:用于决定如果网络上仍然有数据向旧的 Server Socket传输数据,是否允许新的 Server Socket绑定到与旧的 ServerSocket同样的端口上。SO_REUSEADDR选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些操作系统中不允许重用端口;
(5) CONNECT_TIMEOUT_MILLIS:客户端连接超时时间,由于NIO原生的客户端并不提供设置连接超时的接口,因此, Netty采用的是自定义连接超时定时器负责检测和超时控制;
(6) TCP_NODELAY:激活或禁止 TCP_NODELAY套接字选项,它决定是否使用 Nagle算法。如果是时延敏感型的应用,建议关闭 Nagle算法。
channel接口:用于指定客户端使用的 channel接口,对于TCP客户端连接,默认使用 NioSocketChannel,代码如下。

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

BootstrapChannelFactory利用 channelClass类型信息,通过反射机制创建NioSocketChanne对象设置 Handler接口: Bootstrap为了简化 Handler的编排,提供了 Channelinitializer,它继承了 ChannelHandlerAdapter,当TCP链路注册成功之后,调用 initHannel接口,用于设置用户 Channelhandler。它的代码如下。

@Override
@SuppressWarnings("unchecked")
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    ChannelPipeline pipeline = ctx.pipeline();
    boolean success = false;
    try {
        initChannel((C) ctx.channel());
        pipeline.remove(this);
        ctx.fireChannelRegistered();
        success = true;
    } catch (Throwable t) {
        logger.warn("Failed to initialize a channel. Closing: " + ctx.channel(), t);
    } finally {
        if (pipeline.context(this) != null) {
            pipeline.remove(this);
        }
        if (!success) {
            ctx.close();
        }
    }
}

其中 initChanne为抽象接口,用户可以在此方法中设置 Channelhandler,代码如下。

.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch)
            throws Exception {
        ByteBuf delimiter = Unpooled.copiedBuffer("$_"
        ch.pipeline().addLast(new EchoClientHandler());
    }
});

最后一个比较重要的接口就是发起客户端连接,代码如下。

ChannelFuture f= b.connect(host, port). sync();

由于客户端连接方法比较复杂,下个小节对此进行详细讲解。

2.2、客户端连接操作

首先要创建和初始化 NioSocketChannel,代码如下。

private ChannelFuture doConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }

    final ChannelPromise promise = channel.newPromise();
    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;
}

从 NioEventLoopGroup中获取 NioEventLoop,然后使用其作为参数创建NioSocketChannel,代码如下。

@Override
Channel createChannel() {
    EventLoop eventLoop = group().next();
    return channelFactory().newChannel(eventLoop);
}

初始化 Channel之后,将其注册到 Selector上,代码如下。

ChannelPromise regFuture = channel.newPromise();
channel.unsafe().register(regEuture);

链路创建成功之后,发起异步的TCP连接,代码如下。

private static void doConnect0(
        final ChannelFuture regFuture, final Channel channel,
        final SocketAddress remoteAddress, 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()) {
                if (localAddress == null) {
                    channel.connect(remoteAddress, promise);
                } else {
                    channel.connect(remoteAddress, localAddress, promise);
                }
                promise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                promise.setFailure(regFuture.cause());
            }
        }
    });
}

由上述代码可以看出,从 doConnect0操作开始,连接操作切换到了 Netty的NIO线程 NioEventLoop中进行,此时客户端返回,连接操作异步执行。doConnect0最终调用 Headhandler的 connect方法,代码如下。

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

AbstractNioUnsafe的 connect操作如下。

boolean wasActive = isActive();
if (doConnect(remoteAddress, localAddress)) {
    fulfillConnectPromise(promise, wasActive);
} else {
//后续代码省略
}

需要注意的是, SocketChannel执行 connect()操作后有以下三种结果
(1)连接成功,返回True;
(2)暂时没有连接上,服务端没有返回ACK应答,连接结果不确定,返回 False;
3)连接失败,直接抛出O异常。
如果是第二种结果,需要将 NioSocketChannel中的 selectionKey设置为OP_CONNECT,监听连接结果异步连接返回之后,需要判断连接结果,如果连接成功,则触发 ChannelActive事件,代码如下。

private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    // trySuccess() will return false if a user cancelled the connection attempt.
    boolean promiseSet = promise.trySuccess();

    // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
    // because what happened is what happened.
    if (!wasActive && isActive()) {
        pipeline().fireChannelActive();
    }

    // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
    if (!promiseSet) {
        close(voidPromise());
    }
}

ChannelActive事件处理在前面章节已经详细说明,最终会将 NioSocketChannel中的selectionKey设置为 SelectionKey.OP_READ,用于监听网络读操作。
如果没有立即连接上服务端,则注册 SelectionKey.OP_CONNECT到多路复用器,代码如下

@Override
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();
        }
    }
}

2.3、异步连接结果通知

NioEventLoop的 Selector轮询客户端连接 Channel,当服务端返回握手应答之后,对连接结果进行判断,代码如下。

if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
    // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
    // See https://github.com/netty/netty/issues/924
    int ops = k.interestOps();
    ops &= ~SelectionKey.OP_CONNECT;
    k.interestOps(ops);

    unsafe.finishConnect();
}

下面对 finishConnect方法进行分析,代码如下。

boolean wasActive = isActive();
doFinishConnect();
fulfillConnectPromise(connectPromise, wasActive);

dofinishConnect用于判断JDK的 SocketChannel的连接结果,如果返回true表示连接成功,其他值或者发生异常表示连接失败

@Override
protected void doFinishConnect() throws Exception {
    if (!javaChannel().finishConnect()) {
        throw new Error();
    }
}

连接成功之后,调用 fulfilIConnectPromise方法,触发链路激活事件,该事件由 ChannelPipeline进行传播,代码如下。

private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    // trySuccess() will return false if a user cancelled the connection attempt.
    boolean promiseSet = promise.trySuccess();

    // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
    // because what happened is what happened.
    if (!wasActive && isActive()) {
        pipeline().fireChannelActive();
    }

    // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
    if (!promiseSet) {
        close(voidPromise());
    }
}

前面章节已经对 fireChannelActive方法进行过讲解,主要用于修改网络监听位为读操作。

2.4、客户端连接超时机制

对于 SocketChannel接口,JDK并没有提供连接超时机制,需要NIO框架或者用户自己扩展实现。Netty利用定时器提供了客户端连接超时控制功能,下面我们对该功能进行详细讲解。
首先,用户在创建 Netty客户端的时候,可以通过 ChannelOption.CONNECT_TIMEOUT_MILLIS配置项设置连接超时时间,代码如下。

b.group(group).channel(NioSocketChannel.class)
    .option(ChannelOption.TCP_NODELAY, true)
    .option(Channeloption.CONNECT_TIMEOUT_MILLIS, 3000);

发起连接的同时,启动连接超时检测定时器,代码如下。

// Schedule connect timeout.
int connectTimeoutMillis = config().getConnectTimeoutMillis();
if (connectTimeoutMillis > 0) {
    connectTimeoutFuture = eventLoop().schedule(new Runnable() {
        @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);
}

一旦超时定时器执行,说明客户端连接超时,构造连接超时异常,将异常结果设置到connectPromise中,同时关闭客户端连接,释放句柄。如果在连接超时之前获取到连接结果,则删除连接超时定时器,防止其被触发,代码如下。

@Override
public void finishConnect() {
    // Note this method is invoked by the event loop only if the connection attempt was
    // neither cancelled nor timed out.

    assert eventLoop().inEventLoop();
    assert connectPromise != null;

    try {
        boolean wasActive = isActive();
        doFinishConnect();
        fulfillConnectPromise(connectPromise, wasActive);
    } catch (Throwable t) {
        if (t instanceof ConnectException) {
            Throwable newT = new ConnectException(t.getMessage() + ": " + requestedRemoteAddress);
            newT.setStackTrace(t.getStackTrace());
            t = newT;
        }

        // Use tryFailure() instead of setFailure() to avoid the race against cancel().
        connectPromise.tryFailure(t);
        closeIfClosed();
    } finally {
        //无论连接是否成功,只要获取到连接结果,之后就删除连接超时定时器
        // Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used
        // See https://github.com/netty/netty/issues/1770
        if (connectTimeoutFuture != null) {
            connectTimeoutFuture.cancel(false);
        }
        connectPromise = null;
    }
}