Recently, I plan to deepen my knowledge of Java network programming (IO, NIO, Socket programming, Netty).

Java I/O streams

Java Network Programming

Java NIO: Buffers

Java NIO: Channels

Java NIO: selector

Java NIO requires an understanding of the three core concepts of buffers, channels, and selectors as a complement to Java I/O to improve the efficiency of mass data transfer.

The traditional Java solution for monitoring multiple sockets is to create a thread for each Socket and block the thread at the read call until the data is readable. This works well on systems with low concurrency, where many threads need to be created (one thread per connection). Too many threads cause frequent context switches, threads are system resources, and the maximum number of threads that can be created is limited and far less than the number of network connections that can be established.

NIO’s selectors are designed to solve this problem by providing the ability to ask multiple channels simultaneously if they are ready to perform I/O, such as if a SocketChannel object has more bytes to read and if ServerSocketChannel has a client connection that has already arrived.

By using selectors, we can listen for the ready state of multiple channels in one thread!

The core concept

Selector: Manages the collection of selectable channels & updates the ready state of the selectable channels

Selectable channels: Sockets are selectable for all channels that inherit from SelectableChannel, but file channels are not. Only selectable channels can be registered with the selector.

Select key: The selectable channel registers with the selector and returns the select key, so the select key is an encapsulation of the channel and selector registration relationship

The relationship between the three: Selectable channels register with the selector and return the select key

Selector use

The usual steps for using a selector are:

  1. Construction selector

  2. Selectable channels are registered with the selector

  3. Selector selection (selects ready channels)

  4. Read and write to the ready channel

  5. Repeat 2 to 4

    Here are some steps to explain

Construction selector

Using static factory method construction (underneath creating Selector instances using SelectorProvider, which supports Java SPI extensions)

Selector selector = Selector.open();	
Copy the code

Selectable channels are registered with the selector

Only channels running in non-blocking mode can be registered with the selector

The methods that are registered are defined in the SelectableChannel class, and are registered with optional operations (the four optional operations are defined in the SelectionKey), as well as with attachments

Select key is returned after successful registration. The specific API is as follows:

// Parameter selector + optional operation
public final SelectionKey register(Selector sel, int ops)
// The version with attachments
public abstract SelectionKey register(Selector sel, int ops, Object att)
Copy the code

Selector selection

The select method selects the ready channel and places the SelectionKey associated with that ready channel into the selectedKeys set of the selector.

Read and write to the ready channel

Read and write operations on ready channels are shown in Demo below

Demo

I wrote a demo to integrate the above steps, the code is mainly two parts: optional channel registration & selector selection and ready channel read and write operations

Optional channel registration & selector selection

  // Construct selector
  Selector selector = Selector.open();
  // Initialize the ServerSocketChannel binding local port and set it to non-blocking mode
  ServerSocketChannel ch = ServerSocketChannel.open();
  ch.bind(new InetSocketAddress("127.0.0.1".7001));
  ch.configureBlocking(false);
  // The channel registers with the selector and cares about the ACCEPT operation (because it is a Server)
  ch.register(selector, SelectionKey.OP_ACCEPT);
  // Loop through to select the channel ready state
  while (true) {
    int n = selector.select();
    if (n == 0) {
      continue;
    }
    Iterator<SelectionKey> it = selector.selectedKeys().iterator();
    // Walk through all ready channels
    while (it.hasNext()) {
      SelectionKey key = it.next();
      // If the ready action is ACCEPT, ACCEPT the client connection and register with the selector.
      if (key.isAcceptable()) {
        try {
          SocketChannel cch = ch.accept();
          cch.configureBlocking(false);
          // The client channel registers with the selector selector and cares about the READ operation
          cch.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
        } catch(IOException e) { e.printStackTrace(); }}else {
        // If it is another ready operation, commit it to the thread pool
        pool.submit(() -> handle(key));
      }
      // Remove the select keyit.remove(); }}Copy the code

Read and write to the ready channel

public static void handle(SelectionKey key) {
    // If the channel is read-ready
    if (key.isReadable()) {
        key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));	
        SocketChannel ch = (SocketChannel) key.channel();
        ByteBuffer bf = (ByteBuffer) key.attachment();
        try {
            int n = ch.read(bf);
          	// Read the data (ASCII) and simply output it
            if (n > 0) {
                bf.flip();
                StringBuilder builder = new StringBuilder();
                while (bf.hasRemaining()) {
                    builder.append((char) bf.get());
                }
                System.out.print(builder.toString());
                bf.clear();
                key.interestOps(key.interestOps() | SelectionKey.OP_READ);
                key.selector().wakeup();
            } else if (n < -1) { // Close the connectionch.close(); }}catch (IOException e) {
            //}}}Copy the code

Selector depth

Optional operations

There are four optional operations (OP_READ, OP_WRITE, OP_ACCEPT, and OP_ACCEPT). The following shows the Socket channel’s support for these four optional operations

OP_READ OP_WRITE OP_ACCEPT OP_CONNECT
SocketChannel support support Does not support support
SeverScoketChannel support support support Does not support
DatagramChannel support support Does not support Does not support

In summary: the client Socket channel does not care about ACCEPT, the server Socket channel does not care about CONNECT, and the datagram Socket channel only cares about READ and WRITE

Select key (SelectionKey)

Selectable channels are registered with the selector and a selectable key object is returned that represents an association between the channel and the selector. There are a few main attributes to know about him

  • InterestOps: Collection of selectable operations of interest, initialized when selectable channels are registered with the picker and can be modified

  • ReadyOps: A collection of ready selectable operations that are updated when selectors are selected and cannot be modified by clients.

  • Attachment: Select key can be associated with an object, called an attachment (for example, to a buffer object)

Selector selection process

Before we look at the selection process, let’s look at what the three key sets in the selector mean

  • A collection of registered keys, returned by the keys method. The corresponding selection key is added to the collection when the channel is registered

  • The set of selected keys into which the ready channel’s selection key is placed when selected by the selector

  • The collection of cancelled keys to which the selection key is added after the cancel method for the selection key is called

Specific selection process:

  1. If the set of canceled keys is not empty, each canceled key is removed from the other two sets and the associated channel is unregistered, and finally the set of canceled keys is emptied.
  2. Queries the ready state of channels in the collection of registered keys (system calls), updates the collection of selected keys and the collection of readyOps for keys. Update the readyOps collection for a key if it was already in the collection of selected keys prior to the selection; Otherwise add the key to the selected collection and reset the readyOps collection for that key.)

Best practices

We typically use a selector to manage all the alternative channels and delegate the ready channel services to other threads, with only one thread monitoring the ready state of the channel and using a coordinated worker thread pool to read and write data

In this way, the operation bit needs to be removed from the interestOps collection before the relevant operation, so as to avoid putting the selection key into the selected collection the next time the selector is selected. After the relevant operation is done, it adds the operation to the interestOps collection and wakes up the selector for the next selection

The following is a simple read pseudocode

/ / read in place
if (key.isReadable()) {
  	// Remove READ from interestOps
	key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
	/ /... Data read processing
  	// Add READ to interestOps
 	key.interestOps(key.interestOps() | SelectionKey.OP_READ);
  	// Wake up the selector and reselect it (because the interestOps of the key has changed)
 	key.selector().wakeup();
}
Copy the code

conclusion

  1. Alternative channelRegistered toThe selectorOn the backSelect key
  2. The core idea of the selector is to use one (or a few) selectors to manage all the optional channels, with each selector using one thread to monitor the ready state, and a worker thread pool to handle data reads and writes for the ready channels