一、Netty核心源码之连接请求源码分析
在上篇的服务器启动源码分析中,原文链接
我们得知:服务器端的 NioServerSocketChannel 实例将自己注册到了 bossGroup 上(讲得更细一些,是 bossGroup 中 EventLoop 的 Selector 上),监听客户端连接,如下图所示。
接下来,我们需要去研究的是服务器和客户端的连接请求源码
1.1、监听 accept 事件,接受连接 & 创建一个新的 NioSocketChannel
之前说过,NioEventLoop 中的死循环,会不断执行以下三个过程
-
select:轮训注册在其中的 Selector 上的 Channel 的 IO 事件
-
processSelectedKeys:在对应的 Channel 上处理 IO 事件
-
runAllTasks:再去以此循环处理任务队列中的其他任务
我们先以 Debug 模式启动 EchoServer,然后将断点放在 NioEventLoop 中 run 方法里面死循环代码块的 processSelectedKeys()语句上。再启动 EchoClient的main方法。追踪服务端代码的执行,过程如下:
注意: 在启动服务端之前,先不要在服务端的NioEventLoop类中打断点,因为NioEventLoop的run方法中,会被阻塞住,一直进行select收集selectedKeys,只有>0时才会继续往下执行,而此时即使客户端启动并进行连接,也无法连接成功,所以,要在服务端启动后,客户端启动前打上断点。
上图最后一个断点处调用的 AbstractNioMessageChannel$NioMessageUnsafe.read 方法的定义如下:
/**
* NioMessageUnsafe 是一个定义在 AbstractNioMessageChannel 中的内部类
*/
private final class NioMessageUnsafe extends AbstractNioUnsafe {
// 可以看做存放请求数据的容器
private final List<Object> readBuf
= new ArrayList<Object>();
@Override
public void read() {
......
}
}
继续执行代码进入这个 read 方法,如下:
可以发现 read 方法里面通过 ServerSocketChannel 的 accept 方法创建了一个与客户端交互的 SocketChannel。
1.2 将新的 NioSocketChannel 注册到 workerGroup 上
上一张图“这里发生了什么???”处留下了一个未解开的谜团,我们来看下这里面到底在干什么:
继续追踪上图的最后一个红色框圈住的 register 方法,最终过程如下所示:
不错,我们之前一直重复的“bossGroup 会将 NioServerSocketChannel 产生的 NioSocketChannel 注册到 workerGroup”就发生在这里!
1.3. 监听 NioSocketChannel 上的 Read 事件
“bossGroup 会将 NioServerSocketChannel 产生的 NioSocketChannel 注册到 workerGroup”这个过程完成之后,会触发 NioSocketChannel 中 doBeginRead 的执行(追到这一步的过程很漫长,此处不再给出过程),如下图所示:
这个过程完成了对 Read 事件的关注(上一篇文章中讲过,将 Channel 注册到 Selector 上的时候,会注册对哪些事件感兴趣,即关注哪些事件)。接下来 EventLoop 就可以对这个 NioSocketChannel 监听并处理 Read 事件了。
至此,Netty 服务端接收客户端连接请求的源码剖析已经讲清楚了。总结如下:
1)服务器端 bossGroup 中的 EventLoop 轮训 Accept 事件、获取事件后在 processSelectedKey()方法中调用 unsafe.read()方法,这个 unsafe 是内部类 io.netty.channel.nio.AbstractNioChannel.NioUnsafe 的实例,unsafe.read()方法由两个核心步骤组成:doReadMessages()和 pipeline.fireChannelRead()。
2)doReadMessages()用于创建 NioSocketChannel 对象,包装了 JDK 的 SocketChannel 对象,并且添加了 pipeline、unsafe、config 等成分。
3)pipeline.fireChannelRead()用于触发服务端 NioServerSocketChannel 的所有入站 Handler 的 channelRead()方法,在其中的一个类型为 ServerBootstrapAcceptor 的入站 Handler 的 channelRead()方法中将新创建的 NioSocketChannel 对象注册到 workerGroup 中的一个 EventLoop 上,该 EventLoop 开始监听 NioSocketChannel 中的读事件。
二、ChannelPipeline 源码剖析
这一节对 ChannelPipeline 的源码进行梳理和总结。
在梳理 ChannelPipeline 源码之前,我再唠叨一下 ChannelPipeline、ChannelHandler、ChannelHandlerContext 三者的关系,这在之前的文章中已经进行过详细的讲解:
1)每当 ServerSocketChannel 创建一个新的连接,就会创建一个 SocketChannel 对应目标客户端
2)每一个新创建的 SocketChannel 包含一个全新的 ChannelPipeline
3)每一个 ChannelPipeline 内部都含有一个由 ChannelHandlerContext 构成的双向链表
4)ChannelHandlerContext 都包装了一个 ChannelHandler
ChannelPipeline 的继承关系如下:
ChannelPipeline 本身是一个接口,它又继承了 ChannelInboundInvoker、ChannelOutboundInvoker、Iterable<Entry<String, ChannelHandler>>三个接口。这个继承关系很好理解,ChannelPipeline 要遍历其中的每一个 Handler 处理入站事件和出站事件,自然要继承 ChannelInboundInvoker、ChannelOutboundInvoker、Iterable<Entry<String, ChannelHandler>>三个接口。
它提供的接口有:
可以看到这些接口分这么几类:
-
元素操作类:以 get、add、remove、replace 等开头的
-
Pipeline 里 Handler 责任链中 IO 事件处理方法触发类:以 fire、write 等开头的
-
其他
对于往 Pipeline 中添加 ChannelHandlerContext 元素这一过程,在前文 1.2 小节中已经进行了源码分析。而对于删除、修改和获取 Pipeline 中元素的过程,分析思路类似,本质就是在操作一个双向链表,因此本节不再赘述。本节着重分析 IO 事件在 Pipeline 中流转过程的源码。
在 ChannelPipeline 的源码的注释中,有这么一张字符画:
它表达意思和下面这幅图是一样的:
那就是:出站事件会依次流经 Pipeline 中每一个 ChannelHandlerContext,遇到 OutboundHandler 就被处理;入站事件会依次流经 Pipeline 中每一个 ChannelHandlerContext,遇到 InboundHandler 就被处理。
还以上文一直使用的 EchoServer/EchoClient 为例,以 Debug 模式启动 EchoServer,然后将断点放在 NioEventLoop 中 run 方法里面死循环代码块的 processSelectedKeys()语句上。再以 Run 模式启动 EchoClient。追踪服务端代码的执行直到 pipeline 露面,过程如下:
上图最后一个红框圈住的 fireChannelRead()方法表示触发 pipeline 中所有的能够处理该入站事件(Read 事件)的 HandlerContext 的 channelRead()方法。我们追进去(Step into):
可以发现,确实是这样:循环查找 HandlerContext 双向链表中的元素,直到找到能够处理入站事件 Read 的 InboundHandler 类型的 HandlerHandlerContext,然后交由这个 HandlerHandlerContext 处理入站事件 Read。
三、ChannelHandler 源码剖析
这一节对 ChannelHandler 的源码进行梳理和总结。
通常,一个 Pipeline 中有多个 Handler,例如一个典型的服务器在每个管道中都会有协议解码器、协议编码器、业务处理程序,分别用来将接收到的二进制数据转换为 Java 对象,以及将要发送的 Java 转换为二进制数据,以及根据接收到的 Java 对象执行业务处理过程。那么每一个协议解码器、协议编码器、业务处理程序都可以定义成一个 Handler。
注:如果 Handler 里面的数据处理过程很快,可以放在当前 EventLoop 中处理,否则就要放入任务队列进行异步处理,或者开辟新的线程来处理。
ChannelHandler 是 Netty 中的一个顶级接口,它的子类和子接口有很多:
在这些子类和子接口中,有的是为了让使用者自定义入站/出站处理过程的接口(如 ChannelInboundHandler 和 ChannelOutboundHandler),有的是为使用者自定义入站/出站处理过程时提供便利的 Adapter,有的是 Netty 内置的用于实现网络协议的 Handler。
ChannelHandler 的源码很简单,如下:
public interface ChannelHandler {
/**
* 回调函数,Handler 加入 Pipeline 后触发
*/
void handlerAdded(ChannelHandlerContext ctx) throws Exception;
/**
* 回调函数,Handler 从 Pipeline 中移除后触发
*/
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
/**
* 回调函数,Handler 处理 IO 事件时遇到异常后触发
*/
@Deprecated
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
......
}
ChannelOutboundHandler 的源码也很简单,包含一些跟连接和写出数据相关的方法,如下:
public interface ChannelInboundHandler extends ChannelHandler {
/**
* Handler 对应的 Channel 被注册到 EventLoop 上时被调用
*/
void channelRegistered(ChannelHandlerContext ctx) throws Exception;
/**
* Handler 对应的 Channel 从 EventLoop 上解除注册时被调用
*/
void channelUnregistered(ChannelHandlerContext ctx) throws Exception;
/**
* Handler 对应的 Channel 处于活动状态时被调用
*/
void channelActive(ChannelHandlerContext ctx) throws Exception;
/**
* Handler 对应的 Channel 处于非活动状态时被调用
*/
void channelInactive(ChannelHandlerContext ctx) throws Exception;
/**
* 读 Handler 对应的 Channel 中的数据时,该方法被调用
*/
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception;
/**
* 读 Handler 对应的 Channel 中的数据完毕时,该方法被调用
*/
void channelReadComplete(ChannelHandlerContext ctx) throws Exception;
......
}
ChannelInboundHandler 或 ChannelOutboundHandler 这两个接口是每个自定义 Handler
要实现的,这两个接口像是一种协议,约定了每个 Handler 要处理哪些工作。当然,为了方便使用者开发自己的 Handler,Netty
提供了一些 Handler 半成品,比如
ChannelInboundHandlerAdapter、ChannelOutboundHandlerAdapter、ChannelDuplexHandler
这三个类,使用者只需继承这些半成品类然后重写某些方法即可(这样就不用逐个实现 ChannelInboundHandler 或
ChannelOutboundHandler 中的全部方法)。请读者自行阅读这些半成品的源码,他们仅仅对
ChannelInboundHandler 或 ChannelOutboundHandler 做了简单的实现。
四、ChannelHandlerContext 源码剖析
这一节对 ChannelHandlerContext 的源码进行梳理和总结。
ChannelHandlerContext 是一个接口,并且继承了 AttributeMap、ChannelInboundInvoker、ChannelOutboundInvoker(所谓 Invoker 就是触发器/调用器,用来调用 ChannelInboundHandler 或者 ChannelOutboundHandler 中的事件处理方法)三个接口。
因此 ChannelHandlerContext 有三个作用:
1)维护一些额外属性,这个作用继承于 AttributeMap。
public interface AttributeMap {
/**
* 根据 AttributeKey 返回 Attribute
*/
<T> Attribute<T> attr(AttributeKey<T> key);
/**
* 检查 AttributeKey 是否存在
*/
<T> boolean hasAttr(AttributeKey<T> key);
}
2)调用其内部的 ChannelInboundHandler 中的入站事件处理方法来处理 IO 事件,这个作用继承于 ChannelInboundInvoker。ChannelInboundInvoker 在 ChannelInboundHandler 外面包装了一层,达到在触发/调用 ChannelInboundHandler 相应方法前后拦截并做一些特定操作的目的。
public interface ChannelInboundInvoker {
......
/**
* 触发/调用 ChannelInboundHandler#channelActive(ChannelHandlerContext)方法
*/
ChannelInboundInvoker fireChannelActive();
/**
* 触发/调用 ChannelInboundHandler#channelInactive(ChannelHandlerContext)方法
*/
ChannelInboundInvoker fireChannelInactive();
/**
* 触发/调用 ChannelInboundHandler#exceptionCaught(ChannelHandlerContext)方法
*/
ChannelInboundInvoker fireExceptionCaught(Throwable cause);
/**
* 触发/调用 ChannelInboundHandler#channelRead(ChannelHandlerContext)方法
*/
ChannelInboundInvoker fireChannelRead(Object msg);
/**
* 触发/调用 ChannelInboundHandler#channelReadComplete(ChannelHandlerContext)方法
*/
ChannelInboundInvoker fireChannelReadComplete();
......
}
3)调用其内部的 ChannelOutboundHandler 中的出站事件处理方法来处理 IO 事件,这个作用继承于 ChannelOutboundInvoker。ChannelOutboundInvoker 在 ChannelOutboundHandler 外面包装了一层,达到在触发/调用 ChannelOutboundHandler 相应方法前后拦截并做一些特定操作的目的。
public interface ChannelOutboundInvoker {
/**
* 触发/调用 ChannelOutboundHandler 中的相应方法
*/
ChannelFuture bind(SocketAddress localAddress);
ChannelFuture connect(SocketAddress remoteAddress);
ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress);
ChannelFuture disconnect();
ChannelFuture close();
ChannelFuture deregister();
ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise);
ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise);
ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);
ChannelFuture disconnect(ChannelPromise promise);
ChannelFuture close(ChannelPromise promise);
ChannelFuture deregister(ChannelPromise promise);
ChannelOutboundInvoker read();
ChannelFuture write(Object msg);
ChannelFuture write(Object msg, ChannelPromise promise);
ChannelOutboundInvoker flush();
ChannelFuture writeAndFlush(Object msg, ChannelPromise promise);
ChannelFuture writeAndFlush(Object msg);
......
}
除了以上从 AttributeMap、ChannelInboundInvoker、ChannelOutboundInvoker 继承来的三个作用,ChannelHandlerContext 还有一些自有方法,如下:
public interface ChannelHandlerContext
extends AttributeMap,
ChannelInboundInvoker,
ChannelOutboundInvoker {
/**
* 获取当前 Context 关联的的 Channel
*/
Channel channel();
/**
* 获取当前 Context 关联的的 EventExecutor
*/
EventExecutor executor();
......
/**
* 获取当前 Context 关联的的 ChannelHandler
*/
ChannelHandler handler();
/**
* 检测当前 Context 关联的 ChannelHandler 是否从 Pipeline 中被移除了
*/
boolean isRemoved();
......
/**
* 获取当前 Context 关联的的 ChannelPipeline
*/
ChannelPipeline pipeline();
/**
* 获取当前 Context 关联的的 ByteBufAllocator
*/
ByteBufAllocator alloc();
......
}
总之,ChannelHandlerContext 包装了 ChannelHandler 的一切,以方便 Pipeline 使用 ChannelHandler。
五. ChannelPipeline、ChannelHandler、ChannelHandlerContext 三者的创建过程源码剖析
这一节对 ChannelPipeline、ChannelHandler、ChannelHandlerContext 三者的创建过程进行梳理和总结。如下:
1)任何一个 ChannelSocket(无论是 NioSocketChannel 还是 NioServerSocketChannel)创建的时候都会创建一个 ChannelPipeline,这发生在调用 AbstractChannel(它是 NioSocketChannel 和 NioServerSocketChannel 的父类)的构造方法的时候。如下图所示:
2)当系统内部或者使用者调用 ChannelPipeline 的 addxxx 方法添加 new 出来的 ChannelHandler 实例的时候,都会创建一个包装该 ChannelHandler 的 ChannelHandlerContext。如下图所示:
3)这些 ChannelHandlerContext 构成了 ChannelPipeline 中的双向循环链表。
六. ChannelPipeline 调用 ChannelHandler 源码剖析
这一节对 ChannelPipeline 调用 ChannelHandler 的过程进行梳理和总结。如下:
当一个请求进来的时候,EventLoop 中的 run 方法中的死循环中的 processSelectedKeys,会触发/调用 Pipeline 中的相关方法来处理。如果是处理入站事件,则这些方法由 fire 开头(如 fireChannelRead),表示开始事件在管道中的流动,让后面的 InboundHandler 逐个处理。当处理完业务,要进行响应数据发送以及关闭连接等操作的时候,即处理出站事件的时候,同样要触发/调用 Pipeline 中的相关方法来处理,如 write 方法。
在本文使用的 EchoServer/EchoClient 案例中,Netty 为每一个 NioServerSocketChannel 和 NioSocketChannel 创建的 ChannelPipeline 的真实类型都是 DefaultChannelPipeline,它对 ChannelPipeline 接口进行了实现。比如其中的 addLast 方法、fireChannelRead 方法和各种 write 方法的实现为:
// 以下代码段摘自 DefaultChannelPipeline 源码
......
@Override
public final ChannelPipeline addLast(
String name,
ChannelHandler handler) {
return addLast(null, name, handler);
}
@Override
public final ChannelPipeline addLast(
EventExecutorGroup group,
String name,
ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
// 对操作双线循环链表的代码做同步处理
synchronized (this) {
// 检查 handler 是否能被多个 pipeline 重复使用
checkMultiplicity(handler);
// 创建包装 handler 的 context
newCtx = newContext(group, filterName(name, handler), handler);
// 添加 context 到双向循环链表
addLast0(newCtx);
......
}
......
}
private void addLast0(AbstractChannelHandlerContext newCtx) {
AbstractChannelHandlerContext prev = tail.prev;
newCtx.prev = prev;
newCtx.next = tail;
prev.next = newCtx;
tail.prev = newCtx;
}
......
@Override
public final ChannelPipeline fireChannelRead(Object msg) {
// 从双向链表的头部节点开始,逐个寻找下一个 InboundHandler 处理 Read 事件
// 入站事件的流向为从前往后
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
return this;
}
......
@Override
public final ChannelFuture write(Object msg) {
// 从双向链表的尾部节点开始,逐个寻找下一个 OutboundHandler 处理 write 事件
// 出站事件的流向为从后往前
return tail.write(msg);
}
@Override
public final ChannelFuture write(Object msg, ChannelPromise promise) {
// 从双向链表的尾部节点开始,逐个寻找下一个 OutboundHandler 处理 write 事件
// 出站事件的流向为从后往前
return tail.write(msg, promise);
}
@Override
public final ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
// 从双向链表的尾部节点开始,逐个寻找下一个 OutboundHandler 处理 write 事件
// 出站事件的流向为从后往前
return tail.writeAndFlush(msg, promise);
}
@Override
public final ChannelFuture writeAndFlush(Object msg) {
// 从双向链表的尾部节点开始,逐个寻找下一个 OutboundHandler 处理 write 事件
// 出站事件的流向为从后往前
return tail.writeAndFlush(msg);
}
......
对这些处理 IO 事件的方法追踪下去,会发现最终都是去调用各种 Handler 中的方法,这就是我们将自定义 Handler 通过 addLast 添加到 Pipeline 中会发挥作用的底层原理。
Pipeline 就像一个串联的插槽,每个具备特定功能的 Handler 组件可以被热插拔到 Pipeline 中。每一个 IO 事件都会流经 Pipeline,遇到适配的 Handler 就会被处理,当前 Handler 处理完成后会继续抛给后一个 Handler 处理。这个过程如下面的流程图所示(该图给出的是处理入站事件的流程,处理出站事件的流程类似):
七. NioEventLoop 源码剖析
这一节对 NioEventLoop 的源码进行梳理和总结。
我们先来看下 NioEventLoop 的继承关系图。
1)NioEventLoop 实现了 EventLoop 接口,因此它要完成处理注册其上的 Channel 的所有 IO 操作的工作,这是 EventLoop 接口的要求(这里的 EventLoop 是一种职能声明接口,它并没有约定实现类应该具备哪些方法以完成对 IO 事件的处理)。
2)NioEventLoop 间接继承于 ScheduledExecutorService,它是一个定时任务接口,因此 EventLoop 具备接受定时任务的功能,在?上一篇文章展示向定时任务队列中添加定时任务以实现异步处理时使用的 eventloop 的 schedule 方法也继承于此。
3)NioEventLoop 间接继承于 SingleThreadEventExecutor,也就是说 NioEventLoop 是一个包含单线程的线程池。我们在前文频繁提到的 eventloop 的 runAllTasks 方法就继承于这里。
同样,在?上一篇文章展示向任务队列中添加任务以实现异步处理时使用的 eventloop 的 execute 方法也继承于此。
4)实际上 NioEventLoop 中的 execute 方法才是 NioEventLoop 中 run 方法(包含了那个核心的死循环过程)的调用者,而 NioEventLoop 中的 execute 方法可以在很多地方被触发,例如 EchoClient 启动的时候,向 EchoServer 发起连接建立请求,EchoServer 创建 NioSocketChannel 并将其注册到 NioEventLoop 上的时候,会触发 NioEventLoop 中的 execute 方法的执行。过程如下:
此外,向任务队列添加任务、添加 Handler 到 Pipeline 等等过程都会触发 NioEventLoop 中 execute 方法的执行。
5)NioEventLoop 中的 execute 方法的执行轨迹如下图所示:
6)以上 run 方法中,第一步 select 的目的是调用 Java NIO 中 Selector 中的 select()或者 selectNow()方法,把注册到当前 NioEventLoop 中发生 IO 事件的 Channel 对应的 SelectionKey 保存到 Selector 的内部集合中(关于 Java NIO 中 Selector 中的 select()或者 selectNow()方法的作用,可以参考?上一篇文章,返回注册到当前 NioEventLoop 中的发生 IO 事件的 Channel 的个数。
第二步 processSelectedKeys 的目的是遍历 Selector 的内部 SelectionKey 集合(每一个 SelectionKey 关联了发生 IO 事件的 Channel),对每一个 Channel 上的 IO 事件进行处理。
第三步 runAllTasks 的目的是处理任务队列中的异步任务。任务队列中的任务可以是第二步处理 IO 事件的时候添加的,以实现处理过程的异步化。
结束语
本文第 1、2 两节通过追踪 Netty 服务端启动过程和接收请求过程的源码,展示 Netty 中 ServerBootStrap、EventLoopGroup、EventLoop、ChannelPipeline、ChannelHandler、ChannelHandlerContext 这些核心组件的工作原理,后面几节又对部分组件的源码做了专项的梳理和总结,以帮助读者加深对 Netty 架构和核心组件的理解。那再次回顾这张 Netty 的架构图,你是否看到了更多的细节呢?
💥推荐阅读💥