NIO和Netty全文
Netty源码
io.netty.example下有案例
启动过程
- 分析
echo案例 SSL配置
group
- 创建
两个group对象,并进行配置 - 调用
group初始构造,会传递0参 - 默认为
设备核数*2 group下的EventExecutor数组,跟其中的NioEventLoop元素,都有一个父接口EventExecutor
EventLoop
- 最终生成
group中的NioEventLoop的函数解析
MultithreadEventExecutorGroup(int nThreads, //线程数
Executor executor, //如果为null,默认为ThreadPerTaskExecutor
EventExecutorChooserFactory chooserFactory, //单例DefaultEventExecutorChooserFactory
Object... args //创建执行器的时候传入的参数
- 生成
executor - 为
children数组,创建NioEventLoop - 如果创建失败,有异常,要全部关闭
- 为每一个
NioEventLoop增加一个管理监听器 - 所有单例线程放到
Set中
bootstrap
group方法,将bossgroup交给父类AbstractBootstrap,将workergroup交给自己ServerBootstrapchannel方法,通过AbstractBootstrap的ReflectiveChannelFactory的channelFactory反射创建类NioServerSocketChanneloption方法将参数放在Map<ChannelOption<?>, Object>中管理handler方法,AbstractBootstrap绑定到bossgroup,与ServerSocketChannel相关childHandler,ServerBootstrap绑定到workergroup,与SocketChannel相关
bind
bind方法中进行通道绑定doBind中调用initAndRegister创建NioServerSocketChannelnewChannel()创建了唯一的ChannelID,NioMessageUnsafe操作消息,DefaultChannelPipeline的pipeline,NioServerSocketChannelConfig配置对象init(chanel)同步处理options,初始化channelPipepline,添加ChannelIntiailizer,主要功能和pipeline进行关联- 绑定端口调用
DefaultChannelPipeline中的unsafe.bind(),调用AbstractBootstrap的bind方法,然后调用NioServerSocketChannel的doBind,再调用ServerSocketChannel的方法,最后调用Net的本地方法bind - 绑定完端口后,执行
safeSetSuccess告诉promise任务完成 - 最后进入
NioEventLoop的run方法,进行事件监听 select事件 ->process处理IO事件 ->runAllTasks处理其他事件
pipeline
addLast时,会先生成contextaddLast最终执行,插在tail前面
请求过程
- 接受连接
- 创建
NioSocketChannel - 注册到
workerGroup - 注册
select事件
客户端请求
- 当有客户端请求时,服务端
select该事件,processSelectedKey处理该事件,并检测出标识位为16,accept事件 AbstractNioMessageChannel类的unsafe调用read方法,并断言是否为当前线程- 调用
doReadMessages,通过NioServerSocketChannel,使用SocketUtils获取请求,并包装成NioSocketChannel放到ArrsyList中 pipeline.fireChannelRead,开始依次调用pipeline中的handler,去处理- 从
head开始调用 - 会反复执行
invokeChannelRead - 进入
ServerBootstrap的内部类ServerBootstrapAcceptor,并将该请求的channel注册到workergroup
register
register轮询使用线程,其中的next()方法,使用&的方式,决定使用一个线程- 包装成
DefaultChannelPromise - 调用
AbstractChannel中的register0 - 调用
AbstractChannel中的doRegister,实际上由AbstractNioChannel实现 AbstractNioChannel调用doRegister完成最终的注册- 在第一次注册时,会调用
pipeline.invokeHandlerAddedIfNeeded();,通过自定义的ChannelInitializer将handler添加 - 注册完成后,进行该连接对应的
pipeline的注册,及handler的注册 AbstractNioChannel中doBeginRead,为新连接的通道注册感兴趣的事
pipeline源码
- 当
ServerSocket创建一个新的连接,就会创建一个Socket,对应目标客户端 - 当创建一个
Socket就会分配一个全新的ChannelPipeline - 每一个
ChannelPipeline都是双向链表,内部包含多个ChannelHandlerContext - 每个
ChannelHandlerContext,包装一个ChannelHandler - 当一个请求进来时,进入
Socket对应的pipeline,经过所有的handler,也就是责任链/过滤器模式
ChannelPipeline
- 继承关系,接口多继承
- 可以对链表进行操作,
addLast/addFirst/addBefore...,遍历链表继承了接口Iterable` - 可以调用
数据出入站的相关方法,继承了ChannelInboundInvoker/ChannelOutboundInvoker head是outbound和inbound,但是inbound设置为false,只为了能够接受入站信息,然后传给下一个inbound的handlertail是inbound
出入站调用关系
- 入站操作时,通过调用
fireChannelRead,将数据传给下一个handler findContextInbound通过while循环找到一个inbound的context- 由
ctx.write调用出站时,会直接找当前handler的前一个outboundhanlder findContextOutbound,从tail往前找一个outbound的context
ChannelHandlerContext
- 继承关系
ChannelInboundInvoker/ChannelOutboundInvoker针对出入站的过程,在出入站handler在包装一层,在触发handler前后做一些特定操作ChannelHandlerContext增加了能够获取上下文环境中的channel/executor/handler/pipeline...
ChannleHandler
handlerAdded,被添加是调用;handlerRemoved,被移除时调用,而在连接注册成功之后,才会真正的为这个生成pipeline-handler的双向链表,handler才会被添加ChannelDuplexHandler处理入站/出站IO事件
生成过程
- 在反射创建
Channel,会调用AbstractChannel的构造函数,创建pipeline - 创建
pipeline - 生成
DefaultChannelPipeline,生成链表头尾;同时为当前pipeline绑定channel;生成future和promise用于异步回调使用 - 在完成
channel注册之后,会为pipeline进行注册 - 在
addLast的过程中,会为handler进行包装,包装成HandlerContext,为了防止多线程导致安全问题,需要同步执行;如果没有被注册,会通过CAS的方式进行安全注册
调度流程
- 入站从
head开始 - 执行
read,最终需要inbound类的handler调用处理 - 出站从
tail开始调用,从内部像外面写
read流程
- 流程图
fireChannelRead,从head开始invokeChannelRead- 调用真正的
handler执行read动作要做的事 - 通过前一个
handler执行ctx.fireChannelRead(msg);,找到下一个inboundHandler,继续执行invokeChannelRead
心跳包源码
- 三种
handler能够检测连接的有效性
IdleStateHandler当连接空闲时间太长,触发一个IdleStateEvent,可以通过重写userEventTrigged进行业务处理ReadTimeoutHandler,继承IdleStateHandler,在指定的时间没有发生读事件,抛出异常,并自动关闭这个链接WriteTimeoutHandler,当调用write时,创建一个定时任务,根据传入的promise的完成情况,判断是否超出写时间。当一个写操作不能再一定的时间内完成,抛出异常,并自动关闭这个链接
IdleStateHandler四个主要属性
private final boolean observeOutput; //是否考虑出站慢的情况,默认为false;网络不稳定,传输慢
private final long readerIdleTimeNanos; //读事件空闲时间,0禁用
private final long writerIdleTimeNanos; //写事件空闲时间
private final long allIdleTimeNanos; //读或写事件空闲时间
ticksInNanos返回当前纳秒时间,十亿分之一秒initialize方法,将state状态设置为1,防止重复初始化,initOutputChanged出入站数据监控- 当
IdleStateHandler被添加时,会调用initialize方法,只要参数大于0,就为其创建定时任务 schedule定时任务,添加到当前任务队列
定时任务类
- 共同父类
AbstractIdleTask,通道关闭直接返回,run(ctx)由子类实现 AllIdleTimeoutTaskReaderIdleTimeoutTaskWriterIdleTimeoutTask
ReaderIdleTimeoutTask
- 读时间时,记录第一次发生事件的标识位,后面判断
是否是某次读事件后的第一次空闲;读完成后,reading设置为false,并记录最近的read时间 - 当定时任务触发时,如果读完成后,
!reading检查有效,会检查上一次read完成的时间,与现在的差值,与设置的空闲时间的差值,如果差值为负数,则重设定时任务,并抛出读空闲事件 - 否则,仅仅重新设置
channelIdle抛出事件,触发用户自定义的userEventTriggered
WriterIdleTimeoutTask
lastWriteTime通过监听器,监听写事件- 逻辑大致相同,但是需要处理缓慢出站的情况
hasOutputChanged,检查缓冲区,如果是某次出站事件后的第一次空间则必须触发,如果出站缓慢有可能造成OOM- 通过缓冲区的情况进行检查
AllIdleTimeoutTask
- 计算离读写两个发生的最近的时间
EventLoop
- 第一次添加时,启动
run方法 - 继承关系,单线程的
ScheduledExecutorService定时任务接口EventLoop接口,一旦channel被注册,就处理其IO操作SingleThreadEventExecutor单个线程线程池
启动EventLoop
- 对于
ServerSocketChannel来说,第一次注册过程时,会调用execute的方法,创建新的EventLoop,并启动起run方法 - 调用父类
SingleThreadEventExecutor中的execute startThreaddoStartThread启动线程,并进入run方法run方法,select事件 ->process处理IO事件 ->runAllTasks处理其他事件execute中,如果线程停止,并且删除任务失败,则执行拒绝策略;如果线程休眠或者任务不是NonWakeupRunnable,则唤醒线程addTask,如果线程关闭,拒绝,否则,添加到队列中
三个主要过程
run中的select- 默认阻塞一秒,如果有定时任务,则在定时任务剩余时间的基础上+0.5秒
//满足以下条件直接break
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
// - Selected something, //有事件
// - waken up by user, or //被用户唤醒
// - the task queue has a pending task. //任务队列有任务
// - a scheduled task is ready for processing //有定时任务
break;
}
- 定时任务添加0.5秒
processSelectedKeysprocessSelectedKeysOptimized遍历selectedKeys.keys,处理完后置空processSelectedKey(SelectionKey k, AbstractNioChannel ch)根据时间类型判断- 根据
ioRation执行runAllTasks,如果任务队列中的任务比较多,ioRation调小,就能多执行任务队列中的任务
异步任务源码
- 通过
eventLoop提交的异步任务,实际上是同一个线程排队执行
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("hello client", StandardCharsets.UTF_8));
} catch (Exception e) {
e.printStackTrace();
}
}
});
handler提交
- 异步线程池
//异步线程池,可以将任务提交到该线程
final static EventExecutorGroup group = new DefaultEventExecutorGroup(16);
- 提交
group.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//接受客户端信息
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
Thread.sleep(10000);
System.out.println(Thread.currentThread().getName());
System.out.println(Thread.currentThread().getId());
System.out.println(new String(bytes, CharsetUtil.UTF_8));
ctx.writeAndFlush(Unpooled.copiedBuffer("hello client", StandardCharsets.UTF_8));
return null;
}
});
submit添加任务,最终也会进入addTask的逻辑中AbstractChannelHandlerContext的write中,当执行下一个outbound的线程不是当前线程时,会将该任务封装成task,放入IO线程队列中,等待IO任务执行完毕后,执行该任务
context添加
- 线程池,轮询使用,
handler正常方式写业务
static final EventExecutorGroup group = new DefaultEventExecutorGroup(2);
复制代码
- 在
handler添加时,将handler添加到线程池中,不添加时优先使用IO线程
p.addLast(group, new EchoServerHandler());
- 将
handler添加到group中之后,后续出入站方法都会在另一个线程异步执行,最终handler执行的是当前group线程中的task