Bootstrap初始化过程

netty的客户端引导类是Bootstrap,我们看一下spark的rpc中客户端部分对Bootstrap的初始化过程

TransportClientFactory.createClient(InetSocketAddress address)

只需要贴出Bootstrap初始化部分的代码

  1. // 客户端引导对象
  2. Bootstrap bootstrap = new Bootstrap();
  3. // 设置各种参数
  4. bootstrap.group(workerGroup)
  5. .channel(socketChannelClass)
  6. // Disable Nagle's Algorithm since we don't want packets to wait
  7. // 关闭Nagle算法
  8. .option(ChannelOption.TCP_NODELAY, true)
  9. .option(ChannelOption.SO_KEEPALIVE, true)
  10. .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, conf.connectionTimeoutMs())
  11. .option(ChannelOption.ALLOCATOR, pooledAllocator);
  12. // socket接收缓冲区
  13. if (conf.receiveBuf() > 0) {
  14. bootstrap.option(ChannelOption.SO_RCVBUF, conf.receiveBuf());
  15. }
  16. // socket发送缓冲区
  17. // 对于接收和发送缓冲区的设置应该用如下的公式计算:
  18. // 延迟 *带宽
  19. // 例如延迟是1ms,带宽是10Gbps,那么缓冲区大小应该设为1.25MB
  20. if (conf.sendBuf() > 0) {
  21. bootstrap.option(ChannelOption.SO_SNDBUF, conf.sendBuf());
  22. }
  23. final AtomicReference<TransportClient> clientRef = new AtomicReference<>();
  24. final AtomicReference<Channel> channelRef = new AtomicReference<>();
  25. // 设置handler(处理器对象)
  26. bootstrap.handler(new ChannelInitializer<SocketChannel>() {
  27. @Override
  28. public void initChannel(SocketChannel ch) {
  29. TransportChannelHandler clientHandler = context.initializePipeline(ch);
  30. clientRef.set(clientHandler.getClient());
  31. channelRef.set(ch);
  32. }
  33. });
  34. // Connect to the remote server
  35. long preConnect = System.nanoTime();
  36. // 与服务端建立连接,启动方法
  37. ChannelFuture cf = bootstrap.connect(address);

分为几个主要的步骤:

  • 首先创建一个Bootstrap对象,调用的是无参构造器
  • 设置各种参数,如通道类型,关闭Nagle算法,接收和发送缓冲区大小,设置处理器
  • 调用connect与服务端建立连接

接下来,我们主要通过两条线索来分析Bootstrap的启动过程,即构造器和connect两个方法,而对于设置参数的过程仅仅是给内部的一些成员变量赋值,所以不需要详细展开。

Bootstrap.Bootstrap()

Bootstrap继承了AbstractBootstrap,看了一下他们的无参构造方法,都是个空方法。。。。。。所以这一步,我们就省了,瞬间感觉飞起来了有没有_

Bootstrap.connect(SocketAddress remoteAddress)

  1. public ChannelFuture connect(SocketAddress remoteAddress) {
  2. // 检查非空
  3. ObjectUtil.checkNotNull(remoteAddress, "remoteAddress");
  4. // 同样是对一些成员变量检查非空,主要检查EventLoopGroup,ChannelFactory,handler对象
  5. validate();
  6. return doResolveAndConnect(remoteAddress, config.localAddress());
  7. }

主要是做了一些非空检查,需要注意的是,ChannelFactory对象的设置,前面的spark中在对Bootstrap初始化设置的时候调用了.channel(socketChannelClass)方法,这个方法如下:

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

创建了一个ReflectiveChannelFactory对象,并赋值给内部的channelFactory成员。这个工厂类会根据传进来的Class对象通过反射创建一个Channel实例。

doResolveAndConnect

从这个方法的逻辑中可以看出来,创建一个连接的过程分为两个主要的步骤;

  • 初始化一个Channel对象并注册到EventLoop中
  • 调用doResolveAndConnect0方法完成tcp连接的建立

值得注意的是,initAndRegister方法返回一个Future对象,这个类型通常用于异步机制的实现。在这里,如果注册没有立即成功的话,会给返回的futrue对象添加一个监听器,在注册成功以后建立tcp连接。

  1. private ChannelFuture doResolveAndConnect(final SocketAddress remoteAddress, final SocketAddress localAddress) {
  2. // 初始化一个Channel对象并注册到EventLoop中
  3. final ChannelFuture regFuture = initAndRegister();
  4. final Channel channel = regFuture.channel();
  5. if (regFuture.isDone()) {
  6. // 如果注册失败,世界返回失败的future对象
  7. if (!regFuture.isSuccess()) {
  8. return regFuture;
  9. }
  10. return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise());
  11. } else {// 如果注册还在进行中,需要向future对象添加一个监听器,以便在注册成功的时候做一些工作,监听器实际上就是一个回调对象
  12. // Registration future is almost always fulfilled already, but just in case it's not.
  13. final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
  14. regFuture.addListener(new ChannelFutureListener() {
  15. @Override
  16. public void operationComplete(ChannelFuture future) throws Exception {
  17. // Directly obtain the cause and do a null check so we only need one volatile read in case of a
  18. // failure.
  19. Throwable cause = future.cause();
  20. if (cause != null) {
  21. // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
  22. // IllegalStateException once we try to access the EventLoop of the Channel.
  23. promise.setFailure(cause);
  24. } else {
  25. // Registration was successful, so set the correct executor to use.
  26. // See https://github.com/netty/netty/issues/2586
  27. promise.registered();
  28. // 注册成功后仍然调用doResolveAndConnect0方法完成连接建立的过程
  29. doResolveAndConnect0(channel, remoteAddress, localAddress, promise);
  30. }
  31. }
  32. });
  33. return promise;
  34. }

initAndRegister

仍然分为两个步骤:

  • 通过channel工厂类创建一个channel对象,通过反射获取指定的channel类型的无参构造器,调用构造器来创建对象
  • 调用init方法对channel对象进行初始化,init方法是一个抽象方法,Bootstrap和ServerBootstrap的实现不同
  • 将channel注册到EventLoopGroup中

注意看源码中的一段注释,这段注释对netty的线程模型的理解很有帮助,大致意思是说:

  • 如果当前的代码是在EventLoopEvent线程中执行的,那么代码运行到这里说明channel已经成功注册到EventLoopEvent上了,此时再调用bind() 或 connect()方法肯定是没有问题的
  • 如果当前代码不是在EventLoopEvent线程中执行的,也就是说当前线程是另外的线程,在这里继续调用bind() 或 connect()方法仍然是安全的,并不会由于并发引起方法执行顺序的错乱,原因是netty中一个channel只会绑定到一个线程上,所有关于这个channel的操作包括注册,bind或connect都会以排队任务的形式在一个线程中串行执行,这种做法也为netty规避了很多线程安全问题,从而减少了很多加锁,同步的代码,减少了线程之间的竞争资源导致的线程切换,侧面上提高了线程执行效率。

final ChannelFuture initAndRegister() {

Channel channel = null;

try {

// 通过channel工厂类创建一个channel对象

channel = channelFactory.newChannel();

// 调用init方法对channel进行一些初始化的设置

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

}

// as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor

return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);

}

  1. // 注册到EventLoopGroup中
  2. ChannelFuture regFuture = config().group().register(channel);
  3. // 如果发生异常,需要关闭已经建立的连接
  4. if (regFuture.cause() != null) {
  5. if (channel.isRegistered()) {
  6. channel.close();
  7. } else {
  8. channel.unsafe().closeForcibly();
  9. }
  10. }
  11. // If we are here and the promise is not failed, it's one of the following cases:
  12. // 1) If we attempted registration from the event loop, the registration has been completed at this point.
  13. // i.e. It's safe to attempt bind() or connect() now because the channel has been registered.
  14. // 2) If we attempted registration from the other thread, the registration request has been successfully
  15. // added to the event loop's task queue for later execution.
  16. // i.e. It's safe to attempt bind() or connect() now:
  17. // because bind() or connect() will be executed *after* the scheduled registration task is executed
  18. // because register(), bind(), and connect() are all bound to the same thread.
  19. return regFuture;
  20. }

NioSocketChannel初始化

DEFAULT_SELECTOR_PROVIDER是默认的SelectorProvider对象,这时jdk中定义的一个类,主要作用是生成选择器selector对象和通道channel对象

  1. public NioSocketChannel() {
  2. this(DEFAULT_SELECTOR_PROVIDER);
  3. }

newSocket中通过调用provider.openSocketChannel()方法创建了一个SocketChannel对象,它的默认实现是SocketChannelImpl。

public NioSocketChannel(SelectorProvider provider) {

this(newSocket(provider));

}

然后经过几次调用,最后调用了下面的构造器,首先调用了父类AbstractNioByteChannel的构造器,

然后创建了一个SocketChannelConfig对象,这个类有点类似于门面模式,对NioSocketChannel对象和Socket对象的一些参数设置和获取的接口进行封装。

public NioSocketChannel(Channel parent, SocketChannel socket) {

super(parent, socket);

config = new NioSocketChannelConfig(this, socket.socket());

}

我们在接着看父类AbstractNioByteChannel的构造方法

AbstractNioByteChannel(Channel parent, SelectableChannel ch)

没有做任何工作,直接调用了父类的构造方法,注意这里多了一个参数SelectionKey.OP_READ,这个参数表示channel初始时的感兴趣的事件,channel刚创建好之后对read事件感兴趣

protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {

super(parent, ch, SelectionKey.OP_READ);

}

AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp)

主要还是调用父类的构造方法

  1. protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
  2. // 父类构造方法
  3. super(parent);
  4. this.ch = ch;
  5. this.readInterestOp = readInterestOp;
  6. try {
  7. // 设置非阻塞
  8. ch.configureBlocking(false);
  9. } catch (IOException e) {
  10. try {
  11. // 如果发生异常,关闭该channel
  12. ch.close();
  13. } catch (IOException e2) {
  14. if (logger.isWarnEnabled()) {
  15. logger.warn(
  16. "Failed to close a partially initialized socket.", e2);
  17. }
  18. }
  19. throw new ChannelException("Failed to enter non-blocking mode.", e);
  20. }
  21. }

AbstractChannel(Channel parent)

最关键的初始化逻辑在这个最顶层的基类中,其中很重的两个对象Unsafe对象和ChannelPipeline对象,前者封装了jdk底层api的调用,后者是实现netty对事件的链式处理的核心类。

  1. protected AbstractChannel(Channel parent) {
  2. this.parent = parent;
  3. // 创建一个ChannelId对象,唯一标识该channel
  4. id = newId();
  5. // Unsafe对象,封装了jdk底层的api调用
  6. unsafe = newUnsafe();
  7. // 创建一个DefaultChannelPipeline对象
  8. pipeline = newChannelPipeline();
  9. }

小结

前面一小节,我们主要简单分析了一下NioSocketChannel的初始化过程,可以看到最主要的逻辑在AbstractChannel的构造方法中,这里我们看到了两个重要的类的创建过程。

Bootstrap.init

回到AbstractBootstrap.initAndRegister方法中,在完成通过反射调用NioSocketChannel构造方法并创建一个实例后,紧接着就要对这个新创建的Channel实例进行初始化设置工作,我们看一下Bootstrap对新创建的Channel的初始化过程:

  • 向channel的Pipeline中添加一个处理器,ChannelPipeline我们可以理解为一个流水线,在这条流水线上有各种各样的处理器,一个channel事件产生后会在这个流水线上进行传播,依次经过所有的处理器

  • 设置参数,也就是以ChannelOption为key的一些参数,可以通过DefaultChannelConfig.setOption方法看到具体可以设置哪些参数。

  • 设置属性

    void init(Channel channel) throws Exception {

    ChannelPipeline p = channel.pipeline();

    // 向ChannelPipeline中添加一个处理器,这个处理器就是我们之前设置的处理器

    p.addLast(config.handler());

    1. final Map<ChannelOption<?>, Object> options = options0();
    2. // 设置参数,最终是通过调用SocketChannelConfig的一些参数设置接口设置参数
    3. synchronized (options) {
    4. setChannelOptions(channel, options, logger);
    5. }
    6. final Map<AttributeKey<?>, Object> attrs = attrs0();
    7. // 设置属性
    8. synchronized (attrs) {
    9. for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
    10. channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
    11. }
    12. }

    }

MultithreadEventLoopGroup.register

在完成channel的创建和初始化之后,我们就要将这个channel注册到一个EventLoop中,NioNioEventLoop继承自MultithreadEventLoopGroup, 通过调用SingleThreadEventLoop的register方法完成注册

  1. public ChannelFuture register(Channel channel) {
  2. return next().register(channel);
  3. }

可以看到,通过next()方法选出了其中的一个EventLoop进行注册。MultithreadEventLoopGroup是对多个真正的EventLoopGroup的封装,每个实现了实际功能的真正的EventLoopGroup运行在一个线程内,

所以我们接下来应该看单个的EventLoopGroup的注册方法。

SingleThreadEventLoop.register

这里创建了一个DefaultChannelPromise对象,用于作为返回值。

  1. public ChannelFuture register(Channel channel) {
  2. return register(new DefaultChannelPromise(channel, this));
  3. }

最终调用了Unsafe的register方法将channel绑定到当前的EventLoopGroup对象上。

public ChannelFuture register(final ChannelPromise promise) {

ObjectUtil.checkNotNull(promise, "promise");

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

return promise;

}

AbstractChannel.AbstractUnsafe.register

  • 首先是做一些前置检查,包括变量非空检查,重复注册检查,检查channel类型和EventLoopGroup类型是否匹配

  • 将这个channel绑定到指定的eventLoop对象上,

  • 调用register0完成注册

    1. public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    2. // 做一些非空检查
    3. if (eventLoop == null) {
    4. throw new NullPointerException("eventLoop");
    5. }
    6. // 如果重复注册,通过future对象抛出一个异常
    7. // 一个channel只能注册到一个EventLoopGroup对象上
    8. if (isRegistered()) {
    9. promise.setFailure(new IllegalStateException("registered to an event loop already"));
    10. return;
    11. }
    12. // 检查channel类型和EventLoopGroup类型是否匹配
    13. if (!isCompatible(eventLoop)) {
    14. promise.setFailure(
    15. new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
    16. return;
    17. }
    18. // 将channel内部的eventLoop成员设置为相应的对象
    19. // 也就是将这个channel绑定到指定顶eventLoop上
    20. AbstractChannel.this.eventLoop = eventLoop;
    21. // 这里做了一个判断,如果当前处于eventLoop对应的线程内,那么直接执行代码
    22. // 如果当前运行的线程与eventLoop不是同一个,那么将这个注册的任务添加到eventLoop的任务队列中
    23. if (eventLoop.inEventLoop()) {
    24. register0(promise);
    25. } else {
    26. try {
    27. eventLoop.execute(new Runnable() {
    28. @Override
    29. public void run() {
    30. register0(promise);
    31. }
    32. });
    33. } catch (Throwable t) {
    34. logger.warn(
    35. "Force-closing a channel whose registration task was not accepted by an event loop: {}",
    36. AbstractChannel.this, t);
    37. closeForcibly();
    38. closeFuture.setClosed();
    39. safeSetFailure(promise, t);
    40. }
    41. }
    42. }

AbstractChannel.AbstractUnsafe.register0

这个方法实现了实际的注册逻辑,

  • 依然要做一些前置的设置和检查工作,包括在注册过程中不可取消,检查channel是否存活,

  • 调用jdk的api完成注册。例如,对于jdk Nio的通道的注册就是调用SelectableChannel.register(Selector sel, int ops, Object att)

  • 调用所有的已添加的处理器节点的ChannelHandler.handlerAdded方法,实际上这也会调用handler.handlerRemoved方法,如果在此之前有handler被移除掉的话

  • 通知future对象已经注册成功了

  • 触发一个channel注册成功的事件,这个事件会在pipeline中传播,所有注册的handler会依次接收到该事件并作出相应的处理

  • 如果是第一次注册,还需要触发一个channel存活的事件,让所有的handler作出相应的处理

    1. private void register0(ChannelPromise promise) {
    2. try {
    3. // check if the channel is still open as it could be closed in the mean time when the register
    4. // call was outside of the eventLoop
    5. // 将ChannelPromise设置为不可取消,并检查channel是否还存活,通过内部的jdk的channel检查是否存活
    6. if (!promise.setUncancellable() || !ensureOpen(promise)) {
    7. return;
    8. }
    9. // 是否第一次注册,
    10. // TODO 说明情况下会注册多次??
    11. boolean firstRegistration = neverRegistered;
    12. // 完成实际的注册,即底层api的调用
    13. // 如果对于jdk Nio的通道的注册就是调用SelectableChannel.register(Selector sel, int ops, Object att)
    14. doRegister();
    15. // 更新标志变量
    16. neverRegistered = false;
    17. registered = true;
    18. // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
    19. // user may already fire events through the pipeline in the ChannelFutureListener.
    20. // 调用所有的已添加的处理器节点的ChannelHandler.handlerAdded方法
    21. pipeline.invokeHandlerAddedIfNeeded();
    22. // 通过future对象已经注册成功了
    23. safeSetSuccess(promise);
    24. // 触发一个channel注册成功的事件,这个事件会在pipeline中传播,
    25. // 所有注册的handler会依次接收到该事件并作出相应的处理
    26. pipeline.fireChannelRegistered();
    27. // Only fire a channelActive if the channel has never been registered. This prevents firing
    28. // multiple channel actives if the channel is deregistered and re-registered.
    29. if (isActive()) {
    30. if (firstRegistration) {
    31. // 如果是第一次注册,还需要触发一个channel存活的事件,让所有的handler作出相应的处理
    32. pipeline.fireChannelActive();
    33. } else if (config().isAutoRead()) {
    34. // This channel was registered before and autoRead() is set. This means we need to begin read
    35. // again so that we process inbound data.
    36. //
    37. // See https://github.com/netty/netty/issues/4805
    38. // 开始接收读事件
    39. // 对于Nio类型的channel, 通过调用jdk的相关api注册读事件为感兴趣的事件
    40. beginRead();
    41. }
    42. }
    43. } catch (Throwable t) {
    44. // Close the channel directly to avoid FD leak.
    45. closeForcibly();
    46. closeFuture.setClosed();
    47. safeSetFailure(promise, t);
    48. }
    49. }

小结

到此,我们就完成了对channel的创建,初始化,和注册到EventLoop过程的分析,整个过程看下来,其实并不复杂,只不过代码的嵌套比较深,继承结构复杂,有些简单的功能可能要看好几层才能找到真正实现的地方,所以还需要耐心和熟悉。这里,我把主干逻辑再提炼一下,去掉所有细枝末节的逻辑,一遍能有一个整体的认识:

  • 首先通过反射创建了一个NioSocketChannel(通过反射调用无参构造器)
  • 然后对channel对象进行初始化,主要是想这个channel的ChannelPipeline中添加用户设置的handler
  • 最后将这个channel注册到一个EventLoop上,注册过程设计jdk底层的selector注册api的调用,调用handler的回调方法,在channelPipeline中触发一个channel注册的事件,这些事件最终回调各个handler对象的channelRegistered方法。

接下来,我们回到Bootstrap.doResolveAndConnect方法中,继续完成建立连接的过程的分析。

Bootstrap.doResolveAndConnect0

连接的建立在方法doResolveAndConnect0中实现:

这个方法的主要工作就是对远程地址进行解析,比如通过dns服务器对域名进行解析,

然后使用解析后的地址进行连接的建立,连接建立调用doConnect方法

  1. private ChannelFuture doResolveAndConnect0(final Channel channel, SocketAddress remoteAddress,
  2. final SocketAddress localAddress, final ChannelPromise promise) {
  3. try {
  4. final EventLoop eventLoop = channel.eventLoop();
  5. // 获取一个地址解析器
  6. final AddressResolver<SocketAddress> resolver = this.resolver.getResolver(eventLoop);
  7. // 如果解析器不支持该地址或者改地址已经被解析过了,那么直接开始创建连接
  8. if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) {
  9. // Resolver has no idea about what to do with the specified remote address or it's resolved already.
  10. doConnect(remoteAddress, localAddress, promise);
  11. return promise;
  12. }
  13. // 对远程地址进行解析
  14. final Future<SocketAddress> resolveFuture = resolver.resolve(remoteAddress);
  15. if (resolveFuture.isDone()) {
  16. final Throwable resolveFailureCause = resolveFuture.cause();
  17. if (resolveFailureCause != null) {
  18. // Failed to resolve immediately
  19. channel.close();
  20. promise.setFailure(resolveFailureCause);
  21. } else {
  22. // Succeeded to resolve immediately; cached? (or did a blocking lookup)
  23. // 解析成功后进行连接
  24. doConnect(resolveFuture.getNow(), localAddress, promise);
  25. }
  26. return promise;
  27. }
  28. // Wait until the name resolution is finished.
  29. // 给future对象添加一个回调,采用异步方法进行连接,
  30. resolveFuture.addListener(new FutureListener<SocketAddress>() {
  31. @Override
  32. public void operationComplete(Future<SocketAddress> future) throws Exception {
  33. if (future.cause() != null) {
  34. channel.close();
  35. promise.setFailure(future.cause());
  36. } else {
  37. doConnect(future.getNow(), localAddress, promise);
  38. }
  39. }
  40. });
  41. } catch (Throwable cause) {
  42. promise.tryFailure(cause);
  43. }
  44. return promise;
  45. }

Bootstrap.doConnect

调用channel的connect方法完成连接过程。

也许是之前看scala代码习惯了,回过头来看java代码感觉很冗余,一大堆代码就表达了那一点逻辑,感觉信息密度太低,现在有很多人认为java会渐渐的没落,而最优可能取代java的语言中,scala绝对是强有力的竞争者之一,没有对比就没有伤害,跟java比,scala语言真的是简洁太多了,几句话就能把所要表达的逻辑精准而又直接地表达出来。好像向声明式编程更靠近了一点。

  1. private static void doConnect(
  2. final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise connectPromise) {
  3. // This method is invoked before channelRegistered() is triggered. Give user handlers a chance to set up
  4. // the pipeline in its channelRegistered() implementation.
  5. final Channel channel = connectPromise.channel();
  6. channel.eventLoop().execute(new Runnable() {
  7. @Override
  8. public void run() {
  9. if (localAddress == null) {
  10. // 调用 channel.connect方法完成连接
  11. channel.connect(remoteAddress, connectPromise);
  12. } else {
  13. channel.connect(remoteAddress, localAddress, connectPromise);
  14. }
  15. connectPromise.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
  16. }
  17. });
  18. }

AbstractChannel.connect

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

DefaultChannelPipeline.connect

这里稍微说明一下,tail是整个链条的尾节点,如果对netty比较熟悉的话,应该知道netty对于io事件的处理采用责任链的模式,即用户可以设置多个处理器,这些处理器组成一个链条,io事件在这个链条上传播,被特定的一些处理器所处理,而其中有两个特殊的处理器head和tail,他们分别是这个链条的头和尾,他们的存在主要是为了实现一些特殊的逻辑。

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

AbstractChannelHandlerContext.connect

中间经过几个调用之后,最终调用该方法。这里有一句关键代码findContextOutbound(MASK_CONNECT),这个方法的代码我就不贴了,大概说一下它的作用,更为具体的机制等后面分析Channelpipeline是在详细说明。这个方法会在处理器链中从后向前遍历,直到找到能够处理connect事件的处理器,能否处理某种类型的事件是通过比特位判断的,每个AbstractChannelHandlerContext对象内部有一个int型变量用于存储标志各种类型事件的比特位。一般,connect事件会有头结点head来处理,也就是DefaultChannelPipeline.HeadContext类,所以我们直接看DefaultChannelPipeline.HeadContext.connect方法

  1. public ChannelFuture connect(
  2. final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
  3. if (remoteAddress == null) {
  4. throw new NullPointerException("remoteAddress");
  5. }
  6. if (isNotValidPromise(promise, false)) {
  7. // cancelled
  8. return promise;
  9. }
  10. // 找到下一个能够进行connect操作的,这里用比特位来标记各种不同类型的操作,
  11. final AbstractChannelHandlerContext next = findContextOutbound(MASK_CONNECT);
  12. EventExecutor executor = next.executor();
  13. if (executor.inEventLoop()) {
  14. // 调用AbstractChannelHandlerContext.invokeConnect
  15. next.invokeConnect(remoteAddress, localAddress, promise);
  16. } else {
  17. safeExecute(executor, new Runnable() {
  18. @Override
  19. public void run() {
  20. next.invokeConnect(remoteAddress, localAddress, promise);
  21. }
  22. }, promise, null);
  23. }
  24. return promise;
  25. }

DefaultChannelPipeline.HeadContext.connect

  1. public void connect(
  2. ChannelHandlerContext ctx,
  3. SocketAddress remoteAddress, SocketAddress localAddress,
  4. ChannelPromise promise) {
  5. unsafe.connect(remoteAddress, localAddress, promise);
  6. }

unsafe对象的赋值:

  1. HeadContext(DefaultChannelPipeline pipeline) {
  2. super(pipeline, null, HEAD_NAME, HeadContext.class);
  3. unsafe = pipeline.channel().unsafe();
  4. setAddComplete();
  5. }

所以我们直接看unsafe.connect

AbstractNioChannel.connect

主要逻辑:

  • 状态检查,非空检查
  • 调用doConnect方法进行连接
  • 如果立即就连接成功了,那么将future对象设置为成功
  • 如果超时大于0,会提交一个延迟调度的任务,在超时时间到达后执行这个任务检查是否连接成功,如果为连接成功连接说明连接超时,需要关闭通道
  • 向future对象添加一个回调,在future被外部调用者取消时将通道关闭

可见建立连接的核心方法是doConnect,这是一个抽象方法,我们看NioSocketChannel,也就是tcp连接的建立过程,查看AbstractNioChannel的实现类发现还有UDP,SCTP等协议

  1. public final void connect(
  2. final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
  3. // 检查promise状态,channel存活状态
  4. if (!promise.setUncancellable() || !ensureOpen(promise)) {
  5. return;
  6. }
  7. try {
  8. // 防止重复连接
  9. if (connectPromise != null) {
  10. // Already a connect in process.
  11. throw new ConnectionPendingException();
  12. }
  13. boolean wasActive = isActive();
  14. // 调用doConnect方法进行连接
  15. if (doConnect(remoteAddress, localAddress)) {
  16. // 如果立即就连接成功了,那么将future对象设置为成功
  17. fulfillConnectPromise(promise, wasActive);
  18. } else {
  19. connectPromise = promise;
  20. requestedRemoteAddress = remoteAddress;
  21. // Schedule connect timeout.
  22. int connectTimeoutMillis = config().getConnectTimeoutMillis();
  23. // 如果超时大于0,那么会在超时到达后检查是否连接成功
  24. if (connectTimeoutMillis > 0) {
  25. connectTimeoutFuture = eventLoop().schedule(new Runnable() {
  26. @Override
  27. public void run() {
  28. ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
  29. ConnectTimeoutException cause =
  30. new ConnectTimeoutException("connection timed out: " + remoteAddress);
  31. // 如果connectPromise能够标记为失败,说明此时还没有连接成功,也就是连接超时了
  32. // 此时需要关闭该通道
  33. if (connectPromise != null && connectPromise.tryFailure(cause)) {
  34. close(voidPromise());
  35. }
  36. }
  37. }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
  38. }
  39. // 向future对象添加一个回调,在future被外部调用者取消时将通道关闭
  40. promise.addListener(new ChannelFutureListener() {
  41. @Override
  42. public void operationComplete(ChannelFuture future) throws Exception {
  43. if (future.isCancelled()) {
  44. if (connectTimeoutFuture != null) {
  45. connectTimeoutFuture.cancel(false);
  46. }
  47. connectPromise = null;
  48. close(voidPromise());
  49. }
  50. }
  51. });
  52. }
  53. } catch (Throwable t) {
  54. promise.tryFailure(annotateConnectException(t, remoteAddress));
  55. closeIfClosed();
  56. }
  57. }

NioSocketChannel.doConnect

  • 首先绑定指定的本地地址

  • 调用SocketUtils.connect建立连接

    protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {

    // 绑定指定的本地地址

    if (localAddress != null) {

    doBind0(localAddress);

    }

    1. // 这个变量标记建立连接的动作是否发起成功
    2. // 成功发起建立连接的工作并不表示连接已经成功建立
    3. boolean success = false;
    4. try {
    5. // 实际建立连接的语句
    6. boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
    7. if (!connected) {
    8. selectionKey().interestOps(SelectionKey.OP_CONNECT);
    9. }
    10. success = true;
    11. // 返回连接是否已经成功建立
    12. return connected;
    13. } finally {
    14. if (!success) {
    15. doClose();
    16. }
    17. }

    }

SocketUtils.connect

可以看到,最终是通过调用jdk的api来实现连接的建立,也就是SocketChannel.connect方法

  1. public static boolean connect(final SocketChannel socketChannel, final SocketAddress remoteAddress)
  2. throws IOException {
  3. try {
  4. return AccessController.doPrivileged(new PrivilegedExceptionAction<Boolean>() {
  5. @Override
  6. public Boolean run() throws IOException {
  7. // 调用jdk api建立连接,SocketChannel.connect
  8. return socketChannel.connect(remoteAddress);
  9. }
  10. });
  11. } catch (PrivilegedActionException e) {
  12. throw (IOException) e.getCause();
  13. }
  14. }

总结

一句话,这代码是真的很深! 非常不直接,初次看的话,如果没有一个代码框架图在旁边参考,很容易迷失在层层的继承结构中,很多代码层层调用,真正有用的逻辑隐藏的很深,所以看这中代码必须要有耐心,有毅力,要有打破砂锅问到底的决心。不过这样的复杂的代码结构好处也是显而易见的,那就是良好的扩展性,你可以在任意层级进行扩展。

总结一下建立连接的过程,我认为可以归结为三个主要的方面:

  • 第一, 实际建立逻辑的代码肯定还是jdk api
  • 第二,这么多方法调用,主要的作用就是迎合框架的要求,本质上是为了代码的扩展性,比如ChannelPipeline的处理器链
  • 第三,另一个主要的工作就是对future对象的处理,这时实现异步的重要手段,future对象也是外部调用者和对象内部状态之间的连接纽带,调用者通过future对象完成一些功能,如查状态,发出取消动作,实现阻塞等待等。

Bootstrap初始化过程源码分析--netty客户端的启动的更多相关文章

  1. A2dp sink 初始化流程源码分析

    A2dp sink的初始化流程和A2dp 的初始化流程,基本一样,这里做简单分析.这里分析的android的版本是Android O. 我们先从service的启动说起吧. 下面 是启动的时候的log ...

  2. A2dp初始化流程源码分析

    蓝牙启动的时候,会涉及到各个profile 的启动.这篇文章分析一下,蓝牙中a2dp profile的初始化流程. 我们从AdapterState.java中对于USER_TURN_ON 消息的处理说 ...

  3. scrapy 源码解析 (一):启动流程源码分析(一)命令行启动

    前言 虽然爬虫的入门级编写并不难,但要让爬虫真正稳定可靠的运行起来,真不是一件容易的事.首先,要用到scrapy,就必须要读懂scrapy这个爬虫框架,如果连这个框架的执行逻辑都搞不懂,那么爬虫也很难 ...

  4. Netty入门一:服务端应用搭建 & 启动过程源码分析

    最近周末也没啥事就学学Netty,同时打算写一些博客记录一下(写的过程理解更加深刻了) 本文主要从三个方法来呈现:Netty核心组件简介.Netty服务端创建.Netty启动过程源码分析 如果你对Ne ...

  5. Dubbo消费方服务调用过程源码分析

    参考:dubbo消费方服务调用过程源码分析dubbo基于spring的构建分析Dubbo概述--调用过程dubbo 请求调用过程分析dubbo集群容错机制代码分析1dubbo集群容错策略的代码分析2d ...

  6. [Android]从Launcher开始启动App流程源码分析

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5017056.html 从Launcher开始启动App流程源码 ...

  7. Spring加载流程源码分析03【refresh】

      前面两篇文章分析了super(this)和setConfigLocations(configLocations)的源代码,本文来分析下refresh的源码, Spring加载流程源码分析01[su ...

  8. spark源码阅读--shuffle读过程源码分析

    shuffle读过程源码分析 上一篇中,我们分析了shuffle在map阶段的写过程.简单回顾一下,主要是将ShuffleMapTask计算的结果数据在内存中按照分区和key进行排序,过程中由于内存限 ...

  9. Spark(五十一):Spark On YARN(Yarn-Cluster模式)启动流程源码分析(二)

    上篇<Spark(四十九):Spark On YARN启动流程源码分析(一)>我们讲到启动SparkContext初始化,ApplicationMaster启动资源中,讲解的内容明显不完整 ...

随机推荐

  1. Unity检视面板的继承方法研究 (二)

    之前做了普通对象的可继承的检视面板类, 现在想要实现对Unity自带的检视面板的继承的话, 要怎样写呢? 万变不离其宗,  仍然是围绕UnityEditor.Editor.CreateEditor 这 ...

  2. 2.1 Scala语言概述

    一.编程范式 命令式编程没有办法充分利用多核CPU: 函数式编程很多变量是不可修改的. 二.Scala简介 特点 scala运行在JVM上,兼容现有的Java程序: 面向对象的编程语言: 一门函数式语 ...

  3. Leetcode142 环形链表

    很多题解没有讲清楚非环部分的长度与相遇点到环起点那部分环之间为何是相等的这个数学关系.这里我就补充下为何他们是相等的.假设非环部分的长度是x,从环起点到相遇点的长度是y.环的长度是c.现在走的慢的那个 ...

  4. 【oracle】迁表结构和数据

    背景:把一些表和数据从某库迁到另一个库 1.命令框: exp yktsh/yktsh_2019@orcl30 file=d:\yktsh20191201.dmp log=d:\daochu; exp ...

  5. zeebe prometheus 监控配置

    zeebe 默认已经集成了prometheus,以下是一个简单的配置,关于grafana 的集成需要调整下 dashboard,目前网上的已经太老了 docker-compose 文件   versi ...

  6. <Trie> 212 <Array> 229

    212. Word Search II class TrieNode{ char val; TrieNode[] children; String word; public TrieNode(char ...

  7. 11/8 <matrix> LC 48 54 59

    48. Rotate Image 先按对角线对称图形,再水平对折. class Solution { public void rotate(int[][] matrix) { //1.transpos ...

  8. 动态规划 | 对输入进行hash处理的LIS 1045

    把序列M处理为有序序列,并且M不存在的序列要在A中删除. 对A进行了处理之后,执行LIS的操作(O(N^2)复杂度).当然可以优化为对数复杂度的,不过pat不卡这个. LCS解法:动态规划 | 保留重 ...

  9. 【LG4437】[HNOI/AHOI2018]排列

    [LG4437][HNOI/AHOI2018]排列 题面 洛谷 题解 题面里这个毒瘤的东西我们转化一下: 对于\(\forall k,j\),若\(p_k=a_{p_j}\),则\(k<j\). ...

  10. oracle-报错 RMAN-03002,RMAN-06172

    RMAN> restore standby controlfile from "/data/oracle/contral.ctl"; Starting restore at ...