Recently reread the Netty source code and found that the bottom layer of the Netty encapsulates the Java NIO Selector/Channel operation is a bit unfamiliar. Reread the Java NIO.

Java NIO consists of three core components:

  • Channel
  • Selector
  • Buffer

The underlying layer of Java NIO is implemented through the IO multiplexing support provided by the operating system. Java uses a unified API to mask the differences in operating system versions.

IO multiplexing refers to the ability of a process/thread to monitor multiple file descriptors simultaneously (a network connection is represented by a file descriptor underlying the operating system). Once one or more file descriptors are readable or writable, the system kernel notifies the process/thread. At the Java application level, how do you monitor multiple file descriptors? One very important NIO component that you need to use is the Selector Selector.

To achieve I/O multiplexing, the channels must be registered with the selector. Then, through the internal mechanism of the selector, you can query whether the registered channels have I/O events ready (readable, writable, complete network connection, etc.).

A selector requires only one thread to monitor, we can simply use a thread, through the selector to manage multiple channels, which is very efficient.

There are many kinds of Channel implementation classes. In the process of using Channel, we mainly focus on two kinds of channels:

  • SocketChannel
  • ServerSocketChannel

SocketChannel transmits connections, and ServerSocketChannel listens for connections.

ServerSocketChannel applies to the server side, while SocketChannel is on both the server and client side. In other words, for a connection, both ends need a SocketChannel transport channel that is responsible for the transport.

Both ServerSocketChannel and SocketChannel support both blocking and non-blocking modes, set by calling the configureBlocking method:

  • SocketChannel. ConfigureBlocking (false) is set to a non-blocking mode
  • SocketChannel. ConfigureBlocking (true) is set to block model

In blocking mode, the connect, read, and write operations on SocketChannel channels are synchronous and blocking, which is inefficient. In non-blocking mode, the operations on SocketChannel channels are asynchronous and efficient.

Gets the SocketChannel transport channel

On the client side, a socket transport channel is obtained through the SocketChannel static method open(), the socket socket is set to non-blocking mode, and the server IP and port are connected through the connect() instance method.

SocketChannel socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); SocketChannel. Connect (new InetSocketAddress (80) "127.0.0.1,");Copy the code

In non-blocking cases, the connection to the server may not actually resume before the socketChannel method returns, so you need to constantly spin to check if you are currently connected to the server.

while(! SocketChannel. FinishConnect ()) {/ / spin and waiting}Copy the code

On the server side, how do I get a transport socket?

When a new connection event arrives, the ServerSocketChannel on the server can successfully query for a new connection event and obtain the socket channel of the new connection by calling the ServerSocketChannel on the server that listens for socket accept() :

ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
Copy the code

Read the SocketChannel transport channel

When a SocketChannel is readable, we can read data from a SocketChannel, call the read method, and read the data into the buffer ByteBuffer:

ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buf);
Copy the code

When reading, because it is asynchronous, we must check the return value of read to determine whether data is currently being read. The return value of the read() method, which is the number of bytes read. If -1 is returned, it indicates that the other party has read the end flag of the output and is ready to close the connection.

Write to SocketChannel transport channel

// ByteBuffer is read mode buffer.flip(); socketChannel.write(buffer);Copy the code

The SocketChannel transmission channel is disabled

Before closing the SocketChannel, if the SocketChannel is being used to write data, it is recommended that you call shutdownOutput() to terminate the output and send an end of output flag to the other party. Then call the socketchannel.close () method to close the socket:

/ / terminate the output method, to the other party to send an output end mark socketChannel. ShutdownOutput (); IOUtil.closeQuietly(socketChannel);Copy the code

Selectors and registrations

What is the selector? What is the relationship between the selector and the channel?

To put it simply: the mission of the selector is to complete IO multiplexing. A channel represents a connection channel, through the selector can monitor IO(input and output) of multiple channels at the same time. The relationship between a selector and a channel is one of monitoring and being monitored.

Typically, a single thread handles a selector, and a selector can monitor many channels. With selectors, a single thread can handle hundreds or even thousands of channels. In extreme cases (tens of thousands of connections), a single thread can handle all channels, dramatically reducing the overhead of context switching between threads.

Register the relationship between channels and selectors. A Channel instance can be registered with a Selector by calling the Channel’s channel. register(Selector sel,int OPS) method. The register method takes two parameters: the first parameter specifies the selector instance that the channel is registered with; The second parameter specifies the type of IO events that the selector will monitor.

The channel I/O event types monitored by the selector are as follows:

  • Can read: SelectionKey OP_READ
  • Write: SelectionKey OP_WRITE
  • Connection: SelectionKey OP_CONNECT
  • Receive: SelectionKey OP_ACCEPT

The event type is defined in the SelectionKey class. If a selector wants to monitor multiple events for a channel, it can do so with the bitwise or operator. For example, monitoring both readable and writable IO events:

int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
Copy the code

What are IO events? This concept can be confusing, but it’s worth mentioning here. The I/O event is not an I/O operation on a channel, but a ready state of an I/O operation on a channel, indicating that the channel has the condition to complete an I/O operation.

For example, if a SocketChannel completes a handshake connection with its peer, it is in the connection-ready state (OP_CONNECT).