1
头图

NIO basics

What is NIO

  1. Java NIO stands for Java non-blocking IO, which refers to the new API provided by the JDK. Starting from JDK 1.4, Java provides a series of improved input/output new features, collectively referred to as NIO, or New IO, which is synchronous non-blocking.
  2. NIO related classes are placed in the java.nio package, and many classes in the original java.io package have been rewritten.
  3. NIO has three core part: Channel (pipeline), Buffer (buffer), Selector (selector).
  4. NIO is programmed buffer. The data is read into a buffer that it processes slightly, and it can be moved back and forth in the buffer when needed, which increases the flexibility in the processing process and uses it to provide a non-blocking high-scalability network.
  5. The non-blocking mode of Java NIO allows a thread to send a request to read data from a channel, but it can only get the currently available data. If there is no data currently available, it means that it will not get it, instead of keeping the thread blocked, so until the data Before becoming readable, the thread can do other things. The same goes for non-blocking writes.

Three core components

概览

Basic introduction to Channel

NIO channels are similar to streams, but have the following differences:

  1. The channel is bidirectional and can be read and written, while the stream is unidirectional and can only be read or written.
  2. The channel can realize asynchronous reading and writing of data
  3. The channel can read data from the buffer or write data to the buffer

Four channels:

  • FileChannel: Read and write data from a file
  • DatagramChannel: Read and write data in the network through UDP protocol
  • SocketChannel: can read and write data in the network through the TCP protocol, often used on the client side
  • ServerSocketChannel: Monitor TCP connections, and create a SocketChannel for each new connection

Buffer (buffer) basic introduction

Buffer in NIO is used for NIO channel (Channel) for interaction.

The buffer is essentially a memory block that can read and write data. It can be understood as a container object (including an array). The object provides a set of methods , which makes it easier to use the memory block. The buffer object has some built-in The mechanism can track and record the status changes of the buffer.

When writing data to the Buffer, the Buffer will record how much data has been written. Once the data is to be read, needs to use the flip() method to switch the Buffer from the write mode to the read mode . In the read mode, you can read all the data previously written to the Buffer.

When all the data has been read, the buffer area needs to be cleared so that it can be written again. There are two ways to clear the buffer, call the clear() or compact() method.

clear() method will clear the entire buffer. The compact() method will only clear the data has been read. Any unread data will be moved to the beginning of the buffer, and the newly written data will be placed behind the unread data in the buffer.

Channel provides a channel for reading data from files and the network, but reading or all must go through Buffer. An array of the corresponding type is maintained in the Buffer subclass to store data.

Basic introduction to Selector

  1. Java's NIO uses a non-blocking I/O method. You can use one thread to handle several client connections, and then the Selector (selector) will be used
  2. Selector can detect whether events occur on multiple registered channels (multiple Channels are registered to the same selector in the form of events) , if an event occurs, get the event and then deal with each event accordingly
  3. Only when the connection actually has a read and write event, it will read and write, reducing system overhead, and it is not necessary to create a thread for each connection, and there is no need to maintain multiple threads
  4. Avoid the overhead caused by context switching between multiple threads
Features of Selector

Netty's I/O thread NioEventLoop aggregates Selector (selector/multiplexer), which can handle hundreds of client connections concurrently.

When a thread reads and writes from a client Socket channel, if no data is available, the thread can perform other tasks.

Threads usually use the idle time of non-blocking I/O to perform I/O operations on other channels, so a single thread can manage multiple input and output channels.

Since the read and write operations are non-blocking, the operating efficiency of the I/O thread can be fully improved, and thread suspension caused by frequent I/O blocking can be avoided.

One I/O thread can concurrently process N client connections and read and write operations, which fundamentally solves the traditional synchronous blocking I/O one connection one thread model, and the architecture performance, elastic scalability and reliability have been greatly improved .

三大核心组件的关系

Basic use of ByteBuffer

Core dependency

<dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.36.Final</version>
</dependency>
/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/28
 * @Description ByteBuffer基本使用,读取文件内容并打印
 */
public class ByteBufferTest {

    public static void main(String[] args) {
        //获取channel
        try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
            //创建ByteBuffer
            final ByteBuffer buffer = ByteBuffer.allocate(1024);
            //读取文件内容,并存入buffer
            channel.read(buffer);
            //切换为读模式
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            //清空缓冲区,并重置为写模式
            buffer.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

Output result:

1234567890abc

ByteBuffer structure

Four attributes are defined in Buffer to provide the data elements it contains.

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
  • capacity : the capacity of the buffer zone. Given by the constructor, once set, it cannot be changed
  • limit : the limit of the buffer zone. The data after limit cannot be read or written. The buffer limit cannot be negative, and cannot be greater than its capacity
  • position : The index of the reading and writing position of the next The position of the buffer cannot be negative, and cannot be greater than limit
  • mark : Record the current position value. position of 161cdbb707a0aa is changed, it can be restored to the position of the mark by calling the reset() method.

At the beginning, position points to the first write position, and limit and capacity are equal to the capacity of the buffer.

初始状态

In the write mode, position is the writing position, limit is equal to the capacity, the following figure shows the state after writing 4 elements.

写入状态

After calling the flip() method to switch to the read mode, position is switched to the reading position, and limit is switched to the reading limit.

读取状态

After reading the limit position, you cannot continue reading.

读取完毕

After calling the clear() method, it returns to the most original state.

clear()

When calling the compact() method, you need to pay attention: This method is a ByteBuffer method, not a Buffer method .

  • compact will compress the unread data forward, and then switch to write mode
  • After the data is moved forward, the value of the original position is not cleared. When writing, will overwrite the previous value of

compact()

Common methods of ByteBuffer

allocate space: allocate()

//java.nio.HeapByteBuffer java堆内存,读写效率较低,受到gc影响
System.out.println(ByteBuffer.allocate(1024).getClass());
//java.nio.DirectByteBuffer 直接内存,读写效率较高(少一次拷贝),不会受gc影响,分配内存效率较低,使用不当则可能会发生内存泄漏
System.out.println(ByteBuffer.allocateDirect(1024).getClass());

flip()

  • The flip() method will to the buffer operation mode , from write->read/read->write

put()

  • The put() method can put a piece of data into the buffer.
  • After performing this operation, the value of postition will be +1, pointing to the next place where it can be placed.

get()

  • The get() method will read a value in the buffer
  • After performing this operation, position will be +1, if it exceeds limit, an exception will be thrown
Note: The get(i) method will not change the value of position.

rewind()

  • This method can only be used in read mode
  • After the rewind() method, the value of position, limit and capacity will be restored to the value before get()

clear()

  • The clear() method will restore each attribute in the buffer to its original state, position = 0, capacity = limit
  • this time buffer data still exists , in the "forgotten" status, will cover the data write operation next time

mark() and reset()

  • The mark() method saves the value of the postion to the mark attribute
  • The reset() method will change the value of position to the value saved in mark

Convert between string and ByteBuffer

Introduce tools:

import io.netty.util.internal.MathUtil;
import io.netty.util.internal.StringUtil;

import java.nio.ByteBuffer;

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/28
 * @Description 工具类
 */
public class ByteBufferUtil {

    private static final char[] BYTE2CHAR = new char[256];
    private static final char[] HEXDUMP_TABLE = new char[256 * 4];
    private static final String[] HEXPADDING = new String[16];
    private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4];
    private static final String[] BYTE2HEX = new String[256];
    private static final String[] BYTEPADDING = new String[16];

    static {
        final char[] DIGITS = "0123456789abcdef".toCharArray();
        for (int i = 0; i < 256; i++) {
            HEXDUMP_TABLE[i << 1] = DIGITS[i >>> 4 & 0x0F];
            HEXDUMP_TABLE[(i << 1) + 1] = DIGITS[i & 0x0F];
        }

        int i;

        // Generate the lookup table for hex dump paddings
        for (i = 0; i < HEXPADDING.length; i++) {
            int padding = HEXPADDING.length - i;
            StringBuilder buf = new StringBuilder(padding * 3);
            for (int j = 0; j < padding; j++) {
                buf.append("   ");
            }
            HEXPADDING[i] = buf.toString();
        }

        // Generate the lookup table for the start-offset header in each row (up to 64KiB).
        for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i++) {
            StringBuilder buf = new StringBuilder(12);
            buf.append(StringUtil.NEWLINE);
            buf.append(Long.toHexString(i << 4 & 0xFFFFFFFFL | 0x100000000L));
            buf.setCharAt(buf.length() - 9, '|');
            buf.append('|');
            HEXDUMP_ROWPREFIXES[i] = buf.toString();
        }

        // Generate the lookup table for byte-to-hex-dump conversion
        for (i = 0; i < BYTE2HEX.length; i++) {
            BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i);
        }

        // Generate the lookup table for byte dump paddings
        for (i = 0; i < BYTEPADDING.length; i++) {
            int padding = BYTEPADDING.length - i;
            StringBuilder buf = new StringBuilder(padding);
            for (int j = 0; j < padding; j++) {
                buf.append(' ');
            }
            BYTEPADDING[i] = buf.toString();
        }

        // Generate the lookup table for byte-to-char conversion
        for (i = 0; i < BYTE2CHAR.length; i++) {
            if (i <= 0x1f || i >= 0x7f) {
                BYTE2CHAR[i] = '.';
            } else {
                BYTE2CHAR[i] = (char) i;
            }
        }
    }

    /**
     * 打印所有内容
     *
     * @param buffer
     */
    public static void debugAll(ByteBuffer buffer) {
        int oldlimit = buffer.limit();
        buffer.limit(buffer.capacity());
        StringBuilder origin = new StringBuilder(256);
        appendPrettyHexDump(origin, buffer, 0, buffer.capacity());
        System.out.println("+--------+-------------------- all ------------------------+----------------+");
        System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), oldlimit);
        System.out.println(origin);
        buffer.limit(oldlimit);
    }

    /**
     * 打印可读取内容
     *
     * @param buffer
     */
    public static void debugRead(ByteBuffer buffer) {
        StringBuilder builder = new StringBuilder(256);
        appendPrettyHexDump(builder, buffer, buffer.position(), buffer.limit() - buffer.position());
        System.out.println("+--------+-------------------- read -----------------------+----------------+");
        System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), buffer.limit());
        System.out.println(builder);
    }

    private static void appendPrettyHexDump(StringBuilder dump, ByteBuffer buf, int offset, int length) {
        if (MathUtil.isOutOfBounds(offset, length, buf.capacity())) {
            throw new IndexOutOfBoundsException(
                    "expected: " + "0 <= offset(" + offset + ") <= offset + length(" + length
                            + ") <= " + "buf.capacity(" + buf.capacity() + ')');
        }
        if (length == 0) {
            return;
        }
        dump.append(
                "         +-------------------------------------------------+" +
                        StringUtil.NEWLINE + "         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |" +
                        StringUtil.NEWLINE + "+--------+-------------------------------------------------+----------------+");

        final int startIndex = offset;
        final int fullRows = length >>> 4;
        final int remainder = length & 0xF;

        // Dump the rows which have 16 bytes.
        for (int row = 0; row < fullRows; row++) {
            int rowStartIndex = (row << 4) + startIndex;

            // Per-row prefix.
            appendHexDumpRowPrefix(dump, row, rowStartIndex);

            // Hex dump
            int rowEndIndex = rowStartIndex + 16;
            for (int j = rowStartIndex; j < rowEndIndex; j++) {
                dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);
            }
            dump.append(" |");

            // ASCII dump
            for (int j = rowStartIndex; j < rowEndIndex; j++) {
                dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);
            }
            dump.append('|');
        }

        // Dump the last row which has less than 16 bytes.
        if (remainder != 0) {
            int rowStartIndex = (fullRows << 4) + startIndex;
            appendHexDumpRowPrefix(dump, fullRows, rowStartIndex);

            // Hex dump
            int rowEndIndex = rowStartIndex + remainder;
            for (int j = rowStartIndex; j < rowEndIndex; j++) {
                dump.append(BYTE2HEX[getUnsignedByte(buf, j)]);
            }
            dump.append(HEXPADDING[remainder]);
            dump.append(" |");

            // Ascii dump
            for (int j = rowStartIndex; j < rowEndIndex; j++) {
                dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]);
            }
            dump.append(BYTEPADDING[remainder]);
            dump.append('|');
        }

        dump.append(StringUtil.NEWLINE +
                "+--------+-------------------------------------------------+----------------+");
    }

    private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) {
        if (row < HEXDUMP_ROWPREFIXES.length) {
            dump.append(HEXDUMP_ROWPREFIXES[row]);
        } else {
            dump.append(StringUtil.NEWLINE);
            dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L));
            dump.setCharAt(dump.length() - 9, '|');
            dump.append('|');
        }
    }

    public static short getUnsignedByte(ByteBuffer buffer, int index) {
        return (short) (buffer.get(index) & 0xFF);
    }

}

Test category:

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/28
 * @Description 字符串和ByteBuffer相互转换
 */
public class TranslateTest {

    public static void main(String[] args) {
        String str1 = "hello";
        String str2;
        String str3;
        // 通过StandardCharsets的encode方法获得ByteBuffer
        // 此时获得的ByteBuffer为读模式,无需通过flip切换模式
        ByteBuffer buffer = StandardCharsets.UTF_8.encode(str1);
        //也可以使用wrap方法实现,无需通过flip切换模式
        ByteBuffer wrap = ByteBuffer.wrap(str1.getBytes());
        ByteBufferUtil.debugAll(wrap);
        ByteBufferUtil.debugAll(buffer);

        // 将缓冲区中的数据转化为字符串
        // 通过StandardCharsets解码,获得CharBuffer,再通过toString获得字符串
        str2 = StandardCharsets.UTF_8.decode(buffer).toString();
        System.out.println(str2);

        str3 = StandardCharsets.UTF_8.decode(wrap).toString();
        System.out.println(str3);
    }

}

operation result:

+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f                                  |hello           |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [5]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 68 65 6c 6c 6f                                  |hello           |
+--------+-------------------------------------------------+----------------+
hello
hello

Sticky bag and half bag

Phenomenon

There are multiple pieces of data sent to the server on the network, and the data is separated by \n.
However, for some reason, these data were recombined when they were received. For example, there are 3 pieces of original data:

  • Hello,world\n
  • I’m Jack\n
  • How are you?\n

Become the following two byteBuffer (sticky package, half package)

  • Hello,world\nI’m Jack\nHo
  • w are you?\n

Reason

sticky package

When sending data, the sender does not send the data one by one, but integrates the data together , and sends them together when the data reaches a certain amount. This will cause multiple messages to be placed in a buffer to be sent out together.

half pack

The size of the receiver's buffer is limited. When the receiver's buffer is full, is needed to truncate the information to , and then continue to put data after the buffer is empty. This will happen when a complete piece of data is finally truncated.

Solution

  1. Traverse the ByteBuffer through the get(index) method, and process it when it encounters \n .
  2. Record the data length from position to index, and apply for a buffer of the corresponding size.
  3. Get the data in the buffer zone through get() write it into the target buffer zone.
  4. Finally, call the compact() method to switch to write mode, because there may still be unread data in the buffer.
/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description 解决黏包和半包
 */
public class ByteBufferTest {

    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(32);
        //模拟黏包和半包
        buffer.put("Hello,world\nI'm Jack\nHo".getBytes(StandardCharsets.UTF_8));
        split(buffer);
        buffer.put("w are you?\n".getBytes(StandardCharsets.UTF_8));
        split(buffer);
    }

    private static void split(ByteBuffer buffer) {
        //切换读模式
        buffer.flip();
        for (int i = 0; i < buffer.limit(); i++) {
            //找到完整消息
            if (buffer.get(i) == '\n') {
                int length = i + 1 - buffer.position();
                final ByteBuffer target = ByteBuffer.allocate(length);
                //从buffer中读取,写入 target
                for(int j = 0; j < length; j++) {
                    // 将buffer中的数据写入target中
                    target.put(buffer.get());
                }
                // 打印查看结果
                ByteBufferUtil.debugAll(target);
            }
        }
        //清空已读部分,并切换写模式
        buffer.compact();
    }
}

operation result:

+--------+-------------------- all ------------------------+----------------+
position: [12], limit: [12]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 2c 77 6f 72 6c 64 0a             |Hello,world.    |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [9], limit: [9]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 49 27 6d 20 4a 61 63 6b 0a                      |I'm Jack.       |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+
position: [13], limit: [13]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 6f 77 20 61 72 65 20 79 6f 75 3f 0a          |How are you?.   |
+--------+-------------------------------------------------+----------------+

File programming

FileChannel

Operating mode

📢: FileChannel can only work in blocking mode !

Obtain

Can not directly open FileChannel, must to get FileChannel by FileInputStream, FileOutputStream or RandomAccessFile, they have getChannel() method.

  • obtained through FileInputStream can only read
  • obtained through FileOutputStream can only write
  • Whether can read and write through RandomAccessFile is determined according to the read and write mode when constructing RandomAccessFile

Read

Fill the ByteBuffer with data through the read() method, the return value indicates how many bytes have -1 read, and 061cdbb707a8ec indicates that the end of the file has been read.

int readBytes = channel.read(buffer);

Write

Because the channel has a write upper limit, the write() method does not guarantee that all the contents of the buffer will be written to the channel at one time. Must be written in accordance with the following rules .

// 通过hasRemaining()方法查看缓冲区中是否还有数据未写入到通道中
while(buffer.hasRemaining()) {
    channel.write(buffer);
}

closure

Channel must be closed, but when calling the close() method of FileInputStream, FileOutputStream, RandomAccessFile, the close() method of Channel will also be called indirectly.

Location

Channel also has an attribute that stores the position of reading data, namely position.

long pos = channel.position();

The value of position in the channel can be set by position (int pos).

long newPos = 10;
channel.position(newPos);

When setting the current position, if it is set to the end of the file:

  • At this time, reading will return -1
  • When writing at this time, content will be appended, but be aware that if the position exceeds the end of the file, there will be a hole (00) between the new content and the original end when writing again.

Forced write

For performance reasons, the operating system caches the data instead of writing it to the disk immediately, but writes all the data to the disk at one time after the cache is full. You can call the force(true) method to write the file content and metadata (file permissions and other information) to the disk immediately.

Common method

FileChannel is mainly used to perform IO operations on local files. Common methods are:

  1. public int read (ByteBuffer dst): Read data from the channel to the buffer.
  2. public int write (ByteBuffer src): Write the data in the buffer to the channel.
  3. public long transferFrom (ReadableByteChannel src, long position, long count): Copy data from the target channel to the current channel.
  4. public long transferTo (long position, long count, WriteableByteChannel target): Copy data from the current channel to the target channel.

Use FileChannel to write text files

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description FileChannel测试写入文件
 */
public class FileChannelTest {

    public static void main(String[] args) {
        try (final FileChannel channel = new FileOutputStream("data1.txt").getChannel()) {
            String msg = "Hello World!!!";
            final ByteBuffer buffer = ByteBuffer.allocate(16);
            buffer.put(msg.getBytes(StandardCharsets.UTF_8));
            buffer.flip();
            channel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Use FileChannel to read text files

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description FileChannel测试读取文件
 */
public class FileChannelTest {

    public static void main(String[] args) {
        try (final FileChannel channel = new FileInputStream("data1.txt").getChannel()) {
            final ByteBuffer buffer = ByteBuffer.allocate(16);
            channel.read(buffer);
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            //清空缓冲区,并重置为写模式
            buffer.clear();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Use FileChannel for data transmission

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description FileChannel测试文件传输
 */
public class FileChannelTest {

    public static void main(String[] args){
        try (final FileChannel from = new FileInputStream("data.txt").getChannel();
             final FileChannel to = new FileOutputStream("data1.txt").getChannel()) {
            // 参数:inputChannel的起始位置,传输数据的大小,目的channel
            // 返回值为传输的数据的字节数
            // transferTo一次只能传输2G的数据
            from.transferTo(0, from.size(), to);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
The transferTo() method corresponds to the transferFrom() method.

Although the transferTo() method has a high transmission efficiency, and the bottom layer uses the zero copy of the operating system for optimization, the transferTo method can only transfer 2G of data at a time.

Solution: You can judge based on the return value of transferTo(). The return value represents how much is transferred, which can be subtracted each time by the size() of from.

long size = from.size();
for (long left = size; left > 0; ) {
  left -= from.transferTo(size - left, size, to);
}

Note on Channel and Buffer

  1. ByteBuffer support typed of put and GET, put into what type of data, get should use the appropriate data type to remove , this may produce ByteUnderflowException exception.
  2. You can convert an ordinary Buffer into a read-only Buffer: asReadOnlyBuffer() method .
  3. NIO provides MapperByteBuffer, which allows files to be directly in 161cdbb707ac12 memory (off-heap memory) , and how to synchronize to the file is done by NIO.
  4. NIO also supports multiple Buffers (ie Buffer arrays) to complete read and write operations, namely Scattering (scattering) and Gathering (gathering) .

    • Scattering: When writing data to the buffer, you can use the Buffer array to write sequentially. After one Buffer array is full, continue writing to the next Buffer array.
    • Gathering (gathering): When reading data from the buffer, it can be read in sequence. After reading one Buffer, read the next in sequence.

network programming

Blocking vs non-blocking

blocked

  • When there is no data to read, including in the process of data replication, the thread must be blocked and wait, and will not occupy the CPU, but the thread is equivalent to an idle state
  • 32-bit JVM has 320k per thread, and 64-bit JVM has 1024k per thread. In order to reduce the number of threads, thread pool technology is required
  • But even if the thread pool is used, if there are many connections established but are inactive for a long time, all threads in the thread pool will be blocked

non-blocking

  • When a Channel has no readable events, the thread does not need to be blocked, it can handle other Channels with readable events
  • In the process of data replication, the thread is actually blocked (AIO improvement)
  • When writing data, the thread just waits for the data to be written to the Channel, without waiting for the Channel to send the data through the network

Blocking case code

Server code:

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description 使用NIO来理解阻塞模式-服务端
 */
public class Server {

    public static void main(String[] args) {
        //1. 创建服务器
        try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
            final ByteBuffer buffer = ByteBuffer.allocate(16);
            //2. 绑定监听端口
            ssc.bind(new InetSocketAddress(7777));
            //3. 存放建立连接的集合
            List<SocketChannel> channels = new ArrayList<>();
            while (true) {
                System.out.println("建立连接...");
                //4. accept 建立客户端连接 , 用来和客户端之间通信
                final SocketChannel socketChannel = ssc.accept();
                System.out.println("建立连接完成...");
                channels.add(socketChannel);
                //5. 接收客户端发送的数据
                for (SocketChannel channel : channels) {
                    System.out.println("正在读取数据...");
                    channel.read(buffer);
                    buffer.flip();
                    ByteBufferUtil.debugRead(buffer);
                    buffer.clear();
                    System.out.println("数据读取完成...");
                }
            }
        } catch (IOException e) {
            System.out.println("出现异常...");
        }
    }

}

Client code:

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description 使用NIO来理解阻塞模式-客户端
 */
public class Client {

    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            // 建立连接
            socketChannel.connect(new InetSocketAddress("localhost", 7777));
            final ByteBuffer buffer = ByteBuffer.allocate(10);
            buffer.put("hello".getBytes(StandardCharsets.UTF_8));
            buffer.flip();
            socketChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

operation result:

  • After the server has just started running: the server is blocked by accept.

    image-20211229202541691

  • After the client and the server establish a connection, before the client sends a message: the server is blocked because the channel is empty.

    image-20211229202703313

  • After the client sends the data, the server processes the data in the channel. After entering the loop again, it is blocked by accept again.

    image-20211229202810523

  • The previous client sent the message again. Because the server was blocked by accept, could not process the information that the client sent to the channel again.

Non-blocking

  • Set the connection to be non-blocking through the configureBlocking(false) method of ServerSocketChannel. If there is no connection at this time, accept will return null
  • SocketChannel by the configureBlocking(false) methods passage from read data is set to nonblocking . If there is no data to read in the channel at this time, read will return -1
/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description 使用NIO来理解阻塞模式-服务端
 */
public class Server {

    public static void main(String[] args) {
        //1. 创建服务器
        try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
            final ByteBuffer buffer = ByteBuffer.allocate(16);
            //2. 绑定监听端口
            ssc.bind(new InetSocketAddress(7777));
            //3. 存放建立连接的集合
            List<SocketChannel> channels = new ArrayList<>();
            //设置非阻塞!!
            ssc.configureBlocking(false);
            while (true) {
                System.out.println("建立连接...");
                //4. accept 建立客户端连接 , 用来和客户端之间通信
                final SocketChannel socketChannel = ssc.accept();
                //设置非阻塞!!
                socketChannel.configureBlocking(false);
                System.out.println("建立连接完成...");
                channels.add(socketChannel);
                //5. 接收客户端发送的数据
                for (SocketChannel channel : channels) {
                    System.out.println("正在读取数据...");
                    channel.read(buffer);
                    buffer.flip();
                    ByteBufferUtil.debugRead(buffer);
                    buffer.clear();
                    System.out.println("数据读取完成...");
                }
            }
        } catch (IOException e) {
            System.out.println("出现异常...");
        }
    }

}
Because it is set to be non-blocking, while(true) will always be executed, and the CPU will always be busy, which will make the performance lower, so this method is not used in actual situations to process requests.

Selector

basic introduction

  1. Java's NIO uses a non-blocking I/O method. You can use one thread to handle several client connections, and then the Selector will be used.
  2. Selector can detect whether events occur on multiple registered channels (multiple channels are registered to the same selector in the form of events) , if an event occurs, it will get the event and then deal with each event accordingly.
  3. Only when the connection actually has a read and write event, it will read and write, reducing the system overhead, and it is not necessary to create a thread for each connection, and there is no need to maintain multiple threads.
  4. Avoid the overhead caused by context switching between multiple threads.

Features

A single thread can cooperate with Selector to complete the monitoring of multiple Channel readable and writable events, which is called multiplexing .

  • multiplexing is only for network IO , ordinary file IO cannot use multiplexing
  • If the non-blocking mode of the Selector is not used, the thread is doing useless work most of the time, and the Selector can guarantee

    • Connect when there is a connectable event
    • Read only when there is a readable event
    • Write only when there is a writable event
Limited to the network transmission capacity, the Channel may not be writable at any time. Once the Channel is writable, it will trigger the writable event of the Selector to write.

Selector related method description

  • selector.select() : //If no event is detected in the registration pipeline, it will continue to block
  • selector.select(1000) : //Block for 1000 milliseconds and return after 1000 milliseconds
  • selector.wakeup() : //Wake up the selector
  • selector.selectNow() : //No blocking, return immediately

Analysis of NIO non-blocking network programming process

  1. When the client connects, it will get the corresponding SocketChannel through SeverSocketChannel.
  2. The Selector monitors, calls the select() method, and returns the number of channels where has an event among all channels registered with the Selector.
  3. Register the SocketChannel to the Selector, public final SelectionKey register(Selector sel, int ops) , one Selector can register multiple SocketChannels.
  4. After registration, it returns a SelectionKey, which will be associated with the Selector (in the form of set
  5. Further obtain each SelectionKey, an event occurs.
  6. Then get the SocketChannel reversely through SelectionKey, using the channnel() method.
  7. The business processing can be completed through the obtained channel.
Four operation flags are defined in SelectionKey: OP_READ indicates that a read event occurs in the channel; OP_WRITE — indicates that a write event occurs in the channel; OP_CONNECT — indicates that a connection is established; OP_ACCEPT — Request a new connection.

Related methods of SelectionKey

methoddescribe
public abstract Selector selector();Get the Selector object associated with it
public abstract SelectableChannel channel();Get the associated channel
public final Object attachment()Get the shared data associated with it
public abstract SelectionKey interestOps(int ops);Set or change the type of event monitored
public final boolean isReadable();Is the channel readable
public final boolean isWritable();Whether the channel is writable
public final boolean isAcceptable();Is it possible to establish a connection ACCEPT

Basic use of Selector and Accpet events

Next, we use Selector to implement multiplexing and improve the server code.

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description Selector基本使用-服务端
 */
public class Server {

    public static void main(String[] args) {
        try (ServerSocketChannel ssc = ServerSocketChannel.open();
             final Selector selector = Selector.open()) {//创建selector 管理多个channel
            ssc.bind(new InetSocketAddress(7777));
            ssc.configureBlocking(false);
            // 将通道注册到选择器中,并设置感兴趣的事件
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            ByteBuffer buffer = ByteBuffer.allocate(16);
            while (true) {
                // 如果事件就绪,线程会被阻塞,反之不会被阻塞。从而避免了CPU空转
                // 返回值为就绪的事件个数
                int ready = selector.select();
                System.out.println("selector就绪总数: " + ready);
                // 获取所有事件
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    final SelectionKey key = iterator.next();
                    //判断key的事件类型
                    if (key.isAcceptable()) {
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                        final SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("获取到客户端连接...");
                    }
                    // 处理完毕后移除
                    iterator.remove();

                }
            }
        } catch (IOException e) {
            System.out.println("出现异常...");
        }
    }
}

After the event occurs, either process it or use the key.cancel() method to cancel . You can’t do nothing. will still trigger next time. This is because the bottom layer of nio uses horizontal triggering.

When the event corresponding to the channel in the , SelectionKey will be placed in another collection, but selecionKey will not automatically remove , so we need to manually remove 161cdbb707b4f4 after processing an event One of the selecionKey. Otherwise, the event that has already been processed will be processed again, which will cause an error.

Read event

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description Read事件-服务端
 */
public class Server {

    public static void main(String[] args) {
        try (ServerSocketChannel ssc = ServerSocketChannel.open();
             final Selector selector = Selector.open()) {//创建selector 管理多个channel
            ssc.bind(new InetSocketAddress(7777));
            ssc.configureBlocking(false);
            // 将通道注册到选择器中,并设置感兴趣的事件
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            ByteBuffer buffer = ByteBuffer.allocate(16);
            while (true) {
                // 如果事件就绪,线程会被阻塞,反之不会被阻塞。从而避免了CPU空转
                // 返回值为就绪的事件个数
                int ready = selector.select();
                System.out.println("selector就绪总数: " + ready);
                // 获取所有事件
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    final SelectionKey key = iterator.next();
                    //判断key的事件类型
                    if (key.isAcceptable()) {
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                        final SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("获取到客户端连接...");
                        // 设置为非阻塞模式,同时将连接的通道也注册到选择其中
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) { //读事件
                        SocketChannel channel = (SocketChannel) key.channel();
                        channel.read(buffer);
                        buffer.flip();
                        ByteBufferUtil.debugRead(buffer);
                        buffer.clear();
                    }
                    // 处理完毕后移除
                    iterator.remove();

                }
            }
        } catch (IOException e) {
            System.out.println("出现异常...");
        }
    }
}

Disconnect processing

When the connection between the client and the server off, the server will send a reading event , for abnormal disconnect and normal disconnect requires a different approach to treatment:

  • Normal disconnect

    • When disconnected normally, the return value of the channel.read(buffer) method on the server side is -1, so when the return value is -1, you need to call the key's cancel() method to cancel this event, and move it after the cancellation In addition to the event
  • Abnormal disconnection

    • When abnormal disconnection, IOException will be thrown, catch the exception in the catch block of try-catch and call the cancel() method of key to

Message boundary

⚠️ Does not deal with problems with message boundaries

Set the size of the buffer to 4 bytes, send 2 Chinese characters (Hello), when decoded and printed by decode, garbled characters will appear

ByteBuffer buffer = ByteBuffer.allocate(4);
// 解码并打印
System.out.println(StandardCharsets.UTF_8.decode(buffer));
你�
��

This is because under the UTF-8 character set, 1 Chinese character occupies 3 bytes, and the buffer size is 4 bytes at this time. cannot process all the data in the channel in one read time, so a total of two reads will be triggered Event . This leads to Hello of good word is split into a first half and the second half to send, problems arise when decoding.

💡 Process the message boundary

The transmitted text may have the following three situations:

  • The text is larger than the buffer size, and the buffer needs to be expanded at this time
  • Half pack phenomenon
  • Sticky bag phenomenon occurs

粘包和半包

solution:

  • fixed message length , the data packet size is the same, the server reads according to the predetermined length, when the data sent is less, the data needs to be filled until the length is consistent with the specified length of the message. The disadvantage is a waste of bandwidth
  • Another way of thinking is to split by delimiter. The disadvantage is that it is inefficient and needs to match the delimiter character by character.
  • TLV format, namely Type type, Length length, Value data uses some space at the beginning of the message to store the length of the following data ), such as the Content-Type in the HTTP request header and Content-Length . When the type and length are known, you can easily obtain the message size and allocate the appropriate buffer. The disadvantage is that the buffer needs to be allocated in advance. If the content is too large, it will affect the server throughput

The following demonstrates the second solution, split separator:

We need to register the channel in the Selector after the Accept event, adds a ByteBuffer attachment to each channel, so that each channel uses its own channel when a read event occurs, so as to avoid conflicts with other channels and cause problems .

ByteBuffer buffer = ByteBuffer.allocate(16);
// 添加通道对应的Buffer附件
socketChannel.register(selector, SelectionKey.OP_READ, buffer);

Channel when the data is greater than the buffer, the buffer needs to be expansion operation. The method of determining the expansion in this code: Channel After calling the compact method, the position and limit are equal, indicating that the data in the buffer has not been read (the capacity is too small). At this time, a new buffer is created and its size is expanded Is twice. At the same time, copy the data in the old buffer to the new buffer, and call the attach method of the SelectionKey to put the new buffer as a new attachment in the SelectionKey .

// 如果缓冲区太小,就进行扩容
if (buffer.position() == buffer.limit()) {
  ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2);
  // 将旧buffer中的内容放入新的buffer中
  buffer.flip();
  newBuffer.put(buffer);
  // 将新buffer放到key中作为附件
  key.attach(newBuffer);
}

image-20211230201625204

improved code of as follows 161cdbb707b829:

/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description Read事件完整版-服务端
 */
public class Server {

    public static void main(String[] args) {
        try (ServerSocketChannel ssc = ServerSocketChannel.open();
             final Selector selector = Selector.open()) {//创建selector 管理多个channel
            ssc.bind(new InetSocketAddress(7777));
            ssc.configureBlocking(false);
            // 将通道注册到选择器中,并设置感兴趣的事件
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                // 如果事件就绪,线程会被阻塞,反之不会被阻塞。从而避免了CPU空转
                // 返回值为就绪的事件个数
                int ready = selector.select();
                System.out.println("selector就绪总数: " + ready);
                // 获取所有事件
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    final SelectionKey key = iterator.next();
                    //判断key的事件类型
                    if (key.isAcceptable()) {
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                        final SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("获取到客户端连接...");
                        socketChannel.configureBlocking(false);
                        ByteBuffer byteBuffer = ByteBuffer.allocate(16);
                        //注册到Selector并且设置读事件,设置附件bytebuffer
                        socketChannel.register(selector, SelectionKey.OP_READ, byteBuffer);
                    } else if (key.isReadable()) { //读事件
                        try {
                            SocketChannel channel = (SocketChannel) key.channel();
                            // 通过key获得附件
                            ByteBuffer buffer = (ByteBuffer) key.attachment();
                            int read = channel.read(buffer);
                            if (read == -1) {
                                key.cancel();
                                channel.close();
                            } else {
                                // 通过分隔符来分隔buffer中的数据
                                split(buffer);
                                // 如果缓冲区太小,就进行扩容
                                if (buffer.position() == buffer.limit()) {
                                    ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
                                    // 将旧buffer中的内容放入新的buffer中
                                    buffer.flip();
                                    newBuffer.put(buffer);
                                    // 将新buffer放到key中作为附件
                                    key.attach(newBuffer);
                                }
                            }
                        } catch (IOException e) {
                            //异常断开,取消事件
                            key.cancel();
                        }
                    }
                    // 处理完毕后移除
                    iterator.remove();

                }
            }
        } catch (IOException e) {
            System.out.println("出现异常...");
        }
    }

    private static void split(ByteBuffer buffer) {
        buffer.flip();
        for (int i = 0; i < buffer.limit(); i++) {
            //找到一条完成数据
            if (buffer.get(i) == '\n') {
                // 缓冲区长度
                int length = i + 1 - buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                // 将前面的内容写入target缓冲区
                for (int j = 0; j < length; j++) {
                    // 将buffer中的数据写入target中
                    target.put(buffer.get());
                }
                ByteBufferUtil.debugAll(target);
            }
        }
        // 切换为写模式,但是缓冲区可能未读完,这里需要使用compact
        buffer.compact();
    }
}
/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description Read事件完整版-客户端
 */
public class Client {

    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            // 建立连接
            socketChannel.connect(new InetSocketAddress("localhost", 7777));
            final ByteBuffer buffer = ByteBuffer.allocate(32);
            buffer.put("01234567890abcdef3333\n".getBytes(StandardCharsets.UTF_8));
            buffer.flip();
            socketChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

ByteBuffer size allocation

  • Each channel needs to record messages that may be segmented, because ByteBuffer cannot be used by multiple channels , so it is necessary to maintain an independent ByteBuffer for each channel
  • The ByteBuffer cannot be too large. For example, if a ByteBuffer is 1Mb, it needs 1Tb of memory to support one million connections. Therefore, a variable-size ByteBuffer needs to be designed.
  • Distribution ideas:

    • One way of thinking is to first allocate a smaller buffer, such as 4k. If you find that the data is not enough, then allocate an 8k buffer and copy the content of the 4k buffer to the 8k buffer. The advantage is that the message is continuous and easy to process, but the disadvantage is that data copying consumes performance.
    • Another idea is to use multiple arrays to form a buffer. One array is not enough, and the extra content is written into a new array. The difference from the previous one is that the message storage is not continuous and the analysis is complicated. The advantage is to avoid the performance loss caused by copying.

Write event

Buffer data is written by the server channel, possible because the channel capacity is less than the data size of the Buffer, resulting in not a one-time data is written to all of the Buffer Channel, the case will need to write in multiple , particularly Proceed as follows:

  1. Perform a write operation, write the contents of the buffer to the SocketChannel, and then determine whether there is data in the buffer
  2. If there is still data in the Buffer, needs to register the SockerChannel to the Seletor, pay attention to the write event, and put the uncompleted Buffer into the SelectionKey as an attachment.
/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description Write事件-服务端
 */
public class Server {

    public static void main(String[] args) {
        try (ServerSocketChannel ssc = ServerSocketChannel.open();
             final Selector selector = Selector.open()) {
            ssc.bind(new InetSocketAddress(7777));
            ssc.configureBlocking(false);
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                int ready = selector.select();
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    final SelectionKey key = iterator.next();
                    //判断key的事件类型
                    if (key.isAcceptable()) {
                        final SocketChannel socketChannel = ssc.accept();
                        socketChannel.configureBlocking(false);
                        StringBuilder sb = new StringBuilder();
                        for (int i = 0; i < 3000000; i++) {
                            sb.append("a");
                        }
                        final ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
                        final int write = socketChannel.write(buffer);
                        System.out.println("accept事件器写入.."+write);
                        // 判断是否还有剩余内容
                        if (buffer.hasRemaining()) {
                            // 注册到Selector中,关注可写事件,并将buffer添加到key的附件中
                            socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
                        }
                    }else if (key.isWritable()) {
                        SocketChannel socket = (SocketChannel) key.channel();
                        // 获得事件
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        int write = socket.write(buffer);
                        System.out.println("write事件器写入.."+write);
                        // 如果已经完成了写操作,需要移除key中的附件,同时不再对写事件感兴趣
                        if (!buffer.hasRemaining()) {
                            key.attach(null);
                            key.interestOps(0);
                        }
                    }
                    // 处理完毕后移除
                    iterator.remove();

                }
            }
        } catch (IOException e) {
            System.out.println("出现异常...");
        }
    }
}
/**
 * @author 神秘杰克
 * 公众号: Java菜鸟程序员
 * @date 2021/12/29
 * @Description Write事件-客户端
 */
public class Client {

    public static void main(String[] args) {
        try (SocketChannel socketChannel = SocketChannel.open()) {
            // 建立连接
            socketChannel.connect(new InetSocketAddress("localhost", 7777));
            int count = 0;
            while (true) {
                final ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
                count += socketChannel.read(buffer);
                System.out.println("客户端接受了.."+count);
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

operation result:

服务端

客户端


神秘杰克
765 声望382 粉丝

Be a good developer.