Java NIO: Detailed analysis of "zero copy" in NIO and efficiency comparison with IO

Overview

In the current various RPC frameworks and network programming frameworks, the bottom layer uses a large number of Java NIO as a guarantee of efficiency. Compared with IO, NIO has unparalleled performance advantages to ensure the carrying capacity of the system under various high concurrency scenarios. What is mentioned is "zero copy" . "Zero copy" is a key factor in determining the performance of NIO, but it is limited by the support of the underlying operating system.

The "zero copy" feature is achieved by calling the native method through the program layer , and then using the special IO at the bottom of the operating system. Just like the literal meaning, the operating system has passed some design optimizations to reduce the amount of data in the process of operating The number of times of copying and switching "context" greatly improves the efficiency of data transmission.

Verify the improvement of "zero copy" efficiency

Compare the speed of NIO and traditional IO by simulating the client to send data to the server

The only material: prepare a 100M large file, the larger the file, the more obvious it will be

Examples of traditional IO efficiency verification:

Server

package com.leolee.zeroCopy;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @ClassName IOServer
 * @Description: IO服务端接收数据,不对接受数据做任何处理,只用于模拟客户端的数据接收
 * @Author LeoLee
 * @Date 2020/10/13
 * @Version V1.0
 **/
public class IOServer {


    public static void main(String[] args) throws IOException {

        ServerSocket serverSocket = new ServerSocket(8899);

        while (true) {
            Socket socket = serverSocket.accept();
            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());

            byte[] bytes = new byte[4096];
            while (true) {
                int readCount = dataInputStream.read(bytes, 0, bytes.length);
                if (readCount == -1) {
                    break;
                }
            }
        }
    }
}

Client

package com.leolee.zeroCopy;

import java.io.*;
import java.net.Socket;

/**
 * @ClassName IOClient
 * @Description: IO客户端读取文件并发送给服务器
 * @Author LeoLee
 * @Date 2020/10/13
 * @Version V1.0
 **/
public class IOClient {

    public static void main(String[] args) throws IOException {

        //建立到服务端的连接
        Socket socket = new Socket("127.0.0.1", 8899);

        //源文件
        String file = "C:" + File.separator + "Users" + File.separator + "LeoLee" + File.separator + "Desktop" + File.separator + "sqldeveloper-4.1.5.21.78-x64.zip";
        //读取目标数据
        InputStream inputStream = new FileInputStream(file);

        //发送数据
        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());

        byte[] bytes = new byte[4096];
        long readcout;
        long total = 0;
        long startTime = System.currentTimeMillis();

        while ((readcout = inputStream.read(bytes)) >= 0) {
            total += readcout;
            dataOutputStream.write(bytes);
        }

        System.out.println("发送总字节数:" + total + ",耗时:" + (System.currentTimeMillis() - startTime));

        dataOutputStream.close();
        inputStream.close();
        socket.close();

    }
}

After running the server and the client in turn, the client output is as follows:

Examples of traditional NIO efficiency verification:

Server

package com.leolee.zeroCopy;

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

/**
 * @ClassName NIOServer
 * @Description: NIO服务端接收数据,不对接受数据做任何处理,只用于模拟客户端的数据接收
 * @Author LeoLee
 * @Date 2020/10/13
 * @Version V1.0
 **/
public class NIOServer {

    public static void main(String[] args) throws IOException {

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        //该选项用来决定如果网络上仍然有数据向旧的ServerSocket传输数据,是否允许新的ServerSocket绑定到与旧的ServerSocket同样的端口上,该选项的默认值与操作系统有关,在某些操作系统中,允许重用端口,而在某些系统中不允许重用端口。
        //
        //当ServerSocket关闭时,如果网络上还有发送到这个serversocket上的数据,这个ServerSocket不会立即释放本地端口,而是等待一段时间,确保接收到了网络上发送过来的延迟数据,然后再释放端口。
        //
        //值得注意的是,public void setReuseAddress(boolean on) throws SocketException必须在ServerSocket还没有绑定到一个本地端口之前使用,否则执行该方法无效。此外,两个公用同一个端口的进程必须都调用serverSocket.setReuseAddress(true)方法,才能使得一个进程关闭ServerSocket之后,另一个进程的ServerSocket还能够立刻重用相同的端口
        serverSocket.setReuseAddress(true);
        serverSocket.bind(new InetSocketAddress("127.0.0.1", 8899));

        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

        while (true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            //阻塞模式
            socketChannel.configureBlocking(true);

            int readCount = 0;

            while (-1 != readCount) {
                readCount = socketChannel.read(byteBuffer);
                byteBuffer.rewind();
            }
        }
    }
}

Client

package com.leolee.zeroCopy;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

/**
 * @ClassName NIOClient
 * @Description: NIO客户端读取文件并发送给服务器
 * @Author LeoLee
 * @Date 2020/10/13
 * @Version V1.0
 **/
public class NIOClient {

    public static void main(String[] args) throws IOException, InterruptedException {

        //建立到服务器连接
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8899));
        //设置为阻塞模式:为了保证更准确的验证“零拷贝”的效率,所以设置为阻塞,一直读完所有的文件数据再传递到服务端
        socketChannel.configureBlocking(true);

        //源文件
        String file = "C:" + File.separator + "Users" + File.separator + "LeoLee" + File.separator + "Desktop" + File.separator + "sqldeveloper-4.1.5.21.78-x64.zip";

        FileChannel fileChannel = new FileInputStream(file).getChannel();

        long startTime = System.currentTimeMillis();

        //发送数据
        //mac电脑(linux系统)中,可以直接使用long transferCount = fileChannel.transferTo(position, fileChannel.size(), socketChannel);
        //并不需要如下循环,原因windows对一次传输的数据大小有限制(8388608bytes),所以不能依次传输所有数据,需要循环来传递
        //参考与https://blog.csdn.net/forget_me_not1991/article/details/80722386
        long position = 0;
        long size = fileChannel.size();
        long total = 0;
        while (position < size) {
            long transferCount = fileChannel.transferTo(position, fileChannel.size(), socketChannel);//这一步体现零拷贝
            System.out.println("发送:" + transferCount);
            if (transferCount <= 0) {
                break;
            }
            total += transferCount;
            position += transferCount;
        }


        System.out.println("发送总字节数:" + total + ",耗时:" + (System.currentTimeMillis() - startTime));

        fileChannel.close();
    }
}

After running the server and the client in turn, the client output is as follows:

Explanation of results

Obviously, it can be seen that the same file is transferred through different methods, and NIO is obviously faster than traditional IO to transfer data.

13224 and 771

This is simply a sky and an underground! ! ! It may be more obvious on a linux system! ! !

Analysis of Zero Copy Principle

Why is traditional IO slow? ? ?

When traditional IO performs data transfer, take the file data transfer scenario as an example:

  1. The program sends a read operation to the operating system in user space, and the context is switched from user space to kernel space.
  2. The kernel space made a data request to the disk, and then the first data copy was made from the disk through DMA (Direct Memory Access)
  3. After the kernel space gets the data, it will copy the data to the user space where the program is located. A switch from kernel space to user space occurs. This is the second data copy
  4. At this point, the program is about to start its data transfer operation, so another copy of data is copied. This is the third data copy and a write operation is issued to the system.
  5. The user space switches to the kernel space again, followed by the fourth data copy. After the kernel space gets the data, it starts to send data through the socket module
  6. The kernel space returns the program that wrote the result to the user space, and the context switch occurs again

In the process of traditional IO transferring data, a total of four data copies have occurred, accompanied by four context switches, and the socket module has a "queue" task for sending data, which is the reason for the low efficiency of traditional IO . In this process, the user space only plays a role of data transfer, which is quite redundant operation! It is a great performance loss.

Incomplete "zero copy" (send data via sendfile())

Compared with traditional IO's read() syscall and write() syscall, sendfile() syscall can omit the process of copying data from kernel space to user space. To put it simply, the program in the user space tells the kernel space by calling the native method (syscall system call) that you send me a file data instead of telling me what the file data is, you just send it for me. When the kernel space sends the file data, the result is returned to the user space.

The important operation of this method is to increase the copy of kernel space data between read and write operations, and the data read from the disk is copied to the socket buffer.

Upgraded version of "zero copy"

Complete "zero torture"

  1. The program sends a sendfile() system call to the system in user space, and the context switches to kernel space
  2. The kernel space copies the data from the disk to the kernel buffer through DMA (Direct Memory Access), and correspondingly generates a series of descriptors in the socket buffer. The content of these descriptors includes the data location pointed to and how long the data is , So that you can locate the corresponding buffer data
  3. After the protocol engine sends the data, the target data is directly read in the kernel buffer through the descriptor and sent without any copy behavior

This descriptor is the key to "zero copy"!

The initial reading of data from disk to kemel buffer and socket buffer is NIO's scatter, and the protocol engine obtains data through kemel buffer and socket buffer, which is the gather operation.

For the NIO example above, fileChannel.transferTo  is essentially a "zero copy" operation. Since its implementation code is under the sun package, we will not do source code analysis here, as long as we know that it calls the native method and is implemented through the operating system The "zero copy" will do.

In the final analysis, we must realize that the operation of "zero-copy" depends on the capabilities of the operating system. Most operating systems now support "zero-copy" operations. We don't have to worry about this.

Those who need code come here to get it: demo project address

Guess you like

Origin blog.csdn.net/qq_25805331/article/details/109062107