一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
Reactor编程模型
知识索引
- 什么是Reactor
- 单线程模式
- 多线程模式
- 多Reactor多线程模式
1 什么是Reactor
1.1 Reactor起源
在前面的I/O事件驱动模式一章中我们了解到了bio是如何工作的,在bio中由于接受连接和读数据都是阻塞的,为了提高并发我们不得不为每一条连接创建线程。处理完业务逻辑后,随着连接关闭后线程也同样要销毁了,但是这样不停地创建和销毁线程,不仅会带来性能开销,也会造成浪费资源,而且如果要连接几万条连接,创建几万个线程去应对也是不现实的。当然我们可以使用资源复用的方式解决这个问题。也就是不用再为每个连接创建线程,而是创建一个线程池,将连接分配给线程,然后一个线程可以处理多个连接的业务。
但是由于bio阻塞的存在,线程资源其实是被大量浪费的,经常白白被占用,没有做任何事情,从而导致大量的线程资源被阻塞占用,多线程的意义就在于充分利用cpu的空闲时间片,而这种情况下明显背道而驰,所以有没有一种方式可以充分的做到线程资源的利用呢?答案是有的,这就是 I/O多路复用。
I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作,举例来说就是原本烧两壶水需要两个人,两个人坐在水壶旁边,一边打盹一边等着听水壶响的声音,这样等到水开了就能立刻使用,这两个人在这期间不能做任何事情,现在水壶变得智能了,水开的时候会给指定的人发短信,这样只需要一个人就可以监控多壶水,不只是烧开水这个事件,其他的蒸米饭的事件也可以这样实现,这就是I/O多路复用技术所做的事,让线程资源可以从阻塞中解放出来进行重复利用,大大提高了cpu的效率。
但原本系统提供的 I/O 多路复用接口,如果直接拿来使用,是面向过程编程,开发效率低下,所以前辈们基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写。也就是Reactor模式
1.2 Reactor模式介绍
事实上,Reactor模式也叫 Dispatcher 模式,Reactor模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下
Reactor负责监听和分发事件,事件类型包含连接事件、读写事件;- 处理资源池负责处理事件,如 read,业务逻辑,send;
Reactor 模式是灵活多变的,可以应对不同的业务场景,灵活在于:
- Reactor 的数量可以只有一个,也可以有多个;
- 处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程;
将上面的两个因素排列组设一下,理论上就可以有 4 种方案选择:
- 单
Reactor单进程 / 线程; - 单
Reactor多进程/ 线程; - 多
Reactor单进程 / 线程; - 多
Reactor多进程 / 线程;
其中多 Reactor单进程 / 线程实现方案相比单Reactor单进程 / 线程方案,不仅复杂而且也没有性能优势,因此实际中并没有应用。
剩下的 3 个方案都是比较经典的,且都有应用在实际的项目中,如redis,nodejs就是单Reactor单进程/线程,如nginx就是单Reactor多进程,Netty就是多Reactor多线程
在java语言中关于Reactor模式的应用,Doug Lea在《Scalable IO in Java》一文中进行了说明gee.cs.oswego.edu/dl/cpjslide… Lea,所以nio`包就是基于这篇文章实现的,我们这篇文章的所有代码也是基于这篇文章提供的基本代码扩展实现而来
和bio一样,Reactor要实现I/O连接服务端也要做两件事,第一是接受客户端的连接,第二是处理连接中产生的数据,在Reactor模型中这两件事分别对应两个事件,是非阻塞的,接受连接为SelectionKey.OP_ACCEPT事件,读数据为SelectionKey.OP_READ事件,这两个事件在Selector上注册,Selector作为事件轮询器,会轮询产生的事件,这些事件由它所对应的处理器所处理,Reactor模型下由于接受连接和读都不会阻塞,所以可以只用一个线程处理所有连接的所有数据请求,也就是单线程redis的底层实现方式,下面我们通过jdk提供的nio的api实现Reactor的多种场景模式开发
2 Reactor模式基于NIO的实现
2.1 单线程模式
下图展示的就是单线程下基本的Reactor设计模式
首先我们明确下java.nio中相关的几个概念:
Channels:支持非阻塞读写的socket连接
Buffers:用于被Channels读写的字节数组对象
Selectors:用于判断channle发生IO事件的选择器
SelectionKeys:负责IO事件的状态与绑定
Reactor模式在java的实现核心为以下几步:
1:创建ServerSocketChannel,并监听端口作为服务端
2:在ServerSocketChannel上注册一个Selector,用来轮询服务端接受到的事件
3:在Selector上注册accpet事件(即连接事件)的处理器
4:在accpet事件的处理器中在接受到连接后注册该连接对应的read事件(即读事件)的处理器到Selector
5:在read事件处理器中编写读到数据后进行的处理业务
下面我们就围绕上面的步骤实现一个单Reactor单线程的案例,nio的代码虽然看起来比较多,但是核心都是以上5个步骤
Reactor
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Reactor implements Runnable {
final Selector selector;
final ServerSocketChannel serverSocket;
public Reactor(int port) throws IOException {
// 1.创建一个轮询器
selector = Selector.open();
// 2.创建服务端SocketChannel
serverSocket = ServerSocketChannel.open();
// 3.监听端口
serverSocket.socket().bind(new InetSocketAddress(port));
// 4.配置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
// 5.注册accept事件(此处对应bio的ServerSocket#accept())
SelectionKey accept = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
// 6.调用Acceptor()为回调方法
accept.attach(new Acceptor(selector,serverSocket));
}
public void run() {
try {
while (!Thread.interrupted()) {//循环
// 1.等待selector上产生事件
selector.select();
// 2.获取selector上发生的事件集合
Set selected = selector.selectedKeys();
// 3.遍历
Iterator it = selected.iterator();
while (it.hasNext()){
// 4.分发事件
dispatch((SelectionKey)(it.next()));
}
// 5.移除所有已经分发过的事件
selected.clear();
}
} catch (IOException ex) { /* ... */ }
}
private void dispatch(SelectionKey k) {
// 此处的事件对应注册的回调
// 在本案例中SelectionKey.OP_ACCEPT对应的回调为Acceptor,并且该事件是非阻塞的(Reactor构造方法的第4步配置)
// 在本案例中SelectionKey.OP_READ对应的回调为Handler,并且该事件是非阻塞的
Runnable r = (Runnable)(k.attachment()); //调用SelectionKey绑定的调用对象
if (r != null)
r.run();
}
}
代码说明:
1:Selector为一个轮询器,作用是轮询产生的事件
2:本案例中包含两个事件,一个是ACCEPT事件,对应SelectionKey.OP_ACCEPT,在Reactor构造方法的第5步注册,该事件在本案例中对应的处理为Acceptor类的run方法,另一个是READ事件,对应SelectionKey.OP_READ,在Acceptor的run方法第3步注册,该事件在本案例中对应的处理为Handler类的run方法
3:nio需要显式设置服务端socket非阻塞serverSocket.configureBlocking(false);
Acceptor
accpet事件处理器
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Acceptor implements Runnable {
private Selector selector;
private ServerSocketChannel serverSocket;
public Acceptor(Selector selector, ServerSocketChannel serverSocket) {
this.selector = selector;
this.serverSocket = serverSocket;
}
public void run() {
try {
// 1.获取连接到服务端的socket
SocketChannel sc = serverSocket.accept();
if (sc != null){
// 2.设置该socket为非阻塞的
sc.configureBlocking(false);
// 3.在serverSocket上注册read事件
SelectionKey read = sc.register(selector, SelectionKey.OP_READ);
// 4.调用read方法的回调方法
read.attach(new Handler(sc,read));
}
}
catch(IOException ex) { /* ... */ }
}
}
Handler
read事件处理器
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Handler implements Runnable {
SocketChannel socket;
SelectionKey sk;
public Handler(SocketChannel socket, SelectionKey sk) {
this.socket = socket;
this.sk = sk;
}
public void run() {
try {
read();
} catch (IOException e) {
e.printStackTrace();
}
}
private synchronized void read() throws IOException {
// 1.读取socket传输过来的数据
byte[] bytes = new byte[1024];
int read = socket.read(ByteBuffer.wrap(bytes));
// 2.读取结果为空时关闭Channel
if(read==-1){
closeChannel();
return;
}
// 3.对数据进行编码
String msg = new String(bytes);
// 4.进行业务处理
process(msg);
// 5.响应结果
send("success");
}
private void send(String msg) {
// 1.发送数据
ByteBuffer result = ByteBuffer.wrap(msg.getBytes());
while(result.hasRemaining()){// 判断是否存在待发送的数据
try {
socket.write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void process(String msg) {
System.out.println("时间戳:"+System.currentTimeMillis()+"服务端线程"+Thread.currentThread().getId()+"接受请求数据:"+msg);
}
private void closeChannel() {
try {
sk.cancel();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Server
服务端主进程
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Server {
public static void main(String[] args) {
try {
// 创建Socket服务端,监听2000端口
Reactor ss = new Reactor(2000);
ss.run();
} catch (IOException ex) {
}
}
}
客户端
和bio使用相同的客户端
Client
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
// 1.创建socket连接
try {
Socket socket = new Socket("127.0.0.1", 2000);
new ClientSocketHandler(socket).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ClientSocketHandler
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class ClientSocketHandler extends Thread {
Socket socket;
public ClientSocketHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
for (int i = 0; i < 2; i++) {
// 1.发送请求
String msg = "请求"+i;
System.out.println("客户端发送请求:"+msg);
socket.getOutputStream().write(msg.getBytes());
socket.getOutputStream().flush();
// 2.获取响应
byte[] input = new byte[1024];
socket.getInputStream().read(input);
System.out.println(new String("客户端获得响应:"+new String(input)));
// 3.间隔时间2s
Thread.sleep(2000);
}
socket.close();
} catch (Exception e) {
}
}
}
测试结果:
服务端
客户端
结果说明:
单线程服务端使用一个线程处理客户端的socket,并且没有阻塞,可以在客户端的输出结果中看到,服务端几乎是同时打印了两个客户端Socket的请求0和请求1
2.2 多线程模式
在多处理器场景下,为实现服务的高性能我们可以有目的的采用多线程模式:
1、增加Worker线程,专门用于处理非I/O操作,因为通过上面的程序我们可以看到,反应器线程需要迅速触发处理流程,而如果处理过程也就是process()方法产生阻塞会拖慢反应器线程的性能,所以我们需要把一些非I/O操作交给Woker线程来做;
2、拆分并增加反应器Reactor线程,一方面在压力较大时可以饱和处理I/O操作,提高处理能力;另一方面维持多个Reactor线程也可以做负载均衡使用;线程的数量可以根据程序本身是CPU密集型还是IO密集型操作来进行合理的分配;
Reactor多线程设计模式具备以下几个特点:
1:通过卸载非IO操作来提升Reactor 线程的处理性能,这类似与POSA2 中Proactor的设计;
2:比将非IO操作重新设计为事件驱动的方式更简单;
3:但是很难与IO重叠处理,最好能在第一时间将所有输入读入缓冲区;(这里我理解的是最好一次性读取缓冲区数据,方便异步非IO操作处理数据)
4:可以通过线程池的方式对线程进行调优与控制,一般情况下需要的线程数量比客户端数量少很多;
下面是Reactor多线程设计模式的一个示意图与示例代码(我们可以看到在这种模式中在Reactor线程的基础上把非I/O操作放在了Worker线程中执行):
Handler
改造单线程Handler
public class Handler implements Runnable {
// 创建线程池
static Executor pool = Executors.newFixedThreadPool(10);
SocketChannel socket;
SelectionKey sk;
public Handler(SocketChannel socket, SelectionKey sk) {
this.socket = socket;
this.sk = sk;
}
public void run() {
try {
read();
} catch (IOException e) {
e.printStackTrace();
}
}
private synchronized void read() throws IOException {
// 1.读取socket传输过来的数据
byte[] bytes = new byte[1024];
int read = socket.read(ByteBuffer.wrap(bytes));
// 2.读取结果为空时关闭Channel
if(read==-1){
closeChannel();
return;
}
// 3.对数据进行编码
String msg = new String(bytes);
// 4.进行业务处理
pool.execute(()-> process(msg));
// 5.响应结果
send("success");
}
private void send(String msg) {
// 1.发送数据
ByteBuffer result = ByteBuffer.wrap(msg.getBytes());
while(result.hasRemaining()){// 判断是否存在待发送的数据
try {
socket.write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void process(String msg) {
System.out.println("时间戳:"+System.currentTimeMillis()+"服务端线程"+Thread.currentThread().getId()+"接受请求数据:"+msg);
}
private void closeChannel() {
try {
sk.cancel();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码说明:
1:创建线程池static Executor pool = Executors.newFixedThreadPool(10);
2:process()放在线程池中处理
开启服务端和客户端,查看服务端执行结果:
可以看到此时处理业务的是不同的线程。
2.3 多Reactor多线程模式
为了进一步提高服务的I/O效率,我们应当将连接事件和读事件分别再两种Selector中进行分发,这样读的过程中服务依然可以接入新的连接,这是对多线程模式的进一步完善,使用反应器线程池,一方面根据实际情况用于匹配调节CPU处理与I/O读写的效率,提高系统资源的利用率,另一方面在静态或动态构造中每个反应器线程都包含对应的Selector,Thread,dispatchloop,下面是一个简单的代码示例与示意图(Netty就是基于这个模式设计的,一个处理Accpet连接的mainReactor线程,多个处理I/O事件的subReactor线程)
接下来我们按照图示写一个多Reactor的完整案例。
Reactor
相比于多线程模式的案例没有变化
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Reactor implements Runnable {
final Selector selector;
final ServerSocketChannel serverSocket;
public Reactor(int port) throws IOException {
// 1.创建一个轮询器
selector = Selector.open();
// 2.创建服务端SocketChannel
serverSocket = ServerSocketChannel.open();
// 3.监听端口
serverSocket.socket().bind(new InetSocketAddress(port));
// 4.配置ServerSocketChannel为非阻塞
serverSocket.configureBlocking(false);
// 5.注册accept事件(此处对应bio的ServerSocket#accept())
SelectionKey accept = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
// 6.调用Acceptor()为回调方法
accept.attach(new Acceptor(serverSocket));
}
public void run() {
try {
while (!Thread.interrupted()) {//循环
// 1.等待selector上产生事件
selector.select();
// 2.获取selector上发生的事件集合
Set selected = selector.selectedKeys();
// 3.遍历
Iterator it = selected.iterator();
while (it.hasNext()){
// 4.分发事件
dispatch((SelectionKey)(it.next()));
}
// 5.移除所有已经分发过的事件
selected.clear();
}
} catch (IOException ex) { /* ... */ }
}
private void dispatch(SelectionKey k) {
// 此处的事件对应注册的回调
// 在本案例中SelectionKey.OP_ACCEPT对应的回调为Acceptor,并且该事件是非阻塞的(Reactor构造方法的第4步配置)
// 在本案例中SelectionKey.OP_READ对应的回调为Handler,并且该事件是非阻塞的
Runnable r = (Runnable)(k.attachment()); //调用SelectionKey绑定的调用对象
if (r != null)
r.run();
}
}
Acceptor
相比于多线程模式的案例有点变化:
- 在此时基于
cpu核心数创建多个子Reactor,用来对注册read事件的Selector进行分发。 - 此时每一个子
Reactor对应一个处理读事件的Selector - 当
Accpetor接收到连接后,对每次接受到的连接产生read事件分别注册到子Reactor对应的Selector上
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Acceptor implements Runnable {
int coreNums = Runtime.getRuntime().availableProcessors();
Selector[] selectors = new Selector[coreNums];
private ServerSocketChannel serverSocket;
int index = 0;
public Acceptor(ServerSocketChannel serverSocket) {
this.serverSocket = serverSocket;
for (int i = 0; i < coreNums; i++) {
try {
Selector readSelector = Selector.open();
selectors[i] = readSelector;
new Thread(new SubReactor(readSelector)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void run() {
try {
// 1.获取连接到服务端的socket
SocketChannel sc = serverSocket.accept();
if (sc != null){
// 2.设置该socket为非阻塞的
sc.configureBlocking(false);
// 3.唤醒selector
selectors[index].wakeup();
// 4.在serverSocket上注册read事件
SelectionKey read = sc.register(selectors[index], SelectionKey.OP_READ);
// 5.调用read方法的回调方法
read.attach(new Handler(sc,read));
index++;
if(index == coreNums - 1){
index = 0;
}
}
}
catch(IOException ex) { /* ... */ }
}
}
SubReactor
用来对注册读事件的Selector上产生的事件进行分发,并在分发后打印当前产生事件的Selector
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class SubReactor implements Runnable {
private Selector selector;
public SubReactor(Selector selector) {
this.selector = selector;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {//循环
// 1.等待selector上产生事件
selector.select();
// 2.获取selector上发生的事件集合
Set selected = selector.selectedKeys();
// 3.遍历
Iterator it = selected.iterator();
while (it.hasNext()){
// 4.分发事件
dispatch((SelectionKey)(it.next()));
}
// 5.移除所有已经分发过的事件
selected.clear();
System.out.println("selector:"+selector+"产生事件");
}
} catch (IOException ex) { /* ... */ }
}
private void dispatch(SelectionKey k) {
// 此处的事件对应注册的回调
// 在本案例中SelectionKey.OP_ACCEPT对应的回调为Acceptor,并且该事件是非阻塞的(Reactor构造方法的第4步配置)
// 在本案例中SelectionKey.OP_READ对应的回调为Handler,并且该事件是非阻塞的
Runnable r = (Runnable)(k.attachment()); //调用SelectionKey绑定的调用对象
if (r != null)
r.run();
}
}
Handler
相比于多线程模式案例中没有变化
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Handler implements Runnable {
static Executor pool = Executors.newFixedThreadPool(10);
SocketChannel socket;
SelectionKey sk;
public Handler(SocketChannel socket, SelectionKey sk) {
this.socket = socket;
this.sk = sk;
}
public void run() {
try {
read();
} catch (IOException e) {
e.printStackTrace();
}
}
private synchronized void read() throws IOException {
// 1.读取socket传输过来的数据
byte[] bytes = new byte[1024];
int read = socket.read(ByteBuffer.wrap(bytes));
// 2.读取结果为空时关闭Channel
if(read==-1){
closeChannel();
return;
}
// 3.对数据进行编码
String msg = new String(bytes);
// 4.进行业务处理
pool.execute(()-> process(msg));
// 5.响应结果
send("success");
}
private void send(String msg) {
// 1.发送数据
ByteBuffer result = ByteBuffer.wrap(msg.getBytes());
while(result.hasRemaining()){// 判断是否存在待发送的数据
try {
socket.write(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void process(String msg) {
System.out.println("时间戳:"+System.currentTimeMillis()+"服务端线程"+Thread.currentThread().getId()+"接受请求数据:"+msg);
}
private void closeChannel() {
try {
sk.cancel();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Server
相比于多线程模式案例中没有变化
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Server {
public static void main(String[] args) {
try {
// 创建Socket服务端,监听2000端口
Reactor ss = new Reactor(2000);
ss.run();
} catch (IOException ex) {
}
}
}
Client
相比于多线程模式案例中没有变化
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
// 1.创建socket连接
try {
Socket socket = new Socket("127.0.0.1", 2000);
new ClientSocketHandler(socket).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ClientSocketHandler
相比于多线程模式案例中没有变化
/**
* Copyright (c) 2022 itmentu.com, All rights reserved.
*
* @Author: yang
*/
public class ClientSocketHandler extends Thread {
Socket socket;
public ClientSocketHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
for (int i = 0; i < 2; i++) {
// 1.发送请求
String msg = "请求"+i;
System.out.println("客户端发送请求:"+msg);
socket.getOutputStream().write(msg.getBytes());
socket.getOutputStream().flush();
// 2.获取响应
byte[] input = new byte[1024];
socket.getInputStream().read(input);
System.out.println(new String("客户端获得响应:"+new String(input)));
// 3.间隔时间2s
Thread.sleep(2000);
}
socket.close();
} catch (Exception e) {
}
}
}
执行结果:
结果说明:
可以明显看到此时是由多个Reactor的Selector在工作,多个线程在进行porcess处理