NIO与零拷贝
零拷贝基本介绍
- 零拷贝是网络编程 的关键,很多性能优化都离不开
- 在java 程序中,常用的零拷贝有mmap(内存映射) 和 sendFile,那么,他们在OS李,到底是怎样一个设计? 我们分析mmap和sendFile这两个零拷贝
- 另外我们看一下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);
mmap优化
- mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据,这样, 在进行网络传输时, 就可以减少内核空间到用户空间的拷贝次数,如下图
sendFile优化
- linux2.1版本提供了sendFile函数,其基本原理如下: 数据根本不经过用户态,直接从内核缓冲区进入到socketBuffer, 同时, 由于和用户态完全无关,就减少了一次上下文切换
- linux在 2.4 版本中,做了一些修改,避免从内核缓冲区拷贝到SocketBuffer的操作,直接拷贝到协议栈,从而再次减少了数据拷贝,具体如下图和小结这里其实有一次cpu 拷贝,
kernel buffer -> socket buffer
但是,拷贝信息很少,比如length,offset 信息很少消耗低,可以忽略, 实际是 2两次拷贝
使用传统方式进行数据拷贝代码实现
Socket socket = new Socket("localhost", 7001);
String fileName = "protoc.3.6.1-win32.zip";
FileInputStream fileInputStream = 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 = fileInputStream.read(buffer)) >= 0) {
total += readCount;
dataOutputStream.write(buffer);
}
System.out.println("发送字节数: " + total + "耗时: "+ (System.currentTimeMillis() - startTime));
dataOutputStream.close();
socket.close();
fileInputStream.close();
服务端代码
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 exception) {
exception.printStackTrace();
}
}
使用零拷贝方式进行数据传输
客户端代码
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",7001));
String fileName = "protoc.3.6.1-win32.zip";
// 得到一个文件的channel
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
// 准备发送
long startTime = System.currentTimeMillis();
// 在linux下一个transferTo 方法就可以完成传输
// 在windows下 一次调用transferTo只能发送8M文件,
// 就需要分段传输文件, 而且要注意传输时的位置
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
System.out.println("发送总的字节数= " + transferCount + "耗时: " + (System.currentTimeMillis() - startTime));
fileChannel.close();
服务端
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 e) {
break;
}
// 将buffer倒带
byteBuffer.rewind(); // position 编程0 , mark作废
}
}