服务器启动流程
Nio是怎么启动的
//1 netty 中使用 NioEventLoopGroup (简称 nio boss 线程)来封装线程和 selector
Selector selector = Selector.open();
//2 创建 NioServerSocketChannel,同时会初始化它关联的 handler,以及为原生 ssc 存储 config,
//netty封装的ServerSocketChannel 叫做NioServerSocketChannel
NioServerSocketChannel attachment = new NioServerSocketChannel();
//3 创建 NioServerSocketChannel 时,创建了 java 原生的 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//为配合选择器使用,设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//4 启动 nio boss 线程执行接下来的操作
//5 注册(仅关联 selector 和 NioServerSocketChannel),未关注事件
//将 serverSocketChannel 和 serverSocketChannel 关联起来
//attachment:将netty中的NioServerSocketChannel 作为附件与原生的ssc(serverSocketChannel)关联起来
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);
//6 head -> 初始化器 -> ServerBootstrapAcceptor -> tail,初始化器是一次性的,只为添加 acceptor
//7 绑定端口
serverSocketChannel.bind(new InetSocketAddress(8080));
//8 触发 channel active 事件,在 head 中关注 op_accept 事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
其实就做了下面这五件事(五行代码 )
//打开selector
Selector selector = Selector.open();
//打开ssc
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//将channel绑定到selector上
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, nettySsc);
// 绑定端口
serverSocketChannel.bind(8080, backlog);
//通过selectionKey关注一个可连接事件
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
Netty将上面的五步进行处理
服务器代码
public class TestSourceServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler());
}
}).bind(8080);
}
}
因为NioEventLoopGroup
里面已经包含了selector,所以Selector selector = Selector.open();
这行代码便不需要去看了
可以理解为是在NioEventLoopGroup里面打开或创建了selector
将上面的代码debug启动
doBind
doBind这个方法会在主线程里面运行
initAndRegister这个方法:init也是在主线程里面执行,会在两个线程里面进行执行,
initAndRegister会返回一个Future对象
doBind0这个方法的会在nio线程执行
init
创建NioSocketchannel
进入newChannel方法
添加初始化handler
进入init方法
register(切换线程)
进入该方法
进入下面这个方法
在这个方法里面进行线程切换,
进入register0这个方法
进入doXXX这个方法,在这个方法里面进行注册(Nio线程注册)
返回上一级,在doxx下面有一行代码去进行调用之前的初始化handler方法
这个初始化的handler:向nio ssc加入了acceptor handler (在accept 事件发送后建立连接)
doBind0方法运行
再回到最初的位置,这个位置的set便是给之前的promise设置结果,如此便会去执行doBind0这个方法
进入里面的内容
一直进入,直到这个位置,才是真正的使用
再进入,在这里执行了dobind0方法,绑定了8080端口
关注accept事件
值执行完dobind0方法后,进行判断,如下
在这个handler上面进行关注accept事件
进入代码,直到这里才会去进行关注
NioEventLoop
NioEventLoop 既会处理IO事件 , 也会处理 普通任务 和 定时任务
NioEventLoop由 selector 和 一个线程 与 任务队列 组成
selector的位置:
线程位置:
任务队列位置:
selector创建时机
在构造方法调用时创建
两个selector的区别:有两个是为了在遍历selectedKeys提高性能(selector使用数组)
- unwrappedSelector:原始的selector(基于hash表的实现)
- selector:包装后的selector(使用数组)
selector启动
当首次调用 execute 方法时启动
下面这个真正启动的方法是只会执行一次,通过state 状态位控制线程只会启动一次
selector阻塞
当提交任务时,会就是selector阻塞,如下,去唤醒
wakeup方法
只有其它线程提交任务时,才会调用selector 的wakeup方法
wakenUp变量作用
如果有多个其它线程都来提交任务,为了避免wakeup被频繁调用,保证在多线程线程下只能有一个线程对该布尔变量设置成功
select 分支
Nio的线程会在一个run方法里面,进行死循环,死循环里面有select分支进行操作,
什么时候,进入SelectStrategy.SELECT分支去唤醒
- 当没有任务时,才会进入SelectStrategy.SELECT
- 当有任务时,会调用selectNow方法,同时拿到 io事件
select阻塞多久
通过上面的判断进入select方法后,会进入select方法
进入select方法
这个超时时间是如何计算的
所以阻塞的情况有三种:
- 阻塞1s多一点
- 有任务
- 有事件
nio空轮询bug
jdk在linux的selector才会出现这个空轮询bug
nio空轮询bug发生时机:当调用无参的selector事件便会阻塞,或者是在带超时时间的selector方法时超时时间未到时阻塞,当这个bug发生时,即使没有事件,即使没有超时,这个selecor方法仍然会继续运行,不会阻塞住,一直在空转(因为这是一个死循环)
解决方法:使用一个计数selectCnt
如果大于一个预值(默认是512)
旧的会将数据复制到新的selector里面
ioRatio
如果有任务或是有事件发生,程序便会往下运行,进入if-else语句块
ioRatio:控制处理io事件所占用时间的比例(默认是50%:50%处理IO事件,50%处理普通任务)
- ioTime:代表执行io事件处理耗费的时间,
- runAllTasks:运行普通任务(这里面对之前的数进行运算,获取运行普通任务的时间是多长)
ioRatio设置为100的作用:进入if分支,将所有的IO事件处理完毕,再进入finally,处理完所有的普通任务(没有设置超时)
selectedKeys的优化
以前selectedKeys默认的是set集合,遍历的性能较低
netty尝试使用数组的形式进行替换,这样在遍历的时候,效率更加高
体现在之前处理selectedKeys时
进入处理的能够方法
默认进入优化后的
对事件进行处理,判断是哪一种事件,分别进行读写处理
netty的accept流程
Nio的
1)selector.select0 阻塞直到事件发生
2)遍历处理selectedKeys
3)拿到个key,判断事件类型是否为accept
4)创建SocketChannel,设置非阻塞
5)将SocketChannel注册至selector
6)关注selectionKey的read事件
netty的
前三步(123)在netty的EventLoop里面已经做了
后面三件事将会在下面这个方法里面做完
处理第四步的操作,在这里创建了NioSocketChannel
处理第五步操作
调用之前设置的一些初始化器
进行第六步:
启动流程和accept流程差不多
read流程
1)selector.select0 阻塞直到事件发生
2)遍历处理selectedKeys
3)拿到一个key, 判断事件类型是否为read
4)读取操作