Use 3.7W to talk with you about Java network programming

Posted by tim_ver on Wed, 27 Oct 2021 10:19:44 +0200

Yesterday, I went home by car and punched in a force buckle on the car. After I finished, I was bored. I saw that I had collected Akka before. I vaguely remember that it was an execution unit smaller than a thread (that's what I understood at that time). In addition, I learned from Golang before and saw the way that Goroutine can handle requests without brains, I just suffer from the pile of shit code written by WebFlux+Reactor. So I thought: why doesn't Java have this kind of thing and can open lightweight threading without brain?

Later, I saw that Java was promoting a project to realize this function. I forgot the name. However, since it is being implemented, it is not available at present. However, after seeing the comparison between Golang processing method and Java NIO, it is found that the essence of both is to make one thread handle one thing instead of one thread handle multiple things. The implementation of Golang's collaboration lies in Golang's custom scheduler; But we can talk about NIO of Java.

I seem to be limited to NIO. Write about NIO's single threaded, multi-threaded, master-slave multi-threaded Echo Server. Write HelloWorld or something with Netty. I haven't delved into some implementations. I'm ashamed to remember the sign of "rigorous scholarship" hanging on the dormitory bed (this sign also has its origin), so I used my car time to Google. With my own understanding, I decided to write this article.

What is I/O?

Since it is NIO/BIO/AIO, we must first understand what I/O is. I thought it was a simple read-write operation, but later I found it was not so simple.

I/O list is input / output according to literal translation. For example, read from / write to the disk, receive data from / write data to the network card, query / add, delete and modify the database. All operations involving disk and network are collectively referred to as I/O operations. Let me give such a general definition first.

What is blocking?

Some conventions: unless otherwise specified, the blocking here is I/O blocking, excluding the blocking caused by lock contention during synchronization. The blocked scenario in which a thread cannot execute because of locking resources is the same as the scenario in which a thread is blocked while waiting for I/O operations. Both are to obtain a resource but fail, and the blocked thread cannot continue to execute. Only when the resource is obtained can the thread continue to execute.

Before understanding congestion, let's first understand the speed of each device:

CPU: 1ns, register: 1ns, cache: 10ns, memory: 10us, disk: 10ms, network: 100ms

Where: 1s=1000ms, 1ms=1000us, 1us=1000ns, 1ns=1000ps.

I/O operates the disk or network, and the speed difference between I/O operation and memory operation is more than 1000 times and more than 10 ^ 6 times compared with CPU. Therefore, in the eyes of CPU, any I/O operation is very long.

Because of this, the blocking caused by I/O operation will cause the current thread to be scheduled into the waiting queue (see thread state switching for details), and then when the data arrives, switch the thread to the ready list and wait for scheduling.

The so-called blocking refers to the process that a thread waits for an I/O operation to complete. When a thread wants to read data from the network, it needs to wait for the network card to be ready, then transfer the data, and then the kernel copies the data to the user's memory. Writing needs to wait until the network card is ready, and all the write requests queued in front are completed, and then the data transmission is completed. This is a long process.

The blocking in network programming refers to this long network process. The thread is suspended and placed in the waiting queue. When the dry data arrives, the dry data is written out. The program is stopped here and will not be executed below.

Let's look at a picture.

The blue part is blocking, the blocking process during network operation.

Blocking / non blocking IO? Synchronous / asynchronous IO?

Well, now we know that blocking is that the thread is suspended and cannot continue because the I/O operation is too slow.

A standard network read is as follows:

  • one ⃣ The network card receives data and puts it into the kernel space
  • two ⃣ The kernel copies the data to user space.

A standard network is written as follows:

  • one ⃣ ﹥ the kernel copies the data to the kernel space
  • two ⃣ The network card reads data from the kernel space and sends the data.

The basis for judging whether an operation is I/O blocking lies in the first step ⃣ Whether the step is blocked; The second criterion is to judge whether an IO is synchronous / asynchronous ⃣ Check whether the step is blocked.

Can you understand this?

BIO/NIO/AIO

I have written Socket communication. No matter what language you use, it is basically the following process:

Create a new ServerSocket = > set the listening address = > accept a connection and return a Socket = > continue listening. The new thread processes the Socket just returned.

There's nothing wrong with this. A very ordinary Socket/ServerSocket server, isn't it! Early Tomcat was like this.

Now let's take a look at the part involving threads in the whole process, that is, the part of creating new threads to process sockets. Why do you do this?

Because the network read / write of each Socket is a time-consuming process, if we do not open new threads, the subsequent connections will be blocked, so the connection number and throughput of the whole system will be greatly reduced.

One thread per connection seems to be a good solution, and it also solves the problem of unable to connect more than one thread. But will one thread per connection crash when the number of system connections is high? After all, Java threads are operating system threads, and the next thread in Linux is close to a process, so the cost of creating and destroying scheduling is not small at all. Even if we have a wired process pool, it is limited after all. Moreover, thread pool maintenance is also a burden. Is there a solution?

So far, BIO is over, and then it's time to introduce NIO/AIO. NIO refers to non blocking IO, that is, instead of waiting for data to be readable like BIO, NIO returns directly each time. The return value represents the size of readable data, and - 1 means unreadable. Therefore, NIO's non blocking is implemented here, and NIO only returns immediately after the read operation, Instead of waiting for the data to be read before returning. The same is true for write operations.

Smart readers must immediately realize that you haven't changed much. After all, if you want to read data, you have to keep polling the return value to see whether it is readable. This will also cause CPU idling, which is not as good as BIO! That's true. So NIO just doesn't block the program, but it doesn't speed up the total time.

So far, we know that Socket processing requires threads because the network read / write is too slow (here we temporarily ignore the time-consuming operations in the business logic, such as querying the database, etc.), and the threads can't work, so blocking occurs. In addition, we know that the CPU does not work when the program is blocked. Why can't we let another thread execute during this period? But this introduces a new problem. If I schedule another thread, how can I know when the data is ready or can be written?

Um... I smell a hint of asynchronous + callback. Since the program has no way, let's see what solution the operating system can provide us?

Linux provides Epoll (select and poll are basically not used now, I won't talk about it), Mac OS provides Kqueue, and Windows provides IOCP. They are I/O multiplexing technologies.

I/O multiplexing was just mentioned. What does that mean?

Before I say this again, I will introduce some concepts:

  • one ⃣ Interrupt: interrupt refers to the means by which the computer hardware other than CPU places a signal on the bus to remind the CPU of the arrival of something. For example, clock interrupt is to send an interrupt signal at a fixed frequency to tell the CPU the time interval; Disk interrupt generally means that the data is read and placed at the specified memory address. Network interrupt generally means that a data arrives at the network card and is placed at the specified memory address.
  • two ⃣ Interrupt handler: a small program that will call the corresponding interrupt handler according to the interrupt type when the interrupt is captured by the CPU; The ratio mode clock interrupt can be used to trigger the thread scheduler, which is suitable for time-sharing systems.
  • three ⃣ File descriptor FD: everything in Linux is a file. The processing of any device is no more than four operations: open, close, read and write. Therefore, the file descriptor is the ID of an abstract file, which can be a file on disk, a remote process (ip:port), a keyboard and a mouse.
  • four ⃣ DMA: direct memory access, CPU assistant. The CPU is no longer solely responsible for reading and writing I/O devices, but handed over by the CPU to DMA, which is generally a chip on the motherboard.

The essence of I/O multiplexing is the interrupt of network card and other devices + the corresponding interrupt handler = > the encapsulation and use of high-level language.

Why did you say so much? FD is introduced to illustrate that every Socket (a Socket is a remote process in essence. In the Internet world, IP:PORT can uniquely locate a process in the world) has a unique FD representing it. Here, we use NIO's SocketChannel to replace the Socket in BIO. Both represent the sockets of remote processes. Epoll registers these FDS in the Linux file system and manages them through a red black tree. This red black tree stores the structure of (FD, SocketChannel). Through FD, you can quickly find the corresponding SocketChannel (only about this correspondence). When an FD under this red black tree is readable (network card interrupt implementation) or writable (network card interrupt Implementation), Add to the ready list.

By registering a processing function for the interrupt program, you can add the FD corresponding to the interrupt to the ready list and wake up the select method of Epoll every time the network card is interrupted. The select method of Epoll is to traverse the ready list, find the SocketChannel corresponding to each FD, and return. If it is empty, it will block. Wakes up when a new interrupt is generated.

Now we leave the remote process readable / writable to the operating system, which uses network card interrupt to realize asynchronous notification. We can liberate our program without having to wait.

Wait a minute, liberated our program? Automatic notification of read / write availability? How does this look Nice! Let's try to splice it with NIO.

Oh, roar ~ NIO+I/O multiplexing = > reactor model.

Therefore, the Reactor model depends on the operating system. In fact, it depends on hardware interrupts. All hardware notifications in the whole computer are realized through interrupts (to be honest here, other hardware interrupts are realized by polling their own state. In fact, CPU response to interrupts is also realized by viewing pin signals every clock cycle).

NIO+I/O multiplexing is embodied in Java as ServerSocketChannel+SocketChannel+Selector+SelectionKey+Buffer. Take a look at a typical Echo server based on NIO+I/O multiplexing.

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.channels.spi.AbstractSelectableChannel;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author CodeWithBuff
 */
public class NioTcpSingleThread {

    public static void main(String[] args) {
        NioTcpSingleThread.Server.builder().build().run();
    }

    private static final HashMap<SocketChannel, List<DataLoad>> dataLoads = new LinkedHashMap<>();

    private static final ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    private static class DataLoad {
        private int intValue;

        private long longValue;

        private double doubleValue;

        private String stringValue;

        private int[] intArray;

        private long[] longArray;

        private double[] doubleArray;

        private String[] stringArray;
    }

    /**
     * Java NIO There are only four core components for network processing: {@ link Channel}, {@ link Selector}, {@ link SelectionKey}, and {@ link java.nio.Buffer}
     * <br/>
     * Talk about the relationship between {@ link ServerSocketChannel}, {@ link SocketChannel}, {@ link Selector} and {@ link SelectionKey}.
     * <br/>
     * {@link ServerSocketChannel}Not to mention {@ link SocketChannel}, it is nothing more than the difference between establishing a connection on the server and processing a connection (actual I/O interaction). Here, it is collectively referred to as {@ link AbstractSelectableChannel}, that is, the class inherited by both of them.
     * <br/>
     * {@link Selector#select()}Call the system call, poll the port and record the events of interest to the registered {@ link AbstractSelectableChannel}. If one of the events of interest to the registered {@ link AbstractSelectableChannel} occurs, it will return. Otherwise it will be blocked.
     * <br/>
     * For {@ link AbstractSelectableChannel}, how can {@ link Selector} help itself record and poll events of interest? The answer is: just register with {@ link Selector} and set the event type of interest.
     * <br/>
     * After successful registration, a variable of type {@ link SelectionKey} will be returned, through which {@ link AbstractSelectableChannel} and {@ link Selector} can be operated. The {@ link SelectionKey} itself is the credential of {@ link AbstractSelectableChannel} and the {@ link Selector} to which it is registered.
     * Like an order, the relationship between them is recorded. Therefore, in the subsequent operations of successful registration, it is generally implemented with {@ link SelectionKey}. At the same time, {@ link SelectionKey} also has an attachment() method to get the object attached to it.
     * Generally, we use this subsidiary object to handle the actual business of {@ link AbstractSelectableChannel} and {@ link Selector} contained in the current {@ link SelectionKey}.
     * <br/>
     * Just now we talked about {@ link Selector#select()}. It will block until an event of interest occurs. However, sometimes we can determine that an event has occurred immediately or already, so we can call the {@ link Selector#wakeup()} method to let {@ link Selector#select()} return immediately, and then get the
     * {@link SelectionKey}Collection or {@ link Selector#select()} (this is the next loop).
     * <br/>
     * <br/>
     * be careful!!! If a {@ link AbstractSelectableChannel} registers two different event types of interest on the same {@ link Selector}, the two returned {@ link SelectionKey} have no relationship. Although it can be modified again through {@ link SelectionKey}
     * {@link AbstractSelectableChannel}Type of event of interest. {@ link SelectionKey} generates a return only during registration, so there is (Channel + Selector) = SelectionKey. But, gee, it will get stuck when registering multiple, so don't register multiple with the same Channel and the same Selector!!!
     */
    @Builder
    private static class Server implements Runnable {

        @Override
        public void run() {
            System.out.println("Server Start running...");
            Selector globalSelector;
            ServerSocketChannel serverSocketChannel;
            SelectionKey serverSelectionKey;
            try {
                globalSelector = Selector.open();
                serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.bind(new InetSocketAddress(8190));
                serverSocketChannel.configureBlocking(false);
                serverSelectionKey = serverSocketChannel.register(globalSelector, SelectionKey.OP_ACCEPT);
                serverSelectionKey.attach(Acceptor.builder()
                        .globalSelector(globalSelector)
                        .serverSocketChannel(serverSocketChannel)
                        .build()
                );
                while (true) {
                    // select() is a serious blocking method. It will block until any event of interest to the registered (Server)SocketChannel occurs. For example, when a new connection is established, the Channel can read, or the Channel can write
                    // Its return value indicates that there are several events of interest, which is actually useless, so it is directly ignored here
                    globalSelector.select();
                    Set<SelectionKey> selectionKeySet = globalSelector.selectedKeys();
                    for (SelectionKey selectionKey : selectionKeySet) {
                        dispatch(selectionKey);
                        selectionKeySet.remove(selectionKey);
                    }
                }
            } catch (IOException ignored) {
            }
        }

        private void dispatch(SelectionKey selectionKey) {
            Runnable runnable = (Runnable) selectionKey.attachment();
            runnable.run();
        }
    }

    @Data
    @Builder
    private static class Acceptor implements Runnable {

        private final Selector globalSelector;

        private final ServerSocketChannel serverSocketChannel;

        @Override
        public void run() {
            try {
                SocketChannel socketChannel = serverSocketChannel.accept();
                System.out.println("Connection established...");
                socketChannel.configureBlocking(false);
                SelectionKey socketSelectionKey = socketChannel.register(globalSelector, SelectionKey.OP_READ);
                socketSelectionKey.attach(Handler.builder()
                        .socketSelectionKey(socketSelectionKey)
                        .build()
                );
                // At this time, the Channel of interest in reading is registered, so in order to quickly start reading, wake up the selector directly. In fact, let it wait. I'm ready. You should have data there. Go back directly.
                globalSelector.wakeup();
            } catch (IOException ignored) {
            }
        }
    }

    /**
     * "The "write" operation depends on the data read by the "read" operation, so you can't "write" again after "write", you must "read" or "close".
     * <br/>
     * "After the "read" operation, you can continue to "read" without waiting for "write to complete", so "write" can set the type of interest to "read" | "write" instead of just "write".
     */
    @Data
    @Builder
    private static class Handler implements Runnable {

        private final SelectionKey socketSelectionKey;

        @Override
        public void run() {
            SocketChannel socketChannel = (SocketChannel) socketSelectionKey.channel();
            if (!socketChannel.isOpen()) {
                System.out.println("Connection closed");
                try {
                    socketChannel.shutdownInput();
                    socketChannel.shutdownOutput();
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return ;
            }
            if (socketSelectionKey.isReadable()) {
                System.out.println("Read event occurs, ready to read...");
                Reader.builder()
                        .socketChannel(socketChannel)
                        .build()
                        .run();
                // Note: you are interested in both reading and writing (because the client may have a long connection and have to send messages again), but the same SelectionKey can only be one of reading or writing
                socketSelectionKey.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                // After reading, you should be ready to write
                socketSelectionKey.selector().wakeup();
            }
            if (socketSelectionKey.isWritable()) {
                System.out.println("Write event occurs, ready to write...");
                Writer.builder()
                        .socketChannel(socketChannel)
                        .build()
                        .run();
                socketSelectionKey.interestOps(SelectionKey.OP_READ);
                // When you're finished, you'll be free to return immediately
                // socketSelectionKey.selector().wakeup();
            }
        }
    }

    @Data
    @Builder
    private static class Reader implements Runnable {

        private final SocketChannel socketChannel;

        @Override
        public void run() {
            try {
                byteBuffer.clear();
                int readable = socketChannel.read(byteBuffer);
                byte[] bytes = byteBuffer.array();
                String value = new String(bytes, 0, readable);
                System.out.println("I read it: " + value);
                DataLoad dataLoad = DataLoad.builder()
                        .stringValue(value)
                        .build();
                List<DataLoad> tmp = dataLoads.computeIfAbsent(socketChannel, k -> new LinkedList<>());
                tmp.add(dataLoad);
            } catch (IOException ignored) {
            }
        }
    }

    @Data
    @Builder
    private static class Writer implements Runnable {

        private final SocketChannel socketChannel;

        @Override
        public void run() {
            try {
                String value = "Server get: " + dataLoads.get(socketChannel).get(0).getStringValue();
                dataLoads.get(socketChannel).remove(0);
                socketChannel.write(ByteBuffer.wrap(value.getBytes(StandardCharsets.UTF_8)));
            } catch (IOException ignored) {
            }
        }
    }
}

NIO implements the notification mechanism when the data is readable / writable, and then reads and writes, while AIO directly transmits the data to the specified area. In short, it does not need to read and write by itself. When it is called, the data is all ready and more asynchronous. However, Linux is rarely used in actual production, so we won't mention it, but only give an example code:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author CodeWithBuff
 */
public class AioTcpSingleThread {

    public static void main(String[] args) {
        Server.builder().build().run();
        // Prevent the main thread from exiting
        LockSupport.park(Long.MAX_VALUE);
    }

    private static final ConcurrentHashMap<AsynchronousSocketChannel, LinkedBlockingQueue<DataLoad>> dataLoads = new ConcurrentHashMap<>();

    private static final ReentrantLock READ_LOCK = new ReentrantLock();

    private static final ReentrantLock WRITE_LOCK = new ReentrantLock();

    private static final ByteBuffer READ_BUFFER = ByteBuffer.allocate(1024 * 4);

    private static final ByteBuffer WRITE_BUFFER = ByteBuffer.allocate(1024 * 4);

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    private static class DataLoad {
        private int intValue;

        private long longValue;

        private double doubleValue;

        private String stringValue;

        private int[] intArray;

        private long[] longArray;

        private double[] doubleArray;

        private String[] stringArray;
    }

    @Builder
    private static class Server implements Runnable {

        @Override
        public void run() {
            try {
                System.out.println("Server startup...");
                asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
                asynchronousServerSocketChannel.bind(new InetSocketAddress(8190));
                asynchronousServerSocketChannel.accept(null, ACCEPTOR);
            } catch (IOException ignored) {
            }
        }
    }

    private static AsynchronousServerSocketChannel asynchronousServerSocketChannel = null;

    private static final Acceptor ACCEPTOR = new Acceptor();

    private static class Acceptor implements CompletionHandler<AsynchronousSocketChannel, Object> {
        // This method is called asynchronously, so don't worry about blocking the main thread
        @Override
        public void completed(AsynchronousSocketChannel result, Object attachment) {
            System.out.println("Connection establishment: " + Thread.currentThread().getName());
            System.out.println("Connection establishment");
            dataLoads.computeIfAbsent(result, k -> new LinkedBlockingQueue<>());
            // Use a loop to read and write multiple times
            while (result.isOpen()) {
                READ_LOCK.lock();
                // This method is also asynchronous
                result.read(READ_BUFFER, attachment, new Reader(result, READ_BUFFER.array()));
                READ_BUFFER.clear();
                READ_LOCK.unlock();
                WRITE_LOCK.lock();
                String ans = "";
                try {
                    ans = "Server get: " + dataLoads.get(result).take().getStringValue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // Asynchronous
                result.write(ByteBuffer.wrap(ans.getBytes(StandardCharsets.UTF_8)), attachment, new Writer(result));
                WRITE_LOCK.unlock();
            }
            System.out.println("End communication once");
            // Attempt to establish second wave communication
            asynchronousServerSocketChannel.accept(attachment, ACCEPTOR);
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            System.out.println("Failed to establish connection");
        }
    }

    private static class Reader implements CompletionHandler<Integer, Object> {

        private final AsynchronousSocketChannel asynchronousSocketChannel;

        private final byte[] bytes;

        public Reader(AsynchronousSocketChannel asynchronousSocketChannel, byte[] bytes) {
            this.asynchronousSocketChannel = asynchronousSocketChannel;
            this.bytes = bytes;
        }

        @Override
        public void completed(Integer result, Object attachment) {
            System.out.println("Read data: " + Thread.currentThread().getName());
            if (result == 0 || !asynchronousSocketChannel.isOpen()) {
                return ;
            } else if (result < 0) {
                shutdown(asynchronousSocketChannel);
                return ;
            }
            System.out.println("Read data: " + result);
            String value = new String(bytes, 0, result);
            System.out.println("I read it: " + value);
            LinkedBlockingQueue<DataLoad> tmp = dataLoads.get(asynchronousSocketChannel);
            DataLoad dataLoad = DataLoad.builder()
                    .stringValue(value)
                    .build();
            tmp.add(dataLoad);
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            System.out.println("read failure");
        }
    }

    private static class Writer implements CompletionHandler<Integer, Object> {

        private final AsynchronousSocketChannel asynchronousSocketChannel;

        public Writer(AsynchronousSocketChannel asynchronousSocketChannel) {
            this.asynchronousSocketChannel = asynchronousSocketChannel;
        }

        @Override
        public void completed(Integer result, Object attachment) {
            System.out.println("Write data: " + Thread.currentThread().getName());
            if (!asynchronousSocketChannel.isOpen()) {
                return ;
            }
            System.out.println("Write data: " + result);
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            System.out.println("Write failed");
        }
    }

    private static void shutdown(AsynchronousSocketChannel asynchronousSocketChannel) {
        try {
            asynchronousSocketChannel.shutdownInput();
            asynchronousSocketChannel.shutdownOutput();
            asynchronousSocketChannel.close();
        } catch (IOException ignore) {
        }
    }
}

What did NIO solve? Unresolved what?

NIO implements a thread to manage I/O operations of multiple connections, instead of one thread for each connection like BIO. It is essentially realized through interrupt mechanism + system call. This allows all available I/O events to be processed in one thread.

Note that if a Selector (generally, a Selector corresponds to one thread, and a thread corresponds to multiple selectors, which will reduce the Selector efficiency) registers only one connection, NIO and BIO are no different.

Remember why we went from BIO to NIO? Because BIO cannot resist a large number of connections, NIO solves the problem of large connections.

However, NIO will not increase the speed of each request. Remember, even when the number of connections is small, the processing speed is not as fast as BIO.

NIO precautions

Previously, we assumed that there should be no time-consuming business in NIO, but what if there must be, such as database operation? Here we refer to the implementation of NIO framework Netty.

Netty suggests that time-consuming operations should be processed through the incoming custom thread pool, that is, submit the task to the thread pool, and then add asynchronous calls. When the task is processed, continue to the next step. In short, time-consuming tasks are processed in the thread pool, and ordinary tasks are processed directly in NIO threads.

In fact, this also corresponds to the multithreaded Reactor model. In addition, there is a master-slave multi-threaded Reactor, which is to separate the connection operation and make a separate Selector to deal with the connection. The I/O operations after the connection are placed in other selectors, and the business is placed in the process pool.

Let's take a look at the corresponding codes of the two models:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author CodewithBuff
 */
public class NioTcpMultiThread {

    public static void main(String[] args) {
        Server.builder().build().run();
        Runnable target = executorService::shutdown;
        Thread shutdown = new Thread(target);
        Runtime.getRuntime().addShutdownHook(shutdown);
    }

    private static final ConcurrentHashMap<SocketChannel, LinkedBlockingQueue<DataLoad>> dataLoads = new ConcurrentHashMap<>();

    private static final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    private static final ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 4);

    private static final ReentrantLock reentrantLock = new ReentrantLock();

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    private static class DataLoad {
        private int intValue;

        private long longValue;

        private double doubleValue;

        private String stringValue;

        private int[] intArray;

        private long[] longArray;

        private double[] doubleArray;

        private String[] stringArray;
    }

    @Builder
    private static class Server implements Runnable {

        @Override
        public void run() {
            System.out.println("Server Start running...");
            Selector globalSelector;
            ServerSocketChannel serverSocketChannel;
            SelectionKey serverSelectionKey;
            try {
                globalSelector = Selector.open();
                serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.bind(new InetSocketAddress(8190));
                serverSocketChannel.configureBlocking(false);
                serverSelectionKey = serverSocketChannel.register(globalSelector, SelectionKey.OP_ACCEPT);
                serverSelectionKey.attach(Acceptor.builder()
                        .serverSelectionKey(serverSelectionKey)
                        .build()
                );
                while (true) {
                    int a = globalSelector.select();
                    Set<SelectionKey> selectionKeySet = globalSelector.selectedKeys();
                    for (SelectionKey selectionKey : selectionKeySet) {
                        dispatch(selectionKey);
                        selectionKeySet.remove(selectionKey);
                    }
                }
            } catch (IOException ignored) {
            }
        }

        private void dispatch(SelectionKey selectionKey) {
            Runnable runnable = (Runnable) selectionKey.attachment();
            runnable.run();
        }
    }

    @Data
    @Builder
    private static class Acceptor implements Runnable {

        private final SelectionKey serverSelectionKey;

        @Override
        public void run() {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) serverSelectionKey.channel();
            Selector globalSelector = serverSelectionKey.selector();
            SocketChannel socketChannel;
            try {
                socketChannel = serverSocketChannel.accept();
                System.out.println("Connection established...");
                socketChannel.configureBlocking(false);
                SelectionKey socketSelectionKey = socketChannel.register(globalSelector, SelectionKey.OP_READ);
                socketSelectionKey.attach(Handler.builder()
                        .socketSelectionKey(socketSelectionKey)
                        .build()
                );
                globalSelector.wakeup();
            } catch (IOException ignored) {
            }
        }
    }

    @Data
    @Builder
    private static class Handler implements Runnable {

        private final SelectionKey socketSelectionKey;

        @Override
        public void run() {
            if (!socketSelectionKey.channel().isOpen()) {
                System.out.println("Connection closed");
                try {
                    socketSelectionKey.channel().close();
                } catch (IOException ignored) {
                }
                return ;
            }
            dataLoads.computeIfAbsent((SocketChannel) socketSelectionKey.channel(), k -> new LinkedBlockingQueue<>());
            if (socketSelectionKey.isReadable()) {
                Reader reader = Reader.builder()
                        .socketSelectionKey(socketSelectionKey)
                        .build();
                Thread thread = new Thread(reader);
                socketSelectionKey.interestOps(SelectionKey.OP_WRITE);
                thread.start();
            } else if (socketSelectionKey.isWritable()) {
                Writer writer = Writer.builder()
                        .socketSelectionKey(socketSelectionKey)
                        .build();
                Thread thread = new Thread(writer);
                socketSelectionKey.interestOps(SelectionKey.OP_READ);
                thread.start();
            }
        }
    }

    @Data
    @Builder
    private static class Reader implements Runnable {

        private final SelectionKey socketSelectionKey;

        @Override
        public void run() {
            try {
                SocketChannel socketChannel = (SocketChannel) socketSelectionKey.channel();
                String value;
                reentrantLock.lock();
                if (socketChannel.isOpen()) {
                    int readable = socketChannel.read(byteBuffer);
                    if (readable == 0) {
                        value = null;
                        // System.out.println("read empty request");
                    } else if (readable < 0) {
                        value = null;
                        shutdownSocketChannel(socketChannel);
                    } else {
                        value = new String(byteBuffer.array(), 0, readable);
                    }
                } else {
                    value = null;
                }
                reentrantLock.unlock();
                if (value == null) {
                    return ;
                }
                System.out.println("I read it: " + value);
                DataLoad dataLoad = DataLoad.builder()
                        .stringValue(value)
                        .build();
                LinkedBlockingQueue<DataLoad> tmp = dataLoads.computeIfAbsent(socketChannel, k -> new LinkedBlockingQueue<>());
                tmp.add(dataLoad);
                socketSelectionKey.selector().wakeup();
            } catch (IOException ignored) {
            }
        }
    }

    @Data
    @Builder
    private static class Writer implements Runnable {

        private final SelectionKey socketSelectionKey;

        @Override
        public void run() {
            try {
                SocketChannel socketChannel = (SocketChannel) socketSelectionKey.channel();
                LinkedBlockingQueue<DataLoad> queue = dataLoads.get(socketChannel);
                String value = "Server get: " + dataLoads.get(socketChannel).take().getStringValue();
                if (socketChannel.isOpen())
                    socketChannel.write(ByteBuffer.wrap(value.getBytes(StandardCharsets.UTF_8)));
                else {
                    shutdownSocketChannel(socketChannel);
                }
            } catch (IOException | InterruptedException ignored) {
            }
        }
    }

    private static void shutdownSocketChannel(SocketChannel socketChannel) {
        try {
            socketChannel.shutdownInput();
            socketChannel.shutdownOutput();
            socketChannel.close();
        } catch (IOException ignored) {
        }
    }
}

Master slave multithreading:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Night of the thirteenth month
 */
public class NioTcpMainSubThread {

    public static void main(String[] args) throws IOException {
        new Server(Runtime.getRuntime().availableProcessors()).run();
    }

    private static final ConcurrentHashMap<SocketChannel, LinkedBlockingQueue<DataLoad>> dataLoads = new ConcurrentHashMap<>();

    private static final ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 4);

    private static final ReentrantLock reentrantLock = new ReentrantLock();

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    private static class DataLoad {
        private int intValue;

        private long longValue;

        private double doubleValue;

        private String stringValue;

        private int[] intArray;

        private long[] longArray;

        private double[] doubleArray;

        private String[] stringArray;
    }

    /**
     * BossSelector It only establishes a connection with ServerSocketChannel, and is single threaded and runs in the main thread.
     * <br/>
     * Then throw the established connection to workers for processing. Workers are a group of workers. Each Worker has an independent WorkSelector to process the SocketChannel arranged by the current Worker.
     * <br/>
     * The strategy adopted here is to submit in turn, so that each Worker is responsible for the same number of socketchannels as much as possible.
     * <br/>
     * Each Worker runs on an independent thread and only performs polling Read/Write operations. Time-consuming business operations (such as I/O and Compute) are handed over to the thread work pool for processing.
     */
    private static class Server implements Runnable {

        private final Selector bossSelector;

        private final int workerCount;

        public Server(int workerCount) throws IOException {
            this.workerCount = workerCount;
            bossSelector = Selector.open();
        }

        @Override
        public void run() {
            try {
                System.out.println("Server startup...");
                ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                serverSocketChannel.bind(new InetSocketAddress(8190));
                serverSocketChannel.configureBlocking(false);
                SelectionKey serverSelectionKey = serverSocketChannel.register(bossSelector, SelectionKey.OP_ACCEPT);
                serverSelectionKey.attach(new Boss(serverSocketChannel, workerCount));
                while (true) {
                    bossSelector.select();
                    Set<SelectionKey> selectionKeySet = bossSelector.selectedKeys();
                    // Special processing. Because there is and only one SelectionKey, it is not traversed
                    SelectionKey key = selectionKeySet.iterator().next();
                    Runnable runnable = (Runnable) key.attachment();
                    runnable.run();
                    selectionKeySet.remove(key);
                }
            } catch (IOException ignored) {
            }
        }
    }

    /**
     * Process the new connection, generate SocketChannel and select a Worker to submit.
     */
    private static class Boss implements Runnable {

        private final ServerSocketChannel serverSocketChannel;

        private final int workerCount;

        private final Set<SocketChannel>[] socketChannelSets;

        private final Worker[] workers;

        private int index = 0;

        @SuppressWarnings("unchecked")
        public Boss(ServerSocketChannel serverSocketChannel, int workerCount) throws IOException {
            this.serverSocketChannel = serverSocketChannel;
            this.workerCount = workerCount;
            ExecutorService executorService = Executors.newFixedThreadPool(workerCount);
            socketChannelSets = new Set[workerCount];
            workers = new Worker[workerCount];
            for (int i = 0; i < workerCount; ++ i) {
                workers[i] = new Worker();
                socketChannelSets[i] = workers[i].getSocketChannels();
                executorService.submit(workers[i]);
            }
        }

        @Override
        public void run() {
            Set<SocketChannel> socketChannelSet = socketChannelSets[index];
            Selector workerSelector = workers[index].getWorkerSelector();
            ++ index;
            if (index == this.workerCount)
                index = 0;
            try {
                SocketChannel socketChannel = serverSocketChannel.accept();
                System.out.println("Establish connection...");
                socketChannelSet.add(socketChannel);
                workerSelector.wakeup();
            } catch (IOException ignore) {
            }
        }
    }

    /**
     * Handle newly added SocketChannel and poll Read/Write.
     */
    private static class Worker implements Runnable {

        private final Selector workerSelector;

        private final Set<SocketChannel> socketChannels = new HashSet<>();

        public Worker() throws IOException {
            workerSelector = Selector.open();
        }

        @Override
        public void run() {
            while (true) {
                try {
                    if (socketChannels.size() > 0) {
                        for (SocketChannel socketChannel : socketChannels) {
                            socketChannel.configureBlocking(false);
                            SelectionKey selectionKey = socketChannel.register(workerSelector, SelectionKey.OP_READ);
                            selectionKey.attach(new Handler(selectionKey));
                            socketChannels.remove(socketChannel);
                        }
                        System.out.println("New has been added SocketChannel");
                    }
                    workerSelector.select();
                    Set<SelectionKey> selectionKeySet = workerSelector.selectedKeys();
                    for (SelectionKey key : selectionKeySet) {
                        Runnable runnable = (Runnable) key.attachment();
                        runnable.run();
                        selectionKeySet.remove(key);
                    }
                } catch (IOException ignored) {
                }
            }
        }

        public Set<SocketChannel> getSocketChannels() {
            return socketChannels;
        }

        public Selector getWorkerSelector() {
            return workerSelector;
        }
    }

    /**
     * Distribution business processing
     */
    @Data
    @Builder
    private static class Handler implements Runnable {

        private final SelectionKey socketSelectionKey;

        private static final ExecutorService workPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

        @Override
        public void run() {
            if (!socketSelectionKey.channel().isOpen()) {
                System.out.println("Connection closed");
                try {
                    socketSelectionKey.channel().close();
                } catch (IOException ignored) {
                }
                return ;
            }
            dataLoads.computeIfAbsent((SocketChannel) socketSelectionKey.channel(), k -> new LinkedBlockingQueue<>());
            if (socketSelectionKey.isReadable()) {
                Reader reader = Reader.builder()
                        .socketSelectionKey(socketSelectionKey)
                        .build();
                workPool.submit(reader);
                socketSelectionKey.interestOps(SelectionKey.OP_WRITE);
            } else if (socketSelectionKey.isWritable()) {
                Writer writer = Writer.builder()
                        .socketSelectionKey(socketSelectionKey)
                        .build();
                workPool.submit(writer);
                socketSelectionKey.interestOps(SelectionKey.OP_READ);
            }
        }
    }

    @Data
    @Builder
    private static class Reader implements Runnable {

        private final SelectionKey socketSelectionKey;

        @Override
        public void run() {
            try {
                SocketChannel socketChannel = (SocketChannel) socketSelectionKey.channel();
                String value;
                reentrantLock.lock();
                if (socketChannel.isOpen()) {
                    int readable = socketChannel.read(byteBuffer);
                    if (readable == 0) {
                        value = null;
                        // System.out.println("read empty request");
                    } else if (readable < 0) {
                        value = null;
                        shutdownSocketChannel(socketChannel);
                    } else {
                        value = new String(byteBuffer.array(), 0, readable);
                    }
                } else {
                    value = null;
                }
                reentrantLock.unlock();
                if (value == null) {
                    return ;
                }
                System.out.println("I read it: " + value);
                DataLoad dataLoad = DataLoad.builder()
                        .stringValue(value)
                        .build();
                LinkedBlockingQueue<DataLoad> tmp = dataLoads.computeIfAbsent(socketChannel, k -> new LinkedBlockingQueue<>());
                tmp.add(dataLoad);
                socketSelectionKey.selector().wakeup();
            } catch (IOException ignored) {
            }
        }
    }

    @Data
    @Builder
    private static class Writer implements Runnable {

        private final SelectionKey socketSelectionKey;

        @Override
        public void run() {
            try {
                SocketChannel socketChannel = (SocketChannel) socketSelectionKey.channel();
                LinkedBlockingQueue<DataLoad> queue = dataLoads.get(socketChannel);
                String value = "Server get: " + dataLoads.get(socketChannel).take().getStringValue();
                if (socketChannel.isOpen())
                    socketChannel.write(ByteBuffer.wrap(value.getBytes(StandardCharsets.UTF_8)));
                else {
                    shutdownSocketChannel(socketChannel);
                }
            } catch (IOException | InterruptedException ignored) {
            }
        }
    }

    private static void shutdownSocketChannel(SocketChannel socketChannel) {
        try {
            socketChannel.shutdownInput();
            socketChannel.shutdownOutput();
            socketChannel.close();
        } catch (IOException ignored) {
        }
    }
}

Here we know. NIO thread (the thread bound by Selector) only handles I/O. No matter when your business logic is completed and how long it takes, as long as the data is ready and a write event is registered, NIO will notify you when it can be written, and you can write it. In this way, as long as the time-consuming business is not in the NIO thread, it will not affect the I/O operations of other serversockets.

Finally, I want to make it clear that the Selector of the Reactor model only implements the notification of data readability / data writability (the Boss version Selector may only do the connectable notification). Here, we emphasize "yes", which means yes, that is, I/O operation availability notification of a connection. Therefore, it needs to be used with NIO to complete the notification operation of NIO without NIO constantly polling.

It is also very simple for you to let it notify you of an operation. Just register with it and add a callback program (attachment Implementation) so that the Selector can call your callback program to complete further operations when available, but remember not to block your NIO Selector in this program, otherwise it will affect the subsequent I/O availability notification of other connections.

Author: Ma Nong 200
Link: https://juejin.cn/post/6972810594772582431

Topics: Java Go network