抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(二)

发布于 2022-8-1 19:32
浏览
0收藏

1. Main Reactor处理OP_ACCEPT事件

抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(二)-开源基础软件社区
 OP_ACCEPT事件活跃.png


Main Reactor轮询到NioServerSocketChannel上的OP_ACCEPT事件就绪时,Main Reactor线程就会从JDK Selector上的阻塞轮询APIselector.select(timeoutMillis)调用中返回。转而去处理NioServerSocketChannel上的OP_ACCEPT事件

public final class NioEventLoop extends SingleThreadEventLoop {

    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
        ..............省略.................

        try {
            int readyOps = k.readyOps();

            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
               ..............处理OP_CONNECT事件.................
            }


            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
              ..............处理OP_WRITE事件.................
            }


            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                //本文重点处理OP_ACCEPT事件
                unsafe.read();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    }

}

•处理IO就绪事件的入口函数processSelectedKey中的参数AbstractNioChannel ch正是Netty服务端NioServerSocketChannel。因为此时的执行线程为main reactor线程,而main reactor上注册的正是netty服务端NioServerSocketChannel负责监听端口地址,接收客户端连接。

 

•通过ch.unsafe()获取到的NioUnsafe操作类正是NioServerSocketChannel中对底层JDK NIO ServerSocketChannel的Unsafe底层操作类。

 

Unsafe接口是Netty对Channel底层操作行为的封装,比如NioServerSocketChannel的底层Unsafe操作类干的事情就是绑定端口地址处理OP_ACCEPT事件

 

这里我们看到,Netty将OP_ACCEPT事件处理的入口函数封装在NioServerSocketChannel里的底层操作类Unsafe的read方法中。

 抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(二)-开源基础软件社区

image.png


而NioServerSocketChannel中的Unsafe操作类实现类型为NioMessageUnsafe定义在上图继承结构中的AbstractNioMessageChannel父类中。

 

下面我们到NioMessageUnsafe#read方法中来看下Netty对OP_ACCPET事件的具体处理过程:

 

2. 接收客户端连接核心流程框架总览


我们还是按照老规矩,先从整体上把整个OP_ACCEPT事件的逻辑处理框架提取出来,让大家先总体俯视下流程全貌,然后在针对每个核心点位进行各个击破。抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(二)-开源基础软件社区

接收客户端连接.png


main reactor线程是在一个do...while{...}循环read loop中不断的调用JDK NIO serverSocketChannel.accept()方法来接收完成三次握手的客户端连接NioSocketChannel的,并将接收到的客户端连接NioSocketChannel临时保存在List<Object> readBuf集合中,后续会服务端NioServerSocketChannel的pipeline中通过ChannelRead事件来传递,最终会在ServerBootstrapAcceptor这个ChannelHandler中被处理初始化,并将其注册到Sub Reator Group中。

 

这里的read loop循环会被限定只能读取16次,当main reactor从NioServerSocketChannel中读取客户端连接NioSocketChannel的次数达到16次之后,无论此时是否还有客户端连接都不能在继续读取了。

 

因为我们在?《一文聊透Netty核心引擎Reactor的运转架构》一文中提到,netty对reactor线程压榨的比较狠,要干的事情很多,除了要监听轮询IO就绪事件,处理IO就绪事件,还需要执行用户和netty框架本省提交的异步任务和定时任务。

 

所以这里的main reactor线程不能在read loop中无限制的执行下去,因为还需要分配时间去执行异步任务,不能因为无限制的接收客户端连接而耽误了异步任务的执行。所以这里将read loop的循环次数限定为16次。

 

如果main reactor线程在read loop中读取客户端连接NioSocketChannel的次数已经满了16次,即使此时还有客户端连接未接收,那么main reactor线程也不会再去接收了,而是转去执行异步任务,当异步任务执行完毕后,还会在回来执行剩余接收连接的任务。

 抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(二)-开源基础软件社区

Reactor线程运行时结构.png


main reactor线程退出read loop循环的条件有两个:

 

1.在限定的16次读取中,已经没有新的客户端连接要接收了。退出循环。


2.从NioServerSocketChannel中读取客户端连接的次数达到了16次,无论此时是否还有客户端连接都需要退出循环。

 

以上就是Netty在接收客户端连接时的整体核心逻辑,下面笔者将这部分逻辑的核心源码实现框架提取出来,方便大家根据上述核心逻辑与源码中的处理模块对应起来,还是那句话,这里只需要总体把握核心处理流程,不需要读懂每一行代码,笔者会在文章的后边分模块来各个击破它们。

public abstract class AbstractNioMessageChannel extends AbstractNioChannel {

  private final class NioMessageUnsafe extends AbstractNioUnsafe {

        //存放连接建立后,创建的客户端SocketChannel
        private final List<Object> readBuf = new ArrayList<Object>();

        @Override
        public void read() {
            //必须在Main Reactor线程中执行
            assert eventLoop().inEventLoop();
            //注意下面的config和pipeline都是服务端ServerSocketChannel中的
            final ChannelConfig config = config();
            final ChannelPipeline pipeline = pipeline();
            //创建接收数据Buffer分配器(用于分配容量大小合适的byteBuffer用来容纳接收数据)
            //在接收连接的场景中,这里的allocHandle只是用于控制read loop的循环读取创建连接的次数。
            final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
            allocHandle.reset(config);

            boolean closed = false;
            Throwable exception = null;
            try {
                try {
                    do {
                        //底层调用NioServerSocketChannel->doReadMessages 创建客户端SocketChannel
                        int localRead = doReadMessages(readBuf);

                        //已无新的连接可接收则退出read loop
                        if (localRead == 0) {
                            break;
                        }
                        if (localRead < 0) {
                            closed = true;
                            break;
                        }
                        //统计在当前事件循环中已经读取到得Message数量(创建连接的个数)
                        allocHandle.incMessagesRead(localRead);
                    } while (allocHandle.continueReading());//判断是否已经读满16次
                } catch (Throwable t) {
                    exception = t;
                }

                int size = readBuf.size();
                for (int i = 0; i < size; i ++) {
                    readPending = false;
                    //在NioServerSocketChannel对应的pipeline中传播ChannelRead事件
                    //初始化客户端SocketChannel,并将其绑定到Sub Reactor线程组中的一个Reactor上
                    pipeline.fireChannelRead(readBuf.get(i));
                }
                //清除本次accept 创建的客户端SocketChannel集合
                readBuf.clear();
                allocHandle.readComplete();
                //触发readComplete事件传播
                pipeline.fireChannelReadComplete();
                ....................省略............
            } finally {
                ....................省略............
            }
        }
    }
  }
}

这里首先要通过断言assert eventLoop().inEventLoop()确保处理接收客户端连接的线程必须为Main Reactor 线程。

 

而main reactor中主要注册的是服务端NioServerSocketChannel,主要负责处理OP_ACCEPT事件,所以当前main reactor线程是在NioServerSocketChannel中执行接收连接的工作。

 

所以这里我们通过config()获取到的是NioServerSocketChannel的属性配置类NioServerSocketChannelConfig,它是在Reactor的启动阶段被创建出来的。

    public NioServerSocketChannel(ServerSocketChannel channel) {
        //父类AbstractNioChannel中保存JDK NIO原生ServerSocketChannel以及要监听的事件OP_ACCEPT
        super(null, channel, SelectionKey.OP_ACCEPT);
        //DefaultChannelConfig中设置用于Channel接收数据用的buffer->AdaptiveRecvByteBufAllocator
        config = new NioServerSocketChannelConfig(this, javaChannel().socket());
    }

同理这里通过pipeline()获取到的也是NioServerSocketChannel中的pipeline。它会在NioServerSocketChannel向main reactor注册成功之后被初始化。

 抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(二)-开源基础软件社区

ServerChannelPipeline完整结构.png


前边提到main reactor线程会被限定只能在read loop中向NioServerSocketChannel读取16次客户端连接,所以在开始read loop之前,我们需要创建一个能够保存记录读取次数的对象,在每次read loop循环之后,可以根据这个对象来判断是否结束read loop。

 

这个对象就是这里的 RecvByteBufAllocator.Handle allocHandle专门用于统计read loop中接收客户端连接的次数,以及判断是否该结束read loop转去执行异步任务。

 

当这一切准备就绪之后,main reactor线程就开始在do{....}while(...)循环中接收客户端连接了。

 

在 read loop中通过调用doReadMessages函数接收完成三次握手的客户端连接,底层会调用到JDK NIO ServerSocketChannel的accept方法,从内核全连接队列中取出客户端连接。

 

返回值localRead表示接收到了多少客户端连接,客户端连接通过accept方法只会一个一个的接收,所以这里的localRead正常情况下都会返回1,当localRead <= 0时意味着已经没有新的客户端连接可以接收了,本次main reactor接收客户端的任务到这里就结束了,跳出read loop。开始新的一轮IO事件的监听处理。

    public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException {
        try {
            return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() {
                @Override
                public SocketChannel run() throws IOException {
                    return serverSocketChannel.accept();
                }
            });
        } catch (PrivilegedActionException e) {
            throw (IOException) e.getCause();
        }
    }

随后会将接收到的客户端连接占时存放到List<Object> readBuf集合中。

  private final class NioMessageUnsafe extends AbstractNioUnsafe {

        //存放连接建立后,创建的客户端SocketChannel
        private final List<Object> readBuf = new ArrayList<Object>();
}

调用allocHandle.incMessagesRead统计本次事件循环中接收到的客户端连接个数,最后在read loop末尾通过allocHandle.continueReading判断是否达到了限定的16次。从而决定main reactor线程是继续接收客户端连接还是转去执行异步任务。

 

main reactor线程退出read loop的两个条件:

 

1.在限定的16次读取中,已经没有新的客户端连接要接收了。退出循环。

 

2.从NioServerSocketChannel中读取客户端连接的次数达到了16次,无论此时是否还有客户端连接都需要退出循环。

 

当满足以上两个退出条件时,main reactor线程就会退出read loop,由于在read loop中接收到的客户端连接全部暂存在List<Object> readBuf集合中,随后开始遍历readBuf,在NioServerSocketChannel的pipeline中传播ChannelRead事件。

                int size = readBuf.size();
                for (int i = 0; i < size; i ++) {
                    readPending = false;
                    //NioServerSocketChannel对应的pipeline中传播read事件
                    //io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor.channelRead
                    //初始化客户端SocketChannel,并将其绑定到Sub Reactor线程组中的一个Reactor上
                    pipeline.fireChannelRead(readBuf.get(i));
                }

最终pipeline中的ChannelHandler(ServerBootstrapAcceptor)会响应ChannelRead事件,并在相应回调函数中初始化客户端NioSocketChannel,并将其注册到Sub Reactor Group中。此后客户端NioSocketChannel绑定到的sub reactor就开始监听处理客户端连接上的读写事件了。

 

Netty整个接收客户端的逻辑过程如下图步骤1,2,3所示。

 抓到Netty一个Bug,聊一下Netty是如何高效接收网络连接的(二)-开源基础软件社区

netty中的reactor.png


以上内容就是笔者提取出来的整体流程框架,下面我们来将其中涉及到的重要核心模块拆开,一个一个详细解读下。

标签
已于2022-8-1 19:32:06修改
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐