NIO 零拷贝深入分析

什么是零拷贝

零拷贝 大概的理解就是在操作数据时, 不需要将数据 从一个内存区域拷贝到另一个内存区域. 因为少了内存的拷贝, 因此 CPU 的效率就得到的提升,同时它是操作系统层面上的操作LINUX与 WINDOWS 操作的区别相当大 我们来LINUX系统下的操作,如果操作系统提供则有,如果操作系统没提供,java 是无法提供任何相关的操作

通过一张图片我们来分析 IO模式的内存分析

NIO 零拷贝深入分析_第1张图片

1.User space 是 用户空间  Kernel space 是内核空间 Hardware 是磁盘

2. jvm 通过底层read 方法 去请求内核空间 ,然后内核空间 ask 请求磁盘 read数据

3.磁盘 将数据 拷贝到 内核空间 通过DMA直接内存访问 ,然后 从内核空间 将数据拷贝到 用户空间

4.然后程序进行 数据的读取 在将数据拷贝 到 内核空间,内核空间再把数据 写入到 客户端Hardware 图中的write 中Hardware 与read 不是一个 不要混淆

5.操作完成 write 方法返回 结束

上面的IO模式内存分析 进行了四次拷贝 四次上下文的切换 

我们在看看NIO 内存分析


NIO 零拷贝深入分析_第2张图片

1. jvm 发送一个 sendfile(Linux系统调用) 到内核空间 ,内核空间ask 到磁盘

2.磁盘 将数据 写入到 内核空间的缓冲区 ,内核空间的数据 在写入到 socket 缓冲区中

3.内核空间将数据  写入到 网络的客户端中

4.结束

上面的IO模式内存分析 进行了两次拷贝 两次上下文的切换 

这样就数据的操作都在内核空间里进行的 就是一种零拷贝

后来 又进行改进 但是需要底层系统的支持

NIO 零拷贝深入分析_第3张图片

1. jvm 发送一个 sendfile(Linux系统调用) 到内核空间 ,内核空间ask 到磁盘

2.磁盘 通过直接内存的方式将数据 写入到 内核空间的缓冲区或者通过 scatter/gather来读取到内核空间缓冲区

对于scatter/gather这种方式是需要底层操作系统来支持的,另外 在硬件或内核空间操作系统层面上,对于某些缓冲区增加了文件描述符这种概念,在这个文件描述符中可以描述一些特性,比如内存空间 基本特定 长度 偏移量多少,这样也可以减少数据在内核空间中的复制过程,换句话说直接从磁盘中获取数据直接写入到对应的缓冲区中


此时也有一个问题: 对于用户来说 无法直接参与 数据的读取等,如果此时用户需要操作 文件的内容的修改 我们该怎么办,可以使用MappedByteBuffer 内存映射,所谓的内存映射就是 将操作系统中磁盘上的文件映射到内存中,直接修改内存就表示修改磁盘上的文件,前几篇文章有介绍 ,它可以减少不必要的内存拷贝,直接操作内核空间


我们来通过代码对比一下IO 与NIO 文件传输速率

老方式:

package com.nio;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class OldServer {

	/**
	 * @param args
	 * @throws IOException 
	 */
	public static void main(String[] args) throws IOException {
		
		ServerSocket serverSocket = new ServerSocket(8899);
		
		while (true) {
			Socket socket =serverSocket.accept();
			DataInputStream datainputStream = new DataInputStream(socket.getInputStream());
			byte[] byteArray = new byte[4096];
			
			while (true) {
				int readCount  = datainputStream.read(byteArray);
				
				if(-1 == readCount){
					break;
				}
			}
		}

	}

}


package com.nio;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class OldClinet {

	/**
	 * @param args
	 * @throws IOException 
	 */
	public static void main(String[] args) throws IOException {
	
		Socket socket = new Socket("localhost",8899);

	
		FileInputStream inputSteam = new FileInputStream("C:/Users/lm/Desktop/Netty权威指南 第2版 带书签目录 完整版.pdf.zip");
		
		
		DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
		
		byte[] byteArray = new byte[4096];
		int readCount = 0;
		long total = 0;
		
		long startTime = System.currentTimeMillis();

		while((readCount = inputSteam.read(byteArray)) >=0){
			total += readCount;
			dataOutputStream.write(byteArray);
		}
		
		dataOutputStream.close();
		
		System.out.println("发送总字节数 :"+total +",耗时:"+( System.currentTimeMillis() - startTime));
		
	}

}

发送总字节数 :102621561,耗时:3934

新方式:


package com.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;


public class NewServer {

	/**
	 * @param args
	 * @throws IOException 
	 */
	public static void main(String[] args) throws IOException {
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		
		ServerSocket  serverSocket=serverSocketChannel.socket();
		serverSocket.setReuseAddress(true);
		
		serverSocket.bind(new InetSocketAddress(8899));
		
		ByteBuffer by = ByteBuffer.allocate(4096);
		
		while(true){
			SocketChannel socketChannel =serverSocketChannel.accept();
			
			socketChannel.configureBlocking(true);
			
			int readCount = 0;
			
			while(-1!=readCount){
				readCount = socketChannel.read(by);
				by.rewind();
			}
		}

	}

}

package com.nio;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class NewClient {

	/**
	 * @param args
	 * @throws IOException 
	 */
	public static void main(String[] args) throws IOException {
	
		

		SocketChannel socket = SocketChannel.open();

		socket.connect(new InetSocketAddress("localhost",8899));
	
		socket.configureBlocking(true);
		
		FileInputStream inputSteam = new FileInputStream("C:/Users/lm/Desktop/Netty权威指南 第2版 带书签目录 完整版.pdf.zip");
		
		FileChannel filechannel =inputSteam.getChannel();
		
		long startTime = System.currentTimeMillis();
		
		long  transCount = filechannel.transferTo(0, filechannel.size(), socket);
		
		System.out.println("发送总字节数 :"+transCount +",耗时:"+( System.currentTimeMillis() - startTime));
		
		socket.close();

	}

}

发送总字节数 :8388608,耗时:171

总结:从数据耗时上来看 明显零拷贝的方式要快一些

下面对一些方法解释一下:

serverSocket.setReuseAddress(true);

对于这个方法大概的意思就是 当一个TCP链接被关闭的时候,链接会保持一段时间超时的状态,通常称为TIME_WAIT状态(socket的状态),对于一个应用尝试去绑定一个服务器上的某一个端口号上,当服务器这个端口号上的这个链接处于timeout状态时,那么这个应用是不能绑定到这个端口号上,如果启用了setReuseAddress 那么socket链接就可以绑定到这个端口号上,即便这个链接处于timeout状态。也就是说之前有一个socket链接被关闭掉了,但是并不是说 关闭掉的链接的端口号马上就可以被其他socket链接所使用了,相反这个端口号会在被关闭后的一小段时间内处于超时的状态也叫TIME_WAIT状态,如果有一个新的socket想要绑定到这个端口号上时 就会提示端口被占用。

我们再来看:

filechannel.transferTo(long position,long count,WritableByteChannel target);

这个方法的含义:将从通道关联的文件,将文件中的字节传递到给定可写的btye channle中,这个方法比简单的循环效率要高,很多操作系统可以传输字节直接从文件系统缓存中直接传输,而不是去复制他们。

我们再来看这张时序图:从liunx2.4版本之后就是采用这种方式进行零拷贝

NIO 零拷贝深入分析_第4张图片

从liunx2.4之后 socket有文件描述符,并进行了细微的修改并利用到Gatter。

1. jvm 发送一个 sendfile(Linux系统调用) 到内核空间 ,内核空间ask 到磁盘

2.磁盘 通过直接内存的方式将数据 写入到 内核空间的缓冲区,并且将内核空间缓冲区的一个文件描述符写到

socket buffer 中,描述符中包含 kemel buffer 长度大小和地址的引用,而不是把数据本身copy到socket buffer中,这时协议引擎就可以进行真正的数据发送,这时需要从两个地方开始读取,一个是从socket buffer获取长度地址,一个是根据长度/地址获取数据的本身kemel buffer,这样就再也不会出现 从kemel buffer 将数据copy到socket buffer,这才是真正的零拷贝

我们再来介绍一下MappedByteBuffer 通过javadoc文档:

它是一个直接缓存区是一个文件内存映射区域,MappedByteBuffer 它是可以通过Filechanle.map来实现的,MappedByteBuffer 是一种允许java程序直接在内存中访问的特殊的文件,可以将整个文件或者文件的一部分映射到内存中,并有操作系统来完成内容的修改并写入到文件当中,我们的应用程序只需要处理内存的数据,这样可以迅速的处理IO操作,用于内存映射文件的内存本身是在java堆的外面也叫堆外内存。

例子:


package nio;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class NioTest7 {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {

        RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest9.txt","rw");

        FileChannel fileChannel = randomAccessFile.getChannel();

        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        mappedByteBuffer.put(0,(byte)'a');
        //只需操作内存 不需要写入文件 怎么把数据写入文件当中 都是由操作系统来完成 不需要我们去管  
        mappedByteBuffer.put(3,(byte)'b');

        randomAccessFile.close();
    }

}

我们再来看看Buffer的Scattering与Gathering:

分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。

聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。

scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体.

之前我们read或write都是操作一个buffer 装满之后重新定义position位置重新在往里面读或写,而Scattering 不仅可以传一个buffer 还可以传buffer数组 比如我们一个buffer[0] 长度2  buffer[1] 长度5    buffer[2] 长度9,只有把第一个读满 在去读第二个,而Gathering 写也可以传buffer数组。
那么什么时候会这么使用呢:
比如我们在网络操作的时候 自定义协议 第一个传递过来的请求数据 第一个 buffer[1] 长度5   buffer[2] 长度9 作为Header buffer[3] 作为 Body 

例子:

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;

/**
 * 关于Buffer的Scattering与Gathering
 */
public class NioTest9 {
    public static void main(String[] args) throws IOException{
        ServerSocketChannel serverSocketChannel =ServerSocketChannel.open();

        InetSocketAddress address = new InetSocketAddress(8899);

        serverSocketChannel.socket().bind(address);

        int messageLenth = 2 + 3 +4;

        ByteBuffer[] buffers = new ByteBuffer[3];

        buffers[0] = ByteBuffer.allocate(2);
        buffers[1] = ByteBuffer.allocate(3);
        buffers[2] = ByteBuffer.allocate(4);

       SocketChannel socketChannel = serverSocketChannel.accept();

        while (true){
            int byteRead = 0;
            // 如果读到的字节数小于总的 继续读
            while (byteRead  "position:"+ buffer.position()+",limit"+buffer.limit()).forEach(System.out::println);
            }

            Arrays.asList(buffers).forEach(buffer ->{
                buffer.flip();
            });

            long bytesWritee =0;
            while (bytesWritee < messageLenth){
               long r = socketChannel.write(buffers);
                bytesWritee +=r;
            }

            Arrays.asList(buffers).forEach(buffer ->{
                buffer.clear();
            });

            System.out.println("byteRead :"+byteRead+ ",bytesWritee:"+bytesWritee+",messageLenth:"+messageLenth);
        }

    }
}


我们使用telnet localhost 8899

输入hellowor+回车 正好是9个字节

byteRead :9  
position:2,limit2  
position:3,limit3  
position:4,limit4  
byteRead :9,bytesWritee:9,messageLenth:9  

如果输入 hello+回车 是6个字节

byteRead :6  
position:2,limit2  
position:3,limit3  
position:1,limit4  



你可能感兴趣的:(NIO)