一、是什么
先來看如下一段代碼:
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(8888).accept();
socket.getOutputStream().write(arr);
這段代碼就是讀取一個文件,然后再把它寫出去,看起來就幾行代碼,其實涉及到多次拷貝,其流程如下:
讀取需要拷貝的數(shù)據(jù),這個過程有兩個步驟:首先將文件通過DMA(direct memory access,直接內(nèi)存拷貝,不經(jīng)過CPU) 拷貝到系統(tǒng)內(nèi)核的buffer中,然后在內(nèi)核buffer中通過 CPU拷貝到用戶的buffer中,這才完成了讀取文件的步驟。即前四行就發(fā)生了兩次拷貝。
寫數(shù)據(jù)的時候,將數(shù)據(jù)從用戶buffer通過CPU拷貝到socket buffer中,最后從socket buffer通過DMA拷貝到協(xié)議棧。即最后一行也發(fā)生了兩次拷貝。
整個過程,發(fā)生了四次拷貝,三次狀態(tài)的切換。從一開始的用戶態(tài),切換到內(nèi)核態(tài),再切換到用戶態(tài),最后再切換成內(nèi)核態(tài)。一次簡單的讀寫,就有這么多名堂,性能肯定是不好的,所有就出現(xiàn)了零拷貝,零拷貝,不是不拷貝,而是整個過程不需要進行CPU拷貝。
二、零拷貝
1、使用mmap優(yōu)化上述流程:mmap,是指通過內(nèi)存映射,將文件映射到內(nèi)核緩沖區(qū),同時,用戶空間可以共享內(nèi)核空間的數(shù)據(jù),這樣,在進行網(wǎng)絡(luò)傳輸時,就可以減少內(nèi)核空間到用戶空間的拷貝次數(shù)。同樣做上面的事情,使用mmap時整個過程如下:
首先通過DMA拷貝將硬盤數(shù)據(jù)拷貝到內(nèi)核buffer,但是因為用戶buffer可以共享內(nèi)核buffer的數(shù)據(jù),所以步驟二的cpu拷貝就免了;
然后是直接從內(nèi)核buffer通過CPU拷貝到socket buffer,最后DMA拷貝到協(xié)議棧。
整個過程三次拷貝,三次狀態(tài)的切換,相比傳統(tǒng)拷貝,優(yōu)化了一丟丟,但這并不是零拷貝。
2、使用sendFile優(yōu)化:linux 2.1的sendFile:sendFile是linux2.1版本開始提供的一個函數(shù),可以讓文件直接從內(nèi)核buffer進入到socket buffer,不需要經(jīng)過用戶態(tài),過程如下:
- 首先還是將數(shù)據(jù)從硬盤中通過DMA拷貝到內(nèi)核buffer,然后通過CPU拷貝將數(shù)據(jù)從內(nèi)核buffer拷貝到socket buffer,最后通過DMA拷貝到協(xié)議棧。
整個過程還是3次拷貝,但是減少了一次裝態(tài)切換,從用戶態(tài)到內(nèi)核態(tài)再到用戶態(tài),只經(jīng)過了兩次切換。這里還是有一次CPU拷貝,還不是真正的零拷貝。
linux 2.4的sendFile:linux 2.4對sendFile又做了一些優(yōu)化,首先還是DMA拷貝到內(nèi)核buffer,然后再通過CPU拷貝到socket buffer,最后DMA拷貝到協(xié)議棧。優(yōu)化的點就在于,這次的CPU拷貝,拷貝的內(nèi)容很少,只拷貝內(nèi)核buffer的長度、偏移量等信息,消耗很低,可以忽略。因此,這個就是零拷貝。NIO的transferTo
方法就可以實現(xiàn)零拷貝。
三、案例代碼
1、傳統(tǒng)IO拷貝大文件:
- 服務(wù)端:接收客戶端發(fā)來的數(shù)據(jù)
public class OldIoServer {
@SuppressWarnings("resource")
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(6666);
while (true) {
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
byte[] byteArray = new byte[4096];
while (true) {
int readCount = dataInputStream.read(byteArray, 0, byteArray.length);
if (-1 == readCount) {
break;
}
}
}
}
}
public class OldIoClient {
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 6666);
// 需要拷貝的文件
String fileName = "E:\\download\\soft\\windows\\jdk-8u171-windows-x64.exe";
InputStream inputStream = new FileInputStream(fileName);
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
byte[] buffer = new byte[4096];
long readCount;
long total = 0;
long start = System.currentTimeMillis();
while ((readCount = inputStream.read(buffer)) >= 0) {
total += readCount;
dataOutputStream.write(buffer);
}
long end = System.currentTimeMillis();
System.out.println("傳輸總字節(jié)數(shù):" + total + ",耗時:" + (end - start) + "毫秒");
dataOutputStream.close();
inputStream.close();
socket.close();
}
}
這里拷貝了一個JDK,最后運行結(jié)果如下:
傳輸總字節(jié)數(shù):217342912,耗時:4803毫秒
可以看到,將近5秒鐘。接下來看看使用NIO的transferTo
方法耗時情況:
public class NioServer {
public static void main(String[] args) throws IOException {
InetSocketAddress address = new InetSocketAddress(6666);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(address);
ByteBuffer buffer = ByteBuffer.allocate(4096);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
int readCount = 0;
while (-1 != readCount) {
readCount = socketChannel.read(buffer);
buffer.rewind(); // 倒帶,將position設(shè)置為0,mark設(shè)置為-1
}
}
}
}
public class NioClient {
@SuppressWarnings("resource")
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 6666));
String fileName = "E:\\download\\soft\\windows\\jdk-8u171-windows-x64.exe";
FileChannel channel = new FileInputStream(fileName).getChannel();
long start = System.currentTimeMillis();
// 在linux下,transferTo方法可以一次性發(fā)送數(shù)據(jù)
// 在windows中,transferTo方法傳輸?shù)奈募^8M得分段
long totalSize = channel.size();
long transferTotal = 0;
long position = 0;
long count = 8 * 1024 * 1024;
if (totalSize > count) {
BigDecimal totalCount = new BigDecimal(totalSize).divide(new BigDecimal(count)).setScale(0, RoundingMode.UP);
for (int i=1; i<=totalCount.intValue(); i++) {
if (i == totalCount.intValue()) {
transferTotal += channel.transferTo(position, totalSize, socketChannel);
} else {
transferTotal += channel.transferTo(position, count + position, socketChannel);
position = position + count;
}
}
} else {
transferTotal += channel.transferTo(position, totalSize, socketChannel);
}
long end = System.currentTimeMillis();
System.out.println("發(fā)送的總字節(jié):" + transferTotal + ",耗時:" + (end - start) + "毫秒");
channel.close();
socketChannel.close();
}
}
客戶端發(fā)送文件調(diào)用transferTo
方法要注意,在window中,這個方法一次只能傳輸8M,超過8M的文件要分段,像代碼中那樣分段傳輸,在linux中是沒這個限制的。運行后結(jié)果如下:
發(fā)送的總字節(jié):217342912,耗時:415毫秒
從結(jié)果可以看到,BIO與NIO耗時相差一個數(shù)量級,NIO只要0.4s,而BIO要4s。所以在網(wǎng)絡(luò)傳輸中,使用NIO的零拷貝,可以大大提高性能。