尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6160194.html 

  

  本文我们将先从NioEventLoop开始来学习服务端的处理流程。话不多说,开始学习~~~~

  我们从上文中已经知道server在启动的时候会开启两个线程:bossGroup和workerGroup,这两个线程分别是boss线程池(用于接收client请求)和worker线程池(用于处理具体的读写操作),这两个线程调度器都是NioEventLoopGroup,bossGroup有一个NioEventLoop,而worker线程池有n*cup数量个NioEventLoop。那么我们看看在NioEventLoop中的是如何开始的:

  NioEventLoop本质上是一个线程调度器(继承自ScheduledExecutorService),当bind之后就开始run起一个线程:  

 (代码一)
1 @Override
protected void run() {
for (;;) {
boolean oldWakenUp = wakenUp.getAndSet(false);
try {
if (hasTasks()) {
selectNow();
} else {
select(oldWakenUp); if (wakenUp.get()) {
selector.wakeup();
}
} cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
processSelectedKeys();
runAllTasks();
} else {
final long ioStartTime = System.nanoTime(); processSelectedKeys(); final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
} if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
break;
}
}
} catch (Throwable t) {
...
}
}
}

  这个for(;;)里面就是boss线程的核心处理流程:

  【代码一主线】1,不断地监听selector拿到socket句柄然后创建channel。每次run的时候先拿到wakeup的值,并且set进去false(PS:wakeup是什么鬼?一个AtomicBoolean,代表是否用户唤醒,如果不人为将其set成true,永远是false)。

   【代码一主线】2,如果任务队列中已有任务,那么selectNow(),(PS:selectNow是什么鬼?我们知道selector.select()是一个阻塞调用,而selectNow方法是个非阻塞方法,如果没有到达的socket句柄则返回0),因此若队列中已有任务的话应该立即开始执行,而不能阻塞到selector.select()上,否则则调用select()方法,继续看select()里面:

 (代码二)
1 private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
} int selectedKeys = selector.select(timeoutMillis);
selectCnt ++; if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
// 如果selectedKeys不为空、或者被用户唤醒、或者队列中有待处理任务、或者调度器中有任务,则break
break;
}
if (Thread.interrupted()) {
//如果线程被中断则重置selectedKeys,同时break出本次循环,所以不会陷入一个繁忙的循环。
selectCnt = 1;
break;
} long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// selector超时
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// selector多次过早返回,重新建立并打开Selector
...
} currentTimeNanos = time;
}
...
} catch (CancelledKeyException e) {
...
}
}

  我们看到,select()方法进入一个for循环去select阻塞等待socket(这里的selector的实现在是根据操作系统和netty的版本来定的,在最新的netty中是使用的linux的epoll模型),同时入参里有“超时时间”,如果超过了这个时间仍然没有socket到来则重新将selectCnt置为1重新循环等待,直到有socket到来。如果selectedKeys不为空、或者被用户唤醒、或者队列中有待处理任务、或者调度器中有任务,那么就是说该eventLoop有活干了,先break出去去干活,完了再打开selector重新阻塞等待。正常情况下会等待到一个socket,break出去之后回到代码一

  【代码一主线】3,根据ioRatio来选择任务执行策略(PS:ioRatio是什么鬼?看了下用途应该是这样的,这个ioRatio代表该eventLoop期望在I/O操作上花费时间的比例)。而NioEventLoop中有两类操作,一类是I/O操作(读写之类),调用processSelectedKeys;一类是非I/O操作(例如register等),调用runAllTasks。如果ioRatio是100的话那么会按照顺序执行I/O操作->非I/O操作;如果不是会按照这个比例算出一个超时时间,在run任务队列的时候如果超过了这个时间会立即返回,确保I/O操作可以得到及时的调用。

  我们关心的是I/O操作,那么进入processSelectedKeys()看下发生了什么吧。

  

(代码三)
1 private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized(selectedKeys.flip());
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}

  正常情况下会走到processSelectedKeysOptimized中:

  

 (代码四)
1   private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
for (int i = 0;; i ++) {
final SelectionKey k = selectedKeys[i];
if (k == null) {
break;
}
selectedKeys[i] = null; final Object a = k.attachment(); if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
} if (needsToSelectAgain) {
for (;;) {
if (selectedKeys[i] == null) {
break;
}
selectedKeys[i] = null;
i++;
} selectAgain();
selectedKeys = this.selectedKeys.flip();
i = -1;
}
}
}

  

  遍历拿到所有的SelectionKey,然后判断每个SelectionKey的attachment,上篇文章中已经分析过给ServerBootstrap注册的Channel是NioServerSocketChannel(继承自AbstractNioChannel),因此进入processSelectedKey中:

 (代码五)
1   private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
unsafe.close(unsafe.voidPromise());
return;
} try {
int readyOps = k.readyOps();
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
if (!ch.isOpen()) {
return;
}
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops); unsafe.finishConnect();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}

  

  在这里根据传入的SelectionKey的已就绪操作类型来决定下一步的操作,如果是一个读操作,那么进入AbstractNioMessageChannel$NioMessageUnsafe的read实现,这里代码很多,我们只贴一下核心的代码:

(代码六)
1 @Override
public void read() {
...
final ChannelPipeline pipeline = pipeline();
...
try {
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
pipeline.fireChannelRead(readBuf.get(i));
}
...
readBuf.clear();
pipeline.fireChannelReadComplete();
} finally {
}
}

  核心就是这个pipeline.fireChannelRead(readBuf.get(i));,这已经到了pipeline阶段,可能有些人会误以为这是不是已经到了worker线程中,但是不可能啊,我们的代码其实在处于processSelectedKeys的逻辑里面。实际上,不论是boss还是worker,他们都是NioEventLoopGroup,玩法都是一样的,只不过职责不一样而已。boss也有自己的handler,上篇文章中我们提到了netty中的reactor模式的玩法,从Doug Lea的图中可以看出,boss(实际上就是mainReactor)的handler其实就是这个acceptor。

  在此我们顺便学习一下netty中的handler:

  

  从用途上来说,handler分为ChannelInboundHandler(读)和ChannelOutboundHandler(写),增加一层适配器产生了两handler的Adapter,我们使用到的类都是继承自这两个Adapter。我们经常用到的SimpleChannelInboundHandler就继承ChannelInboundHandlerAdapter,用于初始化用户handler链的ChannelInitializer和boss线程绑定的ServerBootstrapAcceptor也都继承于此。

  回到【代码六主线】我们从pipeline.fireChannelRead继续追踪下去会追到ChannelInboundHandler的channelRead的实现,而这里的Hander就是ServerBootstrapAcceptor。

 (代码七)
1 @Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
final Channel child = (Channel) msg; child.pipeline().addLast(childHandler); for (Entry<ChannelOption<?>, Object> e: childOptions) {
try {
if (!child.config().setOption((ChannelOption<Object>) e.getKey(), e.getValue())) {
}
} catch (Throwable t) {
}
} for (Entry<AttributeKey<?>, Object> e: childAttrs) {
child.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
} 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);
}
}

  由于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;
}

  我自己的理解:

  childGroup就是subReactor(也就是worker线程);childHandler就是xxx;childOptions和childAttrs是为channel准备的一些参数。

  回到【代码七主线】在这里做了3件事:

  1.为客户端channel的pipeline中添加childHandler,那么这个childHandler是什么鬼呢?回忆一下上文中的服务端启动代码,有bootStrap.childHandler(xxx)这样的代码,所以此处就是把在服务端启动时我们定义好的Handler链绑定给每个channel。

  2.把我们服务端初始化时的参数绑定到每个channel中。

  3.childGroup.register(child).addListener(new ChannelFutureListener()),后面这个异步listener作用很明确,问题是这个childGroup是什么鬼?我理解应该就是worker线程了。详细说一下childGroup.register(child),继续跟下去,跟到AbstractChannel$AbstractUnsafe中

(代码八)
1 @Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
...
AbstractChannel.this.eventLoop = eventLoop; if (eventLoop.inEventLoop()) {
register0(promise);
} else {
...
} catch (Throwable t) {
}
}
}

  继续register0:

(代码九)
1 private void register0(ChannelPromise promise) {
try {
if (!promise.setUncancellable() || !ensureOpen(promise)) {
return;
}
boolean firstRegistration = neverRegistered;
doRegister();
neverRegistered = false;
registered = true;
safeSetSuccess(promise);
pipeline.fireChannelRegistered();
if (firstRegistration && isActive()) {
pipeline.fireChannelActive();
}
} catch (Throwable t) {
}
}

  这里核心有两步:

  1.doRegister(),其实我们在上篇文章中分析过,就是将channel绑定到selector上。此处有点懵逼,我猜测是绑定到worker线程的selector中,如果有大神知道请留言我的微博。

  2.pipeline.fireChannelRegistered(),继续往下跟跟进到ChannelInboundHandler的channelRegistered方法中,而此时会调用我们定义的ChannelInitializer,将我们定义的handler注册到pipeline中。

  至此【代码一主线】执行完毕,我们浏览了一遍boss线程的在接收socket请求期间的处理流程,过程中是结合reactor模式去理解的,有些地方自己也有点不懂,还请各位指正。

  总结一下:

  1.boss线程就是个loop循环,打开selector -> 获得监听到的SelectionKey -> 处理I/O请求 -> 处理非I/O请求,而我们最关心的就是处理I/O请求(在processSelectedKeys()方法中完成)。

  2.遍历准备就绪的SelectionKey,根据其可操作类型(read or write。。)来决定下一步的具体操作,我们着重去了解了read逻辑。

  3.NioServerSocketChannel调用父类AbstractNioMessageChannel的unsafe类NioMessageUnsafe来处理读取逻辑:调用pipeline处理readbuf。

  4.pipeline.fireChannelRead会调用ServerBootstrapAcceptor的channelRead:初始化客户端channel参数,将该channel绑定到worker线程的selector中,为channel注册用户定义的handler链。

  再精炼一点:

  boss线程只是接收客户端socket并初始化客户端channle,将channel丢给acceptor,acceptor会将这个channel注册到worker线程中。整个loop过程都是一个非阻塞过程(全部异步化),同时boss中不会做耗时的I/O读取,只是将channel丢给worker。因此是一个高效的loop过程。

  

  下文中我们将分析worker线程的处理流程,敬请期待。。。

  

  

(三)Netty源码学习笔记之boss线程处理流程的更多相关文章

  1. (一)Netty源码学习笔记之概念解读

    尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6121065.html  博主最近在做网络相关的项目,因此有契机学习netty,先 ...

  2. (二)Netty源码学习笔记之服务端启动

    尊重原创,转载注明出处,原文地址:http://www.cnblogs.com/cishengchongyan/p/6129971.html  本文将不会对netty中每个点分类讲解,而是一个服务端启 ...

  3. Hadoop源码学习笔记之NameNode启动场景流程三:FSNamesystem初始化源码剖析

    上篇内容分析了http server的启动代码,这篇文章继续从initialize()方法中按执行顺序进行分析.内容还是分为三大块: 一.源码调用关系分析 二.伪代码执行流程 三.代码图解 一.源码调 ...

  4. Hadoop源码学习笔记之NameNode启动场景流程五:磁盘空间检查及安全模式检查

    本篇内容关注NameNode启动之前,active状态和standby状态的一些后台服务及准备工作,即源码里的CommonServices.主要包括磁盘空间检查. 可用资源检查.安全模式等.依然分为三 ...

  5. Hadoop源码学习笔记之NameNode启动场景流程四:rpc server初始化及启动

    老规矩,还是分三步走,分别为源码调用分析.伪代码核心梳理.调用关系图解. 一.源码调用分析 根据上篇的梳理,直接从initialize()方法着手.源码如下,部分代码的功能以及说明,已经在注释阐述了. ...

  6. Hadoop源码学习笔记之NameNode启动场景流程二:http server启动源码剖析

    NameNodeHttpServer启动源码剖析,这一部分主要按以下步骤进行: 一.源码调用分析 二.伪代码调用流程梳理 三.http server服务流程图解 第一步,源码调用分析 前一篇文章已经锁 ...

  7. Netty源码学习笔记

    1.ByteBuf

  8. Adnroid 源码学习笔记:Handler 线程间通讯

    常见的使用Handler线程间通讯: 主线程: Handler handler = new Handler() { @Override public void handleMessage(Messag ...

  9. async-validator 源码学习笔记(三):rule

    系列文章: 1.async-validator 源码学习(一):文档翻译 2.async-validator 源码学习笔记(二):目录结构 rule 主要实现的是校验规则,文件结构为下图: 一.rul ...

随机推荐

  1. ES6深入学习记录(二)promise对象相关

    1.Promise的含义 Promise是异步编程的一种解决方案,比传统的解决方案--回调函数和事件更合理和强大.ES6将其写进了语言标准,统一了用法,原生提供了promise对象. 所谓Promis ...

  2. CSS选 择器 三种样式

    一.CSS三种样式 代码 宽度 高度 实线 颜色  A内联样式是优先级最高的方式 px必须写 A:内联式  弊端:代码多很乱 <body> <div class="divc ...

  3. [bigdata] flume file channel CPU消耗比 memory channel高的原因

    https://www.quora.com/Why-does-flume-take-more-resource-CPU-when-file-channel-is-used-compared-to-wh ...

  4. bzoj 2141: 排队

    2141: 排队 Time Limit: 4 Sec Memory Limit: 259 MB Description 排排坐,吃果果,生果甜嗦嗦,大家笑呵呵.你一个,我一个,大的分给你,小的留给我, ...

  5. 2 云计算系列之KVM的安装与使用

    preface 在上篇博客中,我们讲了云的概念,分类,以及虚拟化技术.我们知道Openstack的虚拟化技术是基于KVM的,所以下面就开始说说如何部署和使用KVM. 下面的讲解包含以下知识点: 安装K ...

  6. easyUI combobox 控件 使用

    1,combobox 二级联动 (参数传递 函数调用) // toolbar 学校班级二级联动 var paramSearch = { school : $('#search-school'), cl ...

  7. c#.net单例模式的学习记录!

    一. 单例(Singleton)模式 单例模式的特点: 单例类只能有一个实例. 单例类必须自己创建自己的唯一实例. 单例类必须给所有其它对象提供这一实例. 单例模式应用: 每台计算机可以有若干个打印机 ...

  8. Asp.net C# 把 Datatable转换成JSON 字符串

    First of all, we have to fetch the records from the database (MS Sqlserver) into the C# DataTable, o ...

  9. QQ远程桌面的使用

    腾讯QQ怎样使用远程桌面: ---------------------- ----------------------

  10. 【转】TCP/IP协议栈及OSI参考模型详解

    OSI参考模型 OSI RM:开放系统互连参考模型(open systeminterconnection reference model) OSI参考模型具有以下优点: 简化了相关的网络操作: 提供设 ...