文章目录
【关于作者】
关于作者,目前在蚂蚁金服搬砖任职,在支付宝营销投放领域工作了多年,目前在专注于内存数据库相关的应用学习,如果你有任何技术交流或大厂内推及面试咨询,都可以从我的个人博客(https://0522-isniceday.top/)联系我
1.三种经典IO
- 阻塞同步IO(BIO)
- 非阻塞同步IO(NIO)
- 非阻塞异步IO(AIO)
详细可看:https://0522-isniceday.top/archives/ji-suan-ji-wang-luo–i-o-mo-xing#more
2.Netty对三种IO的支持
相关api如下:
3.Netty为什么仅仅支持NIO
-
为什么不建议(deprecate)阻塞 I/O(BIO/OIO)?
连接数高的情况下:阻塞 -> 耗资源、效率低
-
为什么删掉已经做好的 AIO 支持?
- Windows 实现成熟,但是很少用来做服务器
- Linux 常用来做服务器,但是 AIO 实现不够成熟
- Linux 下 AIO 相比较 NIO 的性能提升不明显
4.三种IO分别采取的开发模式
其中NIO采取的就是我们下文所要详细讲述的Reactor模式
Thread-Per-Connection相当于就是为每一个连接分配一个线程去处理,但是这里会受到
C10K
的约束代码如下:
package com.linwuee.netty.reactor; import java.net.ServerSocket; import java.net.Socket; /** * @Auther: zhang_yx * @Date: 2021/9/21 15:04 */ public class ThreadPerConnection { public static void main(String[] args) { new Thread(new Server()).start(); } } class Server implements Runnable{ @Override public void run() { try { ServerSocket serverSocket = new ServerSocket(8080); while (!Thread.interrupted()){ //这里会阻塞 final Socket socket = serverSocket.accept(); new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getId()); //TODO 获得已连接套接字,去处理请求 } }).start(); } }catch (Exception e){ System.out.println(e); } } }
5.NIO的三种Reactor模型的实现
5.1.Reactor是什么
Reactor可以看做一种设计模式,它要求主线程(IO处理单元)只负责监听文件描述符(fd)是否有事件发生,有的话就立即将该事件通知工作线程(handler)。主线程不做除了监听事件之外的任何操作,读写数据,客户端的请求等均在工作线程中完成。其中主线程往往是阻塞在某一个调用上,并监听。
其中Reactor模式,是处理并发IO比较常见的模式,使用同步非阻塞IO实现。其会将要处理的IO事件放到一个IO多路复用器中,同时主线程阻塞在该多路复用器(select、poll、epoll)上,直到有IO事件准备就绪了,多路解复用器从阻塞处返回,将准备好的IO事件分发到事件处理器(工作线程handler)中
Reactor是基于非阻塞同步IO+事件驱动技术(或+线程池)的一种并发模型
Reactor模式通过poll、epoll等IO分发技术实现的一个无限循环的事件分发程序。获取到一个已连接事件之后,会触发添加监听更多的读写事件
其中可总结为:注册感兴趣的事件 -> 扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理
其中事件如下,其中
- 客户端的套接字:连接、读、写事件
- 服务端的监听套接字:获得连接事件
- 服务端的已连接套接字:读、写事件
5.2.Reactor模型详细设计
- Handle:描述符,或者文件句柄,代表操作的资源,例如socketfd。其中会注册到
Synchronous Event Demultiplexer
(同步事件多路解复用器)中,进行监听Handle上发生的事件,如:Connect,Read,Write,Close等事件(一个handle会有多个事件); - Synchronous Event Demultiplexer(同步事件多路解复用器):该组件阻塞等待被监听的事件集,等待事件发生,一般使用IO复用技术实现,如select、poll、epoll;
- Reactor:类似于程序门面,持有Event Handler,包含注册、删除Event Handler,其中也持有了Synchronous Event Demultiplexer去执行事件的监听,然后根据发生事件的文件描述符和事件类型交给不同的Event Handler去处理
- Event Handler:事件处理器,处理具体的IO事件,对IO事件作出具体的响应
其中通过类图也大概能够看出流程很简单,大概如下:
- 注册Event Handler(如果是网络IO框架,这一步对应创建监听套接字,并且把监听套接字事件处理器注册到Reactor中),其实相当于创建Acceptor,单线程下的Reactor,多路复用器承担了Acceptor的职责
- Reactor调用多路复用器中的监听方法,阻塞获取handles(例如已连接套接字),此时可将handle关联Event Handler并注册入Reactor中,方便该handle产生事件时能够找到handler执行(所以也说handler是回调方法)
- 获得监听事件并将事件交给对应的Event Handler去执行(每个Handle都关联了一个Handler)
现在针对上述流程,也会产生一个问题,就是说当handler执行事件的处理逻辑时,会阻塞多路复用器中的监听方法,因此此时可将事件的处理丢入线程池中取异步执行。
5.3.Reactor模型的三种实现
上面我们说了,事件的再handler中的处理流程可能会阻塞多路复用器的调用,因此可将事件处理丢入线程池去异步执行
5.3.1.单线程版本的Reactor
上述我们所讲的Reactor流程就是单线程版本的实现,其中多路复用器承担了Acceptor的职责,大概流程如下:
- Accept接收新连接,把新连接IO读写事件注册到同步事件多路解复用器;
- 执行dispatch调用多路解复用器阻塞等待IO事件;
- 分发事件到特定的Handler中处理,测试会阻塞dispatch流程;
当描述符可读的时候,才会去执行read,想要往socket写数据的时候,统一先写到输出缓冲区,等到感知到可写事件的时候,再统一把输出缓冲区的数据写到网卡即可,从而避免了读和写的阻塞。
我们需要明白,多路复用器本质就是将往内核的多次IO操作,通过channel一次完成
单线程版本中的Handler处理会阻塞Reactor的Event Loop线程的IO轮训,为此,我们可以把Handler丢到单独的线程池中进行处理
5.3.2.主从版本的Reactor
单线程版本存在的问题主要是Reactor可能因为Event Handler处理速度太占用CPU时间,并且会阻塞dispatch流程,因此可能会影响连接套接字不能及时通过Reactor注册到Synchronous Event Demultiplexer
中进行IO轮训
因此我们可以将监听套接字(handle)与已连接套接字(handle)交给不同的Reactor模型去监听,因此就有了主从模式
主Reactor线程只负责执行dispatch阻塞等待监听套接字的连接事件,当有套接字连接之后,随机选择一个Sub-Reactor,把已连接套接字的IO读写事件注册到选择的sub-Reactor线程中
Sub-Reactor线程负责阻塞等待已连接套接字的IO事件的到来,并且调用IO事件的Event Handler处理IO读写事件。这样就实现了IO的高效分发
但是,不管主从Reactor,其中内部对于事件的处理仍然会阻塞dispatch的执行,因此我们还有单线程+主从模式结合线程池的实现
5.3.3.单线程的Reactor+线程池(多线程)
在read到了数据之后,compute这些操作都是业务需要去处理的,因此可将这些内容交给线程池去具体执行,从而加快了时间处理速度
5.3.4.主从的Reactor+线程池(多线程)
同理
5.Netty关于Reactor的实现
我们始终需要明白,单线程,指的是针对socketfd具体事件的处理是单线程的,存在阻塞dispatch的情况