5.NIO零拷贝与传统IO的文件传输性能比较

【README】

1.本文总结自B站《netty-尚硅谷》,很不错;

2.本文部分内容参考自  NIO效率高的原理之零拷贝与直接内存映射 - 腾讯云开发者社区-腾讯云


【1】零拷贝原理

【1.1】传统IO的文件拷贝

5.NIO零拷贝与传统IO的文件传输性能比较_第1张图片

 【图解】

  • step1)调用 sys_read系统调用,从用户态进入内核态;借助DMA通道把磁盘驱动数据 读入内核读缓冲区(无需拷贝,因为零拷贝讲的是 无需耗费CPU资源的拷贝,而是DMA拷贝
  • step2)借助CPU把 内核缓冲区数据 读取到用户态缓冲区后,从sys_read系统调用返回,从内核态切换回用户态;(第1次拷贝
  • step3)调用 sys_write 系统调用,从用户态进入内核态;借助CPU把 用户态缓冲区 数据写入 socket 缓冲区(第2次拷贝
  • step4)把 socket 缓冲区数据 借助DMA通道 写入到 网卡驱动(无需cpu参与的拷贝,仅DMA);
  • step5)写入完成后,从内核态返回用户态;

小结: 上述过程中,操作系统底层 有4次用户态与内核态的切换,2次cpu拷贝(DMA拷贝不占CPU,不计入);文件读写(拷贝)性能较低;

补充:

  • 虽然DMA拷贝不占用cpu,但它占用系统总线,一定程度上也会影响cpu性能(但这不是本文重点,可以忽略不计);

【1.2】零拷贝

1)零拷贝 :

  • Linux 在 2.4 版本中,做了一些修改,sendFile() 系统调用 避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。

2)零拷贝流程图 :

5.NIO零拷贝与传统IO的文件传输性能比较_第2张图片

 【图解】

  • step1)调用 sys_read系统调用,从用户态进入内核态,借助DMA通道 把磁盘驱动数据 读入到 内核读缓冲区;(DMA拷贝)
  • step2)接着在内核态中,调用系统调用 sendfile ,cpu把 读缓冲区的数据写出到 socket缓冲区;(第1次CPU拷贝
  • step3)借助DMA通道,把 socket缓冲区数据 写出到 网卡缓冲区;(DMA拷贝)
  • step4)最后从内核态返回到用户态;

【小结】

  • 上述过程中,操作系统底层 有2次用户态与内核态的切换,1次cpu拷贝(非真正零拷贝,因为有1次cpu拷贝)
  • 显然,相比传统IO过程,NIO的零拷贝技术的文件传输性能更高
  • 补充*若网卡驱动支持 gather操作,DMA可以直接把数据从内核读缓冲区直接拷贝到网卡驱动,而无需cpu拷贝(真正实现CPU零拷贝);

NIO零拷贝适用于以下场景:

  • 文件较大,读写较慢,追求速度;
  • JVM内存不足,不能加载太大数据;
  • 内存带宽不够,即存在其他程序或线程存在大量的IO操作,导致带宽本来就小;

【2】代码实现

【2.1】基于传统IO传输文件

注意:字节缓冲 4M (字节缓冲大小会影响传输性能,当然了,一定条件下,缓冲越大越好);

1)服务器:

/**
 * @Description 传统IO服务器
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2022年08月20日
 */
public class OldIOServer {

    public static void main(String[] args) throws IOException {
        // 服务器监听端口 7001
        ServerSocket serverSocket = new ServerSocket(7001);

        while (true) {
            // 阻塞式等待客户端请求链接
            Socket socket = serverSocket.accept();
            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
            try {
                byte[] byteArr = new byte[4096];
                // 读取客户端的数据到字节数组
                while (dataInputStream.read(byteArr, 0, byteArr.length) != -1) ;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2)客户端:

/**
 * @Description 传统IO客户端
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2022年08月20日
 */
public class OldIOClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 7001);
        // 传输 一个 zip文件
        InputStream inputStream = new FileInputStream("D:\\cmb\\studynote\\netty\\temp\\springboot.zip");

        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);
        }
        long cost = System.currentTimeMillis() - startTime;
        System.out.println("发送总字节数 " + total + ", 耗时 = " + cost);
        // 关闭资源
        dataOutputStream.close();
        socket.close();
        inputStream.close();
    }
}

3)效果:

发送总字节数 4491230, 耗时 = 50


【2.2】基于NIO零拷贝传输文件

注意:字节缓冲 4M (字节缓冲大小会影响传输性能,当然了,一定条件下,缓冲越大越好);

1)服务器:

/**
 * @Description nio实现零拷贝服务器
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2022年08月20日
 */
public class ZeroCopyNIOServer {
    public static void main(String[] args) throws IOException {
        // 服务器套接字通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定端口
        serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 7001));

        ByteBuffer buffer = ByteBuffer.allocate(4096);
        while (true) {
            // 等待客户端连接
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 读取数据
            while (socketChannel.read(buffer) != -1) {
                // 缓冲倒带, 设置 position=0, mark作废
                buffer.rewind();
            }
        }

    }
}

2)客户端:

/**
 * @Description nio实现零拷贝服务器
 * @author xiao tang
 * @version 1.0.0
 * @createTime 2022年08月20日
 */
public class ZeroCopyNIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 7001));
        FileChannel fileChannel = new FileInputStream("D:\\cmb\\studynote\\netty\\temp\\springboot.zip").getChannel();
        long startTime = System.currentTimeMillis();

        // 在 linux 下,一次调用 transferTo 方法就可以完成传输
        // 在 window下,一次调用 transferTo 只能发送 8M,如果大于8M,则需要分段传输文件
        // transferTo 底层就用到了 零拷贝
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        long cost = System.currentTimeMillis() - startTime;
        System.out.println("客户端发送数据成功,耗时= " + cost + ", 传输的字节总数 = " + transferCount);
    }
}

 3)效果:

客户端发送数据成功,耗时= 10, 传输的字节总数 = 4491230

4)补充: 关于 FileChannel.transferTo 方法 

  • 在 linux 下,一次调用 FileChannel.transferTo 方法就可以完成传输;
  • 在 window下,一次调用 transferTo 只能发送 8M,如果大于8M,则需要分段传输文件;
  • transferTo 底层就用到了 零拷贝;

5)transferTo方法底层使用的是 sendfile 系统调用(零拷贝)

  • 该系统调用 实现了数据直接从内核的读缓冲区传输到套接字缓冲区,避免了用户态(User-space) 与内核态(Kernel-space) 之间的数据拷贝。

【4】性能比较

【表】传统IO与NIO零拷贝传输性能对比 (文件大小 4M

Java IO类型

传输耗时

备注

传统IO

50ms

NIO零拷贝

10ms

性能更优

你可能感兴趣的:(netty,nio)