欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
上篇文章:IO系列3-详解IO多路复用(select、poll、epoll),深刻剖析了IO多路复用函数(select、poll、epoll),但并未使用Java代码实现对应的功能。本篇文章将采用Java代码实现IO多路复用,并抽丝剥茧Reactor模式,对Proactor模式进行简要分析。
1.IO多路复用代码实现
代码实现:
- 一个线程即负责处理accept请求,也负责处理read/write请求;
- Java代码中结合注释Linux函数(select、poll、epoll)
public class IoMultiplexingSingleThreadv1 {
private ServerSocketChannel server = null;
//linux 多路复用器(select poll epoll kqueue)
private Selector selector = null;
int port = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();
// 非阻塞
server.configureBlocking(false);
// 绑定
server.bind(new InetSocketAddress(port));
//如果在epoll模型下,open--> epoll_create -> fd3
selector = Selector.open(); // select poll epoll 优先选择:epoll
/**
* server 约等于 listen状态的 fd4
* register
* 如果:
* select,poll:jvm里开辟一个数组 fd4 放进去
* epoll: epoll_ctl(fd3,ADD,fd4,EPOLLIN)
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) { //死循环
Set<SelectionKey> keys = selector.keys();
System.out.println("keys size = " + keys.size());
/**
* 1.调用多路复用器(select,poll or epoll(epoll_wait))
* select()是啥意思:
* ① select,poll 其实 内核的select(fd4) poll(fd4)
* ② epoll: 其实 内核的 epoll_wait()
* 参数可以带时间:有时间设置一个超时
* 没有时间,0: 阻塞,selector.wakeup()唤醒 结果返回0
*
* 懒加载:
* 其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用
*/
while (selector.select() > 0) {
// 返回的有状态的fd集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
// 多路复用器,只要给我状态,我一个一个的去处理他们的R/W。同步好辛苦!
// NIO :自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了
while (iter.hasNext()) {
SelectionKey key = iter.next();
//不移除会重复循环处理
iter.remove();
if (key.isAcceptable()) {
// 重点,如果要去接受一个新的连接
//语义上,accept接受连接且返回新连接的FD
//那新的FD怎么办?
//select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
//epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
acceptHandler(key);
} else if (key.isReadable()) {
readHandler(key); //连read 还有 write都处理了
//在当前线程,这个方法可能会阻塞,如果阻塞了十年,其他的IO早就没了。。。
//所以,为什么提出了 IO THREADS
//redis 是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的
//tomcat 8,9 异步的处理方式 IO和处理上 解耦
} else if(key.isWritable()) {
//要明白:你想什么时候写?不是依赖send-queue是不是有空间
//1,你准备好要写什么了,这是第一步
//2,第二步你才关心send-queue是否有空间
//3,so,读 read 一开始就要注册,但是write依赖以上关系,什么时候用什么时候注册
//4,如果一开始就注册了write的事件,进入死循环,一直调起!!!
writeHandler(key);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
System.out.println("acceptHandler handler..");
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//目的是调用accept接受客户端 fd7
SocketChannel client = ssc.accept();
// 非阻塞
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192);
//调用了register
/*
select,poll:jvm里开辟一个数组 fd7 放进去
epoll: epoll_ctl(fd3,ADD,fd7,EPOLLIN)
*/
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("新客户端:" + client.getRemoteAddress());
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey key) {
System.out.println("read handler.....");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
/*在read方法中直接将数据发出去
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();*/
// 关心 OP_WRITE 其实就是关系send-queue是不是有空间
client.register(key.selector(),SelectionKey.OP_WRITE,buffer);
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void writeHandler(SelectionKey key) {
System.out.println("write handler...");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
while (buffer.hasRemaining()) {
try {
client.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buffer.clear();
// 取消write事件
key.cancel();
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
IoMultiplexingSingleThreadv1 service = new IoMultiplexingSingleThreadv1();
service.start();
}
}
将服务端启动,通过nc 127.0.0.1 9090
访问服务端,日志如下:
服务器启动了。。。。。
keys size = 1
acceptHandler handler..
新客户端:/127.0.0.1:52629
read handler.....
write handler...
1.2 select、poll、epoll函数底层实现
涉及到TCP/IP网络连接的三次握手、四次挥手,自行查询相关资料即可。
1.2.1 poll
OS:POLL jdk native 用户空间 保存了fd
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 4
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0 //server.configureBlocking(false);
bind(4, {sa_family=AF_INET, sin_port=htons(9090)
listen(4, 50)
poll([{fd=5, events=POLLIN}, {fd=4, events=POLLIN}], 2, -1) = 1 ([{fd=4, revents=POLLIN}])
// while (selector.select() > 0)
accept(4, = 7 //新的客户端
fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK)
poll([{fd=5, events=POLLIN}, {fd=4, events=POLLIN}, {fd=7, events=POLLIN}], 3, -1) = 1(一个fd有事件) -1(非阻塞下,没有事件)
1.2.2 epoll
OS:EPOLL
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 4\
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0\
bind(4, {sa_family=AF_INET, sin_port=htons(9090)\
listen(4, 50)
epoll_create(256) = 7 (epfd)
epoll_ctl(7, EPOLL_CTL_ADD, 4,
epoll_wait(7, {{EPOLLIN, {u32=4, u64=2216749036554158084}}}, 4096, -1) = 1
// while (selector.select() > 0)
accept(4 =8 //client的fd
fcntl(8, F_SETFL, O_RDWR|O_NONBLOCK)
epoll_ctl(7, EPOLL_CTL_ADD, 8, {EPOLLIN,
epoll_wait(7,
2. Reactor模式
我们熟悉的 select/poll/epoll 就是内核提供给用户态的多路复用系统调用,线程可以通过一个系统调用函数从内核中获取多个事件。具体可详见:IO系列3-详解IO多路复用(select、poll、epoll)
在获取事件时,先把我们要关心的连接传给内核,再由内核检测:
- 如果没有事件发生,线程只需阻塞在这个系统调用,而无需像前面的线程池方案那样轮训调用 read 操作来判断是否有数据。
- 如果有事件发生,内核会返回产生了事件的连接,线程就会从阻塞状态返回,然后在用户态中再处理这些连接对应的业务即可。
基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写。也就是
Reactor模式
Reactor 模式也叫Dispatcher
模式,我觉得这个名字更贴合该模式的含义,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程。
Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成,负责的事情如下:
- Reactor 负责监听和分发事件(可以单/多个),事件类型包含连接事件、读写事件
- 处理资源池负责处理事件(可以单/多线程或进程),如 read -> 业务逻辑 -> send 即可以组成经典的三种方案:
- 单 Reactor 单进程 / 线程;
- 单 Reactor 多线程 / 进程;
- 多 Reactor 多进程 / 线程;
2.1 单 Reactor 单进程 / 线程
2.1.1 方案示意图
C 语言实现的是「单 Reactor 单进程」的方案,因为 C 语编写完的程序,运行后就是一个独立的进程,不需要在进程中再创建线程。而 Java 语言实现的是「单 Reactor 单线程」的方案,因为 Java 程序是跑在 Java 虚拟机这个进程上面的,虚拟机中有很多线程,我们写的 Java 程序只是其中的一个线程而已。
可以看到进程里有 Reactor、Acceptor、Handler 这三个对象:
- Reactor 对象的作用是监听和分发事件;
- Acceptor 对象的作用是获取连接;
- Handler 对象的作用是处理业务; 对象里的 select、accept、read、send 是系统调用函数,dispatch 和 「业务处理」是需要完成的操作,其中 dispatch 是分发事件操作。
2.1.2 执行过程
- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
2.1.3 优缺点
优点:
单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。
缺点:
- 因为只有一个进程,无法充分利用
多核 CPU
的性能; - Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;
- 不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
2.1.4 应用场景
- 代码如上一节中示例代码
- Redis 是由 C 语言实现的,它采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。
2.2 单 Reactor 多线程 / 多进程
如果要克服「单 Reactor 单线程 / 进程」方案的缺点,那么就需要引入多线程 / 多进程,这样就产生了单 Reactor 多线程 / 多进程的方案。
2.2.1 方案示意图
2.2.2 执行过程
- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
上面的三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤就开始不一样了:
- Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;
- 子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
2.2.3 优缺点
优点:
- 能够充分利用多核 CPU 的性能 缺点:
- 引入多线程,那么自然就带来了多线程竞争资源的问题。例如,子线程完成业务处理后,要把结果传递给主线程的 Reactor 进行发送,这里涉及共享数据的竞争。
要避免多线程由于竞争共享资源而导致数据错乱的问题,就需要在操作共享资源前加上互斥锁,以保证任意时间里只有一个线程在操作共享资源,待该线程操作完释放互斥锁后,其他线程才有机会操作共享数据。
2.2.4 应用场景
主要代码与 第1章节代码一致, 只不过readHandler() 和 writeHandler() 方法改成多线程,如下:
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) {
while (selector.select(50) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
acceptHandler(key);
} else if (key.isReadable()) {
System.out.println("in read().....");
key.cancel(); //现在多路复用器里把key cancel了
// key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
readHandler(key);//还是阻塞的嘛? 即便以抛出了线程去读取,但是在时差里,这个key的read事件会被重复触发
} else if(key.isWritable()){
System.out.println("in read().....");
// key.cancel();
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
writeHandler(key);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void writeHandler(SelectionKey key) {
new Thread(()->{
System.out.println("write handler...");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
System.out.println(Thread.currentThread().getName()+ "write process");
while (buffer.hasRemaining()) {
try {
client.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
buffer.clear();
}).start();
}
public void readHandler(SelectionKey key) {
new Thread(()->{
System.out.println("read handler.....");
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
System.out.println(Thread.currentThread().getName()+ "read process" + read);
if (read > 0) {
client.register(key.selector(),SelectionKey.OP_WRITE,buffer);
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
由于在主线程中不是阻塞执行,所以事件会被重复触发,需要在处理read和write事件前取消事件,如下代码:
key.cancel(); //现在多路复用器里把key cancel了
// key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
事实上,单 Reactor 多进程相比单 Reactor 多线程实现起来很麻烦,主要因为要考虑子进程 <-> 父进程的双向通信,并且父进程还得知道子进程要将数据发送给哪个客户端。
而多线程间可以共享数据,虽然要额外考虑并发问题,但是这远比进程间通信的复杂度低得多,因此实际应用中也看不到单 Reactor 多进程的模式。
另外,「单 Reactor」的模式还有个问题,因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
2.3 多 Reactor 多进程 / 线程
要解决 单 Reactor 问题,就需要使用多 Reactor 多进程/线程方案。
2.3.1 方案示意图
2.3.2 执行过程
- 主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
- 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
- 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下:
- 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。
- 主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。
2.3.3 应用场景
2.3.3.1 代码
启动线程MainThread
:
public class MainThread {
public static void main(String[] args) {
//这里不做关于IO 和 业务的事情
//1,创建 IO Thread (一个或者多个)
SelectorThreadGroup boss = new SelectorThreadGroup(3); //混杂模式
//boss有自己的线程组
SelectorThreadGroup worker = new SelectorThreadGroup(3); //混杂模式
//worker有自己的线程组
//混杂模式,只有一个线程负责accept,每个都会被分配client,进行R/W
// SelectorThreadGroup stg = new SelectorThreadGroup(3);
//2,我应该把 监听(9999)的 server 注册到某一个 selector上
boss.setWorker(worker);
//但是,boss得多持有worker的引用:
/**
* boss里选一个线程注册listen , 触发bind,从而,这个不选中的线程得持有 workerGroup的引用
* 因为未来 listen 一旦accept得到client后得去worker中 next出一个线程分配
*/
boss.bind(9999);
boss.bind(8888);
boss.bind(6666);
}
}
线程组SelectorThreadGroup
:
public class SelectorThreadGroup { //天生都是boss
SelectorThread[] sts;
ServerSocketChannel server=null;
AtomicInteger xid = new AtomicInteger(0);
SelectorThreadGroup stg = this;
public void setWorker(SelectorThreadGroup stg){
this.stg = stg;
}
SelectorThreadGroup(int num){
//num 线程数
sts = new SelectorThread[num];
for (int i = 0; i < num; i++) {
sts[i] = new SelectorThread(this);
new Thread(sts[i]).start();
}
}
public void bind(int port) {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//注册到那个selector上呢?
// nextSelectorV2(server);
nextSelectorV3(server);
} catch (IOException e) {
e.printStackTrace();
}
}
public void nextSelectorV3(Channel c) {
try {
if(c instanceof ServerSocketChannel){
SelectorThread st = next(); //listen 选择了 boss组中的一个线程后,要更新这个线程的work组
st.lbq.put(c);
st.setWorker(stg);
st.selector.wakeup();
}else {
SelectorThread st = nextV3(); //在 main线程种,取到堆里的selectorThread对象
//1,通过队列传递数据 消息
st.lbq.add(c);
//2,通过打断阻塞,让对应的线程去自己在打断后完成注册selector
st.selector.wakeup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void nextSelectorV2(Channel c) {
try {
if(c instanceof ServerSocketChannel){
sts[0].lbq.put(c);
sts[0].selector.wakeup();
}else {
SelectorThread st = nextV2(); //在 main线程种,取到堆里的selectorThread对象
//1,通过队列传递数据 消息
st.lbq.add(c);
//2,通过打断阻塞,让对应的线程去自己在打断后完成注册selector
st.selector.wakeup();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void nextSelector(Channel c) {
SelectorThread st = next(); //在 main线程种,取到堆里的selectorThread对象
//1,通过队列传递数据 消息
st.lbq.add(c);
//2,通过打断阻塞,让对应的线程去自己在打断后完成注册selector
st.selector.wakeup();
// public void nextSelector(Channel c) {
// SelectorThread st = next(); //在 main线程种,取到堆里的selectorThread对象
//
// //1,通过队列传递数据 消息
// st.lbq.add(c);
// //2,通过打断阻塞,让对应的线程去自己在打断后完成注册selector
// st.selector.wakeup();
//重点: c有可能是 server 有可能是client
// ServerSocketChannel s = (ServerSocketChannel) c;
//呼应上, int nums = selector.select(); //阻塞 wakeup()
// try {
// s.register(st.selector, SelectionKey.OP_ACCEPT); //会被阻塞的!!!!!
// st.selector.wakeup(); //功能是让 selector的select()方法,立刻返回,不阻塞!
// System.out.println("aaaaa");
// } catch (ClosedChannelException e) {
// e.printStackTrace();
// }
}
//无论 serversocket socket 都复用这个方法
private SelectorThread next() {
int index = xid.incrementAndGet() % sts.length; //轮询就会很尴尬,倾斜
return sts[index];
}
private SelectorThread nextV2() {
int index = xid.incrementAndGet() % (sts.length-1); //轮询就会很尴尬,倾斜
return sts[index+1];
}
private SelectorThread nextV3() {
int index = xid.incrementAndGet() % stg.sts.length; //动用worker的线程分配
return stg.sts[index];
}
}
具体执行线程SelectorThread
:
public class SelectorThread extends ThreadLocal<LinkedBlockingQueue<Channel>> implements Runnable{
// 每线程对应一个selector,
// 多线程情况下,该主机,该程序的并发客户端被分配到多个selector上
//注意,每个客户端,只绑定到其中一个selector
//其实不会有交互问题
Selector selector = null;
// LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();
LinkedBlockingQueue<Channel> lbq = get(); //lbq 在接口或者类中是固定使用方式逻辑写死了。你需要是lbq每个线程持有自己的独立对象
SelectorThreadGroup stg;
@Override
protected LinkedBlockingQueue<Channel> initialValue() {
return new LinkedBlockingQueue<>();//你要丰富的是这里! pool。。。
}
SelectorThread(SelectorThreadGroup stg){
try {
this.stg = stg;
selector = Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//Loop
while (true){
try {
//1,select()
// System.out.println(Thread.currentThread().getName()+" : before select...."+ selector.keys().size());
int nums = selector.select(); //阻塞 wakeup()
// Thread.sleep(1000); //这绝对不是解决方案,我只是给你演示
// System.out.println(Thread.currentThread().getName()+" : after select...." + selector.keys().size());
//2,处理selectkeys
if(nums>0){
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while(iter.hasNext()){ //线程处理的过程
SelectionKey key = iter.next();
iter.remove();
if(key.isAcceptable()){ //复杂,接受客户端的过程(接收之后,要注册,多线程下,新的客户端,注册到那里呢?)
acceptHandler(key);
}else if(key.isReadable()){
readHander(key);
}else if(key.isWritable()){
}
}
}
//3,处理一些task : listen client
if(!lbq.isEmpty()){ //队列是个啥东西啊? 堆里的对象,线程的栈是独立,堆是共享的
//只有方法的逻辑,本地变量是线程隔离的
Channel c = lbq.take();
if(c instanceof ServerSocketChannel){
ServerSocketChannel server = (ServerSocketChannel) c;
server.register(selector,SelectionKey.OP_ACCEPT);
System.out.println(Thread.currentThread().getName()+" register server listen" + server.getLocalAddress());
}else if(c instanceof SocketChannel){
SocketChannel client = (SocketChannel) c;
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println(Thread.currentThread().getName()+" register client: " + client.getRemoteAddress());
}
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
// catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}
private void readHander(SelectionKey key) {
System.out.println(Thread.currentThread().getName()+" read......");
ByteBuffer buffer = (ByteBuffer)key.attachment();
SocketChannel client = (SocketChannel)key.channel();
buffer.clear();
while(true){
try {
int num = client.read(buffer);
if(num > 0){
buffer.flip(); //将读到的内容翻转,然后直接写出
while(buffer.hasRemaining()){
client.write(buffer);
}
buffer.clear();
}else if(num == 0){
break;
}else if(num < 0 ){
//客户端断开了
System.out.println("client: " + client.getRemoteAddress()+"closed......");
key.cancel();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void acceptHandler(SelectionKey key) {
System.out.println(Thread.currentThread().getName()+" acceptHandler......");
ServerSocketChannel server = (ServerSocketChannel)key.channel();
try {
SocketChannel client = server.accept();
client.configureBlocking(false);
//choose a selector and register!!
stg.nextSelectorV3(client);
// stg.nextSelectorV2(client);
} catch (IOException e) {
e.printStackTrace();
}
}
public void setWorker(SelectorThreadGroup stgWorker) {
this.stg = stgWorker;
}
}
通过 nc 127.0.0.1 8888
访问不同端口,执行结果如下:
Thread-0 register server listen/0:0:0:0:0:0:0:0:6666
Thread-1 register server listen/0:0:0:0:0:0:0:0:9999
Thread-2 register server listen/0:0:0:0:0:0:0:0:8888
Thread-1 acceptHandler......
Thread-4 register client: /127.0.0.1:60787
Thread-1 acceptHandler......
Thread-5 register client: /127.0.0.1:60789
Thread-2 acceptHandler......
Thread-3 register client: /127.0.0.1:60791
这就是一个简单demo,只要大家理解 多Reactor多进程/线程 模式即可,没必要考虑其他问题。如果想看考虑更周全的代码,可以直接看Netty代码。
2.3.3.2 实际应用
大名鼎鼎的两个开源软件 Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案。
采用了「多 Reactor 多进程」方案的开源软件是 Nginx,不过方案与标准的多 Reactor 多进程有些差异。具体差异表现在主进程中仅仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而是由子进程的 Reactor 来 accept 连接,通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。
3. Proactor模式
前面提到的 Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式。异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待。
当我们发起 aio_read
(异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。如果对这块不是很了解的,可参考:IO系列2-深入理解五种IO模型
Proactor 正是采用了异步 I/O 技术,所以被称为异步网络模型。
- Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
- Proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
因此,Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。这里的「事件」就是有新连接、有数据可读、有数据可写的这些 I/O 事件这里的「处理」包含从驱动读取到内核以及从内核读取到用户空间。
举个实际生活中的例子,Reactor 模式就是快递员在楼下,给你打电话告诉你快递到你家小区了,你需要自己下楼来拿快递。而在 Proactor 模式下,快递员直接将快递送到你家门口,然后通知你。
无论是 Reactor,还是 Proactor,都是一种基于「事件分发」的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。
3.1 方案示意图
3.2 执行过程
- Proactor Initiator 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核;
- Asynchronous Operation Processor 负责处理注册请求,并处理 I/O 操作;
- Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor;
- Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理;
- Handler 完成业务处理;
可惜的是,在 Linux 下的异步 I/O 是不完善的, aio
系列函数是由 POSIX 定义的异步操作接口,不是真正的操作系统级别支持的,而是在用户空间模拟出来的异步,并且仅仅支持基于本地文件的 aio 异步操作,网络编程中的 socket 是不支持的,这也使得基于 Linux 的高性能网络程序都是使用 Reactor 方案。
而 Windows 里实现了一套完整的支持 socket 的异步编程接口,这套接口就是 IOCP
,是由操作系统级别实现的异步 I/O,真正意义上异步 I/O,因此在 Windows 里实现高性能网络程序可以使用效率更高的 Proactor 方案。
4. 总结
4.1 Recator三种方案对比:
-
单 Reactor 单进程 / 线程,不用考虑进程间通信以及数据同步的问题,因此实现起来比较简单,这种方案的缺陷在于无法充分利用多核 CPU,而且处理业务逻辑的时间不能太长,否则会延迟响应,所以不适用于计算机密集型的场景,适用于业务处理快速的场景,比如 Redis 采用的是单 Reactor 单进程的方案。
-
单 Reactor 多线程,通过多线程的方式解决了方案一的缺陷,但它离高并发还差一点距离,差在只有一个 Reactor 对象来承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
-
多 Reactor 多进程 / 线程,通过多个 Reactor 来解决了方案二的缺陷,主 Reactor 只负责监听事件,响应事件的工作交给了从 Reactor,Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案,Nginx 则采用了类似于 「多 Reactor 多进程」的方案。
4.2 Reactor 模式与 Proactor 模式对比
Reactor 可以理解为「来了事件操作系统通知应用进程,让应用进程来处理」,而 Proactor 可以理解为「来了事件操作系统来处理,处理完再通知应用进程」。
因此,真正的大杀器还是 Proactor,它是采用异步 I/O 实现的异步网络模型,感知的是已完成的读写事件,而不需要像 Reactor 感知到事件后,还需要调用 read 来从内核中获取数据。
Reactor模式 | Proactor模式 | |
---|---|---|
主动与被动 | 被动处理,即有事件发生时被动处理。如写事件:将handler放到select(),等待可写就绪,然后调用write()写入数据;写完数据后再处理后续逻辑; | 主动发起异步调用,然后循环检测完成时间。如写事件:调用aoi_write后立刻返回,由内核负责写操作,写完后调用相应的回调函数处理后续逻辑 |
实现 | 实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应 | 实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量。 |
优点 | Reactor实现相对简单,对于链接多,但耗时短的处理场景高效;(1) 操作系统可以在多个事件源上等待,并且避免了线程切换的性能开销和编程复杂性;(2)事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁;(3)事务分离:将与应用无关的多路复用、分配机制和与应用相关的回调函数分离开来。 | Proactor在理论上性能更高,能够处理耗时长的并发场景。为什么说在理论上?请自行搜索Netty 5.X版本废弃的原因。 |
缺点 | 处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理; | 实现逻辑复杂;依赖操作系统对异步的支持,目前实现了纯异步操作的操作系统少,实现优秀的如windows IOCP,但由于其windows系统用于服务器的局限性,目前应用范围较小;而Unix/Linux系统对纯异步的支持有限,应用事件驱动的主流还是通过select/epoll来实现。 |
使用场景 | 同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序; | 异步接收和同时处理多个服务请求的事件驱动程序。 |
参考文档:
文章中代码github地址
如何深刻理解Reactor和Proactor?
高性能IO模型分析-Reactor模式和Proactor模式