1. 理论
1. 零拷贝介绍
零拷贝是网络编程的关键, 很多性能优化都需要零拷贝。
在 Java程序中, 常用的零拷贝方式有m(memory)map[内存映射] 和 sendFile。
2. NIO 与 传统IO对比
(1) 传统示意图
user context: 用户态
kernel context: 内核态
User space: 用户空间
Kernel space: 内核空间
Syscall read: 系统调用读取
Syscall write: 系统调用写入
Hard drive: 硬件驱动
kernel buffer: 内核态缓冲区
user buffer: 用户态缓冲区
socket buffer: 套接字缓存
protocol engine: 协议引擎
DMA: Direct Memory Access: 直接内存拷贝(不使用CPU)
总结: 4次拷贝, 3次状态切换, 效率不高
(2)mmap优化流程示意图:
mmap 通过内存映射, 将文件映射到内核缓冲区, 同时, 用户空间可以共享内核空间的数据。
这样, 在进行网络传输时, 就可以减少内核空间到用户空间的拷贝次数。
总结: 3次拷贝, 3次状态切换, 不是真正意义上的零拷贝。
(3) sendFile Linux2.1版本优化流程示意图
数据根本不经过用户态, 直接从内核缓冲区进入到Socket Buffer, 同时, 由于和用户台完全无关, 就减少了一次上下文切换。
但是仍然有一次CPU拷贝, 不是真正的零拷贝(没有CPU拷贝)。
总结: 3次拷贝, 2次切换
(4) sendFile Linux
避免了从内核缓冲区拷贝到Socket buffer的操作, 直接拷贝到协议栈, 从而再一次减少了数据拷贝。
其实是有一次cpu拷贝的, kernel buffer -> socket buffer, 但是拷贝的信息很少, length, offset, 消耗低, 基本可以忽略。
总结: 2次拷贝(如果忽略消耗低的cpu拷贝的话), 2次切换, 基本可以认为是零拷贝了。
3.零拷贝理解
零拷贝是从操作系统的角度来看的。内核缓冲区之间, 没有数据是重复的(只有kernel buffer有一份数据)。
零拷贝不仅仅带来更少的数据复制, 还能带来其他的性能优势: 如更少的上下文切换, 更少的 CPU 缓存伪共享以及无CPU校验和计算。
4. mmap 与 sendFile 总结
mmap适合小数据两读写, sendFile适合大文件传输
mmap 需要3次上下文切换, 3次数据拷贝; sendFile 需要3次上下文切换, 最少2次数据拷贝。
sendFile 可以利用 DMA 方式, 减少 CPU 拷贝, 而 mmap则不能(必须从内核拷贝到Socket缓冲区)。
2.测试
使用传统方法和NIO方法传递一个大文件。文件的大小是106,520,576 字节 (101MB)。
1. 使用传统的IO 方法
服务器端代码
package filecopy; import .IOUtils; import org.apache.commons.lang3.time.StopWatch; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import .ServerSocket; import .Socket; public class OldServerSocket { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(6666); Socket accept = serverSocket.accept(); System.out.println("客户端连接成功" + accept.getRemoteSocketAddress()); InputStream inputStream = accept.getInputStream(); StopWatch stopWatch = new StopWatch(); stopWatch.start(); int copy = IOUtils.copy(inputStream, new FileOutputStream("F:/old2.tar")); stopWatch.stop(); System.out.println("传输完成,用时: " + stopWatch.getTime()); inputStream.close(); accept.close(); } }
客户端代码:
package filecopy; import .IOUtils; import org.apache.commons.lang3.time.StopWatch; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import .Socket; public class OldSocketClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 6666); OutputStream outputStream = socket.getOutputStream(); File file = new File("F:/1.tar"); StopWatch stopWatch = new StopWatch(); stopWatch.start(); IOUtils.copy(new FileInputStream(file), outputStream); stopWatch.stop(); System.out.println("传输完成,用时: " + stopWatch.getTime()); outputStream.close(); } }
结果:
传输完成,用时: 2542
2. 使用NIO方法
服务端代码:
package filecopy; import java.io.FileOutputStream; import java.io.IOException; import .InetSocketAddress; import .ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; public class NewIOServer { public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); ServerSocket serverSocket = serverSocketChannel.socket(); serverSocket.bind(new InetSocketAddress(6666)); serverSocket.setReuseAddress(true); // 输出的文件 FileChannel channel = new FileOutputStream("F:/new2.tar").getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); while (true) { try { SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端连接成功: " + socketChannel.getRemoteAddress()); int readcount = 0; while (-1 != (readcount = socketChannel.read(byteBuffer))) { byteBuffer.clear(); // 将索引重新指会0 channel.write(byteBuffer);// 将存进的ByteBuffer对象写进文件输出流 byteBuffer.flip();// 翻转缓冲区 } } catch (IOException e) { e.printStackTrace(); } } } }
客户端代码:
package filecopy; import org.apache.commons.lang3.time.StopWatch; import java.io.FileInputStream; import java.io.IOException; import .InetSocketAddress; import java.nio.channels.FileChannel; import java.nio.channels.SocketChannel; public class NewIoClient { public static void main(String[] args) throws IOException { SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("localhost", 6666)); //得到一个文件channel FileChannel fileChannel = new FileInputStream("F:/1.tar").getChannel(); StopWatch stopWatch = new StopWatch(); stopWatch.start(); //在linux下一个transferTo 方法就可以完成传输, 底层使用到零拷贝 //在windows 下 一次调用 transferTo 只能发送8m , 就需要分段传输文件, 而且要考虑分段传输的起始位置 // 每次传7M int sendSize = 8 * 1024; // 已经传送的大小(也作为下次传输的起始位置) int sendedSize = 0; long totalSize = fileChannel.size(); while (sendedSize < totalSize) { sendedSize += fileChannel.transferTo(sendedSize, sendSize, socketChannel); } stopWatch.stop(); System.out.println("传输完成,用时: " + stopWatch.getTime()); //关闭 fileChannel.close(); } }