摘自《Netty 即时聊天实战与底层原理》
本章,我们来分析每个新连接在接入过程中,Netty 底层的机制是如何实现的。先来简要回顾一下:
首先是 Netty 中的 Reactor 线程模型。
Netty 中最核心的东西莫过于两种类型的 Reactor 线程。这两种类型的 Reactor 线程可以看作 Netty 中的两组发动机,驱动着 Netty 整个框架的运转。
一种是 boss 线程,专门用来接收新请求,然后封装成 Channel 对象传递给 worker 线程;还有一种类型是 worker 线程,专门用来处理连接上数据的读写。
不管是 boss 线程还是 worker 线程,所做的事情均分为以下 3 个步骤:
- 轮询注册在 Selector 上的 IO 事件;
- 处理 IO 事件;
- 执行异步 Task。
对于 boss 线程来说,第一步轮询出来的基本都是 ACCEPT 事件,表示有新的连接;而 worker 线程轮询出来的基本都是 read 或 write 事件,表示网络的读写事件。
其次是服务端启动流程。
服务端是在用户线程中开启的,通过 bind 方法,在第一次添加异步任务的时候启动 boss 线程([08]#6)。启动之后,当前服务器就可以开始监听了。
1. 新连接接入的总体流程
简单来说,新连接的接入流程可以分为 3 个过程:
- 检测到有新连接;
- 将新连接注册到 worker 线程;
- 注册新连接的读事件。
2. 检测到有新连接
我们已经知道,当调用 bind 方法启动服务端之后,服务端的 Channel —— NioServerSocketChannel 已经注册到 boss Reactor 线程,Reactor 线程不断检测是否有新的连接,直到检测出有 ACCEPT 事件发生。
NioEventLoop
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
eventLoop = ch.eventLoop();
// Only close ch if ch is still registered to this EventLoop. ch could have deregistered
// from the event loop and thus the SelectionKey could be cancelled as part of the
// deregistration process, but the channel is still healthy and should not be closed.
if (eventLoop != this || eventLoop == null) { return; }
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
return;
}
int readyOps = k.readyOps();
// We first need to call finishConnect() before try to trigger a read(...) or write(...)
// as otherwise the NIO JDK channel implementation may throw a NotYetConnectedException.
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once
// there is nothing left to write
ch.unsafe().forceFlush();
}
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
}
上面这段代码是 Reactor 线程在第二个过程做的事情,最后一个 if 表示 boss Reactor 线程已经轮循到 SelectionKey.OP_ACCEPT 事件,即表明有新连接进入,此时将调用 Channel 的 Unsafe 来进行实际的操作。
在服务端启动流程解析章节中,我们已经知道,服务端对应的 Channel 的 Unsafe 是 NioMessageUnsafe,我们进入它的 read 方法,进入新连接处理的第二步。
3. 注册 Reactor 线程
NioMessageUnsafe
private final List<Object> readBuf = new ArrayList<Object>();
@Override
public void read() {
assert eventLoop().inEventLoop();
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
boolean closed = false;
Throwable exception = null;
do {
// 1. 创建 NioSocketChannel
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
// 2. 设置并绑定 NioSocketChannel
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
// ...
}
笔者省去了非关键部分的代码,可以看到,一上来,就用一条断言确定该 read 方法必须来自 Reactor 线程调用,然后获得 Channel 对应的 Pipeline 和 RecvByteBufAllocator.Handle(先暂时不展开)。
接下来,调用 doReadMessages() 不断地读取消息,用 readBuf 作为容器。其实读者可以猜到这里读取的是一个个连接,然后使用 for 循环调用 pipeline.fireChannelRead(),将每个新连接都经过一层服务端 Channel 的 Pipeline 逻辑处理,之后清理容器,触发 pipeline.fireChannelReadComplete()。整个过程还是比较清晰的,下面我们具体分析这两个方法。
- 创建 NioSocketChannel:doReadMessages(List);
- 设置并绑定 NioSocketChannel:pipeline.fireChannelRead(NioSocketChannel)。
3.1 创建 NioSocketChannel
NioMessageUnsafe
private final List<Object> readBuf = new ArrayList<Object>();
@Override
public void read() {
assert eventLoop().inEventLoop();
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
boolean closed = false;
Throwable exception = null;
do {
// 1. 创建 NioSocketChannel
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
// 2. 设置并绑定 NioSocketChannel
// ...
}
doReadMessages 的方法体在 NioServerSocketChannel 类中,下面进入这个方法体来分析(代码稍作精简)。
NioServerSocketChannel
@Override
protected int doReadMessages(List<Object> buf) throws Exception {
// 1. 创建 JDK 领域的 Channel
SocketChannel ch = SocketUtils.accept(javaChannel());
// 2. 封装为 Netty 领域的 Channel
if (ch != null) {
buf.add(new NioSocketChannel(this, ch));
return 1;
}
return 0;
}
在这里,我们终于窥探到 Netty 调用 JDK NIO 的边界:SocketUtils.accept(javaChannel())
。由于 Netty 中 Reactor 线程第一步就扫描有 ACCEPT 事件发生,因此,这里的 accept 方法是立即返回的,返回 JDK 底层 NIO 创建的一条 JDK 层面的 Channel。
接下来,Netty 将 JDK 的 SocketChannel 封装成自定义的 NioSocketChannel,加入 List,这样外层就可以遍历该 List,做后续处理。
我们已经知道,服务端启动过程中会创建一个 NioServerSocketChannel,而创建 NioServerSocketChannel 的过程中又会创建 Netty 的一系列核心组件,包括 Pipeline、Unsafe 等,那么,创建 NioSocketChanel 的时候是否也会创建这一系列组件呢?
NioSocketChannel
public NioSocketChannel(Channel parent, SocketChannel socket) {
super(parent, socket);
config = new NioSocketChannelConfig(this, socket.socket());
}
AbstractNioByteChannel
protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
super(parent, ch, SelectionKey.OP_READ);
}
这里,我们看到 JDK NIO 里熟悉的影子 SelectionKey.OP_READ,一般在原生的 JDK NIO 编程中,我们也会注册这样一个事件,表示对 Channel 的读事件感兴趣。
继续向上追踪,追踪到 AbstractNioByteChannel 的父类 AbstractNioChannel。这里,相信大家应该了解了 NioServerSocketChannel 最终的父类也是 AbstractNioChannel。所以,创建 NioSocketChannel 的模板和创建 NioServerSocketChannel 保持一致。
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
ch.configureBlocking(false);
}
这里的 readInterestOp 表示该 Channel 关心的事件是 SelectionKey.OP_READ,后续会将该事件注册到 Selector,之后设置该通道为非阻塞模式。
AbstractNioChannel 构造方法的第一行代码调用 super(parent)
,便是在 AbstractChannel 构造方法中创建一系列与该 Channel 绑定的组件。
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
分析到这里,是时候了解一下 Netty 中最常用的 Channel 的结构了,如下图所示。
这里的继承关系有所简化,当前,我们只需要了解这么多。
- Channel 继承 Comparable 表示 Channel 是一个可以比较的对象;
- Channel 继承 AttributeMap 表示 Channel 是可以绑定属性的对象,在用户代码中,我们经常使用 channel.attr(...) 来给 Channel 绑定属性,其实就是把属性设置到 AttributeMap 中;
- ChannelOutboundInvoker 是 4.1.x 版本新加的抽象,表示用户代码可以在 Channel 上进行哪些操作;
- DefaultAttributeMap 为 AttributeMap 的默认实现,后面的 Channel 继承了它,可以直接使用;
- AbstractChannel 用于实现 Channel 的大部分方法,其中我们最熟悉的就是在其构造方法中,创建一些 Channel 的基本组件,这里的 Channel 通常包括 SocketChannel 和 ServerSocketChannel;
- AbstractNioChannel 基于 AbstractChannel 做了 NIO 相关的一些操作,保存 JDK 底层的 SelectableChannel 的引用,并且在构造方法中设置 Channel 为非阻塞(设置非阻塞这一点对于 NIO 编程是必不可少的);
- 最后,就是两大 Channel —— NioSocketChannel 和 NioServerSocketChannel,分别对应则会服务端接收新连接过程和新连接读写过程。
我们继续之前的源码分析,在创建一条 NioSocketChannel 并放置在 List 容器里后,就开始 for 循环进行下一步操作。
3.2 设置并绑定 NioSocketChannel
创建完 NioSocketChannel 之后,接下来要对 NioSocketChannel 做一些设置,并且需要将它绑定到一个执行的 Reactor 线程中。
NioMessageUnsafe
private final List<Object> readBuf = new ArrayList<Object>();
@Override
public void read() {
assert eventLoop().inEventLoop();
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
// 1. 创建 NioSocketChannel
// ...
// 2. 设置并绑定 NioSocketChannel
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
// ...
}
readBuf 中承载着所有新建的连接,如果某个时刻,Netty 轮询到多个连接,那么使用 for 循环就可以批量处理这些连接,即 NioSocketChannel。
处理每一个 NioSocketChannel 是通过调用 NioServerSocketChannel 的 pipeline.fireChannelRead(...)
来执行的,在后面章节正式介绍 Pipeline 之前,先简单介绍一下 Pipeline 组件。
在 Netty 的各种类型的 Channel 中,都会包含一个 Pipeline。Pipeline 的字面意思是“管道”,我们可以理解为一条流水线。流水线有起点、有结束,中间还有各种各样的流水线关卡。一件物品,在流水线起点开始处理,经过各种流水线关卡的加工,最终到流水线结束。
对应到 Netty 里,流水线的开始是 HeadContext,流水线的结束是 TailContext。HeadContext 中调用 Unsafe 做具体的操作,TailContext 中用于向用户抛出 Pipeline 中未处理异常以及对未处理消息的警告。我们暂时先了解这么多,关于 Pipeline 的具体分析,后面再说。
在服务端的启动过程中,Netty 给服务端 Channel 自动添加了一个 Pipeline 处理器 ServerBootstrapAcceptor,并已经将用户代码中设置的一系列参数传入了 ServerBootstrapAcceptor 构造方法。接下来,我们来分析 ServerBootstrapAcceptor。
private static class ServerBootstrapAcceptor extends ChannelInboundHandlerAdapter {
private final EventLoopGroup childGroup;
private final ChannelHandler childHandler;
private final Entry<ChannelOption<?>, Object>[] childOptions;
private final Entry<AttributeKey<?>, Object>[] childAttrs;
private final Runnable enableAutoReadTask;
ServerBootstrapAcceptor(final Channel channel, EventLoopGroup childGroup,
ChannelHandler childHandler, Entry<ChannelOption<?>, Object>[] childOptions,
Entry<AttributeKey<?>, Object>[] childAttrs) {
this.childGroup = childGroup;
this.childHandler = childHandler;
this.childOptions = childOptions;
this.childAttrs = childAttrs;
// Task which is scheduled to re-enable auto-read.
// It's important to create this Runnable before we try to submit
// it as otherwise the URLClassLoader may not be able to
// load the class because of the file limit it already reached.
enableAutoReadTask = new Runnable() {
@Override
public void run() {
channel.config().setAutoRead(true);
}
};
}
/**
* 在新连接接入时被调用
*/
@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg;
// 1. 给新连接的 Channel 添加用户自定义的 Handler 处理器,这其实是一个
// 特殊的 ChannelHandler: ChannelInitializer (联系下个代码块)
child.pipeline().addLast(childHandler);
// 2. 设置 ChannelOption,主要和 TCP 连接一些底层参数及 Netty 自身对一个连接的参数有关
setChannelOptions(child, childOptions, logger);
// 3. 设置新连接 Channel 的属性
for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
}
// 4. 绑定 Reactor 线程
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
}
// ...
}
a. 添加用户自定义 Handler
pipeline.fireChannelRead(NioSocketChannel)
最终调用这里的 ServerBootstrapAcceptor 的 channelRead 方法,而 channelRead() 一上来就把这里的 msg 强制转换成 Channel,为什么这里可以强制转换?读者可以思考一下。
拿到该 Channel,也就是拿到了该 Channel 对应的 Pipeline,这个 Pipeline 其实就是在 #3.1 中调用 AbstractChannel 的构造方法时创建的。然后,将用户代码中的 childHandler,添加到 Pipeline 中。
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyServerHandler());
}
})
// ...
childHandler 对应的就是上述用户代码中的 ChannelInitializer。到了这里,NioSocketChannel 中 Pipeline 对应的 Handler 为 head → ChannelInitializer → tail。
b. 设置 ChannelOption
这里的 ChannelOption 也在用户代码中设置,最终传递到 ServerBootstrapAcceptor。ChannelOption 主要是和 TCP 底层参数相关的一些配置以及 Netty 对一条连接的配置。
c. 设置 ChannelAttr
设置 NioSocketChannel 对应的 ChannelAttr,ChannelAttr 和 ChannelOption 一样,也在用户代码中设置,最终传递到 ServerBootstrapAcceptor,一般情况下用不着 ChannelAttr。
d. 绑定 Reactor 线程
对于 childGroup.register(child),这里的 childGroup 就是我们在用户代码里创建的 worker NioEventLoopGroup,我们进入 NioEventLoopGroup 的 register 方法,register 首先调用 next() 方法获取一个 EventLoop 对象 MultithreadEventLoopGroup。
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
@Override
public EventLoop next() {
return (EventLoop) super.next();
}
调用其父类 MultithreadEventExecutorGroup。
@Override
public EventExecutor next() {
return chooser.next();
}
我们发现,MultithreadEventExecutorGroup 中的 next() 方法调用了 chooser 对象的 next() 方法,而这个对象正是我们在 [09] 分析的 EventExecutorChooser,它的作用是从 NioEventLoopGroup 中,选择一个 NioEventLoop,所以,最终 childGroup.register(child)
会调用 NioEventLoop 的 register 方法,由其父类 SingleThreadEventLoop 来实现。
@Override
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
到这里,读者应该会比较眼熟了,这里和服务端启动流程中的注册 Channel 的模板一样,都由 AbstractUnsafe 来执行。下面我们来分析,这一套逻辑应该如何执行。最终,register() 方法会调用如下方法。
AbstractUnsafe
private void register0(ChannelPromise promise) {
// 1. 注册 Selector
doRegister();
neverRegistered = false;
registered = true;
// 2. 配置自定义 Handler
pipeline.invokeHandlerAddedIfNeeded();
safeSetSuccess(promise);
// 3. 传播 ChannelRegistered 事件
pipeline.fireChannelRegistered();
// 4. 注册读事件
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
beginRead();
}
}
}
(1)注册 Selector。 和服务端启动过程一样,先调用 doRegister() 进行真正的注册过程。
AbstractNioChannel
@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
if (!selected) {
// Force the Selector to select now as the "canceled" SelectionKey may still be
// cached and not removed because no Select.select(..) operation was called yet.
eventLoop().selectNow();
selected = true;
} else {
// We forced a select operation on the selector before but the
// SelectionKey is still cached for whatever reason. JDK bug ?
throw e;
}
}
}
}
javaChannel().register(...) 将 NioSocketChannel 所有的事件都由绑定的 Reactor 线程的 Selector 来轮询。
(2)配置自定义 Handler。 到目前为止,NioSocketChannel 的 Pipeline 中有 3 个 Handler:head → ChannelInitializer → tail。接下来,invokeHandlerAddedIfNeeded 最终会调用 ChannelInitializer 的 handlerAdded(...) 方法。
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
if (ctx.channel().isRegistered()) {
// This should always be true with our current DefaultChannelPipeline implementation.
// The good thing about calling initChannel(...) in handlerAdded(...) is that
// there will be no ordering surprises if a ChannelInitializer will add another
// ChannelInitializer. This is as all handlers will be added in the expected order.
initChannel(ctx);
}
}
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
// Guard against re-entrance.
if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) {
try {
initChannel((C) ctx.channel());
} catch (Throwable cause) {
// Explicitly call exceptionCaught(...) as
// we removed the handler before calling initChannel(...).
// We do so to prevent multiple calls to initChannel(...).
exceptionCaught(ctx, cause);
} finally {
// 将自身删除
remove(ctx);
}
return true;
}
return false;
}
这里的 initChannel() 方法又是什么呢?让我们回到服务端启动代码,比如下面这段用户代码。
serverBootStrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new IdleStateHandler(...))
.addLast(new MyServerHandler());
}
});
对照前面的分析,原来,最终 initChannel(...) 会调用用户代码,而一般在用户代码里,我们会添加自定义的一系列 Handler。所以,这个过程其实就是给 NioSocketChannel 配置自定义 Handler,NioSocketChannel 中的 Handler 包括 head → IdleStateHandler → MyServerHandler → tail。
(3)传播 ChannelRegistered 事件。 pipeline.fireChannelRegistered() 其实没有干特别的事情,最终只是把连接注册时间往下传播,调用了每一个 Handler 的 channelRegistered 方法。
(4)注册读事件。 现在,我们还剩下这些代码没有分析。
AbstractUnsafe
private void register0(ChannelPromise promise) {
// ...
// 4. 注册读事件
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
beginRead();
}
}
}
isActive() 在连接已经建立的情况下返回 true,所以进入方法块,即进入 pipeline.fireChannelActive()。接下来的调用过程和服务端启动流程的分析过程一样,最终都会调用如下代码。
AbstractNioChannel
@Override
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();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
读者应该还记得前面分析 register0 方法的时候,向 Selector 注册的事件代码是 0,而 readInterestOp 对应的事件代码是 SelectionKey.OP_READ。参考前文中创建 NioSocketChannel 的过程,稍加推理,就会知道,这里其实就是将 SelectionKey.OP_READ 事件注册到 Selector,表示这条管道已经可以开始处理读事件。至此,新连接接入的流程就算结束了。
3.3 小结
当 boss Reactor 线程在检测到有 ACCEPT 事件之后,创建 JDK 底层的 Channel,然后使用一个 NioSocketChannel 包装 JDK 底层的 Channel,把用户设置的 ChannelOpotion、ChannelAttr、ChannelHandler 都设置到 NioSocketChannel 中。
接着,从 worker Reactor 线程组,也就是 worker NioEventLoopGroup 选择一个 NioEventLoop,把 NioSocketChannel 包装的 JDK 的 Channel 当作 key,自身当作 attachement,注册到 NioEventLoop 对应的 Selector。这样,后续有读写事件发生时,就可以直接获得 attachement,也就是 NioSocketChannel,来处理读写数据逻辑。
最后,对本章再做个总结。
- boss Reactor 线程轮询到有新连接接入;
- 通过封装 JDK 底层的 Channel 创建 NioSocketChannel 及一系列 Netty 核心组件;
- 通过 chooser 选择一个 worker Reactor 线程将该连接绑定上去;
- 注册读事件,开始新连接的读写。