Netty 01—— Netty Server启动流程分析

本文基于Java 8Netty 4.1.69.Final

Netty是一个Java异步网络通信框架。Netty中基于Java NIO实现了异步IO通信,其中实现的NioServerSocketChannel/NioSocketChannel等相关的组件,实际上就是对Java NIO相对组件的封装。本文将从Java NIO启动过程开始,对Netty中NIO Server的启动流程进行学习和分析。

0 NIO Server启动流程

​ Netty中NioServerSocketChannel等组件是在Java NIO基础上封装的,因此,在开始学习Netty NIO启动流程之前,先理清Java NIO的使用步骤,以便接下来的分析中更易于理解Netty Server启动和运行过程。

0.1 启动步骤概述

NIO Server启动和初始化主要经过以下步骤:

  • 初始化选择器/多路复用器Selector
  • 初始化服务器通道ServerSocketChannel并配置异步工作模式
  • 将ServerSocketChannel注册到Selector,获取对应的SelectionKey并注册需要关注的Accept事件
  • 绑定监听地址端口
  • 处理客户端连接
  • 接收数据

启动流程图:

NIOServer启动流程

0.2 注册ServerSocketChannel

需要将服务端通道ssc注册到选择器selector上,并注册需要关注的事件:

mKey = mSSC.register(mSelector,0);
mKey.interestOps(SelectionKey.OP_ACCEPT);

ServerSocketChannel#register方法返回一个SelectionKey对象,selectionKey对象用于表示当前SocketChannel及其关联的Selector的注册关系,内部持有SocketChannel和Selector双方对象引用,并负责维护当前通道关注的事件以及已经发生/就绪事件信息;SelectionKey#interestOps方法用于注册对应的Channel所需要关注的事件,可关注的事件封装在SelectionKey中:

  • OP_READ

    读事件,表示Selector检测到其所关联的Channel中有可读事件发生,包括Channel数据读取就绪、已到达流末端、远程读取通道被关闭或Channel中产生错误挂起等,随后Selector会将OP_READ事件添加到SelectionKey对象中,并标识该事件为就绪事件。

  • OP_WRITE

    写事件,表示Selector检测到其所关联的Channel中有可写事件发生,包括Channel数据写出就绪、远程写入通道被关闭,或Channel中产生错误挂起等,随后Selector会将OP_WRITE事件添加到对应的SelectionKey中,并标识该事件为就绪事件。

  • OP_CONNECT

    连接事件,表示Selector检测到所关联的Channel中有连接事件发生,包括Channel连接完毕就绪或Channel中产生错误等,该事件通常发生在NIO Client成功连接服务器后,Selector会将OP_CONNECT事件添加到对应的SelectionKey中,并标识该事件为就绪事件。

  • OP_ACCEPT

    接受事件,标识Selector检测到所关联的ServerSocketChannel已准备就绪接受新到来的客户端连接请求,或通道中产生了错误,随后Selector会将OP_ACCEPT事件添加到对应的SelectionKey中,并标识该事件为就绪事件。

0.3 Selector收集IO事件

Selector是NIO中的多路复用器,其可以同时管理多个IO通道。注册serverSocketChannel之后,开始循环调用selector.select()收集serverSocketChannel通道上发生的事件:

// 检测selector关联的所有Channel中已就绪的IO事件
// count:发生的IO事件数量
int count = mSelector.select();

select()方法会阻塞当前线程,直到收集到IO事件发生,才会继续运行。如果需要提前结束阻塞,可以调用其重载方法mSelector.selector(interval),指定阻塞时长,当超过该时长没有收集到IO事件,则结束阻塞。或者在其他线程调用mSelector.wakeup()手动结束阻塞,继续执行,wakeup()执行后,select()方法会立即返回。

0.4 处理客户端连接请求

当检测到有OP_ACCEPT事件发生,表示有新的客户端连接请求,需要调用ServerSocketChannel#accept()方法接受并建立客户端连接,然后将该客户端连接通道SocketChannel注册到Selector上,并注册读信息事件,以接收处理客户端发送的数据:

private void handleNewConnection(final SelectionKey key) throws IOException {
    // 处理新接入的请求消息
    // Accept the new connection
    ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
    // 接受新的连接,创建SocketChannel示例,至此,相当于完成了TCP的三次握手,TCP物理链路正式建立。
    SocketChannel socketChannel = serverSocketChannel.accept();
    // 设置为异步非阻塞模式
    socketChannel.configureBlocking(false);
    // 将新连接注册到多路复用器,关联一个bytebuffer附件以存放接收到的数据
    ByteBuffer buffer = ByteBuffer.allocate(INIT_READ_BUFFER_SIZE);
    // Register the new connection to the selector
    socketChannel.register(mSelector, SelectionKey.OP_READ, buffer);
}

0.5 接收数据

当检测到OP_READ事件发生,表示通道中接收到对端发送的数据,或者与对方的连接断开,这时可以从SocketChannel中尝试读取数据:

private void handleReadData(final SelectionKey key) throws IOException {
    // Read the data
    // 读取客户端的请求消息
    // 获取连接通道实例
    SocketChannel socketChannel = (SocketChannel) key.channel();
    // 获取关联的buffer附件
    ByteBuffer readBuffer = (ByteBuffer) key.attachment();
    // 从TCP缓冲区读数据
    int readBytes;
    if ((readBytes = socketChannel.read(readBuffer)) > 0) {
        // 处理读取到的数据
        handleData(readBuffer);
        // 决定是否需要扩容
        if (readBuffer.position() == readBuffer.limit()) {
            final ByteBuffer buffer = ByteBuffer.allocate(readBuffer.capacity() * 2);
            readBuffer.flip();
            buffer.put(readBuffer);
            key.attach(buffer);
        }
    }
    // 链路已经关闭,此时需要关闭SocketChannel,释放资源
    if (readBytes < 0) {
        System.out.println("connection closed: " + socketChannel.getRemoteAddress());
        // 对端链路关闭
        key.cancel();
        socketChannel.close();
    }
}

可以看到,相比于传统的Java BIO,NIO的使用过程要复杂一些,但得益于其同步非阻塞的IO模型,使NIO处理高并发与海量连接成为可能。而Netty对NIO做了进一步的优化和封装,简化了异步IO的开发流程。

理清了NIO的启动流程,接下来分析Netty中对NIO每个关键步骤的封装和执行过程。

1 启动Netty Server

​ 可以非常方便地启动一个Netty Server,通过简单的链式调用初始化相关组件,最后绑定端口即可启动Socket监听:

@Slf4j
public class TestSourceServer {

    public static void main(String[] args) {
        final ServerBootstrap bootstrap = new ServerBootstrap();
      // 执行IO事件的事件循环组
        final NioEventLoopGroup bossGroup = new NioEventLoopGroup();
      // 配置服务端启动器
        bootstrap.group(bossGroup)
                 .channel(NioServerSocketChannel.class)
                 .childHandler(new ChannelInitializer<NioSocketChannel>() {
                     @Override
                     protected void initChannel(final NioSocketChannel ch) throws Exception {
                         ch.pipeline().addLast(new LoggingHandler());
                     }
                 });
      // 开始监听指定端口
        bootstrap.bind(8080);
    }
}

ServerBootStrap是Netty服务端的启动器,通过serverBootStrap对象的一系列链式调用初始化各个组件:

  • group(bossGroup):配置用于执行IO事件/普通事件的事件循环组。
  • channel(NioServerSocketChannel.class):配置需要使用的socket服务端组件,本文分析的是NIO相关的启动流程,所以需要配置服务端组件为NioServerSocketChannel.class。
  • childHandler(initializer):配置客户端连接通道初始化器。服务端建立与客户端连接通道SocketChannel后,Netty会通过调用链最终回调initChannel方法通知用户socketChannel通道已经创建好,此时用户可以在此对socketChannel进行一些业务上的初始化工作,比如设置消息编解码器、添加日志处理器等。上面示例中,在每一次创建的客户端连接通道中的事件处理管道上都添加了一个日志处理器LoggingHandler。

serverBootstrap对象在执行ind()方法绑定端口之前的所有链式调用,都是对服务端进行一些配置,真正启动服务端监听,是从执行bind()方法开始。

2 注册ServerSocketChannel

2.1 创建ServerSocketChannel对象

​ 接下来以debug模式启动[第1节中的示例](#1 从Netty Server最简启动开始),在bootstrap.bind(8080)一行开始执行,一直到AbstractBootstrap#doBind()方法(Line 272):

image-20211019191750365

方法中第一行调用了一个initAndRegister()方法并返回一个ChannelFuture对象,从返回参数可以推断该方法内部执行了异步操作,并且根据方法名可以猜想,该方法内可能实现了注册serverSocketChannel这一步骤。接着往下执行进入initAndRegister()方法一探究竟:

image-20211019192430496

可以看到,方法中先是通过channelFactory工厂对象创建了一个channel对象Channel是Netty中定义的抽象接口,用于表示Netty中具备IO通信能立的组件,示例中的NioServerSocketChannel类便实现了Channel接口。接着往下执行,发现创建的channel对象正是在配置serverBootStrap时配置的NioServerSocketChannel类的实例:

image-20211019194125072

在配置serverBootStrap步骤中,通过bootstrap.channel(NioServerSocketChannel.class)方法设置了实现服务端监听的具体组件,方法中创建了一个ReflectiveChannelFactory对象并将NioServerSocketChannel的类型信息传入,再将reflectiveChannelFactory对象赋值给channelFactory域,完成Server IO Channel 组件的配置:

image-20211023005418257

而reflectiveChannelFactory对象中的newChannel的实现,则通过反射调用NioServerSocketChanel的无参构造方法,完成nioServerSocketChannel对象的创建。因此,接下来程序会运行跳转到NioServerSocketChannel类的无参构造方法:

image-20211019195608828

NioServerSocketChannel类的构造方法中,又通过newSocket方法和调用了selectorProvider.openServerSocketChannel()方法,最终实现对Java NIO的ServerSocketChannel对象的创建:

image-20211019195938140

这里直接通过selectorProvider.openServerSocketChannel()创建ServerSocketChannel对象,而在NIO启动步骤中调用ServerSocketChannel.open()创建的方式,其内部也是通过selectorProvider.openServerSocketChannel()方法来创建:

image-20211019200339392

程序到这里,①完成了创建ServerSocketChannel的步骤

2.2 执行channel注册

​ 继续回到abstractBootStrap.initAndRegister()方法,获取到channel对象后,通过init(channel)方法对channel进行初始化,然后进行注册:

image-20211023044800634

其中config().group()获取到的是在初始化serverBootStrap配置时设置的事件循环组对象bossGroup,config().group().register(channel)抽象方法的实现位于NioEventLoopGroup的父类MultithreadEventLoopGroup中:

// MultithreadEventLoopGroup.java Line 85
@Override
public ChannelFuture register(Channel channel) {
  return next().register(channel);
}

next()方法会从初始化bossGroup时创建的事件循环池(可以理解为线程池,一个事件循环对应一个线程,事件循环对应类是NioEventLoop)中选择一个事件循环对象来执行register(channel)方法。nioEventLoop.register()抽象方法的实现位于NioEventLoop的父类SingleThreadEventLoop类中:

image-20211023050757651

接着运行到AbstractChannel$AbstractUnsafe.register方法(AbstractChannel.java Line 465),而该方法最终又调用register0(promise)方法实现了注册的细节:

image-20211023051152234

这里使用传入的eventLoop对象初始化了channel内部的eventLoop对象,并通过eventLoop.inEventLoop()方法判断是否将注册任务交由eventLoop事件循环对象来执行,实际就是判断eventLoop中的线程是否与当前线程为同一线程环境,来决定是直接调用register0方法,还是交给eventLoop。注意这里的eventLoop对象在nioBossGroup事件循环组对象创建时便已经被创建出来,但eventLoop中所包含的子线程对象尚未被初始化,并没有真正启动线程,只有在第一次像eventLoop提交异步任务时,eventLoop才会初始化并启动线程,也就是将线程延迟启动,这也是Netty中对使用线程资源的优化措施。接着进入regster0方法,内部又调用了doRegster()方法进行真正的注册:

image-20211019205111255

通过javaChannel()获取前面步骤中创建的java NIO ServerSocketChannel对象,通过register方法将其注册到selector中,最后将当前channel对象作为附件保存到selectionKey(在后续处理IO事件中,会以从selectionKey获取selectionKey关联的channel对象),并将注册结果(SelectionKey)赋值给当前channl的selectionKey域。至此,②Netty中ServerSocketChannel注册完毕

3 创建Selector

​ 在serverSocketChannel到selector时,看到selector对象是通过eventLoop().unwrappedSelector()获取的,那selector对象是何时创建的呢?首先通过eventLoop()获取当前channel对象的eventLoop对象,再通过eventLoop.unWrappedSelector()方法获取到nioEventLoop内的unwrappedSelector对象作为channel的selector,unwrappedSelector则在NioEventLoop中被初始化:先调用openSelector()并返回一个SelectorTuple对象,再通过selectorTuple.unwrappedSelector对unwrappedSelector进行赋值,openSelector()方法中,则最终调用了selectorProvider.openSelector()方法创建了selector,并将创建的selector包装到selectorTuple中返回:

image-20211019211421520

其中provider就是SelectorProvider对象实例。至此,③Selector创建完成

这里没有直接使用selectorProvider.openSelector()创建的selector,而是先将其封装了一个selectorTuple对象中再使用unwrappedSelector,是因为Netty在封装该步骤时做了性能上的一些优化,具体过程在NioEventLoop类的selector()方法实现中。

4 bind 开始监听

​ 程序调用AbstractNioChannel.doRegister()方法完成serverSocketChannel注册后,接着开始绑定监听端口。回到AbstractChannel.register0()方法中,doRegister()执行完毕,将registered标记为true,表示当前通道已经注册完毕,接着通过pipeline.fireChannelRegistered()方法将serverSocketChannel注册完成的事件发送到事件处理管道中以通知上层代码处理。回到AbstractBootstrap.doBind()方法中,initAndRegister()执行完通道注册流程,doBind()继续向下运行,通过regFuture判断注册已经完成还是在异步注册,不论如何,接下来都会继续调用doBind0方法进行端口绑定。doBind0方法中,则将绑定任务交给serverSocketChannel的事件循环来执行:

image-20211019215036657

最终在AbstractChannel$AbstractUnsafe.bind方法中调用doBind实现端口绑定,NioServerSocketChannel继承了AbstractChannel并实现了doBind:

image-20211019215602445

可以看到,NioServerSocketChannel.doBind实现中,调用了Java NIO ServerSocketChannel.bind()方法完成端口绑定,④Netty Server绑定监听端口步骤至此完成

5 关注ACCEPT事件

​ AbstractChannel$AbstractUnsafe.bind()方法中完成端口绑定后,接着继续向像eventLoop中提交一个任务用于向serverSocketChannel的事件处理管道发送channelActive事件以告知上层代码处理:

image-20211023060430907

pipeline.fireChannelActive()调用链中,执行到DefaultChannelPipeline\$HeadContext.channelActive():

image-20211019221936341

ctx.fireChannelActive()会将事件继续传递,而readIfIsAutoRead()往下运行则实现了注册OP_ACCEPT事件。readIfIsAutoRead()一直运行到AbstractNioChannel.doBeginRead()方法,并在该方法中实现注册OP_READ事件:

image-20211019222325092

先从serverSocketChannel对应的selectionKey判断,如果channel尚未注册感兴趣的事件,则调用selectionKey.interestOps方法进行注册:

image-20211019222636242

可以看到,未注册感兴趣事件前,channel当前注册的事件值为0,0表示未注册任何事件;serverSocketChannel需要注册的事件(readInterestOp域)值未为16,即对应OP_ACCEPT事件,readInterestOp域在创建srverSocketChannel对象时通过向父类构造方法传入SelectionKey.OP_ACCEPT来赋值:

image-20211019223713424

执行完doBeginRead,⑤关注ACCEPT事件步骤完成。

至此,Netty NIO Server的启动和初始化过程结束,接下来开始监听和处理客户端连接请求,接收客户端发送的数据等。

6 收集和处理IO事件

6.1 启动eventLoop线程

​ 在NIO运行过程中,为了不阻塞主线程的运行,同时保证selector能够一直不断地收集到发生的IO事件,通常会将selector.select()和处理IO事件的代码放到子线程中执行。Netty中也是在子线程中收集和处理所发生的IO事件:将这些任务交给serverSocketChannel的eventLoop事件循环对象异步执行。上文中提到eventLoop在第一次执行任务时才会初始化内部线程对象并启动线程。回顾Netty的启动步骤,程序第一次向eventLoop提交任务是在serverSocketChannel的注册过程,进行真正注册阶段的代码,位于AbstractChannel\$AbstractUnsafe.register()方法中(Line 483):

image-20211021143236547

通过eventLoop.execute方法提交register0任务。这里eventLoop对象是NioEventLoop类的实例,其execute方法实现在NioEventLoop父类SingleThreadEventExecutor中:

image-20211023063715047

再看私有的execute(runnable,boolean)方法,先是通过inEventLoop()方法判断提交任务的线程环境与当前事件循环的子线程环境是否为同一个事件循环的子线程。跟进去不难发现,初次提交任务时其实就是判断eventLoop内部线程是否已经启动(因为线程还未启动,所以这里会返回false):

// SingleThreadEventExecutor(Line 558)
// thread: Thread.currentThread()
// this.thread: eventLoop内部线程,未执行过任务前,this.thread还未启动,this.thread值为null
@Override
public boolean inEventLoop(Thread thread) {
  return thread == this.thread;
}

接着将任务添加到任务队列,判断如果当前eventLoop对象的线程尚未开始运行,则调用startThread()->doStartThread()方法对线程初始化:

image-20211021175116145

可以看到,最终是通过eventLoop内部的线程池对象executor创建出线程,并赋值给eventLoop的thread对象,完成初始化工作,紧接着调用SingleThreadEventExecutor.run()方法,启动无线循环。NioEventLoop类中实现了SingleThreadEventExecutor.run()方法:

// NioEventLoop.java Line 435
@Override
protected void run() {
  int selectCnt = 0;
  for (;;) {
    try {
      int strategy;
      try {
        strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
        switch (strategy) {
          case SelectStrategy.CONTINUE:
            continue;

          case SelectStrategy.BUSY_WAIT:
            // fall-through to SELECT since the busy-wait is not supported with NIO

          case SelectStrategy.SELECT:
            long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
            if (curDeadlineNanos == -1L) {
              curDeadlineNanos = NONE; // nothing on the calendar
            }
            nextWakeupNanos.set(curDeadlineNanos);
            try {
              if (!hasTasks()) {
                strategy = select(curDeadlineNanos);
              }
            } finally {
              // This update is just to help block unnecessary selector wakeups
              // so use of lazySet is ok (no race condition)
              nextWakeupNanos.lazySet(AWAKE);
            }
            // fall through
          default:
        }
      } catch (IOException e) {
        // If we receive an IOException here its because the Selector is messed up. Let's rebuild
        // the selector and retry. https://github.com/netty/netty/issues/8566
        rebuildSelector0();
        selectCnt = 0;
        handleLoopException(e);
        continue;
      }

      selectCnt++;
      cancelledKeys = 0;
      needsToSelectAgain = false;
      final int ioRatio = this.ioRatio;
      boolean ranTasks;
      if (ioRatio == 100) {
        try {
          if (strategy > 0) {
            processSelectedKeys();
          }
        } finally {
          // Ensure we always run tasks.
          ranTasks = runAllTasks();
        }
      } else if (strategy > 0) {
        final long ioStartTime = System.nanoTime();
        try {
          processSelectedKeys();
        } finally {
          // Ensure we always run tasks.
          final long ioTime = System.nanoTime() - ioStartTime;
          ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
        }
      } else {
        ranTasks = runAllTasks(0); // This will run the minimum number of tasks
      }

      if (ranTasks || strategy > 0) {
        if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
          logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                       selectCnt - 1, selector);
        }
        selectCnt = 0;
      } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
        selectCnt = 0;
      }
    } catch (CancelledKeyException e) {
      // Harmless exception - log anyway
      if (logger.isDebugEnabled()) {
        logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                     selector, e);
      }
    } catch (Error e) {
      throw e;
    } catch (Throwable t) {
      handleLoopException(t);
    } finally {
      // Always handle shutdown even if the loop processing threw an exception.
      try {
        if (isShuttingDown()) {
          closeAll();
          if (confirmShutdown()) {
            return;
          }
        }
      } catch (Error e) {
        throw e;
      } catch (Throwable t) {
        handleLoopException(t);
      }
    }
  }
}

6.2 收集IO事件

​ 在子线程执行的无限循环中,eventLoop不断处理IO事件以及程序中提交到eventLoop队列中的非IO任务(NioEventLoop内部不仅会处理IO事件,同时还会执行普通任务:包括Netty程序内部提交的任务,或者用户提交的任务)。Netty NioEventLoop事件循环中处理IO事件,也是先调用selector收集发生的IO事件(收集阶段),再遍历结果进行逐个处理(处理阶段)。只不过由于Netty的封装和优化,这过程看起来要比Java NIO中的处理步骤要复杂多一些。首先看IO事件收集阶段的执行过程,eventLoop中结合了selector的select()select(interval)selectNow()方法来实现收集IO事件,执行过程又分为当前有普通任务没有普通任务两种情况(通过hasTasks()判断)进行。

  • eventLoop中有普通任务待处理:eventLoop任务队列当前有未处理的任务时,直接执行selector.selectNow()方法,selectNow()方法不管有没有IO事件发生,都会立即返回收集结果,不会阻塞线程运行,这过程对应下图代码中的步骤:selectStrategy.calculateStrategy方法中判断如果当前有未处理的普通任务,则调用selectNowSupplier.get()方法,该方法实现中调用了selector.selectNow()并返回收集结果,若没有待处理的普通任务,则将 SelectStrategy.SELECT返回赋值给strategy变量;

image-20211021185216802

  • eventLoop任务队列中没有待处理任务:这时程序会执行到swith的SELECT分支,对应上图中步骤:此处又判断是否存在未处理的普通任务是因为提交和执行任务是异步执行,在步骤执行到步骤期间程序其他地方可能会提交任务进来。当任务队列仍旧为空时,调用select(interval)并返回收集结果给strategy变量。select(interval)方法进一步判断决定是执行selector.selectNow()还是selector.select(interval),selector.select(interval)执行如果在interval指定的时间内没有事件发生则结束阻塞,返回结果:
// NioEventLoop.java Line 808
private int select(long deadlineNanos) throws IOException {
  if (deadlineNanos == NONE) {
    return selector.select();
  }
  // Timeout will only be 0 if deadline is within 5 microsecs
  long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
  return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
}

6.3 处理IO事件

从上面收集IO事件的过程可以看到,strategy变量作用就是记录收集到的IO事件数量(strategy>0说明有IO事件发生并收集到了)。接着到IO事件处理阶段,对应的关键代码:

image-20211021192656320

其中根据判断条件执行了processSelectedKeys()或runAllTasks()方法,processSelectedKeys()实现了接收和处理IO事件,runAllTask()则实现了执行eventLoop队列中的任务。这里除了strategy,还有个关键的局部变量ioRatio:由于处理IO事件是一个相当耗时的过程,为了平衡处理IO事件和执行队列任务的时间,保证eventLoop队列中的非IO任务能够被执行,这段代码通过ioRatio变量来控制处理IO事件和执行队列任务所耗时间的比例。若本轮循环中收集到IO事件(strategy>0),则通过processSelectedKeys()逐个处理收集到的IO事件。

6.3.1 处理连接请求

processSelectedKeys()方法的调用链中,会遍历发生的IO事件集,并交给processSelectedKey(SelectionKey k, AbstractNioChannel ch)方法处理:

image-20211023071701655

先是获取到发生的IO事件,判断如果是OP_ACCEPT或是OP_READ事件则调用ch.unsafe().read()进行进一步处理。接下来在processSelectedKey(SelectionKey k, AbstractNioChannel ch)方法内打下断点,调试模式启动Netty Server端监听8080端口,并使用TCP客户端工具开启一个连接请求:

image-20211021225635306

点击开始连接,返回Idea,发现程序已经方法第一行断点处,继续往下运行到Line 718处:

image-20211021230126811

首先步骤1先获取IO通道对象ch的内部类NioUnsafe对象实例unsafe,注意此时serverSocketChannel接收到的应当是客户端连接请求事件OP_ACCEPT(刚才在TCP客户端点击发起了连接请求),ch对应的是NioServerSocketChannel类,此时ch对象表示的是服务端channel对象serverSocketChannel,对应的ch.unsafe()对象的实现位于NioServerSocketChannel的父类AbstractNioMessageChannel中。接着步骤2获取SelectionKey中所有收集到的事件并赋值给readyOps变量,通过单步调试可以看到,此时readyOps的值是16,对应的正是OP_ACCEPT事件,因此程序会运行到Line 719,执行unsafe.read()->doReadMessages(readBuf)处理客户端连接请求:

image-20211021231715380

image-20211021231920373

image-20211023074045698

SocketUtils.accept方法中实现了serverSocketChannel.accept()方法调用,接受客户端连接请求并返回对应的NIO连接通道对象,接着用NIO channel对象初始化Netty的NioSocketChannel客户端连接通道对象,并将Netty socketChannel返回给上层调用。至此,⑥接受客户端连接请求,创建连接通道步骤完成。最后,将接受连接事件通过pipeline.fireChannelRead方法发送至serverSocketChannel的事件处理管道,通知上层程序和用户,可以对客户端连接通道socketChannel做一些进一步的初始化工作(包括SocketChannel的注册和用户做的业务上的初始化内容)。接着往下执行,最终会回调到用户在初始化serverBootStrap时设置的childHandler对象方法中:

image-20211021233028241

这里的NioSocketChannel对象,就是在接受客户端连接时创建连接通道。在这里通过ch.pipeline().addLast()方法,把自定义的handler添加到socketChanel的事件处理管道pipeline中,接下来便可以在handler中接收处理和发送消息数据。

6.3.2 接收IO消息

​ Netty Server接受并创建客户端连接通道后,就可以互相收发消息了。有关客户端连接通道NioSocketChannel对象的创建和初始化,与服务端通道NioServerSocketChannel对象的初始化过程基本大致相同,这里不再赘述。从前面的步骤中知道,eventLoop处理IO事件最终都会运行到processSelectedKey(SelectionKey k, AbstractNioChannel ch)方法来进行处理,在这个方法第一行打上断点,并服务端程序保持运行,从TCP客户端发送字串“Hello”给服务器:

image-20211021234000033

接着回到Idea会看到程序已经运行断点处:

image-20211021234657509

可以看到,这时的ch对象为客户端连接通道NioSocketChannel的实例,收集到的IO事件值为1,对应OP_READ读事件(刚才客户端向服务端发送了数据),此时的unsafe.read()方法对应的实现类是位于NioSocketChannel父类AbstractNioByteChannel中的NioByteUnsafe类,接着运行可以看到,程序最后在NioSocketChannel.doReadBytes(byteBuf)方法中完成对socketChannel中数据的读取:

image-20211021235522087

最后将读取到的消息数据通过socketChannel.pipeline().fireChannelRead(byteBuf)方法将读到的数据传递给设置的handler处理器进行处理:

image-20211021235934177

至此,⑦Netty Server端接收并处理用户消息步骤执行完毕

7 小结

​ 以上便是Netty Server从初始化配置,启动服务端,初始化NIO ServerSocketChannel、Selector等组件,监听端口,到接受建立客户端连接通道,接收并处理客户端消息的整个过程。本文的目标在于理清Netty Server启动和初始化的整体过程,文章中依据java NIO的启动流程对Netty Server的运行过程进行了梳理,整个分析过程并没有太大难度。而文章中没有分析说明的诸如事件循环EventLoopGroup、EventLoop组件,Channel的事件处理管道Pipeline组件、事件处理器Handler相关组件、和消息相关的Bytebuf组件等这些Netty的组成部分,是进一步学习理解Netty的关键,在后续对Netty的研究中,将尝试对这些组件进行一一解读。

Last modification:October 24th, 2021 at 02:19 am
If you think my article is useful to you, please feel free to appreciate

Leave a Comment