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
交给自己ServerBootstrap
channel
方法,通过AbstractBootstrap
的ReflectiveChannelFactory
的channelFactory
反射创建类NioServerSocketChannel
option
方法将参数放在Map<ChannelOption<?>, Object>
中管理handler
方法,AbstractBootstrap
绑定到bossgroup
,与ServerSocketChannel
相关childHandler
,ServerBootstrap
绑定到workergroup
,与SocketChannel
相关
bind
bind
方法中进行通道绑定doBind
中调用initAndRegister
创建NioServerSocketChannel
newChannel()
创建了唯一的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
时,会先生成context
addLast
最终执行,插在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的handler
tail
是inbound
出入站调用关系
- 入站操作时,通过调用
fireChannelRead
,将数据传给下一个handler
findContextInbound
通过while
循环找到一个inbound的context
- 由
ctx.write
调用出站时,会直接找当前handler
的前一个outbound
hanlder 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)
由子类实现 AllIdleTimeoutTask
ReaderIdleTimeoutTask
WriterIdleTimeoutTask
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
startThread
doStartThread
启动线程,并进入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秒
processSelectedKeys
processSelectedKeysOptimized
遍历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