Java NIO implementation of Reactor model

Keywords: Programming socket encoding Netty network

The implementation of Reactor model can be divided into the following three types:

  • Single thread model
  • Single Reactor multithreading model
  • Master slave Reactor multithreading model.

Single thread model

The Reactor single thread model refers to that all IO operations are completed on the same thread. The responsibilities of the thread are as follows:

  • As NIO server, receiving TCP connection of client;

  • As NIO client, initiate TCP connection to the server;

  • Read the request or reply message of the communication peer;

  • Send a message request or reply message to the communication peer.

Because the Reactor mode uses asynchronous non blocking IO, all IO operations will not cause blocking. In theory, a thread can handle all IO related operations independently. From the perspective of architecture, a NIO thread can fulfill its responsibilities. For example, receive the TCP connection request message from the client through the Acceptor. After the link is established successfully, Dispatch the corresponding ByteBuffer to the specified Handler for message decoding. The user thread can send messages to the client through NIO thread through message encoding.

Server terminal
public class Reactor1 {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(1234));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (selector.select() > 0) {
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel acceptServerSocketChannel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = acceptServerSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    System.out.println("accept from "+socketChannel.socket().getInetAddress().toString());
                  //  LOGGER.info("Accept request from {}", socketChannel.getRemoteAddress());
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable() && key.isValid()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int count = socketChannel.read(buffer);
                    if (count <= 0) {
                        socketChannel.close();
                        key.cancel();
                        System.out.println("Received invalide data, close the connection");
                        //LOGGER.info("Received invalide data, close the connection");
                        continue;
                    }
                    System.out.println("Received message"+new String(buffer.array()));
                    //LOGGER.info("Received message {}", new String(buffer.array()));
                }
                keys.remove(key);
            }
        }
    }
}
Client
public class Client1 {

    public static void main(String[] args) throws IOException, InterruptedException {
        SocketChannel socketChannel;
        socketChannel = SocketChannel.open();
        //socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("localhost", 1234));
        Date now = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");//Date format can be easily modified
        String str = dateFormat.format( now );
        byte[] requst = str.getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(requst.length);
        buffer.put(requst);
        buffer.flip();
        try {
            while (buffer.hasRemaining()) {
                socketChannel.write(buffer);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
        socketChannel.close();
    }
}

For some small capacity application scenarios, you can use the single thread model. However, it is not suitable for applications with high load and large concurrency. The main reasons are as follows:

A NIO thread can handle hundreds of links at the same time, which can't support the performance. Even if the CPU load of NIO thread reaches 100%, it can't satisfy the coding, decoding, reading and sending of massive messages; When the NIO thread is overloaded, the processing speed will slow down, which will lead to a large number of client connection timeouts. After the timeout, it will often be retransmitted, which aggravates the load of NIO thread, and eventually leads to a large number of message backlog and processing timeouts, which becomes the performance bottleneck of the system; Reliability problem: once NIO thread runs unexpectedly or enters a dead cycle, the whole system communication module will be unavailable, unable to receive and process external messages, resulting in node failure. In order to solve these problems, Reactor multithreading model is introduced.

Single Reactor multithreading model

In the classic reactor mode, although a thread can monitor multiple requests at the same time, all read / write requests and the processing of new connection requests are processed in the same thread, which can not make full use of the advantages of multiple CPU s. At the same time, the read / write operation will block the processing of new connection requests. When the read and write events of IO are obtained, they are handed over to the thread pool for processing, which can reduce the performance overhead of the main reactor, so as to focus on event distribution and improve the throughput of the whole application.

Characteristics of Reactor multithreading model:

  1. There is a special NIO thread - Acceptor thread for listening to the server and receiving the TCP connection request from the client;

  2. A NIO thread pool is responsible for network IO operations, such as reading and writing. The thread pool can be realized by using a standard JDK thread pool. It contains a task queue and N available threads. These NIO threads are responsible for reading, decoding, encoding and sending messages;

  3. One NIO thread can handle N links at the same time, but one link only corresponds to one NIO thread to prevent concurrent operation problems.

In most scenarios, the Reactor multithreading model can meet the performance requirements;

Implementation of server
public class Reactor2 {
    private static ExecutorService pool = Executors.newFixedThreadPool(100);
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(1234));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while(true) {
            if(selector.selectNow() < 0){
                continue;
            }
            Set<SelectionKey> sets = selector.selectedKeys();
            Iterator<SelectionKey> keys = sets.iterator();
            while(keys.hasNext()) {
                SelectionKey key = keys.next();
                keys.remove();
                if(key.isAcceptable()) {
                    ServerSocketChannel Serverchannel = (ServerSocketChannel) key.channel();
                    SocketChannel channel = Serverchannel.accept();
                    channel.configureBlocking(false);
                    System.out.println("accept from "+channel.socket().getInetAddress().toString());
                    channel.register(selector, SelectionKey.OP_READ);
                }else if(key.isValid()&&key.isReadable()) {
                    pool.submit(new Processor(key));
                }
            }
        }
    }
}
class Processor implements Callable {
    SelectionKey key;

    public Processor(SelectionKey key) {
        this.key = key;
    }

    @Override
    public Object call() throws Exception {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        SocketChannel socketChannel = (SocketChannel) key.channel();
        int count = socketChannel.read(buffer);
        if (count <  0) {
            key.cancel();
            socketChannel.close();

            System.out.println("Received invalide data, close the connection");
            return null;
        }else if(count==0) {
            return null;
        }
            System.out.println("Received message"+new String(buffer.array()));
            System.out.println("current thread"+Thread.currentThread().toString());
        return null;
    }
}

In very few special scenarios, there may be performance problems when a NIO thread is responsible for listening and processing all client connections. For example, the concurrent connection of millions of clients, or the server needs to perform security authentication for client handshake, but the authentication itself is very performance consuming. In this kind of scenario, a single Acceptor thread may have insufficient performance. In order to solve the performance problem, a third kind of Reactor thread model, master-slave Reactor multithread model, is produced.

Multiple Reactor modes (master-slave Reactor)

In the Reactor mode used in Netty, multiple reactors are introduced, that is, one main Reactor is responsible for monitoring all connection requests, and multiple sub reactors are responsible for monitoring and processing read / write requests, which reduces the pressure of the main Reactor and the delay caused by too much pressure of the main Reactor. And each sub Reactor belongs to an independent thread, and all operations of each Channel after successful connection are handled by the same thread. This ensures that all the States and contexts of the same request are in the same thread, avoids unnecessary context switching, and facilitates monitoring the response state of the request.

Architecture diagram of multiple Reactor modes

public class MainReactor {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(1234));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        int coreNum = Runtime.getRuntime().availableProcessors();
        FollowerReactor[] followers = new FollowerReactor[coreNum];
        for(int i=0; i<coreNum; i++) {
            followers[i] = new FollowerReactor();
        }

        int index = 0;
        while(selector.select()>0) {
            Set<SelectionKey> keys = selector.selectedKeys();
            for(SelectionKey key:keys) {
                keys.remove(key);
                if(key.isValid()&&key.isAcceptable()) {
                    ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = serverSocketChannel1.accept();
                    socketChannel.configureBlocking(false);
                    System.out.println("Accept request:" + socketChannel.socket().getInetAddress());
                    FollowerReactor follower = followers[++index%coreNum];
                    follower.register(socketChannel);
                    //follower.wakeUp();
                }
            }
        }

    }
}

The above code is the main Reactor, and the sub Reactor is based on twice the number of available cores of the previous machine (consistent with the default number of sub reactors of Netty). For each successfully connected SocketChannel, it is handed over to different sub reactors through round robin. The code of sub Reactor is as follows:

public class FollowerReactor {
    private Selector selector;
    private static  ExecutorService service =Executors.newFixedThreadPool(
            2*Runtime.getRuntime().availableProcessors());
    public void register(SocketChannel socketChannel) throws ClosedChannelException {
        socketChannel.register(selector, SelectionKey.OP_READ);
    }
    public void wakeUp() {
    }
    public FollowerReactor() throws IOException {
        selector = Selector.open();
        select();
    }
    public void wakeup() {
        this.selector.wakeup();
    }
    public void select() {
        service.submit(() -> {
            while(true) {
                if(selector.select(500)<=0) {
                    continue;
                }
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = keys.iterator();
                while(iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if(key.isReadable()) {
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        SocketChannel channel = (SocketChannel) key.channel();
                        int count = channel.read(buffer);
                        if(count<0) {
                            channel.close();
                            key.cancel();
                            System.out.println(channel+"->red end !");
                            continue;
                        }else if(count==0) {
                            System.out.println(channel+",size is 0 !");
                            continue;
                        }else{
                            System.out.println(channel+",message is :"+new String(buffer.array()));

                        }
                    }
                }
            }
        });

    }
}

A static thread pool is created in the sub Reactor, and the size of the thread pool is twice the number of machine cores. Each word of the Reactor changes to a selector instance. Each time a colleague creates a sub Reactor, he submits a task to the thread pool, blocks to the selector method, and continues to execute until the new channel is registered on the selector.

Reference address

If you like my article, you can pay attention to the personal subscription number. Welcome to leave a message and communicate at any time. If you want to join the wechat group, please add the administrator's short stack culture - small assistant (lastpass4u), who will pull you into the group.

Posted by nightowl on Sat, 29 Feb 2020 20:44:02 -0800