整个过程,发生了四次拷贝,三次状态的切换。从一开始的用户态,切换到内核态,再切换到用户态,最后再切换成内核态。一次简单的读写,就有这么多名堂,性能肯定是不好的,所有就出现了零拷贝,零拷贝,不是不拷贝,而是整个过程不需要进行CPU拷贝。
二、零拷贝
1、使用mmap优化上述流程:mmap,是指通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据,这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。同样做上面的事情,使用mmap时整个过程如下:
整个过程三次拷贝,三次状态的切换,相比传统拷贝,优化了一丢丢,但这并不是零拷贝。
2、使用sendFile优化:linux 2.1的sendFile:sendFile是linux2.1版本开始提供的一个函数,可以让文件直接从内核buffer进入到socket buffer,不需要经过用户态,过程如下:
整个过程还是3次拷贝,但是减少了一次装态切换,从用户态到内核态再到用户态,只经过了两次切换。这里还是有一次CPU拷贝,还不是真正的零拷贝。
linux 2.4的sendFile:linux 2.4对sendFile又做了一些优化,首先还是DMA拷贝到内核buffer,然后再通过CPU拷贝到socket buffer,最后DMA拷贝到协议栈。优化的点就在于,这次的CPU拷贝,拷贝的内容很少,只拷贝内核buffer的长度、偏移量等信息,消耗很低,可以忽略。因此,这个就是零拷贝。NIO的transferTo
方法就可以实现零拷贝。
三、案例代码
1、传统IO拷贝大文件:
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("传输总字节数:" + total + ",耗时:" + (end - start) + "毫秒");
dataOutputStream.close();
inputStream.close();
socket.close();
}
}
这里拷贝了一个JDK,最后运行结果如下:
传输总字节数: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设置为0,mark设置为-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方法可以一次性发送数据
// 在windows中,transferTo方法传输的文件超过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("发送的总字节:" + transferTotal + ",耗时:" + (end - start) + "毫秒");
channel.close();
socketChannel.close();
}
}
客户端发送文件调用transferTo
方法要注意,在window中,这个方法一次只能传输8M,超过8M的文件要分段,像代码中那样分段传输,在linux中是没这个限制的。运行后结果如下:
发送的总字节:217342912,耗时:415毫秒
从结果可以看到,BIO与NIO耗时相差一个数量级,NIO只要0.4s,而BIO要4s。所以在网络传输中,使用NIO的零拷贝,可以大大提高性能。
本文分享自微信公众号 - java开发那些事(javawebkf)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。