在 Java 程序中,常用的零拷贝有 mmap ( 内存 映射 ) 和 sendFile。
传统IO: 经过4次拷贝,3次状态切换
mmap 优化: 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。拷贝次数是3次,3次状态切换
sendFile 优化: Linux 2.1 版本提供了sendFile 函数,数据不经过用户态,直接从内核缓冲区进入到socket buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。3次拷贝,2次状态切换
Linux 2.4 版本做了一些修改,避免了从内核缓冲区拷贝到socket buffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝(kernel buffer -> socket buffer 会拷贝一些 length, offset 等描述信息,可以忽略)。 2次拷贝, 2次状态切换
零拷贝:
- 是指从操作系统角度看,不存在CPU拷贝(仅有 kernel buffer 一份数据)
- 不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算
mmap 和 sendFile 的区别:
- mmap 适合小数据量读写,sendFile 适合大文件传输
- mmap 需要4次上下文切换,3次数据拷贝。sendFile 需要3次上下文切换,最少2次数据拷贝
- sendFile 可以利用DMA方式,减少 CPU 拷贝, mmap 则不能
传统 IO 服务器端:
import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 传统IO 服务器端
* @author doubily
*/
public class IOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9527);
while(true) {
Socket socket = serverSocket.accept();
DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
try {
byte[] bytes = new byte[4096];
while (true) {
int readCount = dataInputStream.read(bytes, 0, bytes.length);
if (-1 == readCount) {
break;
}
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
传统 IO 客户端:
/**
* 传统IO 客户端
* @author doubily
*/
public class IOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost", 9527);
String fileName = "F:\\3rdlib.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 服务端:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
/**
* NIO 服务器端
* @author doubily
*/
public class NIOServer {
public static void main(String[] args) throws IOException {
InetSocketAddress address = new InetSocketAddress(9527);
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) {
// e.printStackTrace();
break;
}
// 倒带 Position = 0; mark 标志作废
byteBuffer.rewind();
}
}
}
}
NIO 客户端:
/**
* NIO 客户端
* @author doubily
*/
public class NIOClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 9527));
// 得到文件channel
String fileName = "F:\\3rdlib.zip";
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();
}
}