Netty(十一) - NIO与零拷贝

一、零拷贝基本介绍

  1. 零拷贝是网络编程的关键,很多性能优化都离不开它
  2. 在 Java 程序中,常用的零拷贝有 mmap(内存映射)和 sendFile。mmap 和 sendFile 在 OS 里,到底是怎么样的一个设计?对 mmap 和 sendFile 两个零拷贝进行分析?
  3. NIO 中如何使用零拷贝?
  4. 零拷贝从操作系统角度,是没有 CPU 拷贝,可以有 DMA 拷贝

1.1 传统IO数据读写(4次拷贝3次状态切换)

1)、Java 传统 IO 和 网络编程的代码:

File file = new File("1.txt");
RandomAccessFile raf = new RandomAccessFile(file,"rw");

byte[] arr = new byte[(int) file.length()];
raf.read(arr);

Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);

2)、传统 IO 模型图
传统 IO 模型图
DMA:Direct Memory Access,直接内存访问,即不需要依赖 CPU

1.2 mmap(内存映射)优化(3次拷贝3次状态切换)

1)、mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。

2)、mmap 模型图
mmap 模型图

1.3 sendFile 优化

1)、Linux2.1 版本提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换(3次拷贝2次状态切换)
sendFile Linux2.1

2)、Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 SocketBuffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。这里其实存在一次 CPU拷贝,从 kernel buffer 到 socket buffer,但是,拷贝的信息量很少,比如length、offset等,消耗低,可以忽略(2次拷贝2次状态切换)

sendFile Linux2.4

1.4 简单描述

1)、零拷贝,是从操作系统的角度来说的,因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer有一份数据)
2)、零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算

1.5 mmap和sendFile的区别

1)、mmap适合小数据量读写,sendFile适合大文件传输
2)、mmap需要3次上下文切换,3次数据拷贝;sendFile需要2次上下文切换,最少2次数据拷贝
3)、sendFile可以利用 DMA 方式,减少 CPU 拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)

二、应用案例

2.1 案例要求

1)、使用传统的IO方法传递一个大文件
2)、使用 NIO 零拷贝方式传递(transferTo)一个大文件
3)、比较两种传递方式耗时时间

2.2 传统IO代码

/**
 * @desc 传统IO服务器端
 * @author yxs
 * @date 2021-02-08 11:13
 */
public class OldIOServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(7001);

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

            try {
                byte[] byteArray = new byte[4096];

                while (true){
                    int readCount = dataInputStream.read(byteArray, 0, byteArray.length);
                    if(readCount == -1){
                        break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

}
/**
 * @desc 传统IO客户端
 * @author yxs
 * @date 2021-02-08 11:20
 */
public class OldIOClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost",7001);

        String fileName = "layui-v2.5.7.zip";
        FileInputStream inputStream = new FileInputStream(fileName);

        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());

        byte[] buffer = new byte[4096];
        long readCount;
        long total = 0;

        long startTime = System.currentTimeMillis();

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

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

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

}

2.3 NIO代码

/**
 * @desc NIO服务器端
 * @auther yxs
 * @date 2021/2/17 19:56
 */
public class NewIOServer {

    public static void main(String[] args) throws IOException {
        InetSocketAddress address = new InetSocketAddress(7001);

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        ServerSocket serverSocket = serverSocketChannel.socket();

        serverSocket.bind(address);

        // 创建buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

        while (true){
            SocketChannel socketChannel = serverSocketChannel.accept();

            int readCount = 0;
            while (-1 != readCount){
                try {

                    readCount = socketChannel.read(byteBuffer);

                }catch (Exception ex){
                    //ex.printStackTrace();
                    break;
                }
                byteBuffer.rewind(); // 倒带 position = 0 mark 作废,Rewinds this buffer.  The position is set to zero and the mark is discarded
            }
        }
    }

}
/**
 * @desc NIO客户端
 * @auther yxs
 * @date 2021/2/17 20:11
 */
public class NewIOClient {

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

        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost",7001));
        String fileName = "layui-v2.5.7.zip";

        // 得到一个文件的channel
        FileChannel fileChannel = new FileInputStream(fileName).getChannel();

        // 准备发送
        long startTime = System.currentTimeMillis();

        // 在linux下一个 transferTo 方法就可以完成传输
        // 在windows下一次调用 transferTo 方法只能发送 8m ,需要分段传输文件,而且要注意传输时的位置
        // transferTo 底层使用到零拷贝
        long size = fileChannel.size();
        long maxCopySize = 1024 * 1024 * 8;
        long position = 0;
        long transferCount = 0;
        while (position < size) {
            maxCopySize = (size - position) > maxCopySize ? maxCopySize : (size - position);
            transferCount += fileChannel.transferTo(position, maxCopySize, socketChannel);
            position = position + maxCopySize;
        }
        //long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);

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

        // 关闭
        fileChannel.close();
    }

}

2.4 执行结果

传统IO执行结果

NIO执行结果

猜你喜欢

转载自blog.csdn.net/yangxshn/article/details/113742494
今日推荐