(七)Netty与零拷贝

发现一个比较详细的博文参考一下:
https://blog.csdn.net/a1036645146/article/details/116117112?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-116117112-blog-118678482.pc_relevant_paycolumn_v3&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-116117112-blog-118678482.pc_relevant_paycolumn_v3&utm_relevant_index=1

零拷贝基本介绍

  1. 零拷贝是网络编程的关键,很多性能优化都离不开
  2. 在Java程序中,常用的零拷贝有mmap(内存映射)和sendFile。那么,他们在OS里,到底是怎么样一个涉及?我们分析mmap和sendFile这两个两拷贝
  3. 另外我们看下NIO中如何使用零拷贝

传统IO数据读写

  1. Java传统IO和网络编程的一段代码
	File file = new File("index.html");
	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 底层发生了什么呢?我这里借鉴了一张其他文章的图片,尝试解释这个过程。
在这里插入图片描述
上图中,上半部分表示用户态和内核态的上下文切换。下半部分表示数据复制操作。下面说说他们的步骤:

  1. read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU 拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU) 引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。

  2. 发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。

  3. 发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。

  4. 第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。

  5. write 方法返回,再次从内核态切换到用户态。

如你所见,复制拷贝操作太多了。如何优化这些流程?

mmap优化

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图:
在这里插入图片描述
如上图,user buffer 和 kernel buffer 共享 index.html。如果你想把硬盘的 index.html 传输到网络中,再也不用拷贝到用户空间,再从用户空间拷贝到 Socket 缓冲区。

现在,你只需要从内核缓冲区拷贝到 Socket 缓冲区即可,这将减少一次内存拷贝(从 4 次变成了 3 次),但不减少上下文切换次数。

sendFile

那么,我们还能继续优化吗? Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
在这里插入图片描述

如上图,我们进行 sendFile 系统调用时,数据被 DMA 引擎从文件复制到内核缓冲区,然后调用 write 方法时,从内核缓冲区进入到 Socket,这时,是没有上下文切换的,因为都在内核空间。

最后,数据从 Socket 缓冲区进入到协议栈。此时,数据经过了 3 次拷贝,3 次上下文切换。那么,还能不能再继续优化呢? 例如直接从内核缓冲区拷贝到网络协议栈?

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

在这里插入图片描述
现在,index.html 要从文件进入到网络协议栈,只需 2 次拷贝:第一次使用 DMA 引擎从文件拷贝到内核缓冲区,第二次从内核缓冲区将数据拷贝到网络协议栈;内核缓存区只会拷贝一些 offset 和 length 信息到 SocketBuffer,基本无消耗。

零拷贝的再次理解

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

mmap和sendFile的区别

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

在选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

NIO零拷贝案例

案例要求:

  1. 使用传统的IO方法传递一个大文件
  2. 使用NIO零拷贝方式传递一个大文件(transferTo)
  3. 看看两种传递方式耗时时间分别是多少

传统方式

package com.example.demo.netty.zerocopy;

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

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 (readCount == -1) {
                        break;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
package com.example.demo.netty.zerocopy;

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

public class OldIOClient {

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

        File file = new File("bigFile.war");
        InputStream inputStream = new FileInputStream(file);
        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());

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

        long startTime = System.currentTimeMillis();

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

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

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

NIO零拷贝

package com.example.demo.netty.zerocopy;

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

public class NewIOServer {

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

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        ServerSocket serverSocket = serverSocketChannel.socket();

        serverSocket.bind(address);

        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);

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

            int readCount = 0;
            while (-1 != readCount) {
                try {
                    readCount = socketChannel.read(byteBuffer);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                byteBuffer.rewind();//倒带position=0 mark作废
            }
        }
    }
}
package com.example.demo.netty.zerocopy;

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

public class NewIOClient {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 7001));
        String filename = "bigFile.war";

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

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

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

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

        //关闭
        fileChannel.close();
    }
}

AIO基本介绍

  1. JDK7 引入了,Asynchronous I/O,即AIO ,在进行IO编程中,常用到两种模式:Reactor 和 Proactor, Java 的NIO就是Reactor,当有事件触发时,服务器端得到通知进行相应的处理
  2. AIO 即NIO2.0, 叫异步非阻塞IO。AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,他的特点是,先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
  3. 目前AIO还没有广泛应用,Netty也是基于NIO,而不是AIO,因此就不在这里讲AIO了,有兴趣的可以链接一下http://www.52im.net/thread-306-1-1.html

BIO、NIO、AIO对比

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值