Java Network Programming--NIO Non-blocking Network Programming

Keywords: Java socket network Fragment

Starting with Java 1.4, in order to replace Java IO and network-related APIs and improve the running speed of programs, Java has provided a new non-blocking API for IO operations, namely Java NIO. There are three core components in NIO: Buffer, Channel and Selector. NIO operates on Channel (channel) and Buffer (buffer). Data is always read from channel to buffer or written from buffer to channel. Selector (selector) is mainly used to listen for events of multiple channels, so that a single thread can listen for multiple data channels.

Buffer

Buffers are essentially memory blocks that can be written to data (similar to arrays) and then read again. This memory block is contained in the NIO Buffer object, which provides a set of methods to use memory blocks more easily.
The Buffer API provides easier operation and management than direct operation arrays. Its data operations are divided into write and read. The main steps are as follows:

  1. Write data to a buffer
  2. Call buffer.flip() to convert to read mode
  3. Buffer read data
  4. Call buffer.clear() or buffer.compact() to clear the buffer

There are three important attributes in Buffer:
Capacity: As a memory block, Buffer has a fixed size, also known as capacity.
Position: The position where the data is written in write mode and read in read mode.
limit: Write mode is equal to Buffer's capacity and read mode is equal to the amount of data written.

Buffer uses code examples:

public class BufferDemo {
  public static void main(String[] args) {
    // Build a byte byte buffer with a capacity of 4
    ByteBuffer byteBuffer = ByteBuffer.allocate(4);
    // Default Write Mode to view three important metrics
    System.out.println(
        String.format(
            "Initialization: capacity Capacity:%s, position Location:%s, limit Limitations:%s",
            byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));
    // Write data
    byteBuffer.put((byte) 1);
    byteBuffer.put((byte) 2);
    byteBuffer.put((byte) 3);
    // Look at three important indicators again
    System.out.println(
        String.format(
            "After writing 3 bytes: capacity Capacity:%s, position Location:%s, limit Limitations:%s",
            byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));

    // Convert to read mode (flip method is not invoked, data can also be read, but the position record is read in the wrong place)
    System.out.println("Start reading");
    byteBuffer.flip();
    byte a = byteBuffer.get();
    System.out.println(a);
    byte b = byteBuffer.get();
    System.out.println(b);
    System.out.println(
        String.format(
            "After Reading 2 bytes of data, capacity Capacity:%s, position Location:%s, limit Limitations:%s",
            byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));

    // Continue writing 3 bytes. In read mode, limit=3, position=2. Continue writing can only overwrite writing one data.
    // The clear() method clears the entire buffer. The compact() method clears only the read data. Turn to write mode
    byteBuffer.compact();
    // Clear the read 2 bytes, the remaining 1 byte, you can also write 3 bytes of data
    // If you write more, you will report an exception to java.nio.BufferOverflow Exception
    byteBuffer.put((byte) 3);
    byteBuffer.put((byte) 4);
    byteBuffer.put((byte) 5);
    System.out.println(
        String.format(
            "Ultimately, capacity Capacity:%s, position Location:%s, limit Limitations:%s",
            byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));
  }
}

ByteBuffer Out-of-heap Memory

ByteBuffer provides direct (out-of-heap) and indirect (heap) implementations for performance-critical code. Out-of-heap memory implementations allocate memory objects to memory outside the heap of the Java virtual machine, which is managed directly by the operating system rather than the virtual machine. The result of this is to reduce the impact of garbage collection on the application to a certain extent and to provide the speed of running.

Out-of-heap memory acquisition: ByteBuffer by teBuffer = ByteBuffer. allocateDirect (noBytes)

Benefits of off-heap memory:

  • Network IO or file IO is copied once less than heap buffer. (file/socket-OS memory-jvm heap) In the process of writing files and sockets, GC will move objects, and in the implementation of JVM, data will be copied out of the heap and then written.
  • Outside the scope of GC, the pressure of GC is reduced, but automatic management is realized. In DirectByteBuffer, there is a Cleaner object (PhantomReference). Cleaner executes the clean method before GC executes, triggering Deallocator defined in DirectByteBuffer.

Out-of-heap memory usage recommendations:

  • Use only when performance is really impressive and allocate to large, long-lived objects (network transfer, file read-write scenarios, etc.)
  • Restrict size by the virtual machine parameter MaxDirectMemorySize to prevent exhaustion of the entire machine's memory

Channel

Channel is used to connect the source node to the target node. Channel is similar to traditional IO Stream. Channel itself can not access data directly. Channel can only interact with Buffer.

Channel's API covers TCP/UDP networks and file IO. The commonly used classes are FileChannel, Datagram Channel, Socket Channel, Server Socket Channel.

Standard IO Stream is usually one-way (InputStream/OutputStream), while Channel is a two-way channel, which can read and write in a channel, can read and write in a non-blocking channel, and the channel always reads and writes to the buffer (that is, Channel must cooperate with Buffer for use).

SocketChannel

SocketChannel is used to establish TCP network connection, similar to java.net.Socket. There are two ways to create a Socket Channel. One is that the client initiates a connection with the server on its own initiative, and the other is that the server acquires a new connection. There are two important methods in SocketChannel. One is the write() method. The write() method may return before the content has been written. It needs to call the write() method in the loop. Another is the read() reading method, which may return directly without reading any data at all and can determine how many bytes have been read according to the int value returned.

Core code sample fragment:

// Client initiates connection on its own initiative
SocketChannel socketChannel = SocketChannel.open();
// Set to non-blocking mode
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
// Request data occurs - data is written to the channel
socketChannel.write(byteBuffer);
// Read Server Return-Read Buffer Data
int readBytes = socketChannel.read(requestBuffer);
// Close the connection
socketChannel.close();

ServerSocketChannel

ServerSocket Channel can monitor new TCP connection channels, similar to ServerSocket. The core method of ServerSocketChannel, accept() method, if the channel is in non-blocking mode, then if there is no pending connection, the method will return null immediately. In actual use, it must check whether the returned SocketChannel is null.

Core code sample fragment:

// Create a Web Server
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// Set to non-blocking mode
serverSocketChannel.configureBlocking(false);
// Binding ports
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
  // Get a new tcp connection channel
  SocketChannel socketChannel = serverSocketChannel.accept();
  if (socketChannel != null) {
    // tcp request read / response
  }
}

Selector selector

Selector is also a core component of Java NIO, which can examine one or more NIO channels and determine which channels are ready for reading or writing. Implementing a single thread can manage multiple channels, thereby managing multiple network connections.

A thread using Selector can listen to different events of multiple Channel s. There are four main events, which correspond to four constants in Selection Key, respectively:

  • Connection event SelectionKey.OP_CONNECT
  • Ready event SelectionKey.OP_ACCEPT
  • Read event SelectionKey.OP_READ
  • Write event SelectionKey.OP_WRITE

The core of a thread processing multiple channels is event-driven mechanism. In a non-blocking network channel, developers register event types that are interested in the channel through Selector, and threads trigger corresponding code execution by listening for events. (At the bottom is the multiplexing mechanism of the operating system)

Core code sample fragment:

// Build a Selector selector and register channel
Selector selector = Selector.open();
// Register server Socket Channel to selector
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
// Interested in accept events on server Socket Channel (server Socket Channel only supports accept operations)
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
while (true) {
  // Use the following polling method to poll for events. The select method has blocking effect, and will not return until there is an event notification.
  selector.select();
  // Getting events
  Set<SelectionKey> keys = selector.selectedKeys();
  // Traversing query results
  Iterator<SelectionKey> iterator = keys.iterator();
  while (iterator.hasNext()) {
    // Encapsulated query results
    SelectionKey key = iterator.next();
    // Judge different event types and perform corresponding logical processing
    if (key.isAcceptable()) {
      // Logic for handling connections
    }
    if (key.isReadable()) {
      //Logic for processing read data
    }

    iterator.remove();
  }
}

NIO Network Programming Complete Code

Server-side code example:

// Non-blocking server implemented with Selector (abandoning polling for channel and using message notification mechanism)
public class NIOServer {

  public static void main(String[] args) throws IOException {
    // Create Server Socket Channel on the Web Server
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // Set to non-blocking mode
    serverSocketChannel.configureBlocking(false);

    // Build a Selector selector and register channel
    Selector selector = Selector.open();
    // Register server Socket Channel to selector
    SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
    // Interested in accept events on server Socket Channel (server Socket Channel only supports accept operations)
    selectionKey.interestOps(SelectionKey.OP_ACCEPT);

    // Binding ports
    serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    System.out.println("Successful start-up");

    while (true) {
      // Instead of polling the channel, use the following polling method for events. The select method has blocking effect and will not return until there is an event notification.
      selector.select();
      // Getting events
      Set<SelectionKey> keys = selector.selectedKeys();
      // Traversing query results
      Iterator<SelectionKey> iterator = keys.iterator();
      while (iterator.hasNext()) {
        // Encapsulated query results
        SelectionKey key = iterator.next();
        iterator.remove();
        // Focus on Read and Accept events
        if (key.isAcceptable()) {
          ServerSocketChannel server = (ServerSocketChannel) key.attachment();
          // Register the resulting client connection channel on the selector
          SocketChannel clientSocketChannel = server.accept();
          clientSocketChannel.configureBlocking(false);
          clientSocketChannel.register(selector, SelectionKey.OP_READ, clientSocketChannel);
          System.out.println("Receive a new connection : " + clientSocketChannel.getRemoteAddress());
        }
        if (key.isReadable()) {
          SocketChannel socketChannel = (SocketChannel) key.attachment();
          try {
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            while (socketChannel.isOpen() && socketChannel.read(byteBuffer) != -1) {
              // In the case of long connections, it is necessary to manually determine whether the data has been read out (here is a simple judgement: more than 0 bytes is considered to be the end of the request).
              if (byteBuffer.position() > 0) break;
            }

            if (byteBuffer.position() == 0) continue;
            byteBuffer.flip();
            byte[] content = new byte[byteBuffer.limit()];
            byteBuffer.get(content);
            System.out.println(new String(content));
            System.out.println("Data received,Come from:" + socketChannel.getRemoteAddress());

            // Response result 200
            String response = "HTTP/1.1 200 OK\r\n" + "Content-Length: 11\r\n\r\n" + "Hello World";
            ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
            while (buffer.hasRemaining()) {
              socketChannel.write(buffer);
            }

          } catch (Exception e) {
            e.printStackTrace();
            key.cancel(); // Cancel event subscription
          }
        }

        selector.selectNow();
      }
    }
  }
}

Client code example:

public class NIOClient {

  public static void main(String[] args) throws IOException {
    // Client initiates connection on its own initiative
    SocketChannel socketChannel = SocketChannel.open();
    // Set to non-blocking mode
    socketChannel.configureBlocking(false);
    socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
    while (!socketChannel.finishConnect()) {
      // If not connected, wait all the time.
      Thread.yield();
    }

    Scanner scanner = new Scanner(System.in);
    System.out.println("Please enter:");
    // send content
    String msg = scanner.nextLine();
    ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
    while (byteBuffer.hasRemaining()) {
      socketChannel.write(byteBuffer);
    }

    // Read response
    System.out.println("Receive the server response:");
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    while (socketChannel.isOpen() && socketChannel.read(buffer) != -1) {
      // In the case of long connections, it is necessary to manually determine whether the data has been read out (here is a simple judgement: more than 0 bytes is considered to be the end of the request).
      if (buffer.position() > 0) break;
    }

    buffer.flip();
    byte[] content = new byte[buffer.limit()];
    buffer.get(content);
    System.out.println(new String(content));
    scanner.close();
    socketChannel.close();
  }
}

COMPARISON BETWEEN NIO AND BIO

If the program needs to support a large number of connections, using NIO is the best way.
In Tomcat 8, BIO-related network processing code has been completely removed, and NIO is adopted by default for network processing.

Posted by portia on Mon, 26 Aug 2019 07:46:00 -0700