1 Reactor模式简介
到目前为止,高性能网络编程都绕不开Reactor模式。很多著名的服务器软件或者中间件都是基于Reactor模式实现的。例如,Web服务器Nginx就是基于Reactor模式的;Redis,作为高性能的缓存服务器之一,也是基于Reactor模式的;目前热门的在开源项目中应用极为广泛的高性能通信中间件Netty,还是基于Reactor模式的。
Reactor模式由Reactor线程、Handlers处理器两大角色组成,两大角色的职责分别如下:
- Reactor线程的职责:负责响应IO事件,并且分发到Handlers处理器。
- Handlers处理器的职责:非阻塞的执行业务处理逻辑。
2 多线程OIO的致命缺陷
在Java的OIO编程中,原始的网络服务器程序一般使用一个while循环不断地监听端口是否有新的连接。如果有,就调用一个处理函数来完成传输处理。
@Slf4j
public class ServerDemo {
private final int port;
private final ServerSocket serverSocket;
public ServerDemo(int port) throws IOException {
this.port = port;
this.serverSocket = new ServerSocket();
}
public void start() throws IOException {
// 绑定本地端口
serverSocket.bind(new InetSocketAddress((this.port)));
log.info("the server start success on port [{}]", this.port);
// 不断循环等待客户端连接
while (true) {
// accept方法会阻塞,直到有客户端连接为止
Socket client = serverSocket.accept();
handler(client);
}
}
private void handler(Socket client) {
if (client == null) return;
try {
log.info("handle client request from {}",client.getInetAddress().getHostAddress());
} finally {
// 处理完当前的客户端连接就关闭掉,当前连接
this.close(client);
}
}
private void close(Closeable closeable) {
if(null != closeable){
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这种方法的最大问题是:如果前一个网络连接的handle(socket)没有处理完,那么后面的新连接无法被服务端接收,于是后面的请求就会被阻塞,导致服务器的吞吐量太低。
为了解决这个严重的连接阻塞问题,出现了一个极为经典的模式: Connection Per Thread(一个线程处理一个连接)模式
@Slf4j
public class ConnectionPerDemo {
private final int port;
private final ServerSocket serverSocket;
public ConnectionPerDemo(int port) throws IOException {
this.port = port;
this.serverSocket = new ServerSocket();
}
public void start() throws IOException {
// 绑定本地端口
serverSocket.bind(new InetSocketAddress((this.port)));
log.info("the server start success on port [{}]", this.port);
// 不断循环等待客户端连接
while (true) {
// accept方法会阻塞,直到有客户端连接为止
Socket client = serverSocket.accept();
// 开启一个线程来处理
new Thread(new Handler(client)).start();
}
}
static class Handler implements Runnable {
final Socket socket;
Handler(Socket s) {
socket = s;
}
@Override
public void run() {
handle(socket);
}
private void handle(Socket client) {
if (client == null) return;
try {
log.info("handle client request from {}", client.getInetAddress().getHostAddress());
} finally {
// 处理完当前的客户端连接就关闭掉,当前连接
this.close(client);
}
}
private void close(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
对于每一个新的网络连接都分配给一个线程。每个线程都独自处理自己负责的socket连接的输入和输出。当然,服务器的监听线程也是独立的(当前这个Demo就是主线程接受请求),任何socket连接的输入和输出处理都不会阻塞到后面新socket连接的监听和建立,这样服务器的吞吐量就得到了提升。早期版本的Tomcat服务器就是这样实现的。
Connection Per Thread模式的缺点是对应于大量的连接,需要耗费大量的线程资源,对线程资源要求太高。在系统中,线程是比较昂贵的系统资源。如果线程的数量太多,系统将无法承受。而且,线程的反复创建、销毁、切换也需要代价。因此,在高并发的应用场景下,多线程OIO的缺陷是致命的。
在传统OIO编程中每一次socket传输的IO读写处理都是阻塞的。在同一时刻,一个线程里只能处理一个socket的读写操作,前一个socket操作被阻塞了,其他连接的IO操作同样无法被并行处理。所以,在OIO中,即使是一个线程同时负责处理多个socket连接的输入和输出,同一时刻该线程也只能处理一个连接的IO操作。
2 单线程Reactor模式
Reactor模式有点类似事件驱动模式。 在事件驱动模式中,当有事件触发时,事件源会将事件分发到Handler(处理器),由Handler负责事件处理。 Reactor模式中的反应器角色类似于事件驱动模式中的事件分发器(Dispatcher)角色。
在Reactor模式中有Reactor和Handler两个重要的组件:
- Reactor:负责查询IO事件,当检测到一个IO事件时将其发送给相应的Handler处理器去处理。这里的IO事件就是NIO中选择器查询出来的通道IO事件。
- Handler:与IO事件(或者选择键)绑定,负责IO事件的处理,完成真正的连接建立、通道的读取、处理业务逻辑、负责将结果写到通道等。
2.1 什么是单线程版本的Reactor模式
简单地说,Reactor和Handlers处于一个线程中执行。这是最简单的Reactor模型
2.2 使用的API介绍
需要用到SelectionKey(选择键)的几个重要的成员方法:
void attach(Object o):将对象附加到选择键。
此方法可以将任何Java POJO对象作为附件添加到SelectionKey实例。此方法非常重要,因为在单线程版本的Reactor模式实现中可以将Handler实例作为附件添加到SelectionKey实例。
Object attachment():从选择键获取附加对象。
此方法与attach(Object o)是配套使用的,其作用是取出之前通过attach(Object o)方法添加到SelectionKey实例的附加对象。这个方法同样非常重要,当IO事件发生时,选择键将被select方法查询出来,可以直接将选择键的附件对象取出。
在Reactor模式实现中,通过attachment()方法所取出的是之前通过attach(Object o)方法绑定的Handler实例,然后通过该Handler实例完成相应的传输处理。
总之,在Reactor模式中,需要将attach和attachment结合使用:在选择键注册完成之后调用attach()方法,将Handler实例绑定到选择键;当IO事件发生时调用attachment()方法,可以从选择键取出Handler实例,将事件分发到Handler处理器中完成业务处理。
2.3 Demo
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* @author wyaoyao
* @date 2021/6/23 13:36
*/
@Slf4j
public class EchoServerReactor implements Runnable {
private final int port;
private ServerSocketChannel serverChannel;
private final Selector selector;
private final String host = "localhost";
private volatile boolean closed = false;
private Thread innerThread;
public EchoServerReactor(int port) throws IOException {
this.port = port;
// 打开一个选择器
this.selector = Selector.open();
// 打开通道
this.serverChannel = ServerSocketChannel.open();
//非阻塞
this.serverChannel.configureBlocking(false);
// 注册连接事件
SelectionKey acceptKey = this.serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);
// 对这个连接事件key绑定一个处理器
acceptKey.attach(new AcceptHandler(this.serverChannel, this.selector));
}
/**
* 启动服务
*/
public void start() throws IOException {
// 绑定端口
this.serverChannel.bind(new InetSocketAddress(this.host, this.port));
innerThread = new Thread(this);
innerThread.start();
log.info("echo sever start success on port [{}]", this.port);
}
public void close() throws IOException {
if (!closed) {
this.closed = true;
innerThread.interrupt();
}
}
private void server() throws IOException {
while (!closed && !Thread.interrupted()) {
// 查询感兴趣的事件,当没有事件发生的时候该方法会阻塞
this.selector.select();
// 查询出发生事件的key,遍历这些key,并处理
Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
// Reactor负责dispatch(分派)收到的事件
dispatch(iterator.next());
iterator.remove();
}
// 清除这些处理完的selectionKeys
selectionKeys.clear();
}
}
void dispatch(SelectionKey sk) throws IOException {
Handler handler = (Handler) sk.attachment();
//调用之前attach绑定到选择键的handler处理器对象
if (handler != null) {
try {
handler.handler(sk);
} catch (Exception e) {
sk.cancel();
e.printStackTrace();
}
}
}
@Override
public void run() {
try {
this.server();
} catch (IOException e) {
e.printStackTrace();
}
}
private interface Handler {
void handler(SelectionKey selectionKey) throws IOException;
}
class AcceptHandler implements Handler {
private final ServerSocketChannel serverSocketChannel;
private final Selector selector;
private AcceptHandler(ServerSocketChannel serverSocketChannel, Selector selector) {
this.serverSocketChannel = serverSocketChannel;
this.selector = selector;
}
@Override
public void handler(SelectionKey selectionKey) throws IOException {
// 获取连接
SocketChannel client = this.serverSocketChannel.accept();
//SocketChannel client = (SocketChannel) selectionKey.channel();
// 并且设置非阻塞
client.configureBlocking(false);
InetSocketAddress remoteAddress = (InetSocketAddress) client.getLocalAddress();
log.info("client connect [{}:{}] has accept", remoteAddress.getHostName(), remoteAddress.getPort());
// 防止死锁:当获取已经捕获的事件的SelectionKey的selector.select()方法会阻塞
// 如果在调用register方法的时,正好阻塞了,register也就会阻塞在这
// 所以调用wakeup唤醒selector
selector.wakeup();
// 给SocketChannel注册一个读就绪事件
SelectionKey read = client.register(this.selector, SelectionKey.OP_READ);
read.attach(new ReadHandler(selector));
}
}
class ReadHandler implements Handler {
private ByteBuffer buffer;
private final Selector selector;
ReadHandler(Selector selector) {
this.selector = selector;
this.buffer = ByteBuffer.allocate(1024);
}
@Override
public void handler(SelectionKey selectionKey) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
// 获取当前key关联的channel
SocketChannel channel = (SocketChannel) selectionKey.channel();
// 读取数据
int len = 0;
while ((len = channel.read(buffer)) > 0) {
// 转换为读取模式
buffer.flip();
String s = new String(buffer.array(), 0, len, StandardCharsets.UTF_8);
if (!s.contains("\r\n")) {
// 如果buffer中的数据不足用户一次请求(这里就指一行),就继续读取
continue;
}
stringBuilder.append(s);
// 清除掉
buffer.clear();
}
if (stringBuilder.toString().contains("bye")) {
// 如果客户端发来的是bye,则退出当前会话, 失效这个key
selectionKey.cancel();
channel.close();
return;
}
log.info("accept request ==> {}", stringBuilder.toString());
this.selector.wakeup();
channel.register(this.selector, SelectionKey.OP_WRITE).attach(new WriteHandler(stringBuilder.toString(), selector));
}
}
class WriteHandler implements Handler {
private String request;
private Selector selector;
WriteHandler(String s, Selector selector) {
this.request = s;
this.selector = selector;
}
@Override
public void handler(SelectionKey selectionKey) throws IOException {
SocketChannel channel = (SocketChannel) selectionKey.channel();
if (request == null || request.length() == 0) {
return;
}
String response = "echo: " + request;
log.info("send response ==> {}", response);
channel.write(StandardCharsets.UTF_8.encode(response));
this.selector.wakeup();
// 让这个key关注读取事件
selectionKey.interestOps(SelectionKey.OP_READ);
selectionKey.attach(new ReadHandler(selector));
}
}
public static void main(String[] args) throws IOException, InterruptedException {
EchoServerReactor echoServerReactor = new EchoServerReactor(10010);
echoServerReactor.start();
}
}
- 实现一个简单的客户端测试
import lombok.extern.slf4j.Slf4j;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.channels.SocketChannel;
@Slf4j
public class BlockEchoClient {
private final SocketChannel socketChannel;
private final String serverHost;
private final int serverPort;
public BlockEchoClient(String serverHost, int serverPort) throws IOException {
this.serverHost = serverHost;
this.serverPort = serverPort;
this.socketChannel = SocketChannel.open();
// 连接服务器
SocketAddress remote = new InetSocketAddress(serverHost, serverPort);
socketChannel.connect(remote);
log.info("connect echo server success");
}
public void send(String message) {
try {
BufferedReader reader = getReader(socketChannel.socket());
PrintWriter writer = getWriter(socketChannel.socket());
// 发送数据
writer.println(message + "\r\n");
log.info("send request success; content is [{}]", message);
// 读取服务端的响应
String s1 = reader.readLine();
log.info("get response success; response is [{}]", s1);
} catch (Exception e) {
e.printStackTrace();
}
}
public void close() throws IOException {
if(socketChannel != null){
socketChannel.close();
}
}
public BufferedReader getReader(Socket socket) throws IOException {
InputStream inputStream = socket.getInputStream();
return new BufferedReader(new InputStreamReader(inputStream));
}
public PrintWriter getWriter(Socket socket) throws IOException {
return new PrintWriter(socket.getOutputStream(), true);
}
public static void main(String[] args) throws IOException {
BlockEchoClient blockEchoClient = new BlockEchoClient("localhost",10010);
blockEchoClient.send("java");
blockEchoClient.send("hello");
blockEchoClient.send("bye");
blockEchoClient.send("hhhhhhh");
}
}
3 多线程的Reactor
多线程Reactor的演进分为两个方面: (1)升级Handler。既要使用多线程,又要尽可能高效率,则可以考虑使用线程池。 (2)升级Reactor。可以考虑引入多个Selector(选择器),提升选择大量通道的能力。
多线程版本的Reactor模式大致如下:
- 将负责数据传输处理的IOHandler处理器的执行放入独立的线程池中。这样,业务处理线程与负责新连接监听的反应器线程就能相互隔离,避免服务器的连接监听受到阻塞。
- 如果服务器为多核的CPU,可以将反应器线程拆分为多个子反应器(SubReactor)线程;同时,引入多个选择器,并且为每一个SubReactor引入一个线程,一个线程负责一个选择器的事件轮询。这样充分释放了系统资源的能力,也大大提升了反应器管理大量连接或者监听大量传输通道的能力。