NIO与零拷贝的示例分析

发布时间:2022-01-15 18:04:53 作者:柒染
来源:亿速云 阅读:85

这期内容当中小编将会给大家带来有关NIO与零拷贝的示例分析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

 

一、NIO是什么

先来看如下一段代码:

File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
  
byte[] arr = new byte[(int)file.length()];
raf.read(arr);
  
Socket socket = new ServerSocket(8888).accept();
socket.getOutputStream().write(arr);
 

这段代码就是读取一个文件,然后再把它写出去,看起来就几行代码,其实涉及到多次拷贝,其流程如下:

  • 读取需要拷贝的数据,这个过程有两个步骤:首先将文件通过DMA(direct memory access,直接内存拷贝,不经过CPU) 拷贝到系统内核的buffer中,然后在内核buffer中通过 CPU拷贝到用户的buffer中,这才完成了读取文件的步骤。即前四行就发生了两次拷贝。

  • 写数据的时候,将数据从用户buffer通过CPU拷贝到socket buffer中,最后从socket buffer通过DMA拷贝到协议栈。即最后一行也发生了两次拷贝。

整个过程,发生了四次拷贝,三次状态的切换。从一开始的用户态,切换到内核态,再切换到用户态,最后再切换成内核态。一次简单的读写,就有这么多名堂,性能肯定是不好的,所有就出现了零拷贝,零拷贝,不是不拷贝,而是整个过程不需要进行CPU拷贝。

 

二、零拷贝

1、使用mmap优化上述流程:mmap,是指通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据,这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。同样做上面的事情,使用mmap时整个过程如下:

  • 首先通过DMA拷贝将硬盘数据拷贝到内核buffer,但是因为用户buffer可以共享内核buffer的数据,所以步骤二的cpu拷贝就免了;

  • 然后是直接从内核buffer通过CPU拷贝到socket buffer,最后DMA拷贝到协议栈。

整个过程三次拷贝,三次状态的切换,相比传统拷贝,优化了一丢丢,但这并不是零拷贝。

2、使用sendFile优化:linux 2.1的sendFile:sendFile是linux2.1版本开始提供的一个函数,可以让文件直接从内核buffer进入到socket buffer,不需要经过用户态,过程如下:

  • 首先还是将数据从硬盘中通过DMA拷贝到内核buffer,然后通过CPU拷贝将数据从内核buffer拷贝到socket buffer,最后通过DMA拷贝到协议栈。

整个过程还是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的零拷贝,可以大大提高性能。

上述就是小编为大家分享的NIO与零拷贝的示例分析了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注亿速云行业资讯频道。

推荐阅读:
  1. Java网络编程与NIO详解11:Tomcat中的Connector源码分析(NIO)
  2. Tomcat中NIO模型的示例分析

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

nio

上一篇:管理控制台Cloudera Manager怎么用

下一篇:springboot整合quartz定时任务框架的方法是什么

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》