Netty源码分析:客户端连接

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010412719/article/details/78143558

Netty源码分析:客户端连接

先说结论,Netty 客户端的连接的底层实现最终是借助于Java NIO SocketChannel来实现,Java NIO SocketChannel作为客户端去连接服务端样式代码如下:

        //客户端,首先有一个SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        //连接
        socketChannel.connect(new InetSocketAddress("localhost",8080));

结论已经说完了,或许看完这篇博文,你才会明白这个结论哈,不急,慢慢看。

博文Netty源码分析:服务端启动全过程对服务端的启动进行了全面的分析,本篇博文将对客户端如何连接到服务端进行一个分析。

一般情况下,Netty客户端的启动代码类似如下:

        // 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 {
                            ch.pipeline().addLast(
                                    //new LoggingHandler(LogLevel.INFO),
                                    new EchoClientHandler(firstMessageSize));
                        }
                    });

            // Start the client.
            ChannelFuture f = b.connect(host, port).sync();

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down the event loop to terminate all threads.
            group.shutdownGracefully();
        }

前面几篇博文中对NioEventLoopGroup、Bootstrap已经做过详细的介绍,这里不再介绍,本篇博文主要是跟踪分析ChannelFuture f = b.connect(host, port).sync();这行代码主要做了哪些工作。

Bootstrap.java

    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函数干了两件事情,如下:

1、调用validate()方法进行了相关的校验

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

Bootstrap父类AbstractBootstrap的validate()方法如下:

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

从两个validate()函数我们可以得到:就是检查此Bootstrap对象是否设置了handler、group以及channelFactory这三个对象。

很明显在本博文最开始的地方所列出客户端的如下代码,就是设置了这几个参数。

            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 LoggingHandler(LogLevel.INFO),
                                    new EchoClientHandler(firstMessageSize));
                        }
                    }); 

2、调用了doConnect创建连接

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

看过服务端启动的源码之后,看到这些方法会发现差不多一样的赶脚哈。具体可以看博文Netty源码分析:服务端启动全过程

这个函数的主要工作有如下几点:

1、通过initAndRegister()方法得到一个ChannelFuture的实例regFuture。

2、通过regFuture.cause()方法判断是否在执行initAndRegister方法时产生来异常。如果产生来异常,则直接返回,如果没有产生异常则进行第3步。

3、通过regFuture.isDone()来判断initAndRegister方法是否执行完毕,如果执行完毕来返回true,然后调用doConnect0进行连接。如果没有执行完毕则返回false进行第4步。

4、regFuture会添加一个ChannelFutureListener监听,当initAndRegister执行完成时,调用operationComplete方法并执行doConnect0进行连接。

第3、4点想干的事就是一个:调用doConnect0进行连接。

有了服务端启动源码分析的经验,我们就快速的跟下这些函数具体干了什么。

1、initAndRegister

    final ChannelFuture initAndRegister() {
        //第一步:首先利用反射得到Channel的实例,这里为NioSocketChannel实例。
        final Channel channel = channelFactory().newChannel();
        try {
            //第二步:然后进行初始化
            init(channel);
        } catch (Throwable t) {
            channel.unsafe().closeForcibly();
            // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
            return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
        }
            //第三步:注册
        ChannelFuture regFuture = group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }

        return regFuture;
    } 

该函数主要干了如下三件事:

1、首先利用反射得到Channel的实例,这里为NioSocketChannel实例

2、调用init方法初始化第一步所得到的Channel。

3、将第一步所得到的Channel进行注册。

下面将逐步进行分析。

1、final Channel channel = channelFactory().newChannel()

这行代码是如何利用反射来得到Channel实例的,在博文Netty源码分析:服务端启动全过程有详细的介绍,由于这里的Channel是NioSocketChannel这个类,因此这里主要分析这个类的构造函数。

在看具体的构造函数之前,先看下该类的继承结构,如下图所示,用“红色”框起来的是NioSocketChannel这个Channel与NioServerSocketChannel的相同之处。

下面来看下NioSocketChannel这个类的构造函数。

    public NioSocketChannel() {
        this(newSocket(DEFAULT_SELECTOR_PROVIDER));//注意:这里的newSocket函数利用provider打开了一个Java NIO SocketChannelImpl。
    }

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

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

调到这个构造函数之后,接着调用父类AbstractNioByteChannel的构造函数,其中,传入的参数parent为null,socket为刚刚使用newSocket创建的Java NIO SocketChannelImpl实例。

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

即这里也什么也没有做,而是直接调用父类如下的构造函数,其中传入的参数除了parent、ch之外,还添加了参数readInterestOp = 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) {
                if (logger.isWarnEnabled()) {
                    logger.warn(
                            "Failed to close a partially initialized socket.", e2);
                }
            }

            throw new ChannelException("Failed to enter non-blocking mode.", e);
        }
    }

然后继续调用父类 AbstractChannel 的构造器:

     protected AbstractChannel(Channel parent) {
        this.parent = parent;
        unsafe = newUnsafe();
        pipeline = new DefaultChannelPipeline(this);
    }

到这里,一个完整的NioSocketChannel就初始化完成了,该初始化过程总结如下:

1、 调用NioSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)打开一个新的 Java NIO SocketChannel。

2、初始化了NioSocketChannel中的属性SocketChannelConfig config = new NioSocketChannelConfig(this, socket.socket()),该配置类后续会分析;

3、初始化了 AbstractNioChannel中的如下属性:

1)SelectableChannel ch 被设置为 Java SocketChannel, 即第一点所返回的SocketChannelImple实例。
2)readInterestOp 被设置为 SelectionKey.OP_READ
3)SelectableChannel ch 被配置为非阻塞的 ch.configureBlocking(false)  

4、初始化了 AbstractChannel中的如下属性

1)parent = null
2)unsafe = newUnsafe();//newUnsafe()方法返回的是NioByteUnsafe对象
3) pipeline = new DefaultChannelPipeline(this);//每个Channel都有且仅有一个Pipeline。            

2、 init(channel)

init方法与初始化服务端所对应的NioServerSocketChannel相比,此方法更简单,代码如下:

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

该方法里面的下面两行代码主要是将我们自定义的handler加入到pipeline所持有以AbstractChannelHandlerContext为节点的双向链表中。具体是如何添加的,可以参考博文 Netty源码分析:ChannelPipeline,该方法的剩余代码主要是将options和attrs添加到Channel上。

        ChannelPipeline p = channel.pipeline();
        p.addLast(handler());   

3、ChannelFuture regFuture = group().register(channel);

将channel进行注册,在博文Netty源码分析:服务端启动全过程中有详细的介绍,这里不在介绍。

2、doConnect0(regFuture, channel, remoteAddress, localAddress, promise)

该方法的代码如下:

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

在该方法中,会在channel所关联到的eventLoop 线程中调用 channel.connect方法,注意这里的channel是NioSocketChannel实例。

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

这里的pipeline是DefaultChannelPipeline实例,继续看此pipeline的connect方法

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

在博文 Netty源码分析:ChannelPipeline中有关于tail的详细介绍,这里回顾下:tail是TailContext的实例,继承于AbstractChannelHandlerContext,TailContext并没有实现connect方法,因此这里调用的是其父类AbstractChannelHandlerContext的connect方法。

    @Override
    public ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return connect(remoteAddress, null, promise);
    } 

    @Override
    public ChannelFuture connect(
            final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {

        //...省略了一些目前不关注的代码
        final AbstractChannelHandlerContext next = findContextOutbound();
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeConnect(remoteAddress, localAddress, promise);
        } else {
            safeExecute(executor, new OneTimeTask() {
                @Override
                public void run() {
                    next.invokeConnect(remoteAddress, localAddress, promise);
                }
            }, promise, null);
        }

        return promise;
    } 

此函数中的这行代码:final AbstractChannelHandlerContext next = findContextOutbound();所完成的任务就是在pipeline所持有的以AbstractChannelHandlerContext为节点的双向链表中从尾节点tail开始向前寻找第一个outbound=true的handler节点。

    private AbstractChannelHandlerContext findContextOutbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.prev;
        } while (!ctx.outbound);
        return ctx;
    }

在博文 Netty源码分析:ChannelPipeline中我们分析“在哪里调用了我们自定义的handler”时介绍了在pipeline所持有的以AbstractChannelHandlerContext为节点的双向链表中从头节点head开始向后寻找第一个inbound=true的handler节点,完成此功能的方法为findContextInbound,具体代码如下:

    private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.next;
        } while (!ctx.inbound);
        return ctx;
    } 

看过上篇博文之后,我相信我们都知道这个outbound=ture的节点时哪一个?是head节点,为什么呢?在博文 Netty源码分析:ChannelPipeline中我们知道在DefaultChannelPipeline 的构造器中, 会实例化两个对象: head 和 tail, 并形成了双向链表的头和尾. head 是 HeadContext 的实例, 它实现了 ChannelOutboundHandler 接口, 即head实例的 outbound = true. 因此在调用上面 findContextOutbound()方法时, 找到的符合outbound=true的节点其实就是 head。

继续看,在pipelie的双向链表中找到第一个outbound=true的AbstractChannelHandlerContext节点head后,然后调用此节点的invokeConnect方法,该方法的代码如下,

    private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
        try {
            ((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
        } catch (Throwable t) {
            notifyOutboundHandlerException(t, promise);
        }
    } 

HeadContext类中的handler()方法代码如下:

        @Override
        public ChannelHandler handler() {
            return this;
        } 

该方法返回的是其本身,这是因为HeadContext由于其继承AbstractChannelHandlerContext以及实现了ChannelHandler接口使其具有Context和Handler双重特性。

继续看,看HeadContext类中的connect方法,代码如下:

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

unsafe这个字段是在HeadContext构造函数中被初始化的,如下:

         HeadContext(DefaultChannelPipeline pipeline) {
            super(pipeline, null, HEAD_NAME, false, true);
            unsafe = pipeline.channel().unsafe();
        }   

而此构造函数中的pipeline.channel().unsafe()这行代码返回的就是在本博文前面研究NioSocketChannel这个类的构造函数中所初始化的一个实例,如下:

    unsafe = newUnsafe();//newUnsafe()方法返回的是NioByteUnsafe对象。

继续看NioByteUnsafe类中的 connect方法(准确的说此方法是在AbstractNioUnsafe类中)

        @Override
        public final void connect(
                final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
             //...
                if (doConnect(remoteAddress, localAddress)) {
                    fulfillConnectPromise(promise, wasActive);
                } else {
                    //...
            }
        }

上面只保留了connect的关键代码,相关检查和连接失败的代码省略了,上面这个函数主要是调用了doConnect这个方法,需要注意的是,此方法并不是 AbstractNioUnsafe 的方法, 而是 AbstractNioChannel 的抽象方法. doConnect 方法是在 NioSocketChannel 中实现的。

NioSocketChannel类中的doConnect方法的代码如下:

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

上面方法中javaChannel()方法返回的是NioSocketChannel实例初始化时所产生的Java NIO SocketChannel实例(更具体点为SocketChannelImple实例)。 然后调用此实例的connect方法完成Java NIO层面上的Socket连接。

如果对Java NIO层面的连接以及交互不太清晰,可以看博文Java NIO 之 ServerSocketChannel/SocketChannel ,这里介绍了 Java NIO 层面的客户端于服务端之间的连接以及客户端于服务端之阿金的交互。

总结

Netty中客户端的连接的底层实现是使用Java NIO 的SocketChannel来完成的。

参考资料

1、https://segmentfault.com/a/1190000007282789

猜你喜欢

转载自blog.csdn.net/u010412719/article/details/78143558