一、类介绍
- NioEventLoopGroup:用于创建Nio socket程序的事件轮询组,每个事件轮询都会一个Selector对象
- ServerBootstrap:构建服务通道,设置tcp参数,绑定端口的辅助类
- ServerBootstrapAcceptor:服务通道使用,用于接收请求,轮询EventLoop,注册通道到对应的selector上,并设置通道参数,如果有ChannelInitializer的话还会设置客户端通道处理器
- NioServerSocketChannel:netty封装的服务socket通道,用于接收请求
- ChannelPromise:继承了JDK的Future,当然netty自己也有一个future,增强了一些功能,比如增加了Future监听器
- NioSocketChannel:netty封装客户端通道
- ChannelPipeline:通道排管,当发生通道的读写等事件时,像流水一样从管道中流过,管道中可以设置管道阀(通道处理器)做些过滤操作
- ChannelHandlerContext:封装ChannelHandler,是责任链条上的一个个链子(也可比喻为管道中管道阀),可以携带额外的属性,SkipFlag等信息,在创建它的时候,会解析ChannelHandler方法上的@Skip注解
- ChannelHandler:如果把ChannelPipeline比喻成排管,那么ChannelHandlerContext就是排管上管道阀,而ChannelHandler就是管道阀中起过滤作用的过滤网
- NioByteUnsafe:用于处理客户端读写事件
二、大致流程
2.1 服务端流程
创建Boss事件轮询组,通过ServerBootstrap辅助类创建NioServerSocketChannel通道,并为其设置tcp参数,通道处理等
-》初始化NioServerSocketChannel后,再进行注册,从boss事件轮询组中轮询一个事件轮询对象(EventLoop),将当前通道注册到EventLoop持有的Selector对象中,注册成功后触发通道的注册事件,此时会调用通道处理链
-》绑定端口,绑定成功后触发通道激活事件
netty的注册和绑定都是通过线程池来处理的,注册和绑定都是包装成一个任务设置到NioEventLoop的任务队列中,少部分代码如下:
private void io.netty.channel.nio.NioEventLoop#select() throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
//获取当前纳秒时间
long currentTimeNanos = System.nanoTime();
//从延时队列中获取调度任务,计算时间差,还差多少时间执行
//如果任务有延时任务到了时间,那么selectDeadLineNanos <= currentTimeNanos,delayNanos(currentTimeNanos)计算的值为零或负值。
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
//计算超时时间,用延时差值加0.5ms(四舍五入),然后取整,换算成ms为单位的数值
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
//延时时间小于0,该跳出循环去尝试处理任务了
if (timeoutMillis <= 0) {
//selectCnt表示select了多少次
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
//跳出循环
break;
}
//等待任务调度延时时间
int selectedKeys = selector.select(timeoutMillis);
//自增
selectCnt ++;
//发现感兴趣的事件或者外部线程添加了任务,或者有线程要求wakenup,跳出循环
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks()) {
// Selected something,
// waken up by user, or
// the task queue has a pending task.
break;
}
//selector重新创建的阀值,默认是512,如果大于零,并且选择的次数已经大于重建阀值
//看了下netty4的实现,在netty4中,这里应该还有一段代码逻辑,那段代码逻辑是用于判断select是否有等待 timeoutMillis ,如果等待了selectCnt置为1
//那么可能发生了selector死循环
if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// The selector returned prematurely many times in a row.
// Rebuild the selector to work around the problem.
logger.warn(
"Selector.select() returned prematurely {} times in a row; rebuilding selector.",
selectCnt);
//重建Selector
rebuildSelector();
selector = this.selector;
// Select again to populate selectedKeys.
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = System.nanoTime();
}
//select超过MIN_PREMATURE_SELECTOR_RETURNS(默认三次),打印消息提示,通常是因为延时队列任务延时太久导致的
//一般在1000.5ms之后便会跳出循环
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row.", selectCnt - 1);
}
}
} catch (CancelledKeyException e) {
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector - JDK bug?", e);
}
// Harmless exception - log anyway
}
}
在没有设置延时任务的情况下,select的timeout时间为1000.5ms,每次select都会计数,如果进入select循环的时间与当前时间的差值为负值时(或者任务队列中设置添加了任务,或者被要求wakeup),会跳出循环,除此之外
netty中设置了一个轮询阈值,如果出现jdk的selector bug,它的循环次数达到了512(默认值,可以通过系统参数配置),就重新创建selector
netty在NioEventLoop#processSelectedKey中处理SelectionKey.OP_ACCEPT事件
对应的unsafe实现为NioMessageUnsafe,创建NioSocketChannel,从工作事件轮询组中轮询一个轮询器,将NioSocketChannel注册到它的Selector中,然后触发通道read事件(通常我们没有给父通道设置通道处理器,只有netty自己设置的一个叫ServerBootstrapAcceptor的通道处理器,这个通道处理器在接收客户端请求后触发子通道的注册事件,通常是ChannelInitializer,用于添加其他的子通道处理器),处理完当前通道能读取到的数据后,再触发readComplete事件,如果发生错误,将触发通道的异常处理事件
通道处理会被包装成ChannelHandlerContext,多个之间通过责任链的模式进行调用,通道处理上的每个方法都可以写上@Skip注释,表示跳过某个方法,不进行处理。
优雅关闭:调用每一个事件轮询的关闭方法,关闭通道,取消延时任务队列中还未处理的任务,尽量处理还可以select到的事件,尽量将任务队列的任务处理掉,关闭线程,如果关闭超时将强行退出。
三、池化
PoolChunk:使用平衡二叉树,不包括起始点,默认11层,通过伙伴内存分配法,将内存在相同层数上进行平均分配
一直分配到第11层,每个节点大小为8k,当用户申请大于8k的内存时从这颗树上进行分配,如果用户输入的内存大小不是2的倍数的,那么会自动调整为2的倍数去分配,被占用的节点被标记为12(第12层,一个不存在的层)
定位层数,通过计算公式:maxOrder - (log2(用户输入值调整后的值) - 13),这里的maxOrder是11,所以netty将被分配的节点设置为12。12是不可能被访问到的
PoolSubpage:这个类被划分成了两个类型,一个tiny,另一个是small
- tiny表示申请小于512的内存时使用
从图中可以看到,这个tiny是一个32个元素PoolSubpage数组,从1开始,相邻元素之间以16字节递增。
每个内存页的大小为8k,而下标为1的内存页被划分为16字节的内存块,那么下标为1的元素为一个可以分配8192 / 16 = 512 个16字节的PoolSubpage元素,分配完之后,PoolSubpage会从链表中移除,如果还需要分配新的16字节的空间,那么会从PoolChunk中再划分一个8k的空间构建成PoolSubpage进行初始化加入到链表中,释放时,会放入ThreadLOCALcahe
- small表示申请大于512小于8k的内存时使用
从图中可以看到这个数组长度为4,从512字节开始,后面元素的子页大小都是 512 * 2^(下标)字节