前言:
在Java程序中,零拷贝技术分为两种:mmap(内存映射)和sendFile,首先要了解零拷贝的概念:所谓的零拷贝不是不拷贝,而是不经过CPU拷贝,它还是需要拷贝的(比如将数据从硬盘拷贝到内核态),这个零拷贝是从操作系统(CPU)的角度看的
传统的IO拷贝
首先将硬盘上的数据拷贝到内核,然后在经过CPU拷贝将数据从内核拷贝到应用程序内存(用户态),在应用程序内存,用户可以对数据进行操作修改等,然后在经过CPU拷贝将数据从用户缓冲区拷贝到socket缓冲区,然后在经过DMA拷贝,将数据从socket缓冲区拷贝到网卡。
那么经过以上的步骤,完成了将数据从本地硬盘传输到网络上的过程,
那这个过程数据的拷贝经过了:
硬盘—>内核—>应用程序内存(可以理解为用户态,相当于jvm中)—>socket缓冲区—>网卡,这4次拷贝的过程。
上下文的切换经过了:用户态–>内核态–>用户态 这三次上下文切换
mmap:
首先将硬盘上面的数据数据拷贝到内核,(因为mmap技术,做到了内核态数据和用户态数据共享,此时不需要将数据从内核拷贝到用户态,用户态也可以对数据进行修改),再将内核态的数据经过CPU拷贝,拷贝到socket缓冲区,在经过DMA拷贝,将socket缓冲区的数据拷贝到协议栈;
那经过了mmap的优化后,数据的读写少了一次拷贝的过程,但是mmap还不是真正意思上的零拷贝,因为它还是进行了拷贝
那这个过程数据的拷贝经过了:
硬盘—>内核—>socket缓冲区—>网卡,这3次拷贝的过程。
上下文的切换经过了:用户态–>内核态–>用户态 这三次上下文切换
sendFile
sendFile是Linux2.1版本提供的函数,基本的原理就是数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换;
过程:
首先将硬盘上面的数据经过DMA拷贝(直接内存拷贝),将数据拷贝到内核,再将内核态的数据经过CPU拷贝,拷贝到socket缓冲区,再由DMA拷贝将数据从socket缓冲区拷贝到协议栈。
目前来看sendFile和mmap拷贝是差不多的,但是sendFile比mmap拷贝少了一次上下文的切换,
但是就目前看来,这两种拷贝都没有达到真正的零拷贝。
sendFile(增强)
在Linux2.4的版本中,对sendFile做了一些修改,真正实现了零拷贝;
过程:
首先将硬盘上面的数据拷贝到内核,此时在经过CPU拷贝将数据的描述信息拷贝到socket缓冲区(注意:此时拷贝的是数据的描述信息,数据量很小,所以此次拷贝可以忽略不记),然后此时数据其实还是在内核的(因为刚刚的CPU拷贝只是拷贝了一些数据描述),再将数据从内核直接拷贝到网卡。
在这个版本中,sendFile就实现了真正的零拷贝,虽然还是经过了一次CPU的拷贝,但是数据量很小,所以可以忽略不记。
Java+NIO实现零拷贝
在java中可以使用:
fileChannel.transferTo()
和 fileChannel.map() 等函数来实现零拷贝
其中:
mmap适合小数据量的传输, RocketMQ使用的就是mmap。
sendFile 适合大数据量的传输,Kafka使用的就是sendFile。
Java 实现 mmap示例
package org.xhs.json;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* @Author: hu.chen
* @Description:
* @DateTime: 2022/4/29 1:29 AM
**/
public class MmpTest {
public static void main(String[] args) {
try {
// 获取文件
FileChannel readChannel = FileChannel.open(Paths.get("C://test/1.txt"), StandardOpenOption.READ);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, readChannel.size());
FileChannel writeChannel = FileChannel.open(Paths.get("E://test1/3.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输
writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
Java实现sendFile示例:
示例1:
package org.xhs.json;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* @Author: hu.chen
* @Description:
* @DateTime: 2022/5/11 12:45 PM
**/
public class SendfileTest {
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("C://test/1.txt"), StandardOpenOption.READ);
long len = readChannel.size();
long position = readChannel.position();
FileChannel writeChannel = FileChannel.open(Paths.get("E://test1/3.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//数据传输
//开始发送数据:在Java中使用零拷贝技术调用transferTo方法,这个方法底层使用了零拷贝技术
// 在Linux系统下 使用transferTo 方法,没有文件大小限制,可以将文件调用一次transferTo方法即可传输完成
//但是在Windows系统下调用一次transferTo 方法,最多只能发送 8m 的数据,所以需要将文件进行分段传输
// transferTo 参数介绍:
// 第一个参数:从文件的哪里开始读取
// 第二个参数:读取多少字节
// 第三个参数:将读取的字节,放入需要写入的Channel中
readChannel.transferTo(position, len, writeChannel);
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
示例2:
public void writeHtml(File file) {
if (file.exists() && file.isFile()) {
try {
System.err.println("文件存在");
//文件存在进行输出,此处使用零拷贝技术
//获取到该 channel 关联的 缓存Buffer
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
//得到一个文件 通道
FileChannel fileChannel = new FileInputStream(file).getChannel();
writeBuffer.put(HttpProtocolUtil.sendHead(fileChannel.size(),"200"));
writeBuffer.flip();
channel.write(writeBuffer);
//开始发送数据:在Java中使用零拷贝技术调用transferTo方法,这个方法底层使用了零拷贝技术
// 在Linux系统下 使用transferTo 方法,没有文件大小限制,可以将文件调用一次transferTo方法即可传输完成
//但是在Windows系统下调用一次transferTo 方法,最多只能发送 8m 的数据,所以需要将文件进行分段传输
// transferTo 参数介绍:
// 第一个参数:从文件的哪里开始读取
// 第二个参数:读取多少字节
// 第三个参数:将读取的字节需要放入的SocketChannel
long count = fileChannel.transferTo(0, fileChannel.size(), channel);
System.err.println("传输的总的字节大小:"+count);
//关闭通道
close();
} catch (Exception e) {
e.printStackTrace();
System.err.println("写出静态资源失败");
}
} else {
doWrite(HttpProtocolUtil.send404("404 资源未找到"));
//关闭通道
close();
}
}