零拷贝:所谓零拷贝,不是说不拷贝(数据从硬盘到内核态的DMA拷贝是一定会有的),而是说没有CPU拷贝。网络编程的关键,很多性能优化都离不开零拷贝
传统IO数据拷贝过程:
过程说明:
-
在读取数据的时候数据一开始从硬盘经过DMA copy到内核态中,
-
内核态再经过CPU copy到达user buffer(即用户态)
-
再冲用户态经过CPU copy到socket buffer
-
后再经过DMA copy到达协议栈(协议栈此处可自行查阅资料)
常见的零拷贝技术:
JAVA中的mmap(内存映射)和sendFile
mmap:指通过内存映射,将文件映射到内核缓冲区,以此用户空间可以共享内核空间的数据,这样在网络传输时,减少内核空间到用户空间的拷贝次数
优点:减少了拷贝次数一次,但是状态切换还是3次
sendFile:Linux2.1版本优化中指的是sendFile函数
核心:数据不经过用户态,直接从内核缓冲区进入socket缓冲区,同时,由于与用户态无关,减少了一次上下文切换
优点:拷贝次数由4次转换为3,状态切换由3次转换为2次
说明:Linux2.4版本中做了一些优化,避免了从内核缓冲区拷贝到Socket Buffer的操作,数据从内核态直接拷贝到协议栈,实现零拷贝(其实不可说是完全的零拷贝,因为少部分数据【如数据头,长度等】其实还是由从内核态拷贝到socket Buffer)
案例分析
传统IO server
public class OldIoServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(7001);
while (true){
Socket accept = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(accept.getInputStream());
try {
byte[] bytes = new byte[4096];
while (true) {
int readCount = dataInputStream.read(bytes);
if( -1 == readCount){
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
传统IO client
public class OldIoClient {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("localhost", 7001);
String fileName = "xxxxxx.zip";
InputStream 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();
}
}
NIO方式:使用transferTo()方法
server
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);
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){
// e.printStackTrace();
// 发生异常后直接break而不打印异常栈,则不会导致服务器不可用
break;
}
byteBuffer.rewind();
}
}
}
}
NIO client
public class NewIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 7001));
String fileName = "xxxxxx.zip";
FileChannel fileChannel = new FileInputStream(fileName).getChannel();
long start = System.currentTimeMillis();
//在linux下一个transferTO方法就可以完成传输,而windows下一次只能8M,所以只能分段传输
//所以需要设置传输的位置
long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
long end = System.currentTimeMillis();
System.out.println("transferCount = " + transferCount);
System.out.println("传输整体耗时:"+ (end - start));
}
}
验证说明:
通过比较传统IO传输数据的耗时与使用NIO的方式传输数据的耗时,来说明零拷贝对网络传输的作用