NIO概述

本文深入讲解Java NIO的基本概念、核心组件及工作原理,对比传统IO模型,介绍五种IO模型,并通过示例代码演示NIO的应用。

一、基本概念

1. 概念
1)java.nio全称java non-blocking IO,是指jdk1.4 及以上版本里提供的新api(New IO) ,为所有的原始类型(boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络

2) NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道
2. NIO与传统IO的区别
1)NIO和传统IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的	

2)传统的IO又称BIO,即阻塞式IO,NIO就是非阻塞IO(non-blocking IO)

3)线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)
3. 同步与异步
1)同步就是:如果有多个任务或者事件要发生,这些任务或者事件必须逐个地进行,一个事件或者任务的执行会导致整个流程的暂时等待,这些事件没有办法并发地执行

2)异步就是:如果有多个任务或者事件发生,这些事件可以并发地执行,一个事件或者任务的执行不会导致整个流程的暂时等待
4. 阻塞与非阻塞
1)阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足

2)非阻塞就是:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待

也就是说阻塞和非阻塞的区别关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息
5. 阻塞IO与非阻塞IO
1)一个完整的IO读请求操作包括两个阶段:

	(1)查看数据是否就绪

	(2)进行数据拷贝(内核将数据拷贝到用户线程)

2)对比
	
	(1)当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪

	(2)对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪

	(3)阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息

3)说明

	(1)Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回

	(2)如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待	
6. 同步IO与异步IO
1)对比

	(1)同步IO即 如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞

	(2)而异步IO为 如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞 	

2)说明

	(1)对于同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;

	(2)而异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞

	(3)这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持	

二、五种IO模型

1. 阻塞IO模型
(1)最传统的一种IO模型,即在读写数据过程中会发生阻塞现象	

(2)当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态

(3)典型的阻塞IO模型的例子为:data = socket.read(); 如果数据没有就绪,就会一直阻塞在read方法。
2. 非阻塞IO模型
(1)当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果

(2)如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回

(3)事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU

(4)典型模型:

	while(true){
	    data = socket.read();
	    if(data!= error){
	        处理数据
	        break;
	    }
	}
3. 多路复用IO模型
(1)多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO

(2)在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作

(3)因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用

(4)在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞

(5)多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多

(6)多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询
4. 信号驱动IO模型
(1)在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作
5. 异步IO模型
(1)异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事

(2)从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block	

(3)然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了

(4)也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了

(6)也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写

(7)注意,异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO

前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞


三、NIO组件

在这里插入图片描述

1. Selector
1)概念

	(1)仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好

	(2)当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道(其实就是一个桥梁)

2)常用方法

	(1)创建Selector  			

		Selector selector = Selector.open()

	(2)向Selector注册通道	    

		channel.configureBlocking(false);
		SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

	与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以	

3)监听的事件类型

	(1)Connect	   SelectionKey.OP_CONNECT       	某个channel成功连接到另一个服务器称为“连接就绪”

	(2)Accept        SelectionKey.OP_ACCEPT           一个server socket channel准备好接收新进入的连接称为“接收就绪”

	(3)Read          SelectionKey.OP_READ             有数据可读的通道可以说是“读就绪”

	(4)Write         SelectionKey.OP_WRITE            等待写数据的通道可以说是“写就绪”

4) 检测channel事件就绪

	(1)selectionKey.isAcceptable();

	(2)selectionKey.isConnectable();

	(3)selectionKey.isReadable();

	(4)selectionKey.isWritable();
2. Channel
1)概念

	(1)既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的

	(2)通道可以异步地读写

	(3)通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入

2)通道类型

	(1)FileChannel	           FileChannel 从文件中读写数据

	(2)DatagramChannel           DatagramChannel 能通过UDP读写网络中的数据

	(3)SocketChannel 			   能通过TCP读写网络中的数据   

	(4)ServerSocketChannel       可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel     
3. Buffer

在这里插入图片描述

1)概念

	(1)Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的

	(2)缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存

	(3)当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()你好方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据

	(4)一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面

2)使用Buffer读写数据一般遵循以下四个步骤:

	(1)写入数据到Buffer

	(2)调用flip()方法

	(3)从Buffer中读取数据	

	(4)调用clear()方法或者compact()方法

2)常用方法

	(1)Buffer的分配(字节)

			ByteBuffer buf = ByteBuffer.allocate(1024);

	(2)向Buffer中写数据
	
			int bytesRead = inChannel.read(buf);            从Channel写到Buffer

			buf.put(127);                                   通过Buffer的put()方法写到Buffer里

	(3)flip()方法(flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值)		

			buf.flip();

	(4)从Buffer中读取数据		

			int bytesWritten = inChannel.write(buf);       从Buffer读取数据到Channel   

			byte aByte = buf.get();                        使用get()方法从Buffer中读取数据

3)Buffer的capacity,position和limit	

	(1)capacity

		a. 作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据

	(2)position	

		a. 当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

		b. 当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置

	(3)limit
	
		a. 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity	

		b. 当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

四、代码示例

1. TCP服务端
public class MyNioServer {

    private Selector selector;

    private final static int PORT = 8686;

    private final static int BUF_SIZE = 10240;

    private void initServer() throws IOException, InterruptedException {
        // 创建通道管理器对象selector
        this.selector = Selector.open();

        // 创建一个通道channel
        ServerSocketChannel channel = ServerSocketChannel.open();
        // 将通道设置为非阻塞
        channel.configureBlocking(false);
        // 将通道绑定在8686端口
        channel.socket().bind(new InetSocketAddress(PORT));

        // 将通道管理器和通道绑定,并为该通道注册OP_ACCEPT事件
        // 注册事件后,当该事件到达时,selector.select()会返回(一个key),如果该事件没到达selector.select()会一直阻塞
        SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);

        // 轮询
        while(true){
            // 这是一个阻塞方法,一直等待知道有数据到达,返回值是key的数量(可以有多个)
            selector.select();
            // 如果channel有数据了,将生成的key放入keys集合中
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator iterator = keys.iterator();
            // 使用迭代器遍历集合
            while(iterator.hasNext()){
                // 得到集合中的一个实例
                SelectionKey key = (SelectionKey)iterator.next();
                // 拿到当前key实例之后记得在迭代器中奖这个元素删除,非常重要,否则会出错
                iterator.remove();
                // 判断当前key所代表的channel是否在Acceptable状态,如果是就进行接收
                if(key.isAcceptable()){
                    System.out.println("****** 接收了");
                    doAccept(key);
                }else if(key.isReadable()){
                    System.out.println("****** 读了");
                    doRead(key);
                }else if(key.isWritable() && key.isValid()){
                    System.out.println("****** 写了");
                    doWrite(key);

                }else if(key.isConnectable()){
                    System.out.println("连接成功");
                }
            }
        }
    }

    public void doAccept(SelectionKey key) throws IOException {
        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
        System.out.println("ServerSocketChannel正在循环监听");
        // ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
        SocketChannel clientChannel = channel.accept();
        clientChannel.configureBlocking(false);
        clientChannel.register(key.selector(), SelectionKey.OP_READ);
    }

    public void doRead(SelectionKey key) throws IOException, InterruptedException {
        SocketChannel clientChannel = (SocketChannel)key.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
        long bytesRead = clientChannel.read(byteBuffer);
        while (bytesRead > 0){
            byteBuffer.flip();
            byte[] data = byteBuffer.array();
            String info = new String(data).trim();
            System.out.println("从客户端发送过来的消息是:"+ info);
            byteBuffer.clear();
            bytesRead = clientChannel.read(byteBuffer);
        }
        String toClient = "客户端你好呀,我是服务端, hahahahahaha";
        byteBuffer.put(toClient.getBytes("UTF-8"));
        // 这里如果删掉了,表明position没有在读模式下归0,还是写模式下的position,从而导致channel从buffer读取的时候是空的
        byteBuffer.flip();
        clientChannel.write(byteBuffer);
        byteBuffer.clear();
    }

    public void doWrite(SelectionKey key) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
        String info = "客户单你好呀,我是服务端, hahahahahaha";
        byteBuffer.put(info.getBytes("UTF-8"));
        byteBuffer.flip();
        SocketChannel clientChannel = (SocketChannel)key.channel();
        while(byteBuffer.hasRemaining()){
            clientChannel.write(byteBuffer);
        }
        byteBuffer.compact();
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        MyNioServer myNioServer = new MyNioServer();
        myNioServer.initServer();
    }
}
2. TCP客户端
public class MyNioClient {

    // 创建一个选择器
    private Selector selector;
    private final static int PORT = 8686;
    private final static int BUF_SIZE = 10240;
    private static ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);

    private void initClient() throws IOException, InterruptedException {
        this.selector = Selector.open();
        SocketChannel clientChannel = SocketChannel.open();
        clientChannel.configureBlocking(false);
        clientChannel.connect(new InetSocketAddress(PORT));
        clientChannel.register(selector, SelectionKey.OP_CONNECT);

        while(true){
            selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();
                if(key.isConnectable()){
                    System.out.println("客户端连接了");
                    doConnect(key);
                }else if(key.isReadable()){
                    System.out.println("客户端开始读了");
                    doRead(key);
                }
            }
        }
    }

    public void doConnect(SelectionKey key) throws IOException, InterruptedException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        if(clientChannel.isConnectionPending()){
            clientChannel.finishConnect();
        }
        clientChannel.configureBlocking(false);
        String info = "服务端你好,我是客户端,啦啦啦";
        byteBuffer.put(info.getBytes("UTF-8"));
        byteBuffer.flip();
        clientChannel.write(byteBuffer);
        byteBuffer.clear();
        clientChannel.register(selector, SelectionKey.OP_READ);
    }

    public void doRead(SelectionKey key) throws IOException, InterruptedException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        clientChannel.read(byteBuffer);
        byte[] data = byteBuffer.array();
        String msg = new String(data).trim();
        System.out.println("服务端发送消息" + msg);
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        MyNioClient myNioClient = new MyNioClient();
        myNioClient.initClient();
    }

}	

参考网址

Java NIO: 浅析I/O模型

JAVA NIO: NIO概述

Java NIO详解

Java NIO系列教程(三) Buffer(一系列)

注:文章是经过参考其他的文章然后自己整理出来的,有可能是小部分参考,也有可能是大部分参考,但绝对不是直接转载,觉得侵权了我会删,我只是把这个用于自己的笔记,顺便整理下知识的同时,能帮到一部分人。
ps : 有错误的还望各位大佬指正,小弟不胜感激

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值