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)
...
复制代码
那么bossGroup
和workerGroup
分别是啥?通过debug断点看一下bossGroup
和workerGroup
:
可以看到bossGroup
包含一个NioEventLoop
,workerGroup
包含8(cpu核心数量*2)个NioEventLoop
,再debug看一下bossGroup
的NioEventLoop
: 通过上图可以很清楚的看到一个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) {
}
}
}
复制代码
找了一张图来,可以更直观展示:
简单总结一下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_ACCEPT
accept事件 - 同时我们看到SelectionKey.interestOps也是16,说明注册到当前
Selector
的事件也是SelectionKey.OP_ACCEPT
accept事件 - 从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
来源:掘金