从Netty源码看Reactor模式

reactor模型的介绍

Reactor 多线程模型 有如下特点:

  • 有专门一个线程, 即 Acceptor 线程用于监听客户端的TCP连接请求。
  • 客户端连接的 IO 操作都是由一个特定的 NIO 线程池负责。 每个客户端连接都与一个特定的 NIO 线程绑定, 因此在这个客户端连接中的所有 IO 操作都是在同一个线程中完成的
  • 客户端连接有很多, 但是 NIO 线程数是比较少的, 因此一个 NIO 线程可以同时绑定到多个客户端连接中。

此处介绍摘自永顺大佬的源码之下无秘密 ── 做最好的 Netty 源码分析教程,如果没读过,强烈建议多读几遍,写的非常好。

那么对应到netty源码中,acceptor线程和NIO线程池分别是什么?accept事件又是如何传递到NIO线程池的?

NioEventLoopGroup 与 Reactor 线程模型的对应

先来看一下server创建的例子:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
 .channel(NioServerSocketChannel.class)
 ...
复制代码

那么bossGroupworkerGroup分别是啥?通过debug断点看一下bossGroupworkerGroup

可以看到bossGroup包含一个NioEventLoopworkerGroup包含8(cpu核心数量*2)个NioEventLoop,再debug看一下bossGroupNioEventLoop 通过上图可以很清楚的看到一个NioEventLoop对应一个Thread。

这里忽略Thread的创建时机以及创建过程,到这里可以暂时直白的总结一下,bossGroup是包含一个线程的集合,workerGroup是包含8个线程的集合。

接下来再看看NioEventLoop对应的Thread是做什么的。那么找到Thread对应的Runnable,看下Runnable做了什么就清楚了: 通过idea Jump To Type Source可以跳转到thread对应的Runable实现,我们发现实际就是NioEventLoop->run方法,我对run方法进行了精简,大致流程代码如下:

   @Override
    protected void run() {
        for (;;) {
            try {
                long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                strategy = select(curDeadlineNanos);
                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);
                }
            } catch (Exception e) {
            }
        }
    }
复制代码

找了一张图来,可以更直观展示:

图片来自netty源码分析之揭开reactor线程的面纱

简单总结一下NioEventLoop线程主要做的三件事:

  • 轮询出IO事件
  • 处理IO事件
  • 处理任务队列

继续看processSelectedKeys()又调用了processSelectedKeysOptimized(),代码如下:

private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized();
        } else {
            processSelectedKeysPlain(selector.selectedKeys());
        }
}
 private void processSelectedKeysOptimized() {
        for (int i = 0; i < selectedKeys.size; ++i) {
            final SelectionKey k = selectedKeys.keys[i];
            // null out entry in the array to allow to have it GC'ed once the Channel close
            // See https://github.com/netty/netty/issues/2363
            selectedKeys.keys[i] = null;

            final Object a = k.attachment();
            processSelectedKey(k, (AbstractNioChannel) a);
        }
 }
复制代码

重点在processSelectedKeysOptimized()这个方法,selectedKeys是Selector的属性,在NioEventLoop初始化的时候,通过反射获取的,调用selector.select(TIMEOUT)之后,selectedKeys就包含了就绪的io事件。

通过debug可以得到如下三条信息:

  • 当前SelectionKey.readyOps是16,也就是就绪的io事件是SelectionKey.OP_ACCEPTaccept事件
  • 同时我们看到SelectionKey.interestOps也是16,说明注册到当前Selector的事件也是SelectionKey.OP_ACCEPTaccept事件
  • 从k获得的attachment对象a是NioServerSocketChannel

从以上信息我们可以确认当前的NioEventLoop就是我们寻找的Acceptor线程!

我们再来看下当前NioEventLoop是属于bossGroup还是workerGroup

可以看到当前NioEventLoop是属于只有一个线程的bossGroup,自此,我们明白了bossGroup就是reactor模型中的Acceptor!

Channel 与 ChannelPipeline

在分析事件传递之前,先补一下Channel 与 ChannelPipeline的知识:

在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系如下:

通过上图我们可以看到, 一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表. 这个链表的头是 HeadContext, 链表的尾是 TailContext, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。上面的图示给了我们一个对 ChannelPipeline 的直观认识。

accept事件传递

下面我们来分析一下Acceptor线程是如何传递事件到NIO线程池。

接着上面processSelectedKeys()方法,会调用NioServerSocketChannel.doReadMessages():

@Override
    protected int doReadMessages(List<Object> buf) throws Exception {
        SocketChannel ch = serverSocketChannel.accept();
        
        try {
            if (ch != null) {
                buf.add(new NioSocketChannel(this, ch));
                return 1;
            }
        } catch (Throwable t) {
        }

        return 0;
    }
复制代码

这里我们看到得到了java的客户端SocketChannel,又被包装成netty的NioSocketChannel。接下来这个NioSocketChannel就会在NioServerSocketChannel的ChannelPipeline中传递。

如上图,head 和tail节点没啥好说的,主要是ServerBootstrapAcceptor,这是一个ChannelInboundHandler,她的channelRead函数如下:

public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;

    child.pipeline().addLast(childHandler);

    setChannelOptions(child, childOptions, logger);
    setAttributes(child, childAttrs);

    try {
        childGroup.register(child).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    forceClose(child, future.cause());
                }
            }
        });
    } catch (Throwable t) {
        forceClose(child, t);
    }
}
复制代码

这个msg就是上面创建的NioSocketChannel,childHandler是啥?就是下面这个ChannelInitializer匿名类

  ServerBootstrap b = new ServerBootstrap();
  b.group(bossGroup, workerGroup)
   .channel(NioServerSocketChannel.class)
   .childHandler(new ChannelInitializer<SocketChannel>() {
       @Override
       public void initChannel(SocketChannel ch) throws Exception {
           ChannelPipeline p = ch.pipeline();
           p.addLast(serverHandler);
       }
   });
复制代码

可以看到将childHandler加入到了NioSocketChannel的pipeline中了,同时对channel进行配置,然后把channel注册到childGroup中。

public final void register(EventLoop eventLoop, final ChannelPromise promise) {
    AbstractChannel.this.eventLoop = eventLoop;

    if (eventLoop.inEventLoop()) {
        1. register0(promise);
    } else {
        2. try {
            eventLoop.execute(new Runnable() {
                @Override
                public void run() {
                    register0(promise);
                }
            });
        } catch (Throwable t) {
        }
    }
}
复制代码

这里的注册流程是,从childGroup也就是workerGroup中取一个NioEventLoop(新版本中NioEventLoop的线程不会立即启动),这里会走2给NioEventLoop提交任务,提交任务会检测当前NioEventLoop线程是否启动,如果没有启动,则启动,同时开始执行任务,即register0函数。我们再看下register0函数:

private void register0(ChannelPromise promise) {
    try {
        boolean firstRegistration = neverRegistered;
        doRegister();// javaChannel().register(Selector, 0, this)
        neverRegistered = false;
        registered = true;
        pipeline.invokeHandlerAddedIfNeeded();

        safeSetSuccess(promise);
        pipeline.fireChannelRegistered();
        if (isActive()) {
            if (firstRegistration) {
                pipeline.fireChannelActive();
            } else if (config().isAutoRead()) {
                beginRead();
            }
        }
    } catch (Throwable t) {
    }
}
复制代码

可以看到这里channel实际绑定到selector,那么客户端SocketChannel的io事件就由workerGroup中的一个线程负责select了。这里也就实现了channel绑定到一个线程了。再有新的客户端连接,会重复这个流程。

总结一下,bossGroup中有一个线程循环进行接收accept事件,接收到的事件会在NioServerSocketChannel的pipeline中传递,在ServerBootstrapAcceptor.channelRead中会初始化NioSocketChannel以及NioSocketChannel的pipeline,然后提交任务到workerGroup选择的线程的queue中。workerGroup的线程检测到queue中有任务,就会执行任务,将NioSocketChannel注册到当前NioEventLoop中的selector上,NioSocketChannel后续的io事件也由当前线程负责。


作者:superchris
链接:https://juejin.cn/post/6937074573858832421
来源:掘金
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值