Reactor reactor pattern

So far, high-performance network programming can not get around the reactor mode. Many well-known server software or middleware are implemented based on the reactor mode. For example, Nginx, the “most famous and highest performance” web server in the universe, is based on the reactor model; Redis, as one of the highest performance cache servers, is also based on the reactor model; “, Netty, a high-performance communication middleware widely used in open source projects, is based on the reactor mode. From a development point of view, if you want to complete and be competent in high-performance server development, the reactor mode must be learned and mastered. From the perspective of learning, the reactor mode is equivalent to a very important basic knowledge of high performance and high concurrency. Only by mastering it can we truly master the famous middleware technologies such as Nginx, Redis, and Netty. Because of this, in the interview process of large Internet companies such as Ali, Tencent, and JD.com, questions related to the reactor mode are frequently asked interview questions. In short, the reactor mode is a must-know and necessary mode for high-performance network programming.
What is the Reactor reactor pattern? This article stands on the shoulders of giants and quotes Doug Lea (a master who is infinitely admired, one of the important authors of Concurrent in Java) to define the reactor mode in the article “Scalable IO in Java” , the details are as follows: The reactor mode consists of two roles: Reactor reactor thread and Handlers processor:
(1) Responsibilities of the Reactor reactor thread: responsible for responding to IO events and distributing them to Handlers processors.
(2) Responsibilities of the Handlers processor: non-blocking execution of business processing logic.

Single-threaded Reactor mode

Overall, the Reactor pattern is somewhat similar to the event-driven pattern. In the event-driven mode, when an event is triggered, the event source will distribute the event dispatch to the handler processor for event processing. The reactor role in the reactor pattern is similar to the dispatcher event dispatcher role in the event-driven pattern. As mentioned earlier, in the reactor mode, there are two important components of the Reactor reactor and the Handler processor:
(1) Reactor reactor: responsible for querying IO events, when an IO event is detected, it is sent to the corresponding Handler processor for processing. The IO event here is the channel IO event monitored by the selector in NIO.
(2) Handler processor: bound to IO events (or selection keys), responsible for the processing of IO events. Complete the real connection establishment, read the channel, process business logic, write the result to the channel, etc.
What is the single-threaded version of the Reactor reactor pattern? Simply put, Reactors and Handers are executed in one thread. It is the simplest reactor model
image.png

Based on Java NIO, how to implement a simple single-threaded version of the reactor mode? Several important member methods that need to use the SelectionKey selection key:
Method 1: void attach(Object o)
This method can add any Java POJO object as an attachment to the SelectionKey instance, which is equivalent to the setter method of the attachment property. This method is very important, because in the single-threaded version of the reactor mode, the Handler processor instance needs to be added to the SelectionKey instance as an attachment.
Method 2: Object attachment()
The function of this method is to remove the attachment that was added to the SelectionKey instance through attach(Object o) before, which is equivalent to the getter method of the attachment property and is used in conjunction with attach(Object o). This method is also very important. When an IO event occurs and the selection key is selected by the select method, the attachment of the event can be directly taken out, that is, the previously bound Handler processor instance, and the corresponding processing can be completed through the Handler.
In short, in the reactor mode, a combination of attach and attachment is required: after the registration of the selection key is completed, the attach method is called to bind the Handler processor to the selection key; when an event occurs, the attachment method is called, and the selection key can be retrieved Take out the Handler processor, distribute the event to the Handler processor, and complete the business processing.

Reference code for single-threaded Reactor

package org.example.sicard;

import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Iterator;
import java.util.Set;

public class Reactor implements Runnable {<!-- -->

    Selector selector;

    ServerSocketChannel serverSocketChannel;

    public void reactor() throws ClosedChannelException {<!-- -->
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        selectionKey. attach(new AcceptorHandler());

    }

    @Override
    public void run() {<!-- -->
        while (!Thread.interrupted()){<!-- -->
            Set selected = selector. selectedKeys();
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {<!-- -->
                //The reactor is responsible for the events received by dispatch
                SelectionKey sk = it. next();
                dispatch(sk);
            }
            selected. clear();
        }

    }

    //Reactor dispatch method
    void dispatch(SelectionKey k) {<!-- -->
        Runnable handler = (Runnable) (k. attachment());
        //Call the handler processor object bound to the selection key before
        if (handler != null) {<!-- -->
            handler. run();
        }
    }
    // new connection handler


    class AcceptorHandler implements Runnable {<!-- -->

        @Override
        public void run() {<!-- -->

        }
    }
}

In the above code, a Handler processor is designed, called the AcceptorHandler processor, which is an inner class. After registering the serverSocket service to monitor the connection acceptance event, create an instance of the AcceptorHandler new connection handler, which is attached to the SelectionKey as an attachment.
//Register serverSocket accept (accept) event
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
// Bind the new connection handler as an attachment to the sk selection key
sk. attach(new AcceptorHandler());
When a new connection event occurs, the Handler business processor previously attached to the SelectionKey is taken out to perform various IO processing of the socket.
void dispatch(SelectionKey k) {
Runnable r = (Runnable) (k.attachment());
//Call the handler object bound to the selection key before
if (r != null) {
r. run(); }
}
The AcceptorHandler processor has two responsibilities: one is to accept new connections, and the other is to create an input and output Handler processor for new connections, called IOHandler.
// new connection handler
class AcceptorHandler implements Runnable {
public void run() {
// accept new connections
// Need to create an input and output handler for the new connection
} }
IOHandler, as the name suggests, is responsible for socket data input, business processing, and result output. The sample code is as follows:
package com.crazymakercircle.ReactorModel;
//…
class IOHandler implements Runnable {
final SocketChannel channel;
final SelectionKeysk;
IOHandler (Selector selector, SocketChannel c) throws IOException {
channel = c;
c. configureBlocking(false);
//Only get the selection key, set the interested IO event later
sk = channel. register(selector, 0);
//Use the Handler processor as an attachment to the selection key
sk. attach(this);
// Register read and write ready event
sk.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);
}
public void run() {
// …handle input and output
} }
In the constructor of IOHandler, there are two important points:
(1) Register the new SocketChannel transmission channel to the same selector of the Reactor class. This ensures that the Reactor class and the Handler class are executed in the same thread.
(2) After the registration of the Channel transmission channel is completed, attach the IOHandler itself to the selection key as an attachment. In this way, when the Reactor class distributes events (selection keys), the run method of IOHandler can be executed. If the sample code above is tongue-twisting, don’t worry. In order to thoroughly understand the beauty of it, develop an executable example by yourself. Based on the reactor mode, an EchoServer echo server instance is implemented below. After carefully reading and running this example, you can understand the true meaning of the above tongue-twisting program code.

Disadvantages of single-threaded Reactor reactor mode

The single-threaded Reactor reactor mode is implemented based on Java’s NIO. Compared with the traditional multi-threaded OIO, the reactor mode no longer needs to start thousands of threads, and the efficiency is naturally greatly improved. In the single-threaded reactor mode, both the Reactor reactor and the Handler processor are executed on the same thread. This brings about a problem: when one of the Handlers is blocked, all other Handlers will not be executed. In this scenario, if the blocked Handler is not only responsible for the business of input and output processing, but also includes the AcceptorHandler processor responsible for connection monitoring. This is a very serious problem. Why? Once the AcceptorHandler processor is blocked, the entire service cannot receive new connections, making the server unavailable. Because of this flaw, the single-threaded reactor model is used less frequently. In addition, the current servers are all multi-core, and the single-threaded reactor mode model cannot make full use of multi-core resources. In short, in high-performance server application scenarios, the single-threaded reactor mode is rarely used in practice.

Multi-threaded Reactor mode

The evolution of the multi-thread pool Reactor reactor is divided into two aspects:
(1) The first is to upgrade the Handler processor. If you want to use multithreading and be as efficient as possible, you can consider using a thread pool.
(2) The second is to upgrade the Reactor reactor. Consider introducing multiple Selectors to improve the ability to select a large number of channels. In general, the mode of a multi-threaded pool reactor is roughly as follows:
(1) Put the execution of the IOHandler processor responsible for input and output processing into an independent thread pool. In this way, the business processing thread is isolated from the reactor thread responsible for service monitoring and IO event query, so as to prevent the connection monitoring of the server from being blocked.
(2) If the server is a multi-core CPU, the reactor thread can be split into multiple sub-reactor (SubReactor) threads; at the same time, multiple selectors are introduced, and each SubReactor sub-thread is responsible for a selector. In this way, the ability to fully release system resources; also improves the ability of the reactor to manage a large number of connections and select a large number of channels.

A practical case of a multi-threaded Handler processor

Based on the program code of the EchoHandler echo processor of the previous single-threaded reactor, it is improved, and the new echo processor is: MultiThreadEchoHandler. The main upgrade is the introduction of a thread pool (ThreadPool). The business processing code is executed in its own thread pool, and the business processing thread and the reactor IO event thread are completely isolated. The code for this practice case is as follows

Summary of the Reactor pattern

Before summarizing the reactor mode, first look at the comparison with other modes to strengthen its understanding.

1. Comparison between reactor mode and producer consumer mode

Similarities: In a way, the Reactor pattern is somewhat similar to the Producer Consumer pattern. In the producer-consumer mode, one or more producers add events to a queue, and one or more consumers actively extract (Pull) events from this queue for processing.
The difference is that the reactor mode is based on queries, and there is no special queue to buffer and store IO events. After querying IO events, the reactor will distribute them to the corresponding Handler processors according to different IO selection keys (events) deal with.

2. Comparison between Reactor Pattern and Observer Pattern

The similarity is that in the reactor mode, when the IO events are queried, the service handler uses a single/multiple distribution (Dispatch) strategy to distribute these IO events synchronously.
The observer pattern (Observer Pattern) is also called the publish/subscribe pattern, which defines a dependency relationship that allows multiple observers to listen to a topic (Topic) at the same time. This subject object notifies all observers when the state changes, and they can perform corresponding processing.
The difference is: In the reactor mode, the subscription relationship between the Handler processor instance and the IO event (selection key) is basically an event bound to a Handler processor; after each IO event (selection key) is queried , the reactor will distribute the event to the bound Handler processor; in the observer mode, at the same time, the same topic can be processed by multiple observers who have subscribed.
Finally, summarize the advantages and disadvantages of the reactor pattern. As a high-performance IO mode, the advantages of the reactor mode are as follows:
Fast response, although the same reactor thread itself is synchronous, it will not be blocked by synchronous IO of a single connection;
·Programming is relatively simple, which avoids complex multi-thread synchronization to the greatest extent, and also avoids the overhead of switching between multi-threaded processes;
·Extensible, it is convenient to make full use of CPU resources by increasing the number of reactor threads.
The disadvantages of the reactor pattern are as follows:
· Reactor mode adds a certain complexity, so there is a certain threshold, and it is not easy to debug.
The reactor mode requires the support of IO multiplexing at the bottom of the operating system, such as epoll in Linux. If the bottom layer of the operating system does not support IO multiplexing, the reactor mode will not be so efficient.
·In the same Handler business thread, if there is a long-term data read and write, it will affect the IO processing of other channels in this reactor. For example, when large files are transferred, IO operations will affect the response time of other clients (Clients). For this operation, further improvements to the reactor model are therefore required.