高性能 Netty 源码解析之客户端创建

1,292 阅读8分钟

前言

上篇文章我们讲完了关于服务端的创建,相比于服务端,客户端考虑的东西更多:

  • 线程模型
  • 异步连接
  • 客户端连接超时

所以这篇文章会继续将客户端的创建流程以及源码分析。

版本说明

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.51.Final</version>
        </dependency>

创建流程

客户端创建的流程有以下步骤

  1. 创建 Bootstrap 实例,通过 API 设置客户端的参数,异步发起连接
  2. 创建客户端连接,IO 读写的 Reactor 线程组 NioEventLoopGroup
  3. 指定 Channel 类型
  4. 创建默认的 Channel Handler Pipeline,用于调度和执行网络事件
  5. 异步发起 TCP 连接,判断连接是否成功。如果成功,则直接将 NioSocketChannel 注册到多路复用器上。监听读操作位,用于数据报读取和消息发送;如果不成功,则注册连接监听位到多路复用器,等待连接结果。
  6. 注册对应的网络监听状态位到多路复用器
  7. 由多路复用器在 IO 轮询各 Channel,处理连接结果
  8. 如果来连接成功,设置 Future 结果,发送连接成功事件,触发 ChannelPipileline
  9. 由 ChannelPipileline 调度执行系统和用户的 ChannelHandler 执行业务逻辑

下面是流程图

源码分析

我们先上一份示例代码

   	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 {
                       ch.pipeline().addLast(
                               new MessageDecoder(1024 * 1024, 4, 4));
                       ch.pipeline().addLast("MessageEncoder", new MessageEncoder());
                       ch.pipeline().addLast("readTimeoutHandler",
                               new ReadTimeoutHandler(50));
                       ch.pipeline().addLast("LoginAuthHandler",
                               new LoginAuthReqHandler());
                       ch.pipeline().addLast("HeartBeatHandler",
                               new HeartBeatReqHandler());
                   }
               });
       ChannelFuture future = b.connect(
               new InetSocketAddress(host, port),
               new InetSocketAddress( Constant.LOCALIP,
                        Constant.LOCAL_PORT)).sync();
       future.channel().closeFuture().sync();

看了示例代码,我们可以看到第一步是设置 IO 线程组的接口。由于客户端相对于服务端来说,只需要一个处理 IO 的读写线程组。

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

然后是 TCP 的参数。

    public <T> B option(ChannelOption<T> option, T value) {
        //...
        synchronized (options) {
            if (value == null) {
                options.remove(option);
            } else {
                options.put(option, value);
            }
        }
        return self();
    }

TCP 的参数主要有这些:

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

设置完参数,就是进行 channel 的设置。与服务端不一样的是,客户端是用的 NioSocketChannel。这里依旧是使用反射工厂实例化 ReflectiveChannelFactory。

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

然后开始设置 handler。示例代码中使用的是 ChannelInitializer,实际上它实现了 ChannelInboundHandlerAdapter 这个 adapter。当 TCP 链路注册成功之后,调用 channelRegistered 方法,channelRegistered 方法就会进行 initChannel 的方法调用实现 ChannelHandler 的设置。

    public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        // 注册成功成功后,就调用 initChannel 方法
        if (initChannel(ctx)) {
            // 调用完 initChannel 后就调用 pipeline 中的每一个 Channel
            ctx.pipeline().fireChannelRegistered();

            // 同时我们就可以移除这个作用是为了初始化的 Channel
            removeState(ctx);
        } else {
            // Called initChannel(...) before which is the expected behavior, so just forward the event.
            ctx.fireChannelRegistered();
        }
    }

上面的 initChannel 主要是将当前的 ChannelContext 传进去进行初始化

   private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
        if (initMap.add(ctx)) { // 这里是防止重复初始化
            try { 
            	//这里才是真正调用 initChannel 方法
                //这里是我们示例里面初始化的 channelHandler
                initChannel((C) ctx.channel());
            } catch (Throwable cause) {
                // 如果发生异常就传递异常
                exceptionCaught(ctx, cause);
            } finally {
                ChannelPipeline pipeline = ctx.pipeline();
                if (pipeline.context(this) != null) {
                    pipeline.remove(this);
                }
            }
            return true;
        }
        return false;
    }

最后一步比较重要,就是发起客户端的连接,如下

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

前面基本上的步骤都是进行客户端的设置。现在我们要去看 connect 的代码究竟做了什么。

   private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
   		// 新建一个 Channel 并注册
        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 {	
            // 注册没有完成,就封装成 PendingRegistrationPromise 等待注册完成
            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) {
                        // 设置失败的信息
                        promise.setFailure(cause);
                    } else {
                        // 如果注册成功,就进行连接
                        promise.registered();
                        doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }
doConnect 连接服务端

上面的流程跟服务端差不多,主要区别在于 doResolveAndConnect0 方法。我们进去看看

   private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
                                               final SocketAddress localAddress, final ChannelPromise promise) {
        try {
            final EventLoop eventLoop = channel.eventLoop();
            AddressResolver<SocketAddress> resolver;
            try {
            	//根据 eventloop 获取解析器
                resolver = this.resolver.getResolver(eventLoop);
            } catch (Throwable cause) {
                channel.close();
                return promise.setFailure(cause);
            }
            
			// 解析器不知道如何处理指定的远程地址,或者它已经被解析。
            if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
                doConnect(remoteAddress, localAddress, promise);
                return promise;
            }

            final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress);
			// 如果解析完成的化
            if (resolveFuture.isDone()) {
                final Throwable resolveFailureCause = resolveFuture.cause();
				// 查看有没有报错,如果有就立马关闭
                if (resolveFailureCause != null) {
                    channel.close();
                    promise.setFailure(resolveFailureCause);
                } else {
                    // 
                    doConnect(resolveFuture.getNow(), localAddress, promise);
                }
                return promise;
            }

            // 如果上面没执行完,索性加个监听器等待解析完毕
            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;
    }

执行完上面的代码,最终还是要进行一个 connect 方法

    private static void doConnect(
            final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {

        // 获取 channel,往 channel 里面塞任务
        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);
            }
        });
    }

从这里开始,连接操作切换到了 Netty 的 NIO 线程 NioEventLoop 中进行,此时客户端返回,连接操作异步执行。NioEventLoop 进行服务端连接,一般有三种情况

  1. 连接成功,返回 true
  2. 暂时无法连接成功,服务端没有 ack 应答,所以返回 false
  3. 连接失败,抛出异常

如果是第二种的话,Netty 会继续将 NioEventLoop 的 selectionKey 继续设为 OP_CONNECT,然 selector 继续进行轮询。

   protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
        if (localAddress != null) {
            doBind0(localAddress);
        }

        boolean success = false;
        try {
            boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
            //如果返回 false 继续连接
            if (!connected) {
                selectionKey().interestOps(SelectionKey.OP_CONNECT);
            }
            success = true;
            return connected;
        } finally {
        	//如果失败了就直接关闭链路,进入失败处理流程
            if (!success) {
                doClose();
            }
        }
    }

目前为止,客户端的已经完成了“发起连接”这个动作了。如果成功还好,如果是第二种情况返回 false,后面成功了怎么处理呢?我们继续往下走~

异步连接结果通知

由于上面再 NioSocketChannel 如果暂时不能成功注册的话,就返回继续将 selectionKey 设为 OP_CONNECT。那么后续交给了我们的“线程池” NioEventLoopGroup 继续去监听实行。我们都知道 NioEventLoopGroup 是一个线程池集合。而里面每一个线程池其实是 NioEventLoop。所以后续异步连接结果还是由 NioEventLoop 继续处理。我们去看看它的源码。

    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
       	//...
        try {
        	//获取 selectionkey
            int readyOps = k.readyOps();
            //判断是不是连接事件
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                // 更改操作位
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);
				//关闭连接
                unsafe.finishConnect();
            }
        } 
    }

而 unsafe.finishConnect 的方法是

        public final void finishConnect() {
            assert eventLoop().inEventLoop();

            try {
            	//查看是否活跃
                boolean wasActive = isActive();
                //判断 JDK 的 SocketChannel 的连接结果,
                //如果是 true 则是成功
                //如果是其他或异常则失败
                doFinishConnect();
                fulfillConnectPromise(connectPromise, wasActive);
            } catch (Throwable t) {
                fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress));
            } 
        }

最后调用了 fulfillConnectPromise 方法

        private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
            if (promise == null) {
                return;
            }
            boolean active = isActive();
            boolean promiseSet = promise.trySuccess();
            if (!wasActive && active) {
                pipeline().fireChannelActive();
            }
            if (!promiseSet) {
                close(voidPromise());
            }
        }

上面主要是如果是 channel 是活跃 active 状态就直接激活链路了,调用 fireChannelActive 让事件在 Pipeline 上面传播。fireChannelActive 主要是修改网络监听位ww为读操作。

超时机制

上面讲了连接失败的情况,如果客户端连接超时怎么办呢?要知道原生的 Java NIO 过于简洁(同时也是一种好处吧,给了开发一个极大的发挥空间),所以 Netty 要自己实现这个超时的机制。

首先,我们需要使用 Netty 的超时机制,我们要在客户端启动的时候进行设置

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

发起连接的同时,启动超时检测定时器

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("connect time out : " + remoteAddress);
            if(connectPromise!=null&&connectPromise.tryFailure()) {
            	close(voidPromise());
            }
        }
    }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}

当超时定时器的任务发现可客户端连接超时,就构造异常设置到 connectPromise,关闭客户端连接释放句柄。最后还需要异常定时器

	if(connectTimeoutFuture!=null) {
    	connectTimeoutFuture.cancel(false);
    }
    connectPromise = null;

结语

这一篇文章主要是讲 Netty 客户端的创建历程。

完!