NIO编程

1 NIO概述

Java NIO(New IO)是一个可替代Java IO API(从Java1.4开始),JAVA NIO提供了与标准IO不一样的工做方式。java

Java NIO:Channels and Buffers(通道和缓冲区)安全

标准的IO基于字节流或者字符流进行操做,而NIO基于通道和缓冲区进行操做,数据是老是从通道读取的缓冲区中,或者从缓冲区写入到通道中。服务器

Java NIO:Non-blocking IO(非阻塞IO):Java NIO可让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程仍是能够进行其余事情,当数据被写入缓冲区时,线程能够继续处理它,从缓冲区写入通道也相似。网络

Java NIO:Selectors(选择器):Java NIO引入了选择器的概念,选择器用于监听多个通道的事件(好比:链接打开,数据到达)。所以,单个线程能够监听多个数据通道。app

选择KEYdom

一、SelectionKey.OP_CONNECT(若是选择器检测到相应的通道已经准备好完成他的链接序列或者有个错误即将发生,那么就添加OP_CONNECT到选择key的就绪集合而且将该键添加到选择器的可选择键集合If the selector detects that the corresponding socket channel is ready to complete its connection sequence, or has an error pending, then it will add OP_CONNECT to the key's ready set and add the key to its selected-key set.)异步

二、SelectionKey.OP_ACCEPT(If the selector detects that the corresponding channel is ready to accept another connection, or has an error pending, then it will add OP_ACCEPT to the key's ready set and add the key to its selected-key set.)jvm

三、SelectionKey.OP_READ(If the selector detects that the corresponding channel is ready for reading,  has reached end-of-stream, has been remotely shut down for further reading, or has an error pending, then it will add OP_READ to the key's ready set and add the key to its selected-key set.)socket

四、SelectionKey.OP_WRITE(If the selector detects that the corresponding channel is ready for writing,  has been remotely shut down for further writing, or has an error pending, then it will add OP_WRITE to the key's ready set and add the key to its selected-key set.)tcp

若是你对不止一种事件感兴趣,那么能够用“位或”操做符将常量链接起来,以下:int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

传统IO是单向,NIO是双向

如图,当程序读取文件时,经过输入流进行文件字节流或者字符流读取相似水管同样,写入文件相似。

而NIO则是利用缓冲区(传输的数据就存放在这里)进行双向的数据传输,经过通道将缓冲区传输到文件或者读取到程序。(面向缓冲区),传输的是缓冲区而再也不是字节或者字符流

与传统IO区别:

IO

NIO

面向流

面向缓冲区

阻塞IO

非阻塞IO

选择器

2 Buffer的数据存取

是NIO提供给数据传输和通道一块儿配合使用,存储数据的容器

1)容量(capacity):表示Buffer最大数据容量,缓冲区容量不能为负,而且创建后不能修改。

2)限制(limit):第一个不该该读取或者写入的数据的索引,即位于limit后的数据不能够读写。缓冲区的限制不能为负,而且不能大于其容量(capacity)。

3)位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,而且不能大于其限制(limit)。

4)标记(mark)与重置(reset):标记是一个索引,经过Buffer中的mark()方法指定Buffer中一个特定的position,以后能够经过调用reset()方法恢复到这个position。

@Test
    public void test001(){
        ByteBuffer allocate = ByteBuffer.allocate(1024);
        System.out.println(allocate.limit());//1024
        System.out.println(allocate.position());//0
        System.out.println(allocate.capacity());//1024
        System.out.println("---------王buffer存放数据------------");
        allocate.put("abcd1".getBytes());
        allocate.put("abcd1".getBytes());
        System.out.println(allocate.limit());//1024
        System.out.println(allocate.position());//10
        System.out.println(allocate.capacity());//1024
//        System.out.println("----------错误读取值--------------------");
//        byte[] bytes=new byte[allocate.limit()];
//        allocate.get(bytes);
//        System.out.println(new String(bytes,0,bytes.length));
        System.out.println("----------正确读取值--------------------");
        allocate.flip();//翻转        limit = position;position = 0;mark = -1;
        System.out.println(allocate.limit());//10
        System.out.println(allocate.position());//0
        System.out.println(allocate.capacity());//1024
        byte[] bytes2=new byte[allocate.limit()];
        /**
         *          public ByteBuffer get(byte[] dst) {
         *              return get(dst, 0, dst.length);
         *          }
         *          public ByteBuffer get(byte[] dst, int offset, int length) {
         *              checkBounds(offset, length, dst.length);
         *              if (length > remaining())
         *                  throw new BufferUnderflowException();
         *              int end = offset + length;
         *              for (int i = offset; i < end; i++)
         *                  dst[i] = get();
         *              return this;
         *          }
         *          public final int remaining() {
         *              return limit - position;
         *          }
         * */
        allocate.get(bytes2);//
        System.out.println(new String(bytes2,0,bytes2.length));//abcd1abcd1
        System.out.println(allocate.limit());//10
        System.out.println(allocate.position());//************10*****************表明不能重复读取
        System.out.println(allocate.capacity());//1024;
        allocate.rewind();//        position = 0;mark = -1;与flip有区别哟
        System.out.println("-----------清空缓冲区-----------------");
        allocate.clear();//        position = 0;limit = capacity;mark = -1;
        System.out.println(allocate.limit());//1024
        System.out.println(allocate.position());//0
        System.out.println(allocate.capacity());//1024
        allocate.get(bytes2);
        System.out.println(new String(bytes2,0,bytes2.length));//abcd1abcd1   能够看到数据并无被清空
        System.out.println("---------------mark 和 reset---------------------");
        allocate.reset();//        int m = mark;if (m < 0)throw new InvalidMarkException();position = m;
        allocate.mark();//        mark = position;用于重置,起标记做用
    }

3 直接缓冲区和间接缓冲区

非直接缓冲区:经过allocate()方法分配缓冲区,将缓冲区创建在JVM的内存中,当从物理磁盘读取数据的时候,先读取到物理空间(其实仍是在设备上的),再copy到jvm空间,而后才从jvm空间里进行读取,因为这里涉及到一个copy的过程,因此效率相比直接缓冲区较低。IO缓冲区属于非直接,大多数缓冲区都是非直接缓冲区

直接缓冲区:经过allocateDirect()方法分配直接缓冲区,将缓冲区创建在物理内存中,能够提升效率。很是占内存,安全性相比非直接缓冲区较低

Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操做。也就是说,在每次调用基础操做系统的一个本机 I/O 操做以前(或以后),虚拟机都会尽可能避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。

直接字节缓冲区能够经过调用此类的 allocateDirect() 工厂方法来建立。此方法返回的缓冲区进行分配和取消分配所需成本一般高于非直接缓冲区。直接缓冲区的内容能够驻留在常规的垃圾回收堆以外,所以,它们对应用程序的内存需求量形成的影响可能并不明显。因此,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操做影响的大型、持久的缓冲区。通常状况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。

直接字节缓冲区还能够经过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来建立。该方法返回MappedByteBuffer 。 Java 平台的实现有助于经过 JNI 从本机代码建立直接字节缓冲区。若是以上这些缓冲区中的某个缓冲区实例指的是不可访问的内

3.1 样例比较

//菲直接缓冲区 读写操做
    @Test
    public void test002() throws IOException {
        long start = System.currentTimeMillis();
        //读入流
        FileInputStream fileInputStream = new FileInputStream("F:\\someTools\\test\\1.mp4");
        //写入流
        FileOutputStream fileOutputStream = new FileOutputStream("F:\\someTools\\test\\2.mp4");
        //建立管道
        FileChannel channel = fileInputStream.getChannel();
        FileChannel channel1 = fileOutputStream.getChannel();
        //分配指定大小缓冲区
        ByteBuffer buf=ByteBuffer.allocate(1024);
        while(channel.read(buf)!=-1){
            //开启读取模式
            buf.flip();
            //将数据写入到通道中
            channel1.write(buf);
            buf.clear();
        }
        channel.close();
        channel1.close();
        fileInputStream.close();
        fileOutputStream.close();
        long end = System.currentTimeMillis();
        System.out.println("非直接缓冲区用时:"+(end-start)+"ms");
    }

    //直接缓冲区 读写操做
    @Test
    public void test003() throws IOException {
        long start = System.currentTimeMillis();
        //建立管道
        FileChannel inChannel=FileChannel.open(Paths.get("F:\\someTools\\test\\1.mp4"),StandardOpenOption.READ);
        FileChannel outChanel = FileChannel.open(Paths.get("F:\\someTools\\test\\2.mp4"), StandardOpenOption.READ,StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        //定义映射文件
        MappedByteBuffer inMappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outMappedByteBuffer = outChanel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
        //直接对缓冲区操做
        byte[] buf=new byte[inMappedByteBuffer.limit()];
        inMappedByteBuffer.get(buf);
        outMappedByteBuffer.put(buf);
        inChannel.close();
        outChanel.close();
        long end = System.currentTimeMillis();
        System.out.println("直接缓冲区用时:"+(end-start)+"ms");
    }

使用一个207 MB视频测试,发现直接缓冲区平均用时300多ms而非直接缓冲区用时3000多ms,能够看到性能相差很大。。

4 通道(Channel)的原理获取

通道表示打开到 IO 设备(例如:文件、套接字)的链接。若须要使用 NIO 系统,须要获取用于链接 IO 设备的通道以及用于容纳数据的缓冲区。而后操做缓冲区,对数据进行处理。Channel 负责传输, Buffer 负责存储。通道是由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的链接。Channel 相似于传统的“流”。只不过 Channel自己不能直接访问数据, Channel 只能与Buffer 进行交互。

java.nio.channels.Channel 接口:

            |--FileChannel

            |--SocketChannel

            |--ServerSocketChannel

            |--DatagramChannel

 

  获取通道

  1. Java 针对支持通道的类提供了 getChannel() 方法

            本地 IO:

            FileInputStream/FileOutputStream

            RandomAccessFile

 

            网络IO:

            Socket

            ServerSocket

            DatagramSocket

           

  2. 在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()

  3. 在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()

5 分散读取,汇集写入

分散读取(scattering Reads):将通道中的数据分散到多个缓冲区中

汇集写入(gathering Writes):将多个缓冲区的数据汇集到通道中

//分散读取,汇集写入
    @Test
    public void test004() throws IOException {
        RandomAccessFile raf=new RandomAccessFile("test.txt","rw");
        //获取通道
        FileChannel channel = raf.getChannel();
        //分配指定大小的缓冲区
        ByteBuffer allocate = ByteBuffer.allocate(100);
        ByteBuffer allocate1 = ByteBuffer.allocate(1024);
        //分散读取
        ByteBuffer[] bufs={allocate,allocate1};
        channel.read(bufs);
        for (ByteBuffer byteBuffer:bufs) {
            //切换模式
            byteBuffer.flip();
        }
        System.out.println(new String(bufs[0].array(),0,bufs[0].limit()));
        System.out.println("------------------------------------------------");
        System.out.println(new String(bufs[1].array(),0,bufs[1].limit()));
        System.out.println("------------------汇集写入------------------------");
        RandomAccessFile raf2=new RandomAccessFile("test.txt","rw");
        //获取通道
        FileChannel channel2 = raf2.getChannel();
        channel2.write(bufs);
        raf.close();
        raf2.close();
    }

6 阻塞IO和非阻塞IO的

模型关系:

 

同步阻塞IO(BIO)

伪异步IO

非阻塞IO(NIO)

异步IO(AIO)

客户端个数:IO线程

1:1

M:N(其中M能够大于N)

M:1(1个IO线程处理多个客户端链接)

M:0(不须要启动额外的IO线程,被动回调)

IO类型(是否阻塞)

阻塞概念:应用程序在获取网络数据的时候,若是网络数据传输很慢,那么程序就会一直等待,直到传输完毕。

非阻塞:应用程序直接获取已经准备好的数据,无须等待。JDK1.7以前NIO为同步非阻塞IO,JDK1.7以后升级了NIO,支持异步非阻塞通信模型NIO2.0(AIO),传统IO为同步阻塞IO

同步阻塞IO:当服务器accept时(应用程序获取网络程序时,网络传输数据很慢),他会一直等待客户端发送数据过来,若是客户端没有发送数据过来,那么他就会一直等待(阻塞)直到客户端发送数据后,继续往下执行。我是这样理解的,由于客户端与IO线程是1:1的关系也就是一个客户端发送请求数据,对应一个服务端线程,因此在服务端进行IO操做时(好比等待客户端的数据),服务端不会作其余事(毕竟单线程嘛)只能阻塞等待(好比上面的服务端while循环一直就卡在accept是吧?????)数据准备完成后,才继续执行。

//tcp服务器端...
class TcpServer {

	public static void main(String[] args) throws IOException {
		System.out.println("socket tcp服务器端启动....");
		ServerSocket serverSocket = new ServerSocket(8080);
		// 等待客户端请求
		try {
			while (true) {
                System.out.println("服务器等待新链接。。。。。。。。。。");
				Socket accept = serverSocket.accept();
				new Thread(new Runnable() {

					@Override
					public void run() {
						try {
							InputStream inputStream = accept.getInputStream();
							// 转换成string类型
							byte[] buf = new byte[1024];
							int len = inputStream.read(buf);
							String str = new String(buf, 0, len);
							System.out.println("服务器接受客户端内容:" + str);
						} catch (Exception e) {
							// TODO: handle exception
						}

					}
				}).start();

			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			serverSocket.close();
		}

	}

}

public class TcpClient {
	public static void main(String[] args) throws UnknownHostException, IOException {
		System.out.println("socket tcp 客户端启动....");
		Socket socket = new Socket("127.0.0.1", 8080);
		OutputStream outputStream = socket.getOutputStream();
		outputStream.write("我是客户端".getBytes());
		socket.close();
	}
}

伪异步IO:服务端使用线程池的方式,当客户端一次发送多个请求(或者多个客户端同时请求时),服务端线程池调度一个线程进行处理,虽然在服务端看来某个线程在处理IO时,其余线程依然能够继续运行,可是没有实质性解决同步阻塞问题。毕竟没有链接到达时,你还不是一直在accept????????

//tcp服务器端...
class TcpServer {
     
	public static void main(String[] args) throws IOException {
		ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
		System.out.println("socket tcp服务器端启动....");
		ServerSocket serverSocket = new ServerSocket(8080);
		// 等待客户端请求
		try {
			while (true) {
                System.out.println("服务器等待新链接。。。。。。。。。。");
				Socket accept = serverSocket.accept();
				//使用线程
				newCachedThreadPool.execute(new Runnable() {

					@Override
					public void run() {
						try {
							InputStream inputStream = accept.getInputStream();
							// 转换成string类型
							byte[] buf = new byte[1024];
							int len = inputStream.read(buf);
							String str = new String(buf, 0, len);
							System.out.println("服务器接受客户端内容:" + str);
						} catch (Exception e) {
							// TODO: handle exception
						}

					}
				});
				

			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			serverSocket.close();
		}

	}

}

public class TcpClient {
	public static void main(String[] args) throws UnknownHostException, IOException {
		System.out.println("socket tcp 客户端启动....");
		Socket socket = new Socket("127.0.0.1", 8080);
		OutputStream outputStream = socket.getOutputStream();
		outputStream.write("我是客户端".getBytes());
		socket.close();
	}
}

同步非阻塞IO:服务端有一个线程专门用来监视这些IO,当该线程监视到某个IO完成后服务端就会启动一个线程处理其实也就是作出响应,之因此说非阻塞是由于我只须要扫描(轮询)这个线程上的IO,而不关心该IO是否在处理是否改通道有请求来了等等,只有知足个人要求的IO到达或者完成我才启动一个线程来处理,因此对于服务端来讲我一直在扫描(轮询)我并无阻塞是吧????

以下图:在NIO中,当客户端有新的链接(通道)时,会将其注册到选择器上,只有选择器监听到有请求到达数据准备好后,服务端采用启动一个线程进行处理,而在准备过程当中或者数据等待以前,服务端是不会等待的是吧,因此服务端是没有任何的阻塞问题的。。

//NIO服务端
public class NIOServer {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器端已经被启动。。。。。。");
        //一、建立服务通道
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        //二、切换异步非阻塞
        socketChannel.configureBlocking(false);//jdk1.7以上
        //三、绑定链接
        socketChannel.bind(new InetSocketAddress(8080));
        //四、获取选择器
        Selector selector=Selector.open();
        //五、将通道注册到选择器中 而且指定其监听接受事件
        socketChannel.register(selector,SelectionKey.OP_ACCEPT);
        //六、轮询获取“已经准备就绪的事件”
        while(selector.select()>0){
            //七、获取当前选择器中全部已经注册的选择键
            Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
            while(selectionKeys.hasNext()){
                //八、获取准备就绪事件
                SelectionKey selectionKey = selectionKeys.next();
                //九、判断准备就绪的事件类型
                if(selectionKey.isAcceptable()){
                    //十、若是是 接受就绪,则获取客户端链接
                    SocketChannel socketChannel1=socketChannel.accept();
                    //十一、设置阻塞模式
                    socketChannel1.configureBlocking(false);
                    //十二、注册通道
                    socketChannel1.register(selector,SelectionKey.OP_READ);
                } else if(selectionKey.isReadable()){
                    //1三、获取当前选择器“就绪”状态的通道
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    //1四、读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len=0;
                    while((len=channel.read(buffer))>0){
                        buffer.flip();
                        System.out.println(new String(buffer.array(),0,len));
                        buffer.clear();
                    }
                }
                selectionKeys.remove();
            }
        }
    }
}


public class NIOClient {
    public static void main(String[] args) throws IOException {
        System.out.println("客户端已经启动。。。");
        //一、建立socket 通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
        //二、设置异步非阻塞
        socketChannel.configureBlocking(false);//jdk1.7以上方可
        //指定缓冲区大小
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put(new Date().toString().getBytes());
        //切换到读取模式
        buffer.flip();
        socketChannel.write(buffer);
        buffer.clear();
        socketChannel.close();
    }
}