Location>code7788 >text

The three core components of NIO are detailed and fully explain why NIO has high performance in network IO!

Popularity:235 ℃/2024-07-23 10:20:35

I. Writing at the beginning

We mentioned in a previous blog post in the Java IO in the common three models (BIO, NIO, AIO), in which NIO is our daily development in the use of a more IO model, we come together today to learn in detail.

In traditional IO, mostly dominated by this synchronous blocking IO model, after the program initiates an IO request, the processing thread is in a blocking state until the requested IO data is copied from kernel space to user space. The following diagram can visualize the whole process (source: Silent King II).

image

This model is fine if the application initiating the IO does not have high concurrency. However, it is obvious that in the current Internet, many applications have high concurrency IO requests, and there is an urgent need for an efficient IO model.

The N in NIO can be named NEW to represent a new type of IO model, or it can be interpreted as Non-Blocking, which means non-blocking.Java NIO was introduced in Java version 1.4, and is based on channel and buffer operations, and uses non-blocking IO operations to allow threads to perform other tasks while waiting for IO. The non-blocking IO operation allows threads to perform other tasks while waiting for IO. Common NIO classes include ByteBuffer, FileChannel, SocketChannel, ServerSocketChannel, and so on. (Source: In-depth disassembly of Tomcat & Jetty)

image

Although when the application initiates an IO request, zhodo is initiated multiple times without blocking. However, when the kernel copies the data to the user space, it will still block, in order to ensure the accuracy of the data and the safety and stability of the system.

Second, the three major components of NIO

In the process of communication between the computer and the outside, not all scenarios NIO performance will be good, for the connection is less, concurrent application system in the traditional BIO performance is better, because in NIO the application needs to constantly carry out the process of I/O system call polling data is ready or not is very consuming CPU resources.

image

In order to better familiarize ourselves with and master NIO, we start here with the three main components of NIO, which is also a point that many interviewers in large factories will ask during the interview, although the frequency is not high, but you must be able to!

Three core components:

  • Selector: An event-driven I/O multiplexing-based model that allows a single thread to handle multiple Channels, with multiple Channels registered to a Selector, which then polls to listen for changes in each Channel.
  • Channel: It is a bi-directional, readable and writable data transfer pipeline, through which the input and output work of data is realized, it is only responsible for transporting data, not responsible for processing data, processing data in the Buffer. Generally the pipeline is divided intofile channelcap (a poem)socket channel
  • Buffer: The operations on the data in NIO are done in the Buffer. A read operation fills the Buffer with the data transported from the Channel; a write operation writes the data from the Buffer to the Channel.

In order to better understand the flow of NIO operation based on the three core components, a mind map was drawn as follows:

image

III. Components in detail

In the following, we address the three major components summarized in the previous chapter, one by one, in detail.

3.1 Buffer

In the traditional BIO, data read and write operations are based on streams, write using input byte streams or character streams, and write using the output byte streams or character streams, are essentially byte-based data operations. The NIO library, the use of buffers, either write or write data, will not enter the buffer, by the buffer for the next operation.

image

The above figure is the inheritance relationship structure diagram of Buffer subclasses, we can see that the naming in Buffer is based on the basic data types, and we are in the daily use of the ByteBuffer buffer class the most, it is based on the byte storage, which is the same as the stream.

And getting inside enough of these buffer classes, we can see that they are actually equivalent to an array container. In the source code of Buffer, there are several parameters like this:

public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
}

The specific meanings of these four member variables are as follows:

  1. Capacity: The maximum amount of data that can be stored in the Buffer, set when the Buffer is created and cannot be changed;
  2. LimitLimit: the limit of data that can be read/written in the Buffer. In write mode, limit represents the maximum data that can be written, generally equal to capacity (can be set by limit(int newLimit)); in read mode, limit is equal to the size of the data actually written in the Buffer;
  3. Position: The position (index) of the next data that can be read or written. When switching from write mode to read mode (flip), the position is reset to zero so that reading and writing can start from the beginning;
  4. Mark: Buffer allows the position to be positioned directly at the marker, which is an optional property.

And, the above variables satisfy the following relationship:0 <= mark <= position <= limit <= capacity

Here we need to pay attention to one point, Buffer has two modes, read and write, Buffer is created, the default is write mode, call flip () can be switched to read mode, and then call clear () or compact () method to switch to write mode.

image

1️⃣ Buffer instantiation

Buffer cannot be created by calling a constructor method to create an object, but needs to be instantiated through a static method. Let's take ByteBuffer as an example:

// Allocate heap memory to create the buffer in the JVM's memory
public static ByteBuffer allocate(int capacity).
// Allocating direct memory and building the buffer into physical memory can submit efficiency. But here the data will not be garbage collected, easy to lead to memory overflow.
public static ByteBuffer allocateDirect(int capacity); // Allocate direct memory.

2️⃣ Buffer's core methods

The common methods we use in Buffer are:

  1. get : Read the data in the buffer;
  2. put : Write data to the buffer;
  3. flip : switches the buffer from write mode to read mode, which sets the value of limit to the value of the current position and the value of position to zero;
  4. clear: Empties the buffer, switches the buffer from read mode to write mode, and sets the value of position to 0 and limit to the value of capacity.

image

3️⃣ Test Cases for Buffer

Based on the above theoretical knowledge learning, we write a small test demo to feel the use of Buffer.

[Test case]

public class TestBuffer {
        public static void main(String[] args) {

            // Allocate a CharBuffer with a capacity of 8, defaulting to write mode
            CharBuffer buffer = (8);
            ("Starting state:");
            printState(buffer);

            // Write 3 characters to buffer
            ('a').put('b').put('c'); ("Write 3 characters to buffer:"); printState(buffer); // Write 3 characters to buffer.
            ("State after writing 3 characters:");; printState(buffer); printState(buffer); printState(buffer); printState(buffer)
            printState(buffer);

            // Call the flip() method to switch to read mode.
            // Prepare to read the data in the buffer by setting position to 0 and limit to 3.
            (); // Call flip() method, switch to read mode, // prepare to read data from buffer, set position to 0, limit to 3.
            ("State after calling the flip() method:"); printState(buffer);; // Call the flip() method.
            printState(buffer).

            // Read the characters
            // The hasRemaining() method is used to determine if there are any elements between the current position and the limit.
            // This method returns true if and only if there is at least one element remaining in this buffer.
            while (()) {
                ("Reading characters:" + ());
            }
            // Call the clear() method to empty the buffer, setting the value of position to 0 and the value of limit to the value of capacity
            // Switch from read mode to write mode after calling the clear() method.
            ();
            ("State after calling clear() method:");
            printState(buffer);

        }

        // Print the buffer's capacity, limit, position, and mark positions
        private static void printState(CharBuffer buffer) {
            // capacity
            ("capacity: " + ());
            //limit
            (", limit: " + ()); // next read/write position.
            // next read/write position
            ("", position: " + ()); //Marker.
            //mark
            ("", mark start reading character: " + ()); // next read/write position.
            ("\n");
        }
}

[Output:]

Starting state:
capacity: 8, limit: 8, position: 0, mark Characters to start reading.

State after 3 characters have been written:
capacity: 8, limit: 8, position: 3, mark Characters to start reading.

State after calling flip() method:
capacity: 8, limit: 3, position: 0, mark Characters to start reading: abc

Characters read: a
Characters read: b
Characters read: c

The state after calling the clear() method:
capacity: 8, limit: 8, position: 0, mark Characters to start reading: abc

3.2 Channel

In the above summary, we have already mentioned that Channel, as a two-way data channel, builds a transmission bridge between the external belongings and the program. Read operations fill the data in the Channel into the Buffer, while write operations write the data in the Buffer into the Channel. You can even read and write at the same time!

image

The subclasses of Channel are shown below.
image

There are many channel classes here, but the ones we commonly use in our daily lives are nothing more than theFileChannel: file access channel; SocketChannel, ServerSocketChannel: TCP communication channel; DatagramChannel: UDP communication channel;

FileChannel: channel for file I/O, supports read, write and append operations on files.FileChannel allows data transfer anywhere in the file, supports advanced features such as file locking and memory-mapped files.FileChannel cannot be set to non-blocking mode, so it is only suitable for blocking file operations.

SocketChannel: channel for TCP socket I/O. SocketChannel supports non-blocking mode and can be used with Selector (described below) for efficient network communication. socketChannel allows to connect to a remote host for data transfer.

It is matched by ServerSocketChannel: a channel that listens for connections to TCP sockets. Similar to SocketChannel, ServerSocketChannel also supports non-blocking mode and can be used with Selector.ServerSocketChannel is responsible for listening for new connection requests, and after receiving a connection request, a new SocketChannel can be created to handle the data transfer.

DatagramChannel: channel for UDP socket I/O. DatagramChannel supports non-blocking mode, can send and receive datagrams, and is suitable for connectionless and unreliable network communication.

1️⃣ Channel's Core Methods

  1. read : Read data and write to Buffer;
  2. write : Write the data in the Buffer to the Channel.

2️⃣ Channel's Test Case

RandomAccessFile reader = new RandomAccessFile("E:\\", "r");
FileChannel channel = ();
ByteBuffer buffer = (1024);
(buffer);
("retrieve a character:" + new String(()));

image

3.3 Selector

The concept of selectors has been covered above, we will now focus on how it works:
When an event occurs, such as a new TCP connection being made to a channel, or read and write events occurring on a channel, the channel becomes ready and is polled by the Selector, which adds the relevant channel to the ready set. The Selector adds the relevant Channel to the Ready collection. The Selector will add the relevant channel to the ready set. You can get the set of ready channels by SelectionKey, and then perform the corresponding I/O operations on these ready channels.

Primary monitoring event type:

  • SelectionKey.OP_ACCEPT: Indicates the event that the channel accepts a connection, which is typically used for ServerSocketChannels;
  • SelectionKey.OP_CONNECT: Indicates the event that the channel completes the connection, this is typically used for SocketChannels;
  • SelectionKey.OP_READ: An event indicating that the channel is ready for reading, i.e., there is data to read;
  • SelectionKey.OP_WRITE: An event indicating that the channel is ready for writing, i.e., data can be written.

SelectionKey collection:

  • The set of all SelectionKey: represents the Channels registered to this Selector, this set can be returned by the keys() method;
  • The set of selected SelectionKeys: represents all the Channels available through the select() method that require IO processing, and this set can be returned through selectedKeys();
  • Canceled SelectionKey collection: represents all the Channels that have been de-registered. The SelectionKeys corresponding to these Channels will be completely deleted in the next execution of the select() method, and the program usually does not need to access this collection directly, and there is no exposed method to access it.

The select() method in Selector:

  • int select(): monitor all registered Channels, when there is an IO operation that needs to be processed among them, this method returns and adds the corresponding SelectionKey to the set of selected SelectionKey, this method returns the number of these Channels;
  • int select(long timeout): You can set the timeout duration of the select() operation;
  • int selectNow(): performs a select() operation that returns immediately, as opposed to the parameterless select() method, which does not block the thread;
  • Selector wakeup(): Returns a select() method that has not yet returned immediately.

[Test case]

public static void main(String[] args) {
     try {
         //1. Build a service socket channel through the open() method
         ServerSocketChannel serverSocketChannel = ();
         (false);
         // Encapsulate port 8080
         ().bind(new InetSocketAddress(8080));

         //2. Build a selector object via the open method
         Selector selector = ();
         // Register the ServerSocketChannel with the Selector and listen for OP_ACCEPT events.
         (selector, SelectionKey.OP_ACCEPT); // Register the ServerSocketChannel with the Selector and listen for OP_ACCEPT events.

         while (true) {
             // Listen for connection events on the registered channels and add the corresponding SelectionKey to the set of selected SelectionKeys.
             int readyChannels = (); if (readyChannels = ())
             if (readyChannels == 0) {
                 continue; }
             }
             // Return all the Channels that need IO processing via selectedKeys.
             Set<SelectionKey> selectedKeys = ();
             Iterator<SelectionKey> keyIterator = ();

             while (()) {
                 SelectionKey key = ();
                 // Handle connection events
                 if (()) {
                     ServerSocketChannel server = (ServerSocketChannel) (); // Handle the connection event if (()) {
                     SocketChannel client = (); // Handle the connection event if (()) { ServerSocketChannel server = (ServerSocketChannel) (); }
                     (false); // Register the client channel with Selector.
                     // Register the client channel with the Selector and listen for OP_READ events.
                     (selector, SelectionKey.OP_READ); } else if (()) { { (false)
                 } else if (()) {
                     // Handle the read event
                     SocketChannel client = (SocketChannel) (); } else if (()) { // Handle read events.
                     ByteBuffer buffer = (1024); int bytesRead = (buffer.OP_READ)
                     int bytesRead = (buffer); if (bytesRead >); // handle read event
                     if (bytesRead > 0) {
                         ();
                         ("Received data: " +new String((), 0, bytesRead));
                         // Register the client channel with the Selector and listen for OP_WRITE events.
                         (selector, SelectionKey.OP_WRITE); } else if (bytesRead.OP_WRITE)
                     } else if (bytesRead < 0) {
                         // Client disconnects
                         (); }
                     }
                 } else if (()) {
                     // Handle the write event and return the result immediately
                     SocketChannel client = (SocketChannel) ();
                     ByteBuffer buffer = ("Hello, Client!".getBytes()); ("Hello, Client!".
                     (buffer);

                     // Register the client channel with the Selector and listen for OP_READ events.
                     (selector, SelectionKey.OP_READ);
                 }

                 (); }
             }
         }
     } catch (IOException e) {
         (); }
     }
 }

The code above creates a simple TCP server based on Java NIO. It implements non-blocking I/O and I/O multiplexing using ServerSocketChannel and Selector. The server listens for events in a loop, and when a new connection is requested, it accepts the connection and registers the new SocketChannel with the Selector, paying attention to the OP_READ event. When there is data to read, it reads data from the SocketChannel and writes it to the ByteBuffer, then writes data from the ByteBuffer back to the SocketChannel.

IV. Summary

Here basically a few important components of the NIO introduced, certainly not all, you want to know more about it, or to look through different books. At the same time, later we will be based on this part of the content, write a small chat room.