一、EventLoop
从前文可知,EventLoop对应着一个线程实体,且通过EventLoopGroup来分配,像端口绑定、channel注册等工作都也是EventLoop来完成,并且一些任务的提交和调度都是通过EventLoop来实现。所以EventLoop在netty中扮演着非常重要的角色,本文就重点分析一下EventLoop每个功能。
public interface EventLoop extends EventExecutor, EventLoopGroup {
@Override
EventLoopGroup parent();//返回EventLoopGroup实例
}
从接口来看,EventLoop继承了EventLoopGroup,说明EventLoop具备和EventLoopGroup一样的功能,同时还继承了EventExecutor(顾名思义,就是事件执行器,执行具体的某个事件),也就是说EventLoop可执行某个具体的事件,下边我们以其具体实现NioEventLoop详细分析。
二、NioEventLoop
前文中,NioEventLoopGroup的具体实现有这么个方法。
protected EventLoop newChild(Executor executor, Object... args)
throws Exception {
EventLoopTaskQueueFactory queueFactory
= args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(),
(RejectedExecutionHandler) args[2], queueFactory);
}
EventLoopGroup会循环调用这个方法来初始化EventExecutor数组(也就是EventLoop数组)。threadFactory(DefaultThreadFactory实例)是线程工厂,EventLoop中持有的线程对象就是通过这个工厂提供,跟踪其newThread方法,发现最后生成的是自定义的FastThreadLocalThread线程。
同样的我们看NioEventLoop的构造方法。
NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,
EventLoopTaskQueueFactory queueFactory) {
super(parent, executor, false, newTaskQueue(queueFactory), newTaskQueue(queueFactory),
rejectedExecutionHandler);
//每一个NioEventLoop都会持有同一个SelectorProvider
this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");
//初始选择策略
this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");
//这是一个Selector二元组,里边保存的是unwrappedSelector和selector对象
final SelectorTuple selectorTuple = openSelector();
this.selector = selectorTuple.selector;
this.unwrappedSelector = selectorTuple.unwrappedSelector;
}
接下来挨个分析NioEventLoop的所有方法。
- 静态代码块:配置SELECTOR_AUTO_REBUILD_THRESHOLD,意思是说选择次数超过了这个值就会触发重建selector(具体的使用在代码中详解),根据系统参数io.netty.selectorAutoRebuildThreshold配置,默认是512.
- newTaskQueue(EventLoopTaskQueueFactory queueFactory):返回一个任务队列,会根据传入的队列实例化工厂来生成对应的任务队列。
- SelectorTuple:私有的静态内部类,是对Selector的包装
- openSelector():返回SelectorTuple实例,先根据provider返回Selector对象,再根据是否配置了系统参数(io.netty.noKeySetOptimization,默认是false),如果配置了直接返回SelectorTuple。否则把SelectorImpl中的selectedKeys和publicSelectedKeys参数值设置成自定义的集合,这样的好处就是可以直接从自定义的集合中取数即可。
- selectorProvider():返回SelectorProvider实例,这是通过NioEventLoop的构成方法传递而来。
- newTaskQueue(int maxPendingTasks):返回任务队列
- newTaskQueue0(int maxPendingTasks):返回任务队列
- register(final SelectableChannel ch, final int interestOps, final NioTask<?> task):channel注册
- register0(SelectableChannel ch, int interestOps, NioTask<?> task):具体的注册实现
- getIoRatio():返回ioRatio属性值
- setIoRatio(int ioRatio):设置ioRatio属性值,值的大小只能是大于0小于等于100,默认是50
- rebuildSelector():重建Selector对象
- rebuildSelector0():具体的重建函数,思想就是先获取旧的Selector对象,然后调用openSelector()对象生成一个新的Selector实例,然后把旧的SelectionKey集合注册到新的Selector即可,然后关闭旧的Selector。
- run():这是NioEventLoop的重点方法,具体的业务逻辑实现就是在这个方法中进行
protected void run() {
int selectCnt = 0;//定义轮询次数
for (;;) {
try {
int strategy;//策略
try {
//定位calculateStrategy可知,如果当前没有任务返回的是SelectStrategy.SELECT策略,反正会直接调用selectNow方法,也就是底层方法。
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE://==-2
continue;
case SelectStrategy.BUSY_WAIT://==-3
case SelectStrategy.SELECT://==-1
//当前队列中没有任务,走这里
//返回下一个任务执行所需的截止时间,没有则返回-1
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);//设置唤醒时间
try {
if (!hasTasks()) {//如果当前还是没有任务
strategy = select(curDeadlineNanos);//执行具体的选择方法
//这个方法会根据当前的curDeadlineNanos时间去选择需要阻塞多久,也就是当前没有连接请求,会在这里阻塞curDeadlineNanos时间,刚好可以等待下一个任务执行完成
}
} finally {
nextWakeupNanos.lazySet(AWAKE);
}
default:
}
} catch (IOException e) {
rebuildSelector0();//如果有异常就重建Selector
selectCnt = 0;//归0
handleLoopException(e);
continue;
}
selectCnt++;//+1
cancelledKeys = 0;//无效的key数量
needsToSelectAgain = false;//是否需要重新选择
final int ioRatio = this.ioRatio;//io占比
boolean ranTasks;
if (ioRatio == 100) {//默认是50
try {
if (strategy > 0) {//大于0表示当前有连接需要处理
processSelectedKeys();//处理当前的所有连接
}
} finally {
ranTasks = runAllTasks();//处理完成后,执行任务队列中的任务
}
} else if (strategy > 0) {//大于0表示当前有连接需要处理
final long ioStartTime = System.nanoTime();//io开始时间
try {
processSelectedKeys();
} finally {
// io消耗时间,也就是处理连接花了多久.
final long ioTime = System.nanoTime() - ioStartTime;
//根据ioTime * (100 - ioRatio) / ioRatio公式计算出一个timeout时间
//如果设置为100那么传入的参数就是0,查看runAllTasks方法可知,每当
//任务执行了64个之后,就会判断当前总的执行时间是否超过了timeout时间,
//如果是,直接break返回,否则继续执行。也就是ioRatio越大,表示优先
//处理连接,越小表示优先处理任务
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
} else {
ranTasks = runAllTasks(0); // 这里会执行最小数量的任务(64)
}
if (ranTasks || strategy > 0) {//如果当前执行了任务,或者处理了连接
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
selectCnt = 0;//归0
} else if (unexpectedSelectorWakeup(selectCnt)) {
//表示当前既没有任务队列,也没有连接请求,此时有两种可能,一种是线程被中断了
//一种是CPU发生了空轮询
selectCnt = 0;
}
} catch (CancelledKeyException e) {
// Harmless exception - log anyway
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
selector, e);
}
} catch (Error e) {
throw (Error) e;
} catch (Throwable t) {
handleLoopException(t);
} finally {
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {//是否关闭
closeAll();//关闭
if (confirmShutdown()) {//再次确认
return;
}
}
} catch (Error e) {
throw (Error) e;
} catch (Throwable t) {
handleLoopException(t);
}
}
}
}
从run方法的源码来看,主要任务是执行任务队列中的任务,以及处理客户端的连接,然后根据ioRatio来配置,当前是需要更多的处理连接还是处理任务,以及根据当前的任务和连接处理情况判断是否需要重建selector。
-
unexpectedSelectorWakeup(int selectCnt):如果当前线程被中断返回true,否则根据selectCnt和系统配置的SELECTOR_AUTO_REBUILD_THRESHOLD比较,大于等于它则重建Selector对象,返回true,否则返回false。
-
handleLoopException(Throwable t):异常处理,日志输出
-
processSelectedKeys():根据是否配置了selectedKeys调用不同的而实现
-
cleanup():关闭Selector对象
-
cancel(SelectionKey key):取消当前的key
-
processSelectedKeysPlain(Set selectedKeys):处理连接
-
processSelectedKeysOptimized():处理连接,与上边方法不同的在于集合是通过反射设置到Selector中还是,直接通过Selector返回。
-
processSelectedKey(SelectionKey k, AbstractNioChannel ch):处理具体连接
-
processSelectedKey(SelectionKey k, NioTask task):处理具体的任务
-
closeAll():关闭当前所有的channel
-
invokeChannelUnregistered(NioTask task, SelectionKey k, Throwable cause):直接调用task的channelUnregistered方法
-
wakeup(boolean inEventLoop):调用selector的wakeup方法
-
beforeScheduledTaskSubmitted(long deadlineNanos):返回布尔值
-
afterScheduledTaskSubmitted(long deadlineNanos):返回布尔值
-
unwrappedSelector():返回包装的Selector
-
selectNow()调用底层的selectNow方法
-
select(long deadlineNanos):调用底层带有timeout参数的select方法
-
selectAgain():直接调用底层的selectNow()方法
以上这些方法都是在NioEventLoop类中实现的,我们接着看看其父类的方法。
SingleThreadEventLoop:
父类中有DEFAULT_MAX_PENDING_TASKS这么个属性,他的取值是根据系统参数配置来实现的(io.netty.eventLoop.maxPendingTasks)默认是Integer.MAX_VALUE,还有一个属性就是tailTasks队列,从构造方法来看,队列的大小就是DEFAULT_MAX_PENDING_TASKS。 -
parent():返回EventLoopGroup实例
-
next():返回EventLoop实例
-
register(Channel channel):channel注册
-
register(final ChannelPromise promise):具体的注册实现
-
register(final Channel channel, final ChannelPromise promise):方法的重载
-
executeAfterEventLoopIteration(Runnable task):添加任务到tailTask队列中
-
removeAfterEventLoopIterationTask(Runnable task):从tailTask队列中移除
-
afterRunningAllTasks():执行tailTask所有的任务
-
hasTasks():判断是否有任务可执行
-
pendingTasks():判断当前pending状态的任务数量
-
registeredChannels():返回-1
父类的SingleThreadEventLoop方法中重要的方法就是register方法的调用
继续看父类方法SingleThreadEventExecutor
SingleThreadEventExecutor
查看其源码可知,它定义了五种线程状态: -
ST_NOT_STARTED:线程未启动=1
-
ST_STARTED:线程启动=2
-
ST_SHUTTING_DOWN:线程关闭ing=3
-
ST_SHUTDOWN:线程关闭=4
-
ST_TERMINATED:线程终止=5
EventLoop自身没有继承Runnable接口,线程的实现方式是持有Thread属性,然后定义自身的run方法,然后通过启动Thread线程来调用自身的run方法。当前类中还有一个属性就是taskQueue(任务队列)。
SingleThreadEventExecutor的方法实现都很简单,就不进行一一介绍,大致就是往队列中添加任务、取出任务、执行任务。以及线程的启动、关闭等,其父类的方法也不再分析,方法都比较简单。
三、总结
EventLoop是由EventLoopGroup管理的一个线程,客户端的连接、数据的读写都是在EventLoop中去实现,从前几节的分析可知,netty启动时会传入EventLoopGroup参数,EventLoopGroup会根据系统的参数配置,生成相应个数的线程(也就是EventLoop实例),netty会根据具体算法取出线程,来执行对应的事件(比如channel注册、端口绑定、以及处理客户端连接、读写事件等)。
以上,有任何不对的地方,请指正,敬请谅解。