场景介绍:
在客户端通过读取本地文件并发送给服务器端,记录从发送开始到发送完成的执行时间,采用BIO方式和NIO零拷贝发送进行说明。
先上代码,BIO方式代码如下:
public class OldServer {
public static void main(String[] args) throws Exception{
ServerSocket serverSocket=new ServerSocket(8888);
while(true){
Socket socket=serverSocket.accept();
DataInputStream dataInputStream=new DataInputStream(socket.getInputStream());
byte[] bytes=new byte[4096];
while(true){
int readCount=dataInputStream.read(bytes,0,bytes.length);
if(readCount==-1){
break;
}
}
}
}
}
public class OldClient {
public static void main(String[] args) throws Exception{
Socket soket=new Socket("localhost",8888);
String fileName="F:\\soft\\mysql-installer-community-5.6.25.0.zip";
InputStream inputStream=new FileInputStream(fileName);
DataOutputStream dataOutputStream=new DataOutputStream(soket.getOutputStream());
byte[] bytes=new byte[4096];
long readCount;
long total=0;
long starttime=System.currentTimeMillis();
while((readCount=inputStream.read(bytes))>=0){
total+=readCount;
dataOutputStream.write(bytes);
}
System.out.println("发送字节数:"+total+",耗时"+ (System.currentTimeMillis()-starttime));
dataOutputStream.close();
soket.close();
inputStream.close();
}
}
NIO方式代码如下:
public class NewServer {
public static void main(String[] args) throws Exception{
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
ServerSocket serverSocket=serverSocketChannel.socket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress("localhost",9999));
ByteBuffer byteBuffer=ByteBuffer.allocate(4096);
while(true){
SocketChannel socketChannel=serverSocketChannel.accept();
socketChannel.configureBlocking(true);
int readCount=0;
long allCount=0;
while(-1!=readCount){
try {
readCount=socketChannel.read(byteBuffer);
allCount+=readCount;
}catch (Exception e){
e.printStackTrace();
byteBuffer.clear();
break;
}
byteBuffer.clear();
}
System.out.println(allCount+"---"+System.currentTimeMillis());
socketChannel.close();
}
}
}
public class NewClient {
public static void main(String[] args) throws Exception{
SocketChannel socketChannel=SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost",9999));
socketChannel.configureBlocking(true);
String fileName="F:\\soft\\mysql-installer-community-5.6.25.0.zip";
FileChannel fileChannel=new FileInputStream(fileName).getChannel();
long position = 0;
long size = fileChannel.size();
long startTime = System.currentTimeMillis();
while (position < size) {
//重点代码,是实现零拷贝到远程服务器的关键
long transfer = fileChannel.transferTo(position, fileChannel.size(), socketChannel);
if (transfer <= 0) {
break;
}
position += transfer;
}
System.out.println(System.currentTimeMillis());
System.out.println("发送字节数:"+position+",耗时"+ (System.currentTimeMillis()-startTime));
fileChannel.close();
}
}
同一个文件经过2种不同方式进行拷贝并发送给远程服务器,在客户端打印出来的效率完全不一样,为什么呢?因为NIO使用的是零拷贝方式。
几个名词解释:
用户空间:可以理解为应用程序所管控的内存空间,如jvm堆
内核空间:可以理解为操作系统内核所管控的内存空间
磁盘或网卡等硬件:磁盘上的文件或网络发送过来的数据流
零拷贝:并不是指数据一次都不拷贝,而是数据不用从内核空间拷贝到用户空间。
BIO方式流程如下图所示:
步骤为:
1.当应用程序需要读取磁盘上的某个文件时,需要调用操作系统内核的read方法,并将进程状态切换到内核态,即从用户态到内核态的上下文切换。
2.内核通过直接内存访问(DMA)方式将磁盘上的数据拷贝到内核空间,一次数据拷贝。
3.将内核空间的数据拷贝到用户空间,一次数据拷贝,一次内核态到用户态的上下文切换。
4.应用程序什么也不做,将用户空间的数据拷贝一份作为发送数据的源数据。一次数据拷贝。
5.调用内核的write方法,用户空间的新数据源拷贝到内核空间,一次数据拷贝,一次用户态到内核态的上下文切换。。
6.内核返回发送结果,又一次内核态到用户态的上下文切换。
综上所述:一共需要经历4次用户态和内核态的切换,4次数据拷贝
NIO减少用户空间和内核空间的拷贝,流程如下图所示:
1.应用程序向操作系统内核发起sendfile调用,一次用户态到内核态的上下文切换。
2.内核通过直接内存访问(DMA)方式从磁盘将数据拷贝到内核空间,一次文件拷贝。
3.内核将文件内容拷贝到另外一个缓冲区,一次文件拷贝
4.内核将另外一个缓冲区的数据写入网络或磁盘
5.向应用程序返回发送结果,一次内核态到用户态的上下文切换。
综上所述:一共产生了2次用户态和内核态的切换,2次数据拷贝。
NIO直接传递文件描述符方式,如下图所示:
1.应用程序向操作系统内核发起sendfile调用,一次用户态到内核态的上下文切换。
2.内核通过直接内存访问(DMA)方式从磁盘将数据拷贝到内核空间,一次文件拷贝。
3.内核将文件描述信息拷贝到另外一个缓冲区,即另外一个缓存去里存储的是远缓冲区的内存地址和长度
4.内核将另外一个缓冲区指向的数据写入网络或磁盘
5.向应用程序返回发送结果,一次内核态到用户态的上下文切换。
综上所述:一共产生了2次用户态和内核态的切换,1次数据拷贝,1次文件描述符和长度的拷贝。
注意:上述功能需要Linux2.4以后版本才支持,也就是说要实现所谓的零拷贝,是需要操作系统支持。