一、零拷贝基本介绍
- 零拷贝是网络编程的关键,很多性能优化都离不开它
- 在 Java 程序中,常用的零拷贝有 mmap(内存映射)和 sendFile。mmap 和 sendFile 在 OS 里,到底是怎么样的一个设计?对 mmap 和 sendFile 两个零拷贝进行分析?
- NIO 中如何使用零拷贝?
- 零拷贝从操作系统角度,是没有 CPU 拷贝,可以有 DMA 拷贝
1.1 传统IO数据读写(4次拷贝3次状态切换)
1)、Java 传统 IO 和 网络编程的代码:
File file = new File("1.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);
2)、传统 IO 模型图
DMA:Direct Memory Access,直接内存访问,即不需要依赖 CPU
1.2 mmap(内存映射)优化(3次拷贝3次状态切换)
1)、mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
2)、mmap 模型图
1.3 sendFile 优化
1)、Linux2.1 版本提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换(3次拷贝2次状态切换)
2)、Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 SocketBuffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。这里其实存在一次 CPU拷贝,从 kernel buffer 到 socket buffer,但是,拷贝的信息量很少,比如length、offset等,消耗低,可以忽略(2次拷贝2次状态切换)
。
1.4 简单描述
1)、零拷贝,是从操作系统的角度来说的,因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer有一份数据)
2)、零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算
1.5 mmap和sendFile的区别
1)、mmap适合小数据量读写,sendFile适合大文件传输
2)、mmap需要3次上下文切换,3次数据拷贝;sendFile需要2次上下文切换,最少2次数据拷贝
3)、sendFile可以利用 DMA 方式,减少 CPU 拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
二、应用案例
2.1 案例要求
1)、使用传统的IO方法传递一个大文件
2)、使用 NIO 零拷贝方式传递(transferTo)一个大文件
3)、比较两种传递方式耗时时间
2.2 传统IO代码
/**
* @desc 传统IO服务器端
* @author yxs
* @date 2021-02-08 11:13
*/
public class OldIOServer {
public static void main(String[] args) throws IOException {
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();
}
}
}
}
/**
* @desc 传统IO客户端
* @author yxs
* @date 2021-02-08 11:20
*/
public class OldIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",7001);
String fileName = "layui-v2.5.7.zip";
FileInputStream 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));
dataOutputStream.close();
socket.close();
inputStream.close();
}
}
2.3 NIO代码
/**
* @desc NIO服务器端
* @auther yxs
* @date 2021/2/17 19:56
*/
public class NewIOServer {
public static void main(String[] args) throws IOException {
InetSocketAddress address = new InetSocketAddress(7001);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(address);
// 创建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 作废,Rewinds this buffer. The position is set to zero and the mark is discarded
}
}
}
}
/**
* @desc NIO客户端
* @auther yxs
* @date 2021/2/17 20:11
*/
public class NewIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",7001));
String fileName = "layui-v2.5.7.zip";
// 得到一个文件的channel
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
// 准备发送
long startTime = System.currentTimeMillis();
// 在linux下一个 transferTo 方法就可以完成传输
// 在windows下一次调用 transferTo 方法只能发送 8m ,需要分段传输文件,而且要注意传输时的位置
// transferTo 底层使用到零拷贝
long size = fileChannel.size();
long maxCopySize = 1024 * 1024 * 8;
long position = 0;
long transferCount = 0;
while (position < size) {
maxCopySize = (size - position) > maxCopySize ? maxCopySize : (size - position);
transferCount += fileChannel.transferTo(position, maxCopySize, socketChannel);
position = position + maxCopySize;
}
//long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送总字节数: " + transferCount + ",耗时: " + (System.currentTimeMillis() - startTime));
// 关闭
fileChannel.close();
}
}