This article was first published by Jaychen. Cc

Before I get into the topic, let’s take a look at a simple example of Java network programming: The code is simple, the client and the server communicate, and for every input from the client, the server replies get. Note that the server can allow multiple clients to connect at the same time.

Server-side code:

// Create the server socket
ServerSocket serverSocket = new ServerSocket(20000);
client = serverSocket.accept();

// A message is displayed indicating that the client is connected successfully
System.out.println("Client connection successful");
// Start a new thread to process client requests
new Thread(new ServerThread(client)).start();


// The child thread handles the client input
class ServerThread implements Runnable {...@Override
    public void run(a) {
        boolean flag = true;
        while (flag) {

            // Read the data sent by the client
            String str = buf.readLine();

            // Reply to the client. Get indicates that the data is received
            out.println("get"); }}}Copy the code

Client code:


Socket client = new Socket("127.0.0.1".20000);
boolean flag = true;
while (flag) {

    // Read the user input from the keyboard
    String str = input.readLine();
    // Send the user's input to the server
    out.println(str);

    // Receive the get string sent back by the serverString echo = buf.readLine(); System.out.println(echo); }}Copy the code

The full Java sample code is too large to read, so it is not posted here. If you need to download it directly on Github, here is where to download it.

It can be seen that in order to process requests from multiple clients simultaneously, the server needs to start a thread for each client. This one-thread-per-client mode is very stressful for the server. If there are 1K clients and the corresponding server should start 1K Threads, then the memory consumed by the server, the time taken by threads to switch, and so on are fatal. Even with the use of thread pooling techniques to limit the number of threads, the blocking IO model does not support a large number of connections.

Each client needs one thread to process requests.

NIO

The main reason why the single-thread-per-client mode cannot support a large number of connections is that the readLine blocks the I/O, that is, until the readLine is unable to read the data, it blocks the thread, so that the thread cannot continue to execute. In order to process multiple clients at the same time, the server must start multiple threads at the same time.

So, after Java 1.4, a set of NIO interfaces was introduced. One of the most important features of NIO is the ability to perform non-blocking IO operations: if the data fails to be read, the non-blocking IO does not block the thread, but simply returns zero. In this case, the thread determines that the data is not ready by returning the value, and can do other things without being blocked.

The figure above shows the difference between blocking and non-blocking IO. You can see that although non-blocking IO is not blocked, it is still calling functions to check whether the data is readable. This phenomenon is shown in the code in this form:

while((str = read()) == 0) {}// Continue to read the logic after the data.Copy the code

Non-blocking I/O does not block the thread, but since there is no data to read, the thread has no way to continue executing the logic. It has to keep calling the judgment and waiting for the data to arrive. This case is called synchronous IO. So in summary, NIO is essentially a non-blocking synchronous IO.

IO reuse

Since NIO does not block because data has not yet arrived, there is no need for each client to allocate a thread to continuously poll for data to be read. You can use one thread to listen for all client connections. The thread loops to determine if any client connection is readable, and if so, tells other threads that a client connection is readable. This behavior is called IO reuse. NIO provides a Selector class that listens for all client connections to see if the data is readable.

I/O reuse using selectors. Only one thread needs to care whether the data is coming, and the other threads need to wait for notification. In this way, only the listening thread will loop through the judgment, which does not take up too much CPU. When I talk about selectors in NIO, I have to say something about I/O reuse in Linux programming, because the underlying use of selectors in NIO is system level I/O reuse.

There are two implementation schemes for I/O reuse in Linux:

  • select
  • epoll

On Linux 2.6+ NIO uses epoll, and on 2.4.x it uses select. The epoll function is much better than select in terms of performance, so you can ignore the Linux programming specifics here. It is worth mentioning that The Underlying Java Netty network framework uses NIO technology.

AIO

Back in NIO: Use the listening thread to call the SELECT function to listen for all requests to see if data has arrived, and if so notify other threads to read the data. In this case, while the thread is reading the data, the thread is blocked until the data has been read, and the thread can continue executing logic only after the data has been read. As I said before, this is called synchronous IO. A new set of interfaces, AIO(Asynchronous IO), has been added in JDK 7.

One of the amazing features of AIO is that, instead of waiting for the IO to finish reading, the thread can simply return and continue with other operations. When the data is read, the system informs the thread that the data has been read. An I/O that initiates the operation but does not have to wait for the data to be read is called an asynchronous I/O. With AIO, one thread can initiate multiple IO operations simultaneously, which means that one thread can process multiple requests simultaneously. The well-known Web server Nginx uses asynchronous IO. For more details, see my other article, Apache–MPMs && Nginx Event Drivers.

End

So far, the article has explained the differences between blocking/non-blocking IO and synchronous/asynchronous IO. To talk about THE IO model, it is necessary to cover the five Linux IO models

  • Blocking IO
  • Non-blocking IO
  • IO reuse
  • Signal driven IO
  • Asynchronous I/o

Except for signal-driven IO, the other four main IO models are explained, and understanding the concepts of these IO models is a great help in writing code.