netty入门(二)NIO的零拷贝

零拷贝是网络编程的关键,很多性能优化都离不开它。
零拷贝是指:从操作系统的角度来看,文件的传输不存在CPU的拷贝,只存在DMA拷贝。
在Java程序中,常用的零拷贝有mmap(内存映射)和 sendFile。
零拷贝不仅仅带来更少的数据复制,还能减少线程的上下文切换,减少CPU缓存伪共享以及无CPU校验和计算。

传统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);

我们会调用 read 方法读取 index.html 的内容—— 变成字节数组,然后调用 write 方法,将 index.html 字节流写到 socket 中,那么,我们调用这两个方法,在 OS 底层发生了什么呢?我这里借鉴了一张其他文字的图片,尝试解释这个过程。
在这里插入图片描述
上图中,上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操作。下面说说他们的步骤:
DMA:Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU

1.read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。
2.发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。
3.发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。
4.第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
5.write 方法返回,再次从内核态切换到用户态。

总结:
传统io,四次拷贝,四次切换。两次DMA拷贝,两次CPU拷贝,四次切换:用户态-》内核态-》用户态-》内核态-》用户态

mmap 优化

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。如下图:
在这里插入图片描述
如上图,user bufferkernel buffer 共享 index.html。如果你想把硬盘的 index.html 传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 Socket 缓冲区。
现在,你只需要从内核缓冲区拷贝到 Socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。

总结:
mmap优化,三次拷贝,四次切换。两次DMA拷贝,一次CPU拷贝,四次切换:用户态-》内核态-》用户态-》内核态-》用户态

sendFile

Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换
在这里插入图片描述
如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用,然后掉一共 write 方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为在一个用户空间。
最后,数据从 Socket 缓冲区进入到协议栈。
此时,数据经过了 3 次拷贝,3 次上下文切换。

总结:
sendFile优化,三次拷贝,三次切换。两次DMA拷贝,一次CPU拷贝,四次切换:用户态-》内核态-》用户态-》内核态

Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图:
在这里插入图片描述
总结:
sendFile优化,俩次拷贝,三次切换。两次DMA拷贝,四次切换:用户态-》内核态-》用户态-》内核态。
现在是彻底的零拷贝了,因为一次cpu拷贝都没有。

mmap 和 sendFile 的区别

1.mmap 适合小数据量读写,sendFile 适合大文件传输。
2.mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
3.sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

NIO中的零拷贝(transferTo):

nio的零拷贝是sendfile的方式实现的

服务端:

public class NewIOServer {
    public static void main(String[] args)  {
        try {
            InetSocketAddress address=new InetSocketAddress(7001);
            ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
            ServerSocket socket = serverSocketChannel.socket();
            socket.bind(address);
            //创建buffer
            ByteBuffer allocate = ByteBuffer.allocate(4096);
            while(true){
                SocketChannel accept = serverSocketChannel.accept();
                int readCount=0;
                while(-1!=readCount){
                    try {
                        readCount=accept.read(allocate);
                    } catch (IOException e) {
                        break;
                    }
                    //倒带 position=0 mark作废
                    allocate.rewind();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端:

public class NewIOClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("localhost", 7001));
            String fileName = "netty-api-4.1中文.rar";

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

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

            //在Linux下一个 transferTo 方法就可以完成传输
            //在windows 下一次调用 transferTo 只能发送8M,就需要分段传输文件,注意传输时的位置,
            // 分channel.size()/8*1024*1024次来传输,然后position改一下
            //transferTo 底层使用到零拷贝
            long transferCount=0;
            for (long i = 0; i < Math.ceil((double) channel.size() /(8 * 1024 * 1024)); i++) {
                long l = channel.transferTo(i*(8 * 1024 * 1024), 8 * 1024 * 1024, socketChannel);
                transferCount=transferCount+l;
            }
            //linux下直接传就完事了
            //long transferCount = channel.transferTo(0, channel.size(), socketChannel);

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

//            发送的总的字节数:1007473 耗时:3
//            发送的总的字节数:8388608 耗时:15   实际8545121
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

BIO文件传输对比:
服务端:

public class OldIOServer {
    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, 0, byteArray.length);

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

客户端:

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

        String fileName = "protoc-3.6.1-win32.zip";
        InputStream 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));
//        发送总字节数: 1007473, 耗时: 6

        dataOutputStream.close();
        socket.close();
        inputStream.close();
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值