Netty源码解析之服务端启动

前言

从本篇文章开始,将开启一系列的Netty源码分析。Netty的版本均基于4.1.37.Final作分析。Netty的各个版本的一些实现可能不同,但大致的思想是相同的,希望读者观其大意,理解其思想为主

至于BIO与NIO的优势劣势、Netty是什么、why Netty这些内容都在—> Netty前言 中,这里就不多赘述,直入正题。本篇文章主要分析Netty服务端在启动的时候都做了什么,由于接下来的几篇文章都会围绕服务端启动后的详细细节,所以这里首先介绍一下服务端的启动源码

Sample

先来看一段服务端启动的代码

public static void start(int port) {

  // 创建两个线程池,在此可看成一个黑盒,在下篇文章中会介绍到
  EventLoopGroup bossEventLoop = new NioEventLoopGroup(1);
  EventLoopGroup workerEventLoop = new NioEventLoopGroup();

  try {
    // 服务端启动引导类
    ServerBootstrap bootstrap = new ServerBootstrap();

    // 设置线程池
    bootstrap.group(bossEventLoop, workerEventLoop)
      // 设置服务端channel
      .channel(NioServerSocketChannel.class)
      // 设置一个处理客户端channel的handler
      .childHandler(new ChannelInitializer() {
        @Override
        protected void initChannel(Channel ch) throws Exception {
          ChannelPipeline pipeline = ch.pipeline();
          // 添加某个Handler
          pipeline.addLast(null);
        }
      });

    // 绑定端口
    ChannelFuture channelFuture = bootstrap.bind(port);
    // 阻塞在此,直到被关闭
    channelFuture.channel().closeFuture().sync();
  } catch (Exception e) {
    e.printStackTrace();
  } finally {
    // 优雅关闭资源
    bossEventLoop.shutdownGracefully();
    workerEventLoop.shutdownGracefully();
  }
}

ServerBootstrap为本章分析的重点,这个类引导了各个Netty组件的初始化工作,它存在的意义就是为了简化Netty的启动,内部封装了各种流程。

以此为切入点,接下来我们来一步步的剖析这个引导类的工作

引导类的启动

线程池的设置

这里先剧透一下,EventLoopGroup这个类可以说是Netty的发动机,整个Netty服务端的运转都靠这个类来驱动着,在前言中我们提到,Netty是异步-事件-驱动的,这里的驱动,就是指EventLoopGroup这个线程组类。

在这里只介绍它于引导类中的设置,对于EventLoopGroup的详细介绍将放到下面几篇文章中去。

group(bossEventLoop, workerEventLoop)

在上面的例子代码中,我们看到首先调用了这个方法,设置了两个线程池给引导类,跟进去看看:

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
  // 将此变量设置为自己的成员变量
  // this.group = parentGroup
  super.group(parentGroup);
  if (childGroup == null) {
    throw new NullPointerException("childGroup");
  }
  if (this.childGroup != null) {
    throw new IllegalStateException("childGroup set already");
  }
  // 将第二个gourp设置为childGroup成员变量
  this.childGroup = childGroup;
  return this;
}

这里只需要记住,引导类将其设置为两个成员变量即可

扫描二维码关注公众号,回复: 8831000 查看本文章

Channel的设置

在例子中,我们调用这段代码将channel设置进去

.channel(NioServerSocketChannel.class)

其实就是将Class保存在一个工厂里,保存在成员变量中

public B channel(Class<? extends C> channelClass) {
  if (channelClass == null) {
    throw new NullPointerException("channelClass");
  }
  return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}
public B channelFactory(ChannelFactory<? extends C> channelFactory) {
  ...
  this.channelFactory = channelFactory;
  return (B) this;
}

这个工厂是干啥用的呢?我们这里进入上面具体实例化的工厂类ReflectiveChannelFactory中:

public T newChannel() {
  try {
    return clazz.newInstance();
  } catch (Throwable t) {
    throw new ChannelException("Unable to create Channel from class " + clazz, t);
  }
}

其实就是利用反射去实例化一个对象,这个对象就是我们最上面调用channel()方法中的参数,也就是NioServerSocketChannel这个类。在什么时候会实例化呢?下面就会看到,这里只是简单的设置一下

Hanlder的设置

public ServerBootstrap childHandler(ChannelHandler childHandler) {
  ...
  this.childHandler = childHandler;
  return this;
}

这里简要带过一下,其实以上设置都只是将实例保存到引导类的成员变量中而已,这里记住,设置了childHandler这个成员变量的值

引导类正式启动

到这里,引导类才真正开始做事情,由以下代码开始驱动:

bootstrap.bind(port);

所以,以此为入口来看看都做了什么

public ChannelFuture bind(InetAddress inetHost, int inetPort) {
  // 简单将端口和host包装成一个对象
  return bind(new InetSocketAddress(inetHost, inetPort));
}

public ChannelFuture bind(SocketAddress localAddress) {
  ...
  return doBind(localAddress);
}
private ChannelFuture doBind(final SocketAddress localAddress) {
  // 初始化并注册
  final ChannelFuture regFuture = initAndRegister();
  ...
	// 真正将channel进行绑定
  doBind0(regFuture, channel, localAddress, promise);   
}

这里省略了大量判断的代码,只是确保第一步初始化并注册完成之后,才进行真正的channel绑定到端口的工作。

这里可以将channel看成是socket的一个抽象,可以往channel里写数据读数据,是双向的,一个客户端连接就是一个客户端channel,服务端也有服务端自己的channel。

就像在项目描述那篇文章中的那样,在聊天室的实现中,或是仿支付的实现中,我们都会把客户端channel保存下来,因为这对应了一个连接,到我们需要向客户端写数据的时候就会把该channel拿出来写,就会通过TCP连接写数据到对端。当然除了TCP协议,像UDP之类的协议Netty也有封装,不过Channel是别的对象,但概念都是一样的。

channel的初始化与注册工作

可以说,到现在才来到了最为关键的地方,回忆一下,我们在开头设置了NioServerSocketChannel这个channel类,这个channel属于服务端的channel,主要负责接受客户端的连接,它的初始化工作就在上面initAndRegister这个方法中做的:

final ChannelFuture initAndRegister() {
  Channel channel = null;
  try {
    // 这里调用了我们上面说的工厂,实例化了一个channel
    channel = channelFactory.newChannel();
    // 初始化channel
    init(channel);
  } 

  // 注册channel
  ChannelFuture regFuture = config().group().register(channel);
}

这里分了3个步骤:

  1. 实例化一个channel
  2. 对channel实例进行初始化工作
  3. 注册channel

以上3步都可以是一段长逻辑,所以这里分开解析

实例化Channel

我们可以看看,NioServerSocketChannel这个类的构造函数都做了什么

// JDK的nio类,负责创建一个channel
SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();

public NioServerSocketChannel() {
  this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

这里首先看一下newSocket方法

private static ServerSocketChannel newSocket(SelectorProvider provider) {
  // 这里调用了jdk的nio类,创建了一个jdk底层的channel
  return provider.openServerSocketChannel();
}

Netty本身就是封装了JDK底层nio操作的框架,所以很多地方都是用到了JDK原生的类来做,在接下来的很多地方也会使用到JDK库的NIO类

首先说明一个概念,这里创建出来的Channel是JDK底层的channel,而我们讨论的NioServerSocketChannel是Netty对JDK底层channel的一个封装,就像装饰者模式那样,Netty自己做了一个Channel类装饰了原生的Channel,并且对其功能进行扩展,功能比原生Channel更为强大。那么从何看出它们是装饰者模式的关系呢?
在这里插入图片描述
在这里插入图片描述
由上面两张图可以看出,这两个Channel都实现了同一个接口Channel,并且在Netty封装的Channel中的构造函数中,其将底层Channel保存为自己的一个成员变量,通过javaChannel方法可以获取到

protected SelectableChannel javaChannel() {
  return ch;
}

至于在哪里保存的,我们回到上面的构造函数思路中

public NioServerSocketChannel(ServerSocketChannel channel) {
  // 调用父类做初始化工作,需要注意,这里传入了SelectionKey的OP_ACCEPT事件
  super(null, channel, SelectionKey.OP_ACCEPT);
  // 保存了一系列的配置信息,感兴趣的读者可以深究一下,本篇不赘述这个类
  config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

值得一提的是,NioServerSocketChannel传入了SelectionKey.OP_ACCEPT这样一个参数。需要读者记住。

进入父类的构造方法

protected AbstractNioMessageChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
  // readInterestOp = SelectionKey.OP_ACCEPT
  super(parent, ch, readInterestOp);
}

protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
  super(parent);
  // 这里第二个参数即为我们上面所说的,JDK底层的channel,在此保存在名为ch的成员变量中
  // 通过上面说的javaChannel方法可以拿到此对象
  this.ch = ch;
  // 这里将SelectionKey.OP_ACCEPT保存下来
  this.readInterestOp = readInterestOp;
  try {
    // 将ch设置为非阻塞模式
    ch.configureBlocking(false);
  } 
  ...
}

这里先剧透一下,NioServerSocketChannel这个channel其实关注的是OP_ACCEPT这一事件,由此我们可以知道,NioServerSocketChannel这个channel将只关心新连接接入的事件。在后面注册channel的时候会将此事件注册到selector上,表示有新连接接入时此channel开始工作。

总结

所以,总结一下,在实例化服务端Channel的时候大概做了以下工作:

  1. 创建了一个JDK底层的channel,并且将其保存下来
  2. 将自己关注的事件OP_ACCEPT保存下来
  3. 将JDK底层的channel设置为非阻塞的模式

这里我们最后看一下其又调用了父类的构造函数super(parent)做了什么

protected AbstractChannel(Channel parent) {
  this.parent = parent;
  // 给channel设置一个id
  id = newId();
  // 这个类是Netty操作底层需要用到的类,服务端和客户端的Unsafe都不一样
  // 在下面会介绍它有什么用
  unsafe = newUnsafe();
  // 初始化一个pipeline -> new DefaultChannelPipeline(this);
  pipeline = newChannelPipeline();
}

关于pipeline机制,后面会专门开一篇文章介绍,这里简要概括一下其作用。

其内部是一个双向链表的数据结构,顺序存放了各个Handler,而Handler又是什么呢?

在一个连接的读事件到来后,会在Handler中传播这个读事件,调用各个Handler类的读方法,以便于操作连接中传来的数据,在Handler中就可以处理对端传来的数据。

这层的解耦可以处理编解码、半包问题、业务逻辑处理等等的工作,而pipeline负责保存这一链条式的事件传播,这里就像责任链模式一样,每个Handler都有自己关心的事件,当有读请求,读Handler都会一个个工作起来,若有写请求,也会通过pipeline去写数据,pipeline会调用其中的某个Handler将数据写出去。

初始化Channel

这里到了第二个分支,初始化Channel,调用的是ServerBootstrap的init方法(因为是服务端的引导启动)

// 注意,这里传入的channel实例是Netty封装的Channel
@Override
void init(Channel channel) throws Exception {
  // 以下均为设置一些channel的配置
  final Map<ChannelOption<?>, Object> options = options0();
  synchronized (options) {
    channel.config().setOptions(options);
  }

  ...

  // 拿出上面初始化过的DefaultChannelPipeline实例
  ChannelPipeline p = channel.pipeline();

  // 子group和子handler
  final EventLoopGroup currentChildGroup = childGroup;
  final ChannelHandler currentChildHandler = childHandler;
  final Entry<ChannelOption<?>, Object>[] currentChildOptions;
  final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
  // 设置子属性
  synchronized (childOptions) {
    currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
  }
  synchronized (childAttrs) {
    currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
  }

  // 在该Channel的pipeline中添加一个ChannelInitializer
  p.addLast(new ChannelInitializer<Channel>() {
    @Override
    public void initChannel(Channel ch) throws Exception {
      final ChannelPipeline pipeline = ch.pipeline();
      // 取出在引导类中配置的hanlder
      ChannelHandler handler = config.handler();
      if (handler != null) {
        // 将其设置到pipeline中
        pipeline.addLast(handler);
      }
      // 设置一个最为关键的Handler -> ServerBootstrapAcceptor
      ch.eventLoop().execute(new Runnable() {
        @Override
        public void run() {
          pipeline.addLast(new ServerBootstrapAcceptor(
            currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
        }
      });
    }
  });
}

这里最为关键的是pipeline中的handler初始化,这里只是做了ChannelInitializer这一个Handler的添加,到这里应该会有疑问,ChannelInitializer这个类实现的initChannel方法是做什么用的?

首先我们解释一下ChannelInitializer有什么用处。由于在设置引导类的Handler的时候,只能放入一个Handler,此类就是方便设置多个Handler,并且方便直观的可以设置各个Handler的顺序。

接下来看看为什么实现initChannel这个方法即可完成Hanlder的设置。这里我们回到上面的代码,可以看到,在中间部分我们拿出pipeline实例,然后调用了p.addLast方法去添加ChannelInitializer这样一个Handler,那么进入addLast方法看看都做了什么

// 这里跳过了一些门面方法,直接到具体的实现中去
@Override
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
  final AbstractChannelHandlerContext newCtx;
  synchronized (this) {
    // 检测handler是否重复
    checkMultiplicity(handler);

    // 创建一个context,用于保存handler和group线程组
    newCtx = newContext(group, filterName(name, handler), handler);

    // 将context添加到pipeline中
    addLast0(newCtx);
    
    // If the registered is false it means that the channel was not registered on an eventloop yet.
    // In this case we add the context to the pipeline and add a task that will call
    // ChannelHandler.handlerAdded(...) once the channel is registered.
    // 这里其实Channel是还没有注册的,因为注册是第三个分支,这里只是第二个分支
    // 所以这里registered = false
    if (!registered) {
      newCtx.setAddPending();
      // 这里将该context(ChannelInitializer所属的)保存在该Channel中
      // 以便后面才来调用callHandlerAdded0方法
      // 下面你就会知道,为什么要在后面再调用一次
      callHandlerCallbackLater(newCtx, true);
      return this;
    }
    
    // 拿出之前设置的线程池
    EventExecutor executor = newCtx.executor();
    // 判断当前线程是否是线程池的那个线程
    if (!executor.inEventLoop()) {
      newCtx.setAddPending();
      // 如果不是,交给线程池中的线程去做
      executor.execute(new Runnable() {
        @Override
        public void run() {
          callHandlerAdded0(newCtx);
        }
      });
      return this;
    }
  }
  // 如果是,在本线程做
  // 主要是保证此操作是在制定线程池中的线程做
  callHandlerAdded0(newCtx);
  return this;
}

判断哪个线程执行的逻辑在接下来会出现很多,这里为Netty线程模型,实现Channel操作无锁化串行的关键,他将有关于Channel的操作都交给Channel对应的那条线程去做,每条线程都有专门的一个线程与其对应,这样就可以做到Channel的操作都保证在一条线程内执行,这样就避免了线程安全的问题,也就不需要加锁操作,大大减少了上下文切换的开销

在这里我们可以发现, 一个Handler是对应一个Context上下文的,其实就是做了一个包装,将一个Handler和一个线程池都包装成一个上下文对象Context,在pipeline中的链表结构实际存储的是上下文对象Context。

这里我们主要关注callHandlerAdded0这个方法做了什么

private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) {
  try {
    // 调用context中的handler的handlerAdded方法
    ctx.handler().handlerAdded(ctx);
    ctx.setAddComplete();
  } 
  ...
}

这里我们需要知道,传入的这个Context上下文对象实际上所属的是我们上面讨论的ChannelInitializer这个handler,所以这里调用的handlerAdded方法实际上是ChannelInitializer中的这个方法

public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
  // 在上面已经介绍过,此时是还没注册的,所以不会执行以下方法,在下面的第三个分支Channel的注册
  // 之后,才会来执行下面的逻辑
  if (ctx.channel().isRegistered()) {
    // 初始化channel中的某个东西
    initChannel(ctx);
  }
}

Netty需要保证在channel注册完之后,才来初始化pipeline中的handler的链表关系,所以在之后也会执行initChannel(ctx)这个方法,只是时机往后延了一点,至于是什么时机,在分析第三个分支的时候就会提到,这里我们先来分析一下这个方法都做了什么逻辑

private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
  // 并发的处理,如果有线程抢先一步设置到initMap中,则会返回非null的值
  if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
    try {
      // 初始化
      initChannel((C) ctx.channel());
    } finally {
      // 移除该context,相当于移除该handler
      remove(ctx);
    }
    return true;
  }
  // 既然已经初始化了,就不必再执行一次,直接返回即可,典型的乐观锁最佳实践
  return false;
}

外层其实包装了并发乐观锁的操作,这里最关键的是initChannel方法,有没有觉得很眼熟?其实ChannelInitializer这个Handler类是一个抽象类,initChannel方法其实是由我们去定义,一般我们会在该方法中以我们想要的顺序添加一些我们自己想要添加的Handler

值得一提的是,在执行完我们自定义的逻辑后,调用了remove(ctx)方法,其实该方法就是将该Handler从pipeline中移除,至于为什么读者应该可以知道,该Handler就是用来便利添加别的Handler的,说白了就是一个工具Handler,在使用完其之后在接下来的逻辑处理中没有它的作用了,所以需要从链表中移除。

回到上面的逻辑中,在ChannelInitializer这个Handler中我们添加了一个自定义的Handler和一个ServerBootstrapAcceptor这个Handler,其后者很关键,完成了接受一个新连接的工作,这里读者只需要记得,在服务端的启动流程中是会向服务端Channel注册这样一个Handler的,在后续文章中会介绍其中做了怎样的工作。

总结

至此初始化channel的工作就大致结束了,这里总结一下做了什么事情:

  1. 将子线程池childGroup、子配置信息、子handler、子属性childAttrs拿出来,都放到ServerBootstrapAcceptor这个Handler中
  2. 注册添加用户定义的handler和ServerBootstrapAcceptor这个handler

其实也没做什么事情,最主要的就是添加了一个关键Handler -> ServerBootstrapAcceptor

关于子group、子handler,和channel handler刚开始不太好理解,到后面就懂了,这里简要的描述一下

为什么要区分子和父?

因为这里的父指的是NioServerSocketChannel,该Channel只负责接受客户端连接,在有一个客户端连接进来后, 会创建一个子Channel,即为NioSocketChannel这个实例,该Channel就代表一个客户端的连接,负责关心客户端的读事件,有一个客户端连接进来就创建一个NioSocketChannel代表一个客户端,在创建NioSocketChannel这个channel之后肯定要对其的pipeline初始化,这时候子group、子handler等等就在这时候赋值给NioSocketChannel了,所以我们编写的childHandler配置其实都是对客户端读写事件的业务逻辑

注册Channel

接下来执行我们最后一个分支,注册Channel。回到我们initAndRegister方法的逻辑中去,有这样一行代码

config().group().register(channel)

还记得我们在引导类设置了两个线程池吗?一个我们命名为bossEventLoop,一个命名为workerEventLoop。在这里config().group()其实拿的就是bossEventLoop,而workerEventLoop是我们上面说的childGroup被保存,如果忘了可以看上面的线程池的设置那一节

public final EventLoopGroup group() {
  return bootstrap.group();
}

public final EventLoopGroup group() {
  // 拿的其实是名为group这个成员变量
  // 实则就是我们开头设置的bossEventLoop
  return group;
}

为什么要区分两个线程池,其实用同一个线程池也都是可以的,只不过这里使用的这种线程模型就是Netty中的主从Reactor线程模型,在下面一篇文章中会介绍到。同时这也是Netty官方推荐使用的线程模型。

回到注册方法中去,这里我们可以知道,调用了线程池的register方法对channel进行注册

// 为了简化步骤,这里忽略外部包装的一些方法,在后面会进行分析
// 直入主题
@Override
public ChannelFuture register(final ChannelPromise promise) {
  ObjectUtil.checkNotNull(promise, "promise");
  // 其实这里调用的是channel中的unsafe实例的register方法
  promise.channel().unsafe().register(this, promise);
  return promise;
}

还记得吗,在实例化Channel那一节中介绍到,在构造函数中会初始化一个unsafe实例。接下来进入该方法

@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
  
  // 将该线程池赋值保存到channel中的一个成员变量中
  AbstractChannel.this.eventLoop = eventLoop;

  if (eventLoop.inEventLoop()) {
    register0(promise);
  } else {
    try {
      eventLoop.execute(new Runnable() {
        @Override
        public void run() {
          register0(promise);
        }
      });
    } 
  }
}

这里又是一样的逻辑,判断执行线程是否是线程池中的线程,在上面无锁串行化就已经解释过了,下面如果出现这样的代码将不再赘述,具体会留在之后的文章专门介绍线程池(其实是叫线程组reactor,这里为了便于理解,叫它线程池,因为它也是线程池的一个实现)

这里比较关键的就是保存了线程池对象到channel中,然后就是register0方法,所以进入该方法

private void register0(ChannelPromise promise) {
  try {
    // 调用子类的注册方法
    doRegister();

    // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
    // user may already fire events through the pipeline in the ChannelFutureListener.
    // 在这里调用了我们之前提到过的延迟执行的handlerAdded方法
    pipeline.invokeHandlerAddedIfNeeded();

    safeSetSuccess(promise);
    // 向pipeline传播该channel已经成功注册了的事件
    pipeline.fireChannelRegistered();
    // Only fire a channelActive if the channel has never been registered. This prevents firing
    // multiple channel actives if the channel is deregistered and re-registered.
    // 此时的channel其实还未成功激活,下面逻辑不会执行
    // 但下面的逻辑还是会执行的,只不过时机在后面
    if (isActive()) {
      if (firstRegistration) {
        // 向pipeline传播active事件
        pipeline.fireChannelActive();
      } else if (config().isAutoRead()) {
        // 挺关键的一个注册事件的方法
        beginRead();
      }
    }
  }
}

到这里一路过来都只是做了一些表面的工作,实质注册逻辑是在子类实现的doRegister()这个方法中,这里无论是服务端channel或是客户端channel,都是会调用到AbstractNioChannel的doRegister这个方法中,可以看到,服务端客户端channel的注册方法逻辑都是一样的

protected void doRegister() throws Exception {
  boolean selected = false;
  // 不断重复尝试
  for (;;) {
    try {
      // 调用jdk底层的channel进行注册
      // 将channel注册到线程池中的selector中,需要知道的是,一个线程池对应一个selector实例
      selectionKey = javaChannel().register(eventLoop().selector, 0, this);
      return;
    } 
    ...
  }
}

这里读者需要有nio的基础。其注册工作实质上是调用了jdk底层的channel,对selector进行注册。值得一提的是这里关注的事件为0,表示不关注任何事件,为什么注册了,还不关注任何事件呢?这里的其中一个目的是为了拿到selectionKey这个实例,在后面可以很方便的修改关注的事件,只需要调用其interestOps(int ops)方法即可修改,这里注册到selector中去之后,就可以将selectionKey赋值到channel的一个成员变量中去了,至于什么时候修改关注的事件,在下面会提到。

总结

  1. 将channel与一个线程池进行绑定,由此可以看出,一个线程池只对应一个channel,也就是上面所说的,channel的工作都只会在同一个线程中进行,就是因为这里绑定了某个线程池。
  2. 调用jdk底层的channel,向线程池中的selector(也是jdk底层的nio类)注册,不关心任何事件,为了保存一个SelectionKey对象,以便后续修改关心的事件

channel绑定监听端口

这里我们终于做完了初始化channel的一系列操作,回到最初的doBind方法

private ChannelFuture doBind(final SocketAddress localAddress) {
  // 这里我们初始化好了channel
  final ChannelFuture regFuture = initAndRegister();
  // 这里返回的channel就是我们初始化好的NioServerSocketChannel
  final Channel channel = regFuture.channel();
	
  ...
  // 省略了一些无关逻辑,这里将调用doBind0方法
  doBind0(regFuture, channel, localAddress, promise);
	...
}

关键是最终会调用doBind0方法

private static void doBind0(
  final ChannelFuture regFuture, final Channel channel,
  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()) {
        // 调用channel的bind方法
        channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
      } else {
        promise.setFailure(regFuture.cause());
      }
    }
  });
}

这里关键就在channel的bind方法逻辑

public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
  return pipeline.bind(localAddress, promise);
}

调用其中pipeline成员变量的bind方法

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

其会调用tail的bind方法。其中tail也是一个context,在pipeline初始化的时候会初始化两个固定的context,一个为headContext,一个为tailConetxt,中间是用户自定义添加的handler对应的Context,由此形成一个双向链表的数据结构,关于这一点,在下一篇介绍pipeline中会详细讲述。

这里只需要进入tailContext这个内部类的bind方法看看做了什么

public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
  
	// 从tail尾部往上找一个OutBound的context
  final AbstractChannelHandlerContext next = findContextOutbound();
  EventExecutor executor = next.executor();
  if (executor.inEventLoop()) {
    // 执行context的invokeBind方法
    next.invokeBind(localAddress, promise);
  } else {
    safeExecute(executor, new Runnable() {
      @Override
      public void run() {
        next.invokeBind(localAddress, promise);
      }
    }, promise, null);
  }
  return promise;
}

这里说一下,此时pipeline中若用户没有添加服务端handler的话,就只有初始化的head、tail和一个ServerBootstrapAcceptor这三个context,而ServerBootstrapAcceptor和tail都属于ChannelInboundHandler,所以这里findContextOutbound方法只会找到headContext,因为它是OutBound类型的Handler(顺带一提,它也是InBound类型的Handler),这里进入headContext的invokeBind方法

这里可以看出来,headConetxt和tailConetxt其实既是一个conetxt,又是一个handler

private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
  // 调用headContext的bind方法
  ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
}

这里headConetxt先是调用了handler方法

public ChannelHandler handler() {
  return this;
}

在headContext中就是返回自身实例而已,所以上面说其实是调用headConetxt的bind方法

public void bind(
  ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
  throws Exception {
  unsafe.bind(localAddress, promise);
}

这里其实是调用了channel的unsafe类的bind方法,其中服务端Channel和客户端Channel的底层bind方法注册的东西是不一样的,这一点在接下来的文章会介绍到。这里进入bind方法

public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
  
  // 记录旧的是否激活标志
  // 这里一般会返回false,证明在这之前是还没有激活的
  boolean wasActive = isActive();
  try {
    // 绑定逻辑
    doBind(localAddress);
  } 

  // 在这里一般来说isActive是会返回true的
  // 这里的意思就是,在之前是没激活的,在执行这里的方法后变成激活的
  // 就执行下面的逻辑,主要是为了不重复传播active事件,只在第一次
  // 成功激活的时候传播active事件
  if (!wasActive && isActive()) {
    invokeLater(new Runnable() {
      @Override
      public void run() {
        // 向pipeline传播active事件
        pipeline.fireChannelActive();
      }
    });
  }
	
  // 设置成功到future中,会通知成功
  safeSetSuccess(promise);
}

这里最为关键的是doBind方法中的逻辑,这里这个方法是一个多态方法,服务端和客户端是不一样的,这里关注服务端实现,所以进入NioServerSocketChannel的doBind方法

protected void doBind(SocketAddress localAddress) throws Exception {
  if (PlatformDependent.javaVersion() >= 7) {
    javaChannel().bind(localAddress, config.getBacklog());
  } else {
    javaChannel().socket().bind(localAddress, config.getBacklog());
  }
}

其实这里就是调用了底层jdk的channel,进行bind操作,之后isActive就会返回ture了

然后,最为关键的是,在上面我们提到,在成功调用底层jdk的channel进行bind之后,是视为已激活的,所以会向pipeline传播一个active事件

public final ChannelPipeline fireChannelActive() {
  // 这里传入headConetxt,调用静态方法invokeChannelActive
  AbstractChannelHandlerContext.invokeChannelActive(head);
  return this;
}
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
  EventExecutor executor = next.executor();
  if (executor.inEventLoop()) {
    // 执行headContext的invokeChannelActive方法
    next.invokeChannelActive();
  } else {
    executor.execute(new Runnable() {
      @Override
      public void run() {
        next.invokeChannelActive();
      }
    });
  }
}

这里关键是执行了headContext的invokeChannelActive方法

private void invokeChannelActive() {
  if (invokeHandler()) {
    try {
      ((ChannelInboundHandler) handler()).channelActive(this);
    } catch (Throwable t) {
      notifyHandlerException(t);
    }
  } else {
    fireChannelActive();
  }
}

和上面的逻辑一样,handler返回的是headContext本身,最终调用headConetxt的channelActive方法

public void channelActive(ChannelHandlerContext ctx) throws Exception {
  // 向conetxt传播active事件
  ctx.fireChannelActive();

  // 关键方法
  readIfIsAutoRead();
}

这里就不看传播active事件了,因为其不是我们关注的重点,这里关注readIfIsAutoRead方法

private void readIfIsAutoRead() {
  // autoRead这个配置是默认开启的,所以这里为true
  if (channel.config().isAutoRead()) {
    // 调用channel的read方法
    channel.read();
  }
}

进入channel的read方法

public Channel read() {
  pipeline.read();
  return this;
}

进入pipeline的read方法

public final ChannelPipeline read() {
  tail.read();
  return this;
}

调用了tail的read方法

public ChannelHandlerContext read() {
  // 这里会找到headConetxt
  final AbstractChannelHandlerContext next = findContextOutbound();
  EventExecutor executor = next.executor();
  if (executor.inEventLoop()) {
    // 执行headConetxt的invokeRead方法
    next.invokeRead();
  } 
  ...
  return this;
}

这里九曲十八弯,最终还是调用到headConetxt的read方法

public void read(ChannelHandlerContext ctx) {
  unsafe.beginRead();
}

绕了半天,才到真正的入口,调用了channel的unsafe的beginRead方法

public final void beginRead() {
  assertEventLoop();

  // 确保已经激活才执行下面的逻辑
  if (!isActive()) {
    return;
  }

  try {
    doBeginRead();
  } 
  ...
}

执行doBeginRead方法

protected void doBeginRead() throws Exception {
  // Channel.read() or ChannelHandlerContext.read() was called
  final SelectionKey selectionKey = this.selectionKey;
  if (!selectionKey.isValid()) {
    return;
  }

  readPending = true;

  final int interestOps = selectionKey.interestOps();
  // 判断之前的事情是否是0
  if ((interestOps & readInterestOp) == 0) {
    selectionKey.interestOps(interestOps | readInterestOp);
  }
}

绕了半天,只是为了修改我们之前说的SelectionKey关心的操作位,还记得上面实例化Channel的逻辑吗?在其构造函数中存放了一个服务端感兴趣的事件

public NioServerSocketChannel(ServerSocketChannel channel) {
  // 调用父类做初始化工作,需要注意,这里传入了SelectionKey的OP_ACCEPT事件
  super(null, channel, SelectionKey.OP_ACCEPT);
}

所以,在这里readInterestOp变量即为SelectionKey.OP_ACCEPT

public static final int OP_ACCEPT = 1 << 4;

其中selectionKey中对关心的事件是用了位操作来表示的,其中OP_ACCEPT是1左移4位,也就是二进制表示的10000,而上面的interestOps & readInterestOp,因为与上了0,所以不管是什么数字,结果都是0,所以会进入interestOps | readInterestOp 的逻辑

// interestOps | readInterestOp 在这里其实就 = OP_ACCEPT = 1 << 4
// 所以在selectionKey中,二进制第五位如果为1,则代表其关注的是一个ACCPET事件
selectionKey.interestOps(interestOps | readInterestOp);

总结

这里channel绑定监听端口的逻辑,最为关键的就是其在selector中注册了一个OP_ACCEPT事件,只关注新连接接入,由此可以看出,服务端Channel的工作只是在接受新连接而已。而一个新连接到来后,服务端Channel会做什么事情在之后的文章中做介绍。

到这里,服务端的整体启动就已经完成了,这里需要理解,主要做的事情就是创建了一个服务端Channel,并且其向selector中注册了OP_ACCEPT事件,关注一个新连接,并且底层jdk的channel监听了你指定的端口,持续等待一个新连接访问该端口,即可触发OP_ACCEPT事件。

发布了84 篇原创文章 · 获赞 85 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/94592342