Background knowledge

Synchronous, asynchronous, blocking, and non-blocking

First of all, these concepts are very confusing, but they are covered in NIO, so to summarize.

  • Synchronization: When the API call returns, the caller knows the result of the operation (how many bytes were actually read/written).

  • Asynchronous: As opposed to synchronous, when an API call returns, the caller does not know the result of the operation until a callback notifies the result.

  • Block: Suspends the current thread to wait when there is no data to read or all data cannot be written.

  • Non-blocking: when reading, read as much data as can be read and return, when writing, write as much data as can be written and return.

For I/O operations, according to the Oracle official website, synchronous asynchronism is defined as “whether the caller needs to wait for the I/O operation to complete”. This “waiting for the I/O operation to complete” does not mean that the data must be read or written, but refers to the actual I/O operation. For example, whether the caller should wait while the data is transferred between the TCP/IP stack buffer and the JVM buffer.

Therefore, the common read() and write() methods are synchronous I/O. Synchronous I/O can be divided into blocking and non-blocking modes. If the non-blocking mode detects that no data can be read, the system returns the data without actually performing the I/O operation.

To sum up, there are really only three mechanisms in Java: synchronous blocking I/O, synchronous non-blocking I/O, and asynchronous I/O. We’ll cover the first two below. Asynchronous I/O was introduced in JDK 1.7, called NIO.2.

Traditional IO

As we know, the emergence of a new technology is always accompanied by improvements and improvements, as is the emergence of Java NIO.

Traditional I/O is blocking I/O. The main problem is the waste of system resources. For example, if we call the read() method of InputStream to read data from a TCP connection, the current thread will be suspended until the data arrives, and the thread will do nothing until the data arrives. In order to read data from other connections, we have to start another thread. This may be fine when the number of concurrent connections is small, but when the number of connections reaches a certain size, memory resources are consumed by a large number of threads. On the other hand, thread switching requires changing processor state, such as program counters and register values, so switching between a large number of threads very frequently is also a waste of resources.

As technology evolves, modern operating systems offer new I/O mechanisms that can avoid this waste of resources. From this, Java NIO was born, and NIO’s defining feature is non-blocking I/O. Then we found that the use of simple nonblocking I/O can not solve the problem, because in non-blocking mode, the read () method will return immediately without read data, we don’t know when the data arrived, only endless calls read () method to try again, this is clearly too waste of CPU resources, can know from below, The Selector component was created to solve this problem.

Java NIO core components

1.Channel

concept

All I/O operations in Java NIO are based on Channel objects, just as Stream operations are based on Stream objects, so it’s important to understand what a Channel is. The following is taken from the JDK 1.8 documentation

A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.

From the above, a Channel represents a connection to an entity, which could be a file, a network socket, and so on. In other words, channels are a bridge provided by Java NIO for our programs to interact with the underlying OPERATING system I/O services.

A channel is a very basic and abstract description that interacts with different I/O services, performs different I/O operations, and implements different I/O operations, such as FileChannel and SocketChannel.

A channel is similar to a Stream in that it can read data into a Buffer or write data from a Buffer to a channel.

Of course, there are differences, mainly reflected in the following two points:

  • A Stream can be both read and write, while a Stream is unidirectional.

  • The channel has a non-blocking I/O mode

implementation

The most common implementations of channels in Java NIO are the following, which correspond to the traditional I/O operation classes.

  • FileChannel: Reads and writes files

  • DatagramChannel: indicates UDP network communication

  • SocketChannel: TCP network communication

  • ServerSocketChannel: Listens for TCP connections

2.Buffer

The Buffer used in NIO is not a simple byte array, but a wrapped Buffer class that provides an API for manipulating data, as discussed below.

NIO provides a variety of Buffer types, such as ByteBuffer, CharBuffer, and IntBuffer, corresponding to the Java basic types. The difference is that the unit length of the Buffer is different (read and write according to the corresponding variables).

There are three important variables in Buffer that are key to understanding how buffers work

  • Total capacity

  • Position (current position of pointer)

  • Limit (read/write boundary position)

Capacity is the total length of the array, position is the subscript variable we use to read/write characters, and limit is the terminator position. The situation of the three variables at the beginning of Buffer is shown below

Position moves backwards when a Buffer is read/written, and limit is the boundary of position’s move. It is not hard to imagine that when writing Buffer, limit should be set to capacity, and when reading Buffer, limit should be set to the actual end of the data. (Note: Writing Buffer data to a channel is a Buffer read operation, and reading data from a channel is a Buffer write operation.)

The Buffer class provides helper methods to set position and limit before reading/writing to Buffer

  • Flip (): Set limit to the value of position, then position to 0. Called before the Buffer is read.

  • Rewind (): Simply sets position to 0. This is usually called before re-reading the Buffer data, for example, when multiple channels are written to read the same Buffer data.

  • Clear (): returns to the initial state, that is, limit equals capacity and position is set to 0. Called before writing to Buffer again.

  • Compact (): Moves the unread data (between position and limit) to the beginning of the buffer and sets position to the next position at the end of the data. It’s essentially writing this piece of data back into the buffer.

Then, take a look at an example of using FileChannel to read and write text files, using this example to verify the channel’s readable and writable nature and the basic use of Buffer (note that FileChannel cannot be set to non-blocking mode).

FileChannel channel = new RandomAccessFile("test.txt", "rw").getChannel(); channel.position(channel.size()); ByteBuffer = ByteBuffer. Allocate (20); // Data is written to Buffer bytebuffer. put(" Hello world! \n".getBytes(StandardCharsets.UTF_8)); // Buffer -> Channel byteBuffer.flip(); while (byteBuffer.hasRemaining()) { channel.write(byteBuffer); } channel.position(0); // Move the file pointer to the beginning (read from scratch) CharBuffer CharBuffer = charbuffer.allocate (10); CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); Bytebuffer.clear (); while (channel.read(byteBuffer) ! = -1 || byteBuffer.position() > 0) { byteBuffer.flip(); // Decode charbuffer.clear () with utF-8 decoder; decoder.decode(byteBuffer, charBuffer, false); System.out.print(charBuffer.flip().toString()); byteBuffer.compact(); } channel.close();Copy the code

This example uses two buffers, byteBuffer as the data Buffer read and write by the channel, and charBuffer to store decoded characters. As mentioned above, it is important to note that the last compact() method is necessary even if the charBuffer is large enough to hold the data decoded by byteBuffer. This is because utF-8 encoding of common Chinese characters takes up 3 bytes, so there is a high probability of truncation in the middle, as shown in the following figure:

When the Decoder reads a 0xe4 at the end of the buffer, it cannot map to a Unicode. The purpose of the third argument false to decode() is for the Decoder to treat unmapped bytes and subsequent data as additional data. So the decode() method stops here, and position falls back to 0xe4. This leaves the first byte encoded by the “middle” word in the buffer, which must be compact to the front and concatenated with the correct and subsequent data.

BTW, CharsetDecoder in the example, is also a new feature of Java NIO, so NIO is buffer-oriented (traditional I/O is stream-oriented).

At this point, we understand the basic usage of Channel and Buffer. Next comes the important component of having a single thread manage multiple channels.

3.Selector

What is the Selector

A Selector is a special component that collects the state (or events) of each channel. After registering the channel with the selector and setting up the event of interest, we can silently wait for the event to occur by calling the select() method.

The channel has the following four events for us to monitor:

  • Accept: There are acceptable connections

  • Connect: The connection succeeds

  • Read: Data is available to Read

  • Write: Data can be written

Why do we use Selector

As mentioned earlier, if you use blocking I/O, you need to multithread (waste memory), and if you use non-blocking I/O, you need to retry (waste CPU). This awkward problem is solved by the Selector, because in non-blocking mode, with the Selector, our thread only works on channels that are already in place, so we don’t have to blindly retry them. For example, when no data arrives on all channels, no Read event occurs and our thread is suspended at select(), freeing up CPU resources.

Method of use

Create a Selector and register a Channel, as shown below.

Note: To register a Channel with a Selector, you first need to set the Channel to non-blocking mode, otherwise an exception will be thrown.

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);Copy the code

The second parameter to the register() method is called “interest set,” which is the collection of events you care about. If you care about multiple events, separate them with a bitwise or operator, such as

SelectionKey.OP_READ | SelectionKey.OP_WRITECopy the code

A variable of integer type can be used to identify multiple states. How can it be done? For example, first pre-define some constants, and their values (binary) are as follows

It can be found that the bits with the value of 1 are staggered, so there is no ambiguity in the value obtained after bitwise or operation on them, and it can be inversely deduced which variables are calculated from. How do you tell? Yes, it’s bitwise and. For example, if we have a set of states with the value 0011, we can determine whether the set contains the OP_READ state simply by determining whether the value of “0011&op_read” is 1 or 0.

Then, notice that the register() method returns an object called SelectionKey, which contains information about the registration and can also be used to modify it. As you can see from the complete example below, after select(), we also get the channels ready by getting a collection of selectionkeys.

A complete instance

The concept and theory of things elaborated (actually wrote here, I found that did not write many things, good embarrassment (⊙ˍ⊙)), let’s look at a complete example.

This example uses Java NIO to implement a single-threaded server that simply listens for a client connection, reads a message from the client when the connection is established, and responds to a message from the client.

Note that I use the character ‘\0’ (a byte with a value of 0) to identify the end of the message.

Single-threaded Server

Public class NioServer {public static void main(String[] args) throws IOException {// Create a selector selector selector =  Selector.open(); ServerSocketChannel listenChannel = ServerSocketChannel.open(); // Initialize the TCP connection listening channel. listenChannel.bind(new InetSocketAddress(9999)); listenChannel.configureBlocking(false); // Listenchannel. register(selector, selectionkey. OP_ACCEPT); ByteBuffer = ByteBuffer. Allocate (100); while (true) { selector.select(); Iterator<SelectionKey> keyIter = selectedKeys().iterator(); While (keyiter.hasnext ()) {SelectionKey key = keyiter.next (); If (key.isacceptable ()) {// There is an acceptable connection to accept SocketChannel channel = ((ServerSocketChannel) key.channel()).accept(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); System.out.println(" + channel.getremoteAddress () + ") ); } else if (key.isreadable ()) {// There is data to read buffer.clear(); // The TCP connection is disconnected. If (((SocketChannel) key.channel()).read(buffer) == -1) {key.channel().close(); continue; } // iterate over the data in bytes buffer.flip(); while (buffer.hasRemaining()) { byte b = buffer.get(); If (b == 0) {// At the end of the client message \0 system.out.println (); // Respond to client buffer.clear(); buffer.put("Hello, Client! \0".getBytes()); buffer.flip(); while (buffer.hasRemaining()) { ((SocketChannel) key.channel()).write(buffer); } } else { System.out.print((char) b); }}} keyiter.remove (); }}}}Copy the code

Client

This client is purely for testing purposes and is written in the traditional way to make it look less laborious, with very short code.

To test rigorously, take advantage of the non-blocking I/O benefits of the server by running a large number of clients concurrently, counting server response times, and not sending data immediately after a connection is established.

public class Client { public static void main(String[] args) throws Exception { Socket socket = new Socket("localhost", 9999); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); Os. write("Hello, Server! \0".getBytes()); Int b; while ((b = is.read()) ! = 0) { System.out.print((char) b); } System.out.println(); socket.close(); }}Copy the code

NIO vs IO

After studying NIO, we all have the question: When should WE use NIO and when should we use traditional I/O?

In fact, after understanding their characteristics, the answer is relatively clear, NIO is good at one thread to manage multiple connections, saving system resources, but if each connection to transmit a large amount of data, because it is synchronous I/O, will lead to the overall response speed is very slow; Traditional I/O creates one thread for each connection, taking full advantage of the processor’s parallel processing capabilities, but memory resources can be stretched if there are too many connections.

Summary is: the connection number is large and the amount of data is small with NIO, the connection number is small with I/O (easy to write -).

Next

After studying the NIO core components, you learned the basic approach to non-blocking server implementation. However, you must have noticed that the complete example above actually hides a lot of problems. Example, for example, is simply to read each byte output, real environment is sure to read the complete message on to the next step, as a result of the NIO non-blocking features, one may only read part of news, this is very bad, if the same connection for multiple messages sent, it is not only to the message, Similarly, in this example, a while() loop is used to ensure that all data is written before doing anything else. In practice, for performance reasons, this would not be the case. In addition, to take full advantage of the multi-core parallel processing capabilities of modern processors, a thread group should be used to manage these connected events.

To solve these problems requires a rigorous and tedious design, but fortunately, we have an open source framework available, which is elegant and powerful Netty. Netty is based on Java NIO and provides an asynchronous invocation interface, which is a good choice for developing high-performance servers. I plan to learn more about it next, and then write a note.

Java NIO is designed to provide an API for programmers to enjoy the latest I/O mechanisms of modern operating systems, so it covers a wide range of components and features, such as pipes, paths, Files, etc. There are new components to improve I/O performance, and there are tools to simplify I/O operations. See the links in References at the end.

If you find this useful, move your little hands and pay attention to it!

Java architects learn the public number!

A focus on sharing architecture dry goods wechat public number

Feel useful to share this article to more people see it!