Reactor模式认识

344 阅读9分钟

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模型

单线程reactor模型.png

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();
    }

}

  1. 实现一个简单的客户端测试
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模式大致如下:

  1. 将负责数据传输处理的IOHandler处理器的执行放入独立的线程池中。这样,业务处理线程与负责新连接监听的反应器线程就能相互隔离,避免服务器的连接监听受到阻塞。
  2. 如果服务器为多核的CPU,可以将反应器线程拆分为多个子反应器(SubReactor)线程;同时,引入多个选择器,并且为每一个SubReactor引入一个线程,一个线程负责一个选择器的事件轮询。这样充分释放了系统资源的能力,也大大提升了反应器管理大量连接或者监听大量传输通道的能力。