NIO与零拷贝

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

    二、零拷贝

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

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

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

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


    本文分享自微信公众号 - java开发那些事(javawebkf)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    09-03 11:45
    查看更多