今天我们主要讲下netty运行的原理,我们将从下面四个点来分析netty的运行原理
1、netty io模型
2、netty核心组件
3、netty项目架构
4、netty运行流程
首先我们来看一段netty服务端的代码
public final class NettyServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
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);
}
});
ChannelFuture f = b.bind(8007).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
可以看到的是,与jdk的nio来比较的话,netty来实现服务器端的逻辑还是相当简单的。大家可以自行百度jdk nio的服务器端相关代码。那么为啥低一点我们这里需要去介绍netty的io模型呢,因为netty采用的是大名鼎鼎的主从reactor线程模型。我们可以通过NIO学习--Reactor模型这里可以学习到三种reactor线程模型,那么为啥说netty使用的是多reactor线程模型呢,大家可以看到的是,上面的服务器端代码中有两个NioEventLoopGroup,分别是bossGroup和workerGroup,可以通过字面的意思来猜测,NioEventLoopGroup是一组线程池组,里面装的主要就是NioEventLoop,NioEventLoop的初始化就是在NioEventLoopGroup初始化里面完成的,下面列出NioEventLoop初始化的过程,这段代码位于MultithreadEventExecutorGroup里面
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
/**
* 1、NioEventLoopGroup#NioEventLoopGroup()
* 2、MultithreadEventLoopGroup#MultithreadEventLoopGroup(int, ThreadFactory, Object...)
* 3、MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, EventExecutorChooserFactory, Object...)
*/
if (nThreads <= 0) {
throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
}
// when executor is null, we can use default executor
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
// 创建nThreads个EventExecutor
children = new EventExecutor[nThreads];
// 针对workerGroup中的executor赋值
for (int i = 0; i < nThreads; i++) {
boolean success = false;
try {
children[i] = newChild(executor, args);
success = true;
} catch (Exception e) {
// TODO: Think about if this is a good exception type
throw new IllegalStateException("failed to create a child event loop", e);
} finally {
// 当初始化eventExecurtor数组失败的时候,将之前所有的eventExecuror注销掉
if (!success) {
for (int j = 0; j < i; j++) {
children[j].shutdownGracefully();
}
for (int j = 0; j < i; j++) {
EventExecutor e = children[j];
try {
while (!e.isTerminated()) {
e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
} catch (InterruptedException interrupted) {
// Let the caller handle the interruption.
Thread.currentThread().interrupt();
break;
}
}
}
}
}
// 事件执行器选择器,根据executors的数量来决定选用何种选择器
chooser = chooserFactory.newChooser(children);
final FutureListener<Object> terminationListener = new FutureListener<Object>() {
@Override
public void operationComplete(Future<Object> future) throws Exception {
if (terminatedChildren.incrementAndGet() == children.length) {
terminationFuture.setSuccess(null);
}
}
};
for (EventExecutor e : children) {
e.terminationFuture().addListener(terminationListener);
}
Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
Collections.addAll(childrenSet, children);
readonlyChildren = Collections.unmodifiableSet(childrenSet);
}
其实NioEventLoopGroup的核心初始化流程就是这个方法了,在这个方法里面可以看到的是,里面的for循环中children[i] = newChild(executor, args); 这一句话就是做EventLoop初始化的逻辑,当你使用的是NioEventLoopGroup的时候,就会初始化成NioEventLoop,这里的newChild是一个抽象方法,会调用子类的逻辑来实现,我们可以看到NioEventLoopGroup中就有这个实现的方法
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}
可以看到的是,当我们使用的是NioEventLoopGroup的时候,children数组里面存放的都是NioEventLoop,当你使用其他Group的时候,children数组里面存放的自然都是其他的一些EventLoop。这里我们就把NioEventLoop的关系给大家介绍的很清楚了。
其实这里面我们可以看到的是,bossGroup就是一个reactor, 主要用来处理网络 IO 连接建立操作,通常,mainReactor 只需要一个,因为它一个线程就可以处理。
workderGroup就是另外的一个reactor,主要和建立起来的客户端的 SocketChannel 做数据交互和事件业务处理操作。通常,subReactor 的个数和 CPU 个数相等,每个 subReactor 独占一个线程来处理。netty中默认的情况下subReactor的个数就是cpu核数*2。
相信大家通过上面的介绍已经简单的理解了reactor线程模型了吧。
下面我们来介绍netty的核心组件
1、BootStrap&ServerBootSrap
2、Channel
3、ChannelPipeline
4、ChannelHandler
5、EventLoop&EventLoopGroup
BootStrap&ServerBootSrap
大家仔细观察上面的图,可以看到的是BootStrap和ServerBootStrap基本上是用来做相关配置的,但是这些配置包含了配置EventLoopGroup、配置Channel,可以看到的是Channel和ChannelPipeline有关联,ChannelPipeline又与ChannelHandler有关联,而上面的介绍中,大家已经知道了EventLoopGroup与EventLoop有关联,所以来说这两个引导类,基本上就是我们netty程序的入口,令人感到高兴的是,里面的代码并不多,所以理解了每个类之间的关系,对于理解netty的工作原理还是相当重要的。
channel
Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channel 的 EventLoop 。
在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而 Netty 的 Channel 则提供的一系列的 API ,它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势( 摘自《Netty权威指南( 第二版 )》) :
- 在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。
- Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
- 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,由 Channel 统一负责和调度,功能实现更加灵活。
我们现在下面来介绍NioServerSocketChannel和NioSocketChannel,上面的可以知道,channel主要是netty网络抽象的类,所以来说channel就是我们实际读写事件的入口,那么我们来简单的看下channel中有哪些基本的属性呢
private final SelectableChannel ch; // jdk的channel
protected final int readInterestOp; // 感兴趣的操作
volatile SelectionKey selectionKey; // 注册到selector上后返回的selectionKey
boolean readPending;
private final Runnable clearReadPendingRunnable = new Runnable() {
@Override
public void run() {
clearReadPending0();
}
};
private ChannelPromise connectPromise;
private ScheduledFuture<?> connectTimeoutFuture;
private SocketAddress requestedRemoteAddress;
这个是AbstractNioChannel类中的属性,当然这个类是NioServerSocketChannel的父类了,我们继续向上观察
private final Channel parent;
private final ChannelId id;
private final Unsafe unsafe;
private final DefaultChannelPipeline pipeline;
private final VoidChannelPromise unsafeVoidPromise = new VoidChannelPromise(this, false);
private final CloseFuture closeFuture = new CloseFuture(this);
private volatile SocketAddress localAddress;
private volatile SocketAddress remoteAddress;
private volatile EventLoop eventLoop;
private volatile boolean registered;
private boolean closeInitiated;
private Throwable initialCloseCause;
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
这个是AbstractChannel中的构造方法和部分属性信息,我们通过这个类可以观察到的信息点有哪些呢,每个NioServerSocketChannel都会对应一个channelPipeline和eventLoop,当然里面还有一个非常重要的属性,就是unsafe,可以看到构造方法里面就有unsafe和pipeline的实例化过程,大家感兴趣的可以自己去看下,这里也是非常简单的,newUnsafe是一个抽象的方法,也是会到子类里面去完成unsafe对象的实例化,大家可以去看下NioServerSocketChannel和NioSocketChannel分别产生的unsafe是哪个对象。
其实channel这个类就是相关的层次复杂了一点点,其实当你把整个继承关系弄清楚了之后,还是比较容易的,例如channel和eventloop,channel和pipeline之间的关系等等。相关一些channel的api等后面的运行原理里面会做更加仔细的介绍。这里只是简单分析一下,初始化的一些逻辑。
ChannelPipeline&ChannelHandler
下面呢,我们就简单的介绍一下ChannelPipeline,大家可以看到的是ChannelPipeline是和Channel绑定在一起的,channelPipeline的初始化依赖于Channel的初始化流程, pipeline = newChannelPipeline(); 这里就是channelPipeline实例化的代码,默认实现是DefaultChannelPipeline,我们先简单的分析一下DefaultChannelPipeline
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
大家可以看到的是DefaultChannelPipeline的初始化也是很简单的,其中最重要的就是TailContext和HeadContext,两个属性,可以看到的是DefaultChannelPipeline内部维护的是一个双向链表,首节点已经固定为HeadContext,尾节点已经固定为TailContext。如果仔细观察了之后可以发现HeadContext和TailContext都是AbstractChannelHandlerContext的子类,那我们再去窥探一下AbstractChannelHandlerContext
volatile AbstractChannelHandlerContext next;
volatile AbstractChannelHandlerContext prev;
private final boolean inbound;
private final boolean outbound;
private final DefaultChannelPipeline pipeline;
private final String name;
private final boolean ordered;
AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
boolean inbound, boolean outbound) {
this.name = ObjectUtil.checkNotNull(name, "name");
this.pipeline = pipeline;
this.executor = executor;
this.inbound = inbound;
this.outbound = outbound;
// Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
ordered = executor == null || executor instanceof OrderedEventExecutor;
}
可以通过上面我们可以看到AbstractChannelHandlerContext的部分属性以及构造方法,看到next和prev节点的同时,是不是更加坐实了DefaultChannelPipeline维护了一个基于AbstractChannelHandlerContext为节点的双向链表呢。不止是这样,我们还可以观察到inbound和outbound两个属性,那么这两个属性是用来干嘛的呢,我也不卖关子,直接可以告诉大家,inbound代表入站属性,outbound代表出站属性,而两者都是true的时候就代表当前AbstractChannelHandlerContext里面的handler既处理入站事件,又处理出站事件。大家可以看到的是channelPipeline与channelHandler之间不是直接关联的,而是通过一个第三者来维护着彼此之间的关系的。这样处理的目的为的就是入站事件使用入站的handler按照顺序处理,出站的事件按照出站的顺序依次处理。
* I/O Request
* via {@link Channel} or
* {@link ChannelHandlerContext}
* |
* +---------------------------------------------------+---------------+
* | ChannelPipeline | |
* | \|/ |
* | +---------------------+ +-----------+----------+ |
* | | Inbound Handler N | | Outbound Handler 1 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* | | \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler N-1 | | Outbound Handler 2 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ . |
* | . . |
* | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
* | [ method call] [method call] |
* | . . |
* | . \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler 2 | | Outbound Handler M-1 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* | | \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler 1 | | Outbound Handler M | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* +---------------+-----------------------------------+---------------+
* | \|/
* +---------------+-----------------------------------+---------------+
* | | | |
* | [ Socket.read() ] [ Socket.write() ] |
* | |
* | Netty Internal I/O Threads (Transport Implementation) |
* +-------------------------------------------------------------------+
通过上面的图大家应该可以更好的理解handler与pipeline之间的关系,由于是双向链表,所以既可以从尾节点向首节点完成事件的传递,又可以完成首节点到未节点的传递。当然上面的图其实还是有一点问题的,因为所有的handler应该是在一条线上的,上面为了大家更好的理解,所以画成了这种样子。
/**
* 找到下一个inbound handler
* @return
*/
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
// 查找下一个handler,如果前一个handler不是inbound,继续向前查找,直到找到第一个inbound handler
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}
/**
* 找到前一个outbaund handler
* @return
*/
private AbstractChannelHandlerContext findContextOutbound() {
AbstractChannelHandlerContext ctx = this;
// 查找前一个handler,如果前一个handler不是outbound,继续向前查找,直到找到第一个outbound handler
do {
ctx = ctx.prev;
} while (!ctx.outbound);
return ctx;
}
可以看到的是netty的源码中确实存在寻找下一个inbound AbstractChannelHandlerContext和上一个outbound AbstractChannelHandlerContext的逻辑,似乎一切都在推理之中。
上面已经将netty的核心组件基本上都有一个简单的介绍,下面我们来介绍一下整个运行流程,netty是如何处理各种io事件的。PS:没有介绍netty启动流程,请大家自行查阅相关资料来研究吧。
netty运行流程
由于启动流程不在本篇的讨论范围之内,我就直接切入到netty运行的代码里面给大家介绍相关的逻辑了
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
return;
}
if (eventLoop != this || eventLoop == null) {
return;
}
unsafe.close(unsafe.voidPromise());
return;
}
try {
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
// See https://github.com/netty/netty/issues/924
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
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) {
// NIoMessageUnsafe
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
之前说了一个reactor线程模型,不知道大家是否还记得,作为服务器端在绑定端口之后,服务器端就会起多个线程,以上面的NettyServer代码为例的话,bossGroup会启动一个线程,也就是分析的children数组的数量为1,里面对应着一个NioEventLoop,另外workerGroup会启动cpu核心数*2个线程,就是说这些线程会一直在使用seletor来查询有没有发生io事件,如果有的话,最终就会调用上面的代码,我们来简单的分析下上面的代码,可以看到参数是SelectionKey和AbstractNioChannel,这里面的AbstractNioChannel其实会有多个对象,有可能是NioServerSocketChannel,或者是NioSocketChannel,还有可能是其他的一些Channel,这里面我们主要考虑这连个Channel,可以看到里面的代码都不复杂,最后都是使用unsafe对象来执行对应的业务逻辑,而且前面在channel的初始化的时候已经介绍过unsafe属性吧,可以看到的时候NioEventLoop已经找到了Channel了,上面分析到了事件是在channelPipeline中的双向链表的数据结构中传播的,那么我们来通过源码分析下,unsafe.read()api是怎么找到channelPipeline的,这里我们仅仅介绍NioServerSocketChannel中的unsafe属性的对象-NioMessageUnsafe
private final class NioMessageUnsafe extends AbstractNioUnsafe {
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;
try {
try {
do {
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
allocHandle.incMessagesRead(localRead);
} while (allocHandle.continueReading());
} catch (Throwable t) {
exception = t;
}
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
pipeline.fireChannelReadComplete();
if (exception != null) {
closed = closeOnReadError(exception);
pipeline.fireExceptionCaught(exception);
}
if (closed) {
inputShutdown = true;
if (isOpen()) {
close(voidPromise());
}
}
} finally {
// Check if there is a readPending which was not processed yet.
// This could be for two reasons:
// * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
// * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
//
// See https://github.com/netty/netty/issues/2254
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}
}
}
大家可以看到的是里面有执行pipeline.fireXXXX()方法,似乎已经调用到了pipeline的相关api来完成事件的传播工作,一旦事件到达了pipeline就会到达我们写的ChannelHandler,这样的话,我们就可以处理业务逻辑了。
netty项目架构
如果大家有下载过netty的源码,其实可以发现,netty基本上就是由5大块组成,就是上面着色的一些模块,其实要熟悉netty的原理的话,基本上只用了解transport相关的源码就可以了,当然transport会依赖于common包里面的一些逻辑,其实简单的来阅读的话,基本上就这两个包,就可以了解netty的原理,当然再深入一点点就是理解buffer包里面的代码,handler和codec其实不是很重要,因为codec就是编解码,其实就是一种特殊的handler,而netty提供的handler就是我们上面的channelHandler,就是提供的一种处理读写情况的一种业务逻辑实现,当然我们假如需要使用netty来编码的话,当然必须实现自己的ChannelHandler,当然有些功能可以直接使用netty提供的ChannelHandler来处理,个人推荐的阅读顺序是transport->common->buffer,其他两个自己随便,buffer其中的内存管理,以及零拷贝还是挺复杂的。
未分析的地方
其实在这边文章里面,笔者也有众多未分析的地方,包含但不局限于下面的一些点
1、NioServerSocketChannel接收到客户端的链接之后,是怎么把Channel注册到NioEventLoop上面的Selector上的
2、channelPipeline中的AbstractChannelHandlerContext是如何依次向后传递的,并且能让你无感的,大家写的ChannelHandler并没有明显的调用下一个handler方法
3、netty的启动流程,做了哪些工作?
4、内存管理&buffer模块的工作
5、netty的线程是什么时候启动的?
总结
那么现在来简单的总结一下netty的原理,简单的来说,我们使用ServerBootSrap配置好了NioEventLoopGroup之后,并且绑定了端口之后,我们有很多线程在一直轮询注册到NioEventLoop里面的selector对象有没有发生io事件,如果有发生的话,我们会把事件通过channel里面的unsafe对象传递给ChannelPipeline来处理逻辑,而ChannelPipeline会使用对应的ChannelHandler来处理最终的逻辑。
netty源码阅读心得
其实在粗粒度的读完netty源码之后,发现netty源码不是那么的复杂,对于这种作为通讯的框架来说,无非就是监听某个端口,然后一直在使用类似while(true)之类的代码去轮询检查有没有发生数据的读写或连接等情况的产生,如果有的话,那么就使用逻辑去处理数据,还有就是一定要找到主线逻辑,一般的情况下,在代码启动的时候的逻辑要读懂,还有就是运行的时候逻辑要清楚,另外的话,最好可以清楚的知道,这个框架启动的每个线程是干嘛的,只有这样,才可以说更好的弄懂了这个框架到底的数据是如何处理的,如果有不同意见,欢迎沟通。也希望大家可以帮着完善上面未分析的地方,让大家可以更加清晰的掌握netty的启动流程&运行流程的逻辑。