Netty practice and source code analysis -- on NIO programming

1 Preface

I wanted to write a blog related to Netty a long time ago, but my personal schedule has been delayed until now. I take this opportunity to review advanced Java programming and share Netty's actual combat and source code analysis with you.

2 what is netty?

Netty is a NIO client server framework which enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server. 'Quick and easy' doesn't mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences earned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.

Netty is a client server framework based on NIO, which can quickly and simply develop network applications like client server protocol. It greatly simplifies network programming such as TCP and UDP socket servers. "Fast and simple" does not mean that the resulting application will be affected by maintainability or performance problems. Netty is carefully designed based on experience gained from the implementation of many protocols, such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, netty successfully found a way to achieve ease of development, performance, stability, and flexibility without compromise.

3 Introduction to Java I / O model

When it comes to network communication, it is inseparable from the I/O model. The I/O model can be simply understood as what channel is used to send and receive data.

Java supports three network programming models: BIO, NIO and AIO

  • BIO, synchronous blocking IO. The server implementation mode is one thread per connection, that is, when the client has a request to connect to the server, the server will start a thread for processing. It can be seen that when multiple clients send requests, the server needs to start an equal number of threads, and when the client does not respond, the threads must always wait, In the long run, a large number of threads are required and the thread utilization is low, which will cause waste.

  • NIO is synchronous and non blocking. The server uses one thread to process multiple requests. The requests sent by the client will be registered on the multiplexer (selector), and the client with I/O requests will allocate threads for processing.

  • AIO, asynchronous and non blocking. AIO introduces the concept of asynchronous channel and adopts the Proactor mode to simplify program writing. The thread can only be started after effective requests. The feature is that the operating system notifies the service program to start the thread for processing after completion. It is generally suitable for applications with a large number of connections and a long connection time. The request sent by the client is first handed over to the operating system for processing, and then the OS notifies the thread after processing

Netty is actually NIO based on Java. Next, let's experience these three IO models by writing code

3.1 BIO code implementation

package com.Zhongger;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author zhongmingyi
 * @date 2021/9/15 1:29 afternoon
 */
public class BIOServer {
    public static void main(String[] args) throws IOException {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        ServerSocket serverSocket = new ServerSocket(8989);
        System.out.println("Server started");
        while (true) {
            Socket socket = serverSocket.accept();
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    handle(socket);
                }
            });
        }

    }

    public static void handle(Socket socket) {
        byte[] bytes = new byte[1024];
        try {
            InputStream inputStream = socket.getInputStream();
            int read = 0;
            while (true) {
                read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println("Data sent by the client to the server:" + new String(bytes, 0, read));
                } else {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

In the above code:

  • First, the server opens a ServerSocket, binds it to port 8989, and circularly waits for the client to connect
  • After the client connects to the server, the ServerSocket.accept method can obtain the Socket of the client
  • Whenever a client connects to the server, the thread pool will start a thread to process the IO data stream in the Socket, read the data sent by the client to the server through the read method of InputStream, and print it; When there is no data in the InputStream, finally close the Socket and the thread will be recycled to the thread pool

You can see that in the BIO model, the implementation mode of the server is a Socket connection corresponding to a thread. Here are the knowledge points of BIO. I believe you must have been exposed to Socket programming and realized a simple version of chat tool in the course of [computer network].

4 Java NIO

4.1 basic introduction

The java.nio. * package in JDK 1.4 introduces a new Java I/O library. NIO actually has two explanations:

  • New I/O: the reason is that it is new to the previous I/O class library.
  • Non block I/O: since the old I/O class library was blocking I/O, the goal of the New I/O class library is to make Java support non blocking I/O. therefore, more people like to call it non blocking I/O.

NIO has three core components:

  • Buffer buffer (equivalent to a train carrying goods)
  • Channel pipe (equivalent to track, responsible for transporting Buffer)
  • Selector selector (equivalent to ticket, used to select which Channel the train should be transported through)

NIO is block oriented (Buffer) processing. Data is read into a Buffer that will be processed later. It can move back and forth in the Buffer when necessary, which increases the flexibility in the processing process. NIO can provide a non blocking high scalability network. This allows a thread to read when there is data in the Buffer, and to do other things when there is no available data without blocking the reading; Threads can also write some data to the Buffer without waiting to write all the data. They can also do other things without blocking the write.

4.2 relationship between the three core components

The relationship between the three core components is briefly described as follows:

As shown in the figure:

  • Each Thread corresponds to a Selector, and each Selector corresponds to multiple channels
  • Each Channel has a corresponding Buffer. The Channel is bidirectional and can return to the underlying operating system (for example, Linux, the Channel is bidirectional), which is different from the unidirectional flow in BIO
  • The Channel to which the Selector switches for processing is determined by the Event
  • Buffer is a memory block. The bottom layer is an array. You can write data. You can switch to read data through the flip method. Buffer is also bidirectional

4.3 Buffer buffer

Buffer: buffer is essentially a memory block that can read and write data. It can be understood as a container object (including an array). This object provides a set of methods to use memory blocks more easily. The buffer object has built-in mechanisms to track and record the changes of the buffer. Channel provides a channel for reading data from the network and files, but reading or writing data needs to go through the buffer.

Buffer is a top-level abstract class. Its subclasses have multiple implementations. The common subclasses are as follows:

  • ByteBuffer: used to operate byte buffer
  • CharBuffer: used to manipulate character buffers
  • ShortBuffer: used to manipulate short buffers
  • IntBuffer: used to manipulate integer buffers
  • LongBuffer: used to manipulate long buffers
  • FloatBuffer: used to manipulate floating-point buffers
  • DoubleBuffer: used to manipulate double precision floating-point buffers

The above Buffer management methods are basically the same. You can use the allocate(int capacity) method of the class to obtain the Buffer object. As mentioned earlier, Buffer is the carrier dealing with data, that is, reading the data in the Buffer or writing the data to the Buffer. Therefore, the core methods of Buffer buffer are put() and get() methods, as well as the corresponding overload methods and extension methods.

The Buffer class has the following four properties:

// Invariants: mark <= position <= limit <= capacity
private int mark = -1; 
private int position = 0;
private int limit;
private int capacity;
  • Capacity: the maximum number of data elements that the Buffer can hold. The capacity is set when the Buffer is created and cannot be modified halfway. Capacity specifies the size of the underlying array in the Buffer
  • Limit: the current end point in the Buffer. Read and write operations cannot be performed on the Buffer beyond the limit. The limit can be modified
  • Position: the index position of the next element to be read or written in the Buffer. The position will be automatically updated by the corresponding get() and put() functions to prepare for the next read / write
  • Mark a note location. Used to record the last read / write position.

Simply look at the use of ByteBuffer and experience the transformation of the above four values after writing data and switching to read mode:

		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        System.out.println("Initial time-->limit--->" + byteBuffer.limit());
        System.out.println("Initial time-->position--->" + byteBuffer.position());
        System.out.println("Initial time-->capacity--->" + byteBuffer.capacity());
        System.out.println("Initial time-->mark--->" + byteBuffer.mark());

        System.out.println("--------------------------------------");
        // Add some data to the buffer
        String s = "back-end Dancer";
        byteBuffer.put(s.getBytes());

        // Look at the initial values of the four core variables
        System.out.println("put After-->limit--->" + byteBuffer.limit());
        System.out.println("put After-->position--->" + byteBuffer.position());
        System.out.println("put After-->capacity--->" + byteBuffer.capacity());
        System.out.println("put After-->mark--->" + byteBuffer.mark());

        System.out.println("--------------------------------------");

        byteBuffer.flip();
        System.out.println("flip After-->limit--->" + byteBuffer.limit());
        System.out.println("flip After-->position--->" + byteBuffer.position());
        System.out.println("flip After-->capacity--->" + byteBuffer.capacity());
        System.out.println("flip After-->mark--->" + byteBuffer.mark());


Here we introduce an efficient ByteBuffer, MappedByteBuffer, which can modify files in off heap memory (system memory other than JVM memory):

	public static void mappedByteBufferTest() throws IOException {
        RandomAccessFile file = new RandomAccessFile("/Users/bytedance/Desktop/file.txt", "rw");
        FileChannel fileChannel = file.getChannel();
        //The position of 0 ~ 3 is directly mapped to memory, and this part of the file can be modified
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 3);
        mappedByteBuffer.put(0, (byte) 'Y');
        mappedByteBuffer.put(2, (byte) 'K');
        file.close();
    }

4.4 Channel

The flow in BIO is unidirectional, either input flow or output flow. Then, in NIO, the channel, as the channel of transportation data, is bidirectional. Channel is an abstract class. Common implementations include ServerSocketChannel, SocketChannel, FileChannel and DatagramChannel. DatagramChannel is used for UDP data and the other three are used for TCP data.

FileChannel class is mainly used for IO operations on local files. Common methods include:

  • public int read(ByteBuffer var1) reads data from the Channel and puts it into ByteBuffer
  • public int write(ByteBuffer var1) writes the data from ByteBuffer to the Channel
  • public long transferFrom(ReadableByteChannel var1, long var2, long var4) copies data from ReadableByteChannel to the Channel
  • public long transferTo(long var1, long var3, WritableByteChannel var5) copies data from the Channel to the WritableByteChannel

Let's take a brief look at this example and output the string to the file:

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * @author zhongmingyi
 * @date 2021/9/16 10:38 afternoon
 */
public class FileChannelTest {
    public static void main(String[] args) throws IOException {
        String text = "Hello, Zhongger!";
        //Create a file output stream
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/bytedance/Desktop/file.txt");
        //Get to FileChannel through file output stream
        FileChannel fileChannel = fileOutputStream.getChannel();
        //Create a ByteBuffer and write data to the ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(text.getBytes());
        //Switch to read mode
        buffer.flip();
        //Write ByteBuffer data to FileChannel
        fileChannel.write(buffer);
        //Close file output stream
        fileOutputStream.close();
    }
}

Let's look at an example of reading data from a local file

 	public static void readFromFile() throws IOException {
        //Create a file input stream
        FileInputStream fileInputStream = new FileInputStream("/Users/bytedance/Desktop/file.txt");
        //Get FileChannel through file input stream
        FileChannel fileChannel = fileInputStream.getChannel();
        //Create a ByteBuffer and read the Channel data into the ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        fileChannel.read(buffer);
        //Output data in ByteBuffer
        System.out.println(new String(buffer.array()));
        //Close file input stream
        fileInputStream.close();
    }

Read data from one file to the Buffer, and then write the Buffer to another file

 	public static void readFromOneFileWriteToOtherFile() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("/Users/bytedance/Desktop/file.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/bytedance/Desktop/file2.txt");

        FileChannel fileInputStreamChannel = fileInputStream.getChannel();
        FileChannel fileOutputStreamChannel = fileOutputStream.getChannel();

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
        	//Buffer reset to prevent cross-border
            buffer.clear();
            int read = fileInputStreamChannel.read(buffer);
            if (read == -1) {
                break;
            }
            //Cut read
            buffer.flip();
            fileOutputStreamChannel.write(buffer);
        }

        fileInputStream.close();
        fileOutputStream.close();
    }

Copy one file to another:

 	public static void transferFrom() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("/Users/bytedance/Desktop/file.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("/Users/bytedance/Desktop/file_copy.txt");

        FileChannel fileInputStreamChannel = fileInputStream.getChannel();
        FileChannel fileOutputStreamChannel = fileOutputStream.getChannel();

        fileOutputStreamChannel.transferFrom(fileInputStreamChannel, 0, fileInputStreamChannel.size());

        fileInputStream.close();
        fileOutputStream.close();
    }

Another brief introduction: FileChannel provides a map method to map files to virtual memory. Generally, the whole file can be mapped. If the file is large, it can be mapped in segments. In short, it reduces the copy from kernel state to user state by mapping, so it can improve the replication performance.

	public static void mappedByteBufferTest() throws IOException {
        RandomAccessFile file = new RandomAccessFile("/Users/bytedance/Desktop/file.txt", "rw");
        FileChannel fileChannel = file.getChannel();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 3);
        mappedByteBuffer.put(0, (byte) 'Y');
        mappedByteBuffer.put(2, (byte) 'K');
        file.close();

    }

4.5 Selector

Selector selector is a multiplexer in NIO. A thread corresponds to a selector, and multiple channels can be registered in the selector. When an event occurs in the Channel, the thread can handle the event. If no event occurs, the thread can be free to do other things. The advantage of using selector is that it can process channels with fewer threads. Compared with using multiple threads, it avoids the overhead caused by thread context switching.

4.5.1 creation of selector

Create a Selector object by calling the Selector.open() method, as follows:

Selector selector = Selector.open();

4.5.2 register Channel to Selector

The Channel must be non blocking, otherwise an IllegalBlockingModeException will be thrown

channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);

This method can set the Channel to non blocking

abstract SelectableChannel configureBlocking(boolean block)  

be careful:
The configureBlocking() method of the SelectableChannel abstract class is implemented by the AbstractSelectableChannel abstract class. SocketChannel, ServerSocketChannel and DatagramChannel all directly inherit the AbstractSelectableChannel abstract class. Therefore, they can call the configureBlocking method to set it to non blocking mode.

The second parameter of the register() method is an "interest set", which means what events are interested in when listening to the Channel through the Selector. You can listen to four different types of events:

SelectionKey.OP_ACCEPT
SelectionKey.OP_WRITE
SelectionKey.OP_READ
SelectionKey.OP_CONNECT

The Channel triggers an event, which means that the event is ready. For example, if a Channel successfully connects to another server, it is called "connect". A ServerSocketChannel ready to receive a new incoming connection is called "accept". A data readable Channel can be said to be "read". The Channel waiting to write data can be said to be "write ready".

4.5.3 SelectionKey

A SelectionKey key represents the registration relationship between a specific Channel object and a specific Selector object.

key.attachment(); //Returns the attachment of the SelectionKey, which can be specified when registering the channel.
key.channel(); // Returns the channel corresponding to the SelectionKey.
key.selector(); // Returns the Selector corresponding to the SelectionKey.
key.interestOps(); //Returns the bit mask representing the IO operation to be monitored by the Selector
key.readyOps(); // Returns a bit mask representing the IO operations that can be performed on the corresponding channel```

4.5.4 select Channel from Selector

The Selector maintains the registered Channel collection, and this registration relationship is encapsulated in the SelectionKey.
There are three types of SelectionKey collections maintained by the Selector:

  • Registered key set. The set of keys generated by all channels associated with the selector is called the set of registered keys. Not all registered keys are still valid. This collection is returned through the keys() method and may be empty. The set of registered keys cannot be modified directly; Attempting to do so will raise a java.lang.unsupported operationexception.
  • Selected key set. The set of keys generated by all selectors listening to the associated channel is called the set of selected keys. This collection is returned by the selectedKeys() method and may be empty.
  • Cancelled key set. A subset of the set of registered keys that contain the keys called by the cancel() method (the key has been invalidated), but they have not been unregistered. This collection is a private member of the selector object and cannot be accessed directly.

Note: when a key is cancelled (as can be determined by the isValid() method), it will be placed in the set of cancelled keys of the relevant selector. Registration will not be cancelled immediately, but the key will expire immediately. When the select() method is called again (or at the end of an ongoing select() call), the cancelled keys in the set of cancelled keys will be cleaned up and the corresponding logout will be completed. The channel will be unregistered and the new SelectionKey will be returned. When the channel is closed, all related keys are automatically cancelled (remember, a channel can be registered on multiple selectors). When the selector is closed, all channels registered with the selector will be unregistered and the relevant keys will be immediately invalidated (cancelled). Once the key is invalidated, the selection related method that calls it throws a canceledkeyexception.

Introduction to the select() method:

In the newly initialized Selector object, all three collections are empty. Through the select() method of the Selector, you can select the channels that are ready (these channels contain the events you are interested in). For example, if you are interested in read ready channels, the select() method will return those channels for which the read event is ready. Here are several overloaded select() methods of Selector:

int select(): blocking until at least one channel is ready for the event you registered.
int select(long timeout): the same as select(), but the maximum blocking time is timeout milliseconds.
int selectNow(): non blocking. It returns as soon as a channel is ready.
The int value returned by the select () method indicates how many channels are ready, and how many channels have become ready since the last call to the select () method. The channel that was ready in the previous select() call will not be recorded in this call, and the channel that was ready in the previous select() call but is no longer ready will not be recorded. For example, if the select () method is called for the first time, it will return 1 if one channel becomes ready. If the select () method is called again, it will return 1 again if another channel is ready. If nothing is done with the first ready channel, there are now two ready channels, but only one is ready between each select () method call.

Once the select() method is called and the return value is not 0, you can access the selected key collection by calling the selectedKeys() method of the Selector. As follows:
Set selectedKeys=selector.selectedKeys();
Then it can be placed in the Selector and Channel associated with a SelectionKey. As follows:

		while (true) {
            if (selector.select(1000) == 0) {
                System.out.println("The server is not connected to the client...");
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isAcceptable()) {
                    System.out.println("The server received the client request");
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                if (selectionKey.isReadable()) {
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
                    channel.read(byteBuffer);
                    System.out.println("from client " + new String(byteBuffer.array()));
                }

                iterator.remove();
            }

        }

4.5.5 stop selection method

The selector executes the selection process. The bottom layer of the system will ask whether each channel is ready in turn. This process may cause the calling thread to enter the blocking state. Then we have the following three ways to wake up the thread blocked in the select () method.

  • wakeup() method: call the wakeup() method of the Selector object to make the select() method in the blocked state return immediately
    This method causes the first selection operation on the selector that has not returned to return immediately. If there is no selection currently in progress, the next call to the select() method will return immediately.
  • Close() method: close the Selector through the close() method,
    This method makes any thread blocked in the selection operation wake up (similar to wakeup ()), and all channels registered with the Selector are logged off, and all keys will be cancelled, but the Channel itself will not be closed.

4.5.6 NIO client and server

Server code:

package com.Zhongger;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @author zhongmingyi
 * @date 2021/9/15 3:00 afternoon
 */
public class NIOServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8886));
        serverSocketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            if (selector.select(1000) == 0) {
                System.out.println("The server is not connected to the client...");
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isAcceptable()) {
                    System.out.println("The server received the client request");
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                if (selectionKey.isReadable()) {
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
                    channel.read(byteBuffer);
                    System.out.println("from client " + new String(byteBuffer.array()));
                }

                iterator.remove();
            }

        }

    }
}

Client code:

package com.Zhongger;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @author zhongmingyi
 * @date 2021/9/24 1:27 afternoon
 */
public class NIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8886);
        //Connect server
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("The connection takes time. The client will not block and can do other work");
            }
        }
        //Connect successfully, send data
        String str = "Hello,Zhongger!";
        ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(byteBuffer);
        System.in.read();
    }
}

5 Java NIO summary

  • Event driven model
  • Avoid multithreading
  • Single thread multitasking
  • Non blocking I/O, I/O read / write is no longer blocked
  • block based transmission is usually more efficient than stream based transmission
  • More advanced IO function, zero copy
  • IO multiplexing greatly improves the scalability and practicability of Java network applications

I'm Zhongger. I'm a migrant worker fishing and writing code in an Internet company. Pay attention to me and learn more about Java back-end development. See you next time

Tags: Java NIO

Posted on Sat, 25 Sep 2021 00:37:08 -0400 by waynem801