• 第三章—Java NIO编程:(8)零拷贝


    一、零拷贝基本介绍

    (1)零拷贝是网络编程的关键, 很多性能优化都离不开。

    (2)在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。那么,他们在OS里,到底是怎么样的一个的设计? 我们分析 mmap 和 sendFile 这两个零拷贝。

    (3)另外我们看下NIO 中如何使用零拷贝。

    二、传统IO数据读写

    Java 传统 IO 和 网络编程的一段代码

    File file = new File("test.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);

    传统IO模型

    这其实经历了四次状态转换(用户态->内核态->用户态->内核态)和四次拷贝(两次DMA拷贝和两次CPU拷贝)

    DMA:Direct Memory Access 直接内存拷贝(不使用CPU)

    三、mmap优化

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

    mmap示意图:

    这其实经历了四次状态转换(用户态->内核态->用户态->内核态)和三次拷贝(两次DMA拷贝和一次CPU拷贝)

    四、sendFile 优化

    Linux2.1版本

    Linux 2.1 版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer, 同时由于和用户态完全无关, 就减少了一次上下文切换。

    示意图:

    Linux2.4版本

    Linux 在 2.4 版本中, 做了一些修改,避免了从内核缓冲区拷贝到 Socket Buffer 的操作,直接拷贝到协议栈, 从而再一次减少了数据拷贝。 具体如下图和小结:

    这里其实有 一次cpu 拷贝kernel buffer -> socket buffer但是, 拷贝的信息很少, 比如lenght , offset , 消耗低, 可以忽略。

    零拷贝的再次理解

    (1)我们说零拷贝, 是从操作系统的角度来说的。 因为内核缓冲区之间, 没有数据是重复的(只有 kernel buffer 有一份数据)。

    (2)零拷贝不仅仅带来更少的数据复制, 还能带来其他的性能优势, 例如更少的上下文切换, 更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

    注意:零拷贝从操作系统角度, 是指没有cpu 拷贝。

    五、mmap 和 sendFile 的区别

    (1)mmap 适合小数据量读写, sendFile 适合大文件传输。

    (2)mmap 需要 4 次上下文切换, 3 次数据拷贝; sendFile 需要 3 次上下文切换, 最少 2 次数据拷贝。

    (3)sendFile 可以利用 DMA 方式, 减少 CPU 拷贝, mmap 则不能(必须从内核拷贝到 Socket 缓冲区) 。

    六、零拷贝案例

    1、传统IO方式

    服务器端:

    //Java IO 的服务器
    public class OldServer {
        public static void main(String[] args) throws Exception{
            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);

                        if (-1 == readCount) {
                            break;
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    客户端:

    public class OldClient {
        public static void main(String[] args) throws Exception{
            Socket socket = new Socket("127.0.0.1"7001);

            //String fileName = "protoc-3.6.1-win32.zip";
            String fileName = "剑指offer.zip";

            FileInputStream fileInputStream = new FileInputStream(fileName);

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

            byte[] buffer = new byte[4096];

            long startTime = System.currentTimeMillis();

            long readCount = 0;
            long total = 0;

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

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

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

    2、使用NIO 零拷贝方式传递(transferTo)一个大文件

    服务器端:

    //NIO 服务器
    public class NewIOServer {

        public static void main(String[] args) throws Exception{
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            InetSocketAddress address = new InetSocketAddress(7001);
            serverSocketChannel.bind(address);

            ServerSocket serverSocket = serverSocketChannel.socket();

            //创建 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 作废
                }

            }
        }
    }

    客户端:

    public class NewIOClient {

        public static final long FILE_SIZE = 8 * 1014 * 1024;

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

            SocketChannel socketChannel = SocketChannel.open();
            InetSocketAddress address = new InetSocketAddress("127.0.0.1"7001);

            socketChannel.connect(address);

            //String fileName = "protoc-3.6.1-win32.zip";
            String fileName = "剑指offer.zip";
            //得到一个文件channel
            FileChannel fileChannel = new FileInputStream(fileName).getChannel();

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

            System.out.println(fileChannel.size());

            //在linux下一个transferTo 方法就可以完成传输
            //在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件, 而且要主要
            //传输时的位置 =》 课后思考...
            //transferTo 底层使用到零拷贝
            //long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);

            long transferCount = 0;
            if (fileChannel.size() > FILE_SIZE) {
                transferCount = transfertoGT8M(fileChannel, socketChannel);
            } else {
                transferCount = transferToLT8M(fileChannel, socketChannel);
            }

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

            //关闭
            fileChannel.close();
        }

        private static long transferToLT8M(FileChannel src, WritableByteChannel target) throws IOException {
            return src.transferTo(0, src.size(), target);
        }

        private static long transfertoGT8M(FileChannel src, WritableByteChannel target) throws IOException {
            long fileSize = src.size();
            long count = fileSize % FILE_SIZE == 0 ? fileSize / FILE_SIZE : fileSize / FILE_SIZE + 1;

            System.out.println(count);

            long transferCount = 0;
            for (int i = 0; i < count; i++) {
                transferCount += src.transferTo(i * FILE_SIZE, FILE_SIZE, target);
            }
            return transferCount;
        }
    }

    通过上面的案例可以发现,使用了零拷贝的情况,可以提高60%左右的性能。

     

  • 相关阅读:
    读后感
    周总结(五)【2019.7.292019.8.4】
    周总结(六)【2019.8.52019.8.11】
    “教室派”APP使用体验
    ios的内存管理差异和iso6与以前内存管理的兼容
    UITextView 和UILable 设置行间距
    IOS 各种问题收藏贴
    __bridge,__bridge_transfer和__bridge_retained
    键盘事件的处理
    JSONKit 使用示例
  • 原文地址:https://www.cnblogs.com/niujifei/p/16012148.html
Copyright © 2020-2023  润新知