Netty客户端源码分析

感谢大家的阅读,有什么建议或者错误请在评论区评论出来,我会非常认真地去看每一条评论表
《netty专栏》 https://blog.csdn.net/dreamingzihao/category_12409227.html?spm=1001.2014.3001.5482

简介

java 复制代码
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.nio.charset.StandardCharsets;
import java.util.Date;

public class ServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf byteBuf = (ByteBuf) msg;

        System.out.println(new Date() + ": 服务端读到数据 -> " + byteBuf.toString(StandardCharsets.UTF_8));

        // 回复数据到客户端
        ByteBuf out = getByteBuf(ctx);
        ctx.channel().writeAndFlush(out);
    }

    private ByteBuf getByteBuf(ChannelHandlerContext ctx) {
        byte[] bytes = "服务端已收到消息~".getBytes(StandardCharsets.UTF_8);

        ByteBuf buffer = ctx.alloc().buffer();

        buffer.writeBytes(bytes);
        System.out.println("服务端发出数据: 服务端已收到消息~");
        return buffer;
    }
}
java 复制代码
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClient {
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 8000;


    public static void main(String[] args) {
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        Bootstrap bootstrap = new Bootstrap();
        bootstrap
                .group(workerGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new ClientHandler());
                    }
                });
		ChannelFuture future = bootstrap.connect(HOST, PORT);
    	future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (future.isSuccess()) {
                    // 连接成功处理逻辑
                    Channel channel = future.channel();
                    // ...
                } else {
                    // 连接失败处理逻辑
                    Throwable cause = future.cause();
                    // ...
                }
            }
        });
    }
}

客户端连接的代码如上, 在 Netty 中,Bootstrap 是用于创建客户端的引导类。workerGroup 参数是一个 EventLoopGroup 实例,它表示客户端的工作线程组。工作线程组中的线程负责处理客户端的网络事件、执行数据传输操作以及其他与客户端相关的异步任务。

设置 workerGroup 的作用主要体现在以下几个方面:

  • 网络事件处理:workerGroup 中的线程会监听客户端的连接事件、数据读写事件等网络事件,并负责处理这些事件。它们会根据事件的类型,调用相应的处理器(ChannelHandler)来处理事件
  • 数据传输:workerGroup 中的线程会执行客户端与服务器之间的数据传输操作。例如,它们负责从服务器读取数据并将数据写入到客户端的通道,或者将客户端发送的数据写入到服务器的通道
  • 异步任务执行:workerGroup 中的线程还负责执行客户端的其他异步任务,例如定时任务、定时重连等。这些任务可以利用工作线程组的线程进行执行,避免阻塞客户端的主线程

工作线程组允许并行处理多个连接和请求,并将网络操作与应用程序逻辑解耦,从而提高了代码的可维护性和扩展性。

channel 方法,option 方法和 handler 方法都是设置客户端通道相关的属性,与前文服务端代码分析中的含义大同小异,这里拿 handler 方法做个简单介绍, 其他就不做赘述了。

  • handler 方法的作用是设置客户端的通道处理器,用于对客户端通道的事件和数据进行处理。通过设置不同的通道处理器,可以实现数据的解码、编码、业务逻辑处理和异常处理等功能。

最后说到关键的 connect 方法,它的作用是发起与服务器的连接,并返回一个表示连接结果的 ChannelFuture 对象。通过这个 ChannelFuture 对象,可以异步获取连接的结果,并注册连接的监听器以处理连接成功或失败的情况。

当连接操作完成后,ChannelFutureListeneroperationComplete() 方法会被调用。如果连接成功,可以通过 future.isSuccess() 来判断连接是否成功,然后进行相应的处理。如果连接失败,可以通过 future.cause() 获取连接失败的原因(Throwable),然后进行异常处理。

源码分析

Bootstrap类及其父类结构

根据上图, 可以发现Bootstrap和ServerBootstrap一样都是继承了AbstractBootstrap,并且group(), channel(),option()和handler()方法都是由AbstractBootstrap类实现, Bootstrap和ServerBootstrap并没有去做相应的实现,这几个方法也只是简单的做了下成员变量的设置。

AbstractBootstrap实现的方法如下图所示:

Bootstrap实现的方法如下:

上图中, 绿色解开的锁表示public方法,可以看到, 其实Bootstrap实现的方法不多。

connect方法

这里对connect方法进行讲解,其他的读者自行查看。

java 复制代码
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
    ...
    
    /**
     * Connect a {@link Channel} to the remote peer.
     */
    public ChannelFuture connect() {
        validate();
        SocketAddress remoteAddress = this.remoteAddress;
        if (remoteAddress == null) {
            throw new IllegalStateException("remoteAddress not set");
        }

        return doResolveAndConnect(remoteAddress, config.localAddress());
    }

    /**
     * Connect a {@link Channel} to the remote peer.
     */
    public ChannelFuture connect(String inetHost, int inetPort) {
        return connect(InetSocketAddress.createUnresolved(inetHost, inetPort));
    }

    /**
     * Connect a {@link Channel} to the remote peer.
     */
    public ChannelFuture connect(InetAddress inetHost, int inetPort) {
        return connect(new InetSocketAddress(inetHost, inetPort));
    }

    /**
     * Connect a {@link Channel} to the remote peer.
     */
    public ChannelFuture connect(SocketAddress remoteAddress) {
        if (remoteAddress == null) {
            throw new NullPointerException("remoteAddress");
        }

        validate();
        return doResolveAndConnect(remoteAddress, config.localAddress());
    }

    /**
     * Connect a {@link Channel} to the remote peer.
     */
    public ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
        if (remoteAddress == null) {
            throw new NullPointerException("remoteAddress");
        }
        validate();
        return doResolveAndConnect(remoteAddress, localAddress);
    }

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

可以看到connect方法的主要逻辑如下

  1. 调用validate方法,确保必要的参数和配置已经设置。
  2. 获取保存在 remoteAddress 变量中的远程地址。如果远程地址为 null ,则抛出 IllegalStateException 异常,表示远程地址未设置。
  3. 调用doResolveAndConnect方法,传入远程地址和配置中的本地地址,执行解析和连接操作,并返回表示连接结果的 ChannelFuture 对象。

validate方法

java 复制代码
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
    ...
    @Override
    public Bootstrap validate() {
        super.validate();
        if (config.handler() == null) {
            throw new IllegalStateException("handler not set");
        }
        return this;
    }
    ...
}
java 复制代码
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
	...
    /**
     * Validate all the parameters. Sub-classes may override this, but should
     * call the super method in that case.
     */
    @SuppressWarnings("unchecked")
    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方法调用了AbstractBootstrap父类的validate方法,该父类方法校验了处理事件循环的线程组EventLoopGroup和创建通道(Channel)实例的工厂类ChannelFactory是否被初始化,在Bootstrap中,validate方法则校验了对应客户端的通道处理器。可以发现, 这里只对不可或缺的属性进行了校验。

doResolveAndConnect方法

下面这段代码是 doResolveAndConnect() 方法的实现。它用于解析和建立连接到远程地址的操作,并返回表示连接结果的 ChannelFuture 对象。

java 复制代码
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
	/**
     * @see {@link #connect()}
     */
    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 {
                    // Direclty 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;
        }
    }
}

具体的逻辑如下:

  1. 首先,调用 initAndRegister() 方法进行通道的初始化和注册,返回表示注册结果的 ChannelFuture 对象。通过该对象获取到已注册的通道实例。
  2. 判断注册操作是否已完成,即判断 regFuture 是否已经完成(成功或失败)。
    • 如果 regFuture 已完成且成功,说明通道的注册操作已经完成,可以继续进行解析和连接操作。调用 doResolveAndConnect0() 方法,传入通道、远程地址和本地地址,以及一个新的 Promise 对象,进行解析和连接操作,并返回连接结果的 ChannelFuture 对象。
    • 如果 regFuture 未完成,即通道的注册操作仍在进行中,需要添加一个监听器来等待注册操作完成。创建一个 PendingRegistrationPromise 对象,用于表示注册操作的结果。然后,通过添加监听器到 regFuture ,在注册操作完成时执行相应的操作:
      • 检查注册操作的结果,如果失败,则直接将失败原因设置到 promise 对象,并避免在访问通道的事件循环时引发异常。
      • 如果注册操作成功,设置适当的执行器,并调用 doResolveAndConnect0() 方法进行解析和连接操作,传入通道、远程地址和本地地址,以及 promise 对象。

总结而言,这段代码的作用是解析和建立与远程地址的连接操作,并返回连接结果的 ChannelFuture 对象。它通过注册通道和添加监听器的方式,保证在注册操作完成后进行解析和连接操作,并处理相应的成功或失败情况。

initAndRegister方法

initAndRegister方法进行通道的初始化和注册。

java 复制代码
public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
    final ChannelFuture initAndRegister() {
            Channel channel = null;
            try {
                channel = channelFactory.newChannel();
                init(channel);
            } catch (Throwable t) {
                if (channel != null) {
                    // channel can be null if newChannel crashed (eg SocketException("too many open files"))
                    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 = config().group().register(channel);
            if (regFuture.cause() != null) {
                if (channel.isRegistered()) {
                    channel.close();
                } else {
                    channel.unsafe().closeForcibly();
                }
            }
    
            // If we are here and the promise is not failed, it's one of the following cases:
            // 1) If we attempted registration from the event loop, the registration has been completed at this point.
            //    i.e. It's safe to attempt bind() or connect() now because the channel has been registered.
            // 2) If we attempted registration from the other thread, the registration request has been successfully
            //    added to the event loop's task queue for later execution.
            //    i.e. It's safe to attempt bind() or connect() now:
            //         because bind() or connect() will be executed *after* the scheduled registration task is executed
            //         because register(), bind(), and connect() are all bound to the same thread.
    
            return regFuture;
        }
    
    abstract void init(Channel channel) throws Exception;
}

这里通过channelFactory采用反射机制调用NioSocketChannel类的构造方法生成客户端通道实例channel, 并调用init方法对channel进行初始化(这个方法由子类实现,只有两个实现类, Bootstrap和ServerBootstrap), 紧接着将客户端通道实例注册到对应的事件循环线程组中。这里采用了模版模式, 除了init方法由子类实现外, 其他操作对于ServerBootstrap和Bootstrap来说基本一致,就不再做赘述。

init方法

接着看Bootstrap的init方法,通过init() 方法,可以实现对客户端通道的初始化配置、添加通道处理器和设置属性的操作。这些操作将确保通道在创建后具有适当的配置和属性,以便进行后续的网络事件处理和数据传输。

java 复制代码
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
    ...
	@Override
    @SuppressWarnings("unchecked")
    void init(Channel channel) throws Exception {
        ChannelPipeline p = channel.pipeline();
        p.addLast(config.handler());

        final Map<ChannelOption<?>, Object> options = options0();
        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 = attrs0();
        synchronized (attrs) {
            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
                channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
            }
        }
    }
    ...
}
  1. 首先获取到当前客户端通道的处理链, 这个处理链是在通过反射调用构造方法时生成的,然后调用 config.handler() 方法获取配置中设置的通道处理器,并将其添加到通道的处理器链的末尾。这样,在通道接收到数据时,可以通过处理器链按序处理和转发数据。
  2. 获取配置中设置的通道选项(ChannelOption)和属性(AttributeKey)。
  3. 遍历通道选项集合,并尝试将每个通道选项设置到通道的配置(ChannelConfig)中。如果设置失败,则记录警告日志,表示未知的通道选项。
  4. 遍历属性集合,并将每个属性设置到通道的属性(AttributeMap)中。
  5. 通道选项和属性的操作使用同步块,以确保对选项和属性的修改是线程安全的。

doResolveAndConnect0方法

doResolveAndConnect0() 方法是在解析和建立连接操作中的一部分,用于处理地址解析的逻辑。

java 复制代码
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
	...
	private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
                                               final SocketAddress localAddress, final ChannelPromise promise) {
        try {
            final EventLoop eventLoop = channel.eventLoop();
            final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop);

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

具体的逻辑如下:

  1. 首先,获取通道所属的事件循环(EventLoop )实例和地址解析器(AddressResolver)。
  2. 检查地址解析器是否支持指定的远程地址,以及该地址是否已经解析完成。
    • 如果解析器无法处理指定的远程地址,或者远程地址已经解析完成,说明不需要进行地址解析,直接调用 doConnect() 方法执行连接操作,并返回表示连接结果的 promise 对象。
  3. 如果解析器需要进行地址解析,则通过调用 resolver.resolve(remoteAddress) 方法异步解析远程地址。返回的是一个 Future 对象,表示地址解析的结果。
    • 如果解析操作已完成,通过检查 resolveFuture 的结果进行处理:
      • 如果解析操作失败,关闭通道并设置连接的 promise 为失败状态,同时将失败原因设置到 promise 中。
      • 如果解析操作成功,获取解析结果,并调用 doConnect() 方法执行连接操作,传入解析得到的地址,本地地址和连接的 promise 对象。
    • 如果解析操作仍在进行中,添加一个监听器到 resolveFuture ,在解析操作完成时执行相应的操作:
      • 如果解析操作失败,关闭通道并设置连接的 promise 为失败状态,同时将失败原因设置到 promise 中。
      • 如果解析操作成功,获取解析结果,并调用 doConnect() 方法执行连接操作,传入解析得到的地址,本地地址和连接的 promise 对象。

通过执行地址解析和连接操作,最终会得到表示连接结果的 promise 对象,并在连接成功或失败时设置相应的状态和结果。这段代码的作用是在地址解析过程中处理解析结果,并在解析完成后执行连接操作。

doConnect方法

doConnect() 方法用于执行实际的连接操作,并设置连接结果的 ChannelPromise 对象。

java 复制代码
 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);
            }
        });
    }

具体的逻辑如下:

  1. 首先,获取连接操作所涉及的通道(Channel )实例和连接的相关参数,即远程地址(remoteAddress )和本地地址(localAddress)。
  2. 判断本地地址是否为 null
    • 如果本地地址为 null ,表示不需要指定本地地址,直接调用通道的 connect(remoteAddress, connectPromise) 方法执行连接操作,并传入远程地址和连接的 promise 对象。
    • 如果本地地址不为 null ,表示需要指定本地地址,调用通道的 connect(remoteAddress, localAddress, connectPromise) 方法执行连接操作,并传入远程地址、本地地址和连接的 promise 对象。
  3. 添加一个监听器到连接的 promise 对象,以便在连接操作失败时自动关闭通道。这个监听器是 ChannelFutureListener.CLOSE_ON_FAILURE,它会在连接操作失败时自动关闭通道。

通过执行这段代码,实际的连接操作将在通道所属的事件循环线程中执行。使用 channel.eventLoop().execute() 方法将连接操作提交到事件循环线程的任务队列中,确保在通道注册完成之前执行连接操作。这样做是为了给用户处理程序在 channelRegistered() 方法中设置管道的机会,以便在连接操作执行之前可以对管道进行适当的配置和初始化。

总结而言,这段代码的作用是在事件循环线程中执行实际的连接操作,并设置连接结果的 ChannelPromise 对象。它负责执行连接操作的具体细节,并将连接操作的结果通知给连接的 promise 对象。

执行连接的具体操作

从connect方法一路跟下去,可以发现调用的是ChannelOutboundInvoker接口的connect方法,接口有四个实现,

进入到AbstractChannel实现类, 可以发现调用的是pipeline的connect方法,

java 复制代码
/**
 * A skeletal Channel implementation.
 **/
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
	...
    private final DefaultChannelPipeline pipeline;
    ...

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

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

channelpipeline的默认实现的DefaultChannelPipeline, 进入到对应的类方法

java 复制代码
/**
 * The default {@link ChannelPipeline} implementation.  It is usually created
 * by a {@link Channel} implementation when the {@link Channel} is created.
 */
public class DefaultChannelPipeline implements ChannelPipeline {
    ...
    final AbstractChannelHandlerContext head;
    final AbstractChannelHandlerContext tail;
    ...
	
    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, promise);
    }

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

DefaultChannelPipeline 中,有两个重要的成员变量 headtail ,它们分别表示处理器链的头部和尾部。headAbstractChannelHandlerContext 类型的实例,代表链表中的头部节点,而 tail 则代表链表中的尾部节点。

在该代码片段中,重写了 ChannelPipeline 接口中的 connect() 方法。通过调用 tail.connect 方法 实现了连接操作的委托。

具体的逻辑如下:

  1. DefaultChannelPipeline 类中,通过 headtail 变量分别引用了链表的头部和尾部节点。
  2. connect 方法中,通过调用 tail.connect(remoteAddress, promise) 来执行连接操作。该方法将连接操作委托给链表中的尾部节点进行处理。
channelpipeline的执行顺序

这里说明下channelpipeline的执行顺序:
ChannelPipeline 的执行顺序是从头部(head)到尾部(tail)。在 Netty 中,ChannelPipeline 是一个处理器链,负责处理入站和出站的数据流。

数据在 ChannelPipeline 中的处理顺序如下:

  1. 当数据从网络进入通道时,Netty 会将数据从底层的输入缓冲区读取,并将数据作为事件传递给 ChannelPipeline 的第一个处理器(头部处理器)。
  2. 头部处理器处理该事件,可能对数据进行解码、解析、转换等操作,然后将处理结果传递给下一个处理器。
  3. 按照处理器链中的顺序,每个处理器依次处理传入的数据,进行各自的逻辑操作,然后将处理结果传递给下一个处理器。
  4. 数据会依次经过处理器链中的每个处理器,直到达到链的尾部处理器。
  5. 尾部处理器对数据进行出站处理,可能对数据进行编码、加密、发送等操作,然后将数据发送到网络。

通过这种方式,ChannelPipeline 中的处理器按照链中的顺序逐个处理数据,每个处理器可以根据自身的逻辑进行特定的操作,并将结果传递给下一个处理器。这样可以实现复杂的数据处理逻辑,并将处理过程划分为多个可复用的组件。

需要注意的是,处理器链中的每个处理器可能会有不同的作用,例如解码器、编码器、业务逻辑处理器等,具体的处理顺序和处理逻辑取决于应用程序的需求和配置。

进入到AbstractChannelHandlerContext的connect方法,

java 复制代码
abstract class AbstractChannelHandlerContext extends DefaultAttributeMap
        implements ChannelHandlerContext, ResourceLeakHint {
    ...
    volatile AbstractChannelHandlerContext next;
    volatile AbstractChannelHandlerContext prev;
	...
    
	@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) {

        if (remoteAddress == null) {
            throw new NullPointerException("remoteAddress");
        }
        if (!validatePromise(promise, false)) {
            // cancelled
            return promise;
        }

        final AbstractChannelHandlerContext next = findContextOutbound();
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeConnect(remoteAddress, localAddress, promise);
        } else {
            safeExecute(executor, new Runnable() {
                @Override
                public void run() {
                    next.invokeConnect(remoteAddress, localAddress, promise);
                }
            }, promise, null);
        }
        return promise;
    }
    ...
}

最终调用到了DefaultChannelPipeline的conntect方法,如下

java 复制代码
/**
 * The default {@link ChannelPipeline} implementation.  It is usually created
 * by a {@link Channel} implementation when the {@link Channel} is created.
 */
public class DefaultChannelPipeline implements ChannelPipeline {
    ...
    final class HeadContext extends AbstractChannelHandlerContext
            implements ChannelOutboundHandler, ChannelInboundHandler {

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

可以看到是直接调用Unsafe实例对象的connect方法,进行连接的。Unsafe是Channel接口的一个子接口,继续跟踪下去发现调用到了AbstractNioChannel的connect方法

java 复制代码
/**
 * Abstract base class for {@link Channel} implementations which use a Selector based approach.
 */
public abstract class AbstractNioChannel extends AbstractChannel {
	...
     @Override
    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;
                            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) {
            promise.tryFailure(annotateConnectException(t, remoteAddress));
            closeIfClosed();
        }
    }

    /**
     * Connect to the remote peer
     */
    protected abstract boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception;
    ...
}

可以看到该方法调用doConnect方法进行具体的连接操作, 这个方法是一个抽象方法, 交给具体的子类去实现。由于我们使用的是Nio的客户端通道,所以是通过子类NioSocketChannel去进行对应的连接操作。

java 复制代码
/**
 * {@link io.netty.channel.socket.SocketChannel} which uses NIO selector based implementation.
 */
public class NioSocketChannel extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel {
	...
    private void doBind0(SocketAddress localAddress) throws Exception {
        if (PlatformDependent.javaVersion() >= 7) {
            javaChannel().bind(localAddress);
        } else {
            javaChannel().socket().bind(localAddress);
        }
    }

    @Override
    protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
        if (localAddress != null) {
            doBind0(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();
            }
        }
    }
    ...
}
  1. 首先,检查 localAddress 参数是否为非空,如果不为空,则调用 doBind0(localAddress) 方法进行本地地址绑定操作。这是可选步骤,用于指定本地绑定地址。
  2. 然后,声明一个 success 变量用于记录连接操作是否成功。
  3. try 块中执行连接操作的具体逻辑:
    • 调用 javaChannel().connect(remoteAddress) 方法执行底层的套接字连接操作。该方法会尝试连接指定的远程地址。
    • 如果连接成功(connectedtrue ),则设置 selectionKey().interestOps(SelectionKey.OP_CONNECT),将选择键的操作设置为连接就绪,以便后续的事件通知。
    • success 设置为 true,表示连接操作成功。
    • 返回连接操作的结果(连接成功或失败)。
  4. finally 块中,如果连接操作不成功,则调用 doClose() 方法关闭通道。这是为了确保在连接操作失败时,通道被正确关闭,以释放相关资源。

该方法的作用是执行底层的套接字连接操作,并根据连接的结果进行相应的处理。如果连接成功,会将选择键的操作设置为连接就绪状态,以便后续的事件通知。如果连接操作失败,会关闭通道以释放相关资源。

需要注意的是,该方法是在底层 NIO 通道的事件循环线程中执行的,确保了连接操作的顺序性和线程安全性。具体的底层连接操作实现可能因操作系统和网络库的不同而有所差异。

可以看到,对应的连接操作就是交给java nio进行连接的,这里就不再做讲解, 如有需要, 自行去研究JavaNIO相关实现。

小结

以下是一般的连接操作的实现步骤:

  1. 首先,获取通道的事件循环(EventLoop)实例,该事件循环负责处理通道的事件和任务。
  2. 创建一个连接的 ChannelPromise 对象,用于表示连接的结果和状态。
  3. 调用事件循环的 execute() 方法,将连接操作提交到事件循环的任务队列中。
  4. 在事件循环线程中执行连接操作的具体细节,主要包括:
    • 建立底层的套接字连接。
    • 执行连接的一系列初始化操作,如握手协议、发送连接请求等。
    • 处理连接的结果,包括成功、失败或超时等情况。
    • 将连接的结果和状态设置到连接的 ChannelPromise 对象中。
  5. 当连接操作完成后,通常会触发相应的连接事件,如 channelActive 事件表示连接已激活。
  6. 在连接操作过程中,如果出现异常或连接失败的情况,会设置连接的 ChannelPromise 对象为失败状态,并将失败的原因设置到 promise 对象中。
  7. 返回表示连接结果的 ChannelPromise 对象,供用户使用。

Netty 提供了一致的接口和抽象,以便开发人员可以根据自己的需求和场景来实现和扩展连接操作的细节。

结束

你又学会了新知识,YOU CAN FLAY,YOU ARE A KING!

相关推荐
疯狂的程序猴6 分钟前
iOS App 混淆的真实世界指南,从构建到成品 IPA 的安全链路重塑
后端
旷野说13 分钟前
为什么 MyBatis 原生二级缓存“难以修复”?
java·java-ee·mybatis
8***235516 分钟前
【wiki知识库】07.用户管理后端SpringBoot部分
java
bcbnb18 分钟前
iOS 性能测试的工程化方法,构建从底层诊断到真机监控的多工具测试体系
后端
开心就好202521 分钟前
iOS 上架 TestFlight 的真实流程复盘 从构建、上传到审核的团队协作方式
后端
小周在成长29 分钟前
Java 泛型支持的类型
后端
aiopencode29 分钟前
Charles 抓不到包怎么办?HTTPS 抓包失败、TCP 数据流异常与底层补抓方案全解析
后端
阿蔹32 分钟前
JavaWeb-Selenium 配置以及Selenim classnotfound问题解决
java·软件测试·python·selenium·测试工具·自动化
稚辉君.MCA_P8_Java34 分钟前
Gemini永久会员 C++返回最长有效子串长度
开发语言·数据结构·c++·后端·算法
Penge6661 小时前
Redis-bgsave浅析
redis·后端