美文网首页
阻塞非阻塞 同步异步 IO模型及其应用 NIO实现原理

阻塞非阻塞 同步异步 IO模型及其应用 NIO实现原理

作者: 秋笙fine | 来源:发表于2019-02-15 02:22 被阅读0次

    1.同步异步概念

    2.阻塞非阻塞概念

    3.常见I/O模型:同步阻塞IO,同步非阻塞IO,异步阻塞IO,异步非阻塞IO

    4.UNIX系统下的IO多路复用(OS级别的I/O多路复用是重点,同步非阻塞I/O的应用)***

    5.IO中BIO,NIO,AIO简介

    6.NIO实现原理(NIO也是同步非阻塞I/O的应用)(NIO阻塞代码实例 NIO非阻塞代码实例 这里的Selector真正体现了多路复用)(重点)***

    1.同步异步概念

    同步异步是针对应用程序和内核的交互而言的。
    同步指的是用户进程触发IO操作,等待/轮询的去查看IO操作是否完成。(同步阻塞IO是等待,同步非阻塞是轮询)
    异步指的是用户进程出发IO操作便开始做别的事,IO操作已经完成的时候会得到IO完成的通知。

    2.阻塞非阻塞概念

    阻塞非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态采用的不同操作方式。
    阻塞状态下,读取/写入函数将一直等待IO操作就绪。
    非阻塞状态下,读取/写入函数会立即返回一个状态值。

    3.常见的IO模型

    同步阻塞IO:在此种模型下,用户进程在发起一个IO操作以后,必须等待IO操作的完成。只有当真正完成了IO操作以后,用户进程才能运行。
    应用:Java.io包下的传统IO模型

    同步非阻塞IO:在此种模型下,用户进程在发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不断的进行轮询,从而引入不必要的CPU资源浪费。
    应用:jdk1.4出现的java.nio包

    异步阻塞IO:在此种模型下,用户进程发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后通知应用程序。
    这就是同步异步的最关键区别:同步必须等待/主动轮询IO操作是否完成。
    为什么说是阻塞的呢?因为其是调用了select函数来完成,而select函数本身实现的方式就是阻塞的。其好处为:可以同时监听多个文件句柄,从而提高系统并发性。

    同步非阻塞IO:在此种模型下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理,不需要进行实际的IO读写操作,因为真正的IO读取或写入操作已经由内核完成了。

    4.UNIX系统下的IO多路复用(OS级别的I/O多路复用是重点,同步非阻塞I/O的应用)

    UNIX系统秉承了一切皆文件的思想。
    IO多路复用机制是同步非阻塞IO的应用。
    它利用单独的线程(内核级)统一检测所有的Socket(套接字),一旦某个Socket有了IO数据,则启动响应的Application处理。
    实现原理:在select和poll中利用轮询Socket句柄的方式来实现监测Socket中是否有IO数据到达。

    select底层是数组,poll是链表,epoll是哈希表。
    select和epoll区别:

    1. 每次调用select,都需要把fd集合(句柄集合)从用户态拷贝到内核态,这个开销在fd很多时开销很大
    2. 同时每次调用select都需要在内核遍历传递进来的所有的fd,这个开销在fd很多时也很大
    3. select支持的文件描述符数量太小了,默认是1024
      4.epoll为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数
    4. epoll所支持的FD上线是最大可以打开的文件数目

    5.BIO,NIO,,AIO简介

    BIO:同步并阻塞IO模型的应用,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的开销,所以可以通过线程池机制改善。
    适用场景:适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4之前唯一的选择。

    NIO:同步非阻塞IO模型的应用。服务器实现模式为一个请求一个线程。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有I/O请求时才启动一个线程进行处理。
    适用场景:适用于连接数目多且连续比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

    AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
    适用场景:连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

    NIO实现原理

    NIO是同步非阻塞,一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有IO请求时才进行处理。
    多路复用原理:它利用单独的线程(内核级)统一检测所有的Socket(套接字),一旦某个Socket有了IO数据,则启动响应的Application处理。
    实现原理:在select和poll中利用轮询Socket句柄的方式来实现监测Socket中是否有IO数据到达。

    NIO的适用场景:
    在文件IO中优势和传统IO不明显,但是如果是网络IO,则是其适用场景。

    实现NIO的三要素

    Buffer缓冲区,Channel管道,Selector选择器
    Buffer是用来存储数据的(数组形式),Channel是用来运输的。

    java.nio包下 Buffer为缓冲区抽象类
    public abstract class Buffer extends Object{
    public final int capacity();//1
    public final int limit();//2
    public final int position();//3
    public final Buffer mark();//4
    public final Buffer reset();
    public final Buffer flip();//5
    }

    Capacity说明:容量,缓冲区能够容纳数据元素的最大数量(Buffer缓冲区底层是数组,所以capacity不可变)
    Limit说明:界限,表示缓冲区中可以操作的数据大小(limit后面的数据不可以进行读写操作)
    position说明:位置,表示缓冲区中正在操作的数据位置
    mark说明:标记,表示记录当前position的位置,可以通过reset恢复到mark的位置
    flip说明:调用此方法后,会将position置0,limit置为之前position的值。

    ByteBuffer缓冲区(Buffer的继承子类)主要方法:(除了以上再加)
    put();//将数据写入缓冲区
    get();//将数据从缓冲区读出

    java.nio.channels.channel接口
    主要实现类:FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel

    获取Channel对象的方法:
    本地IO:FileInputStream/FileOutputStream/RandomAccessFile
    网络IO:Socket,ServerSocket,DatagramSocket

    Selector适用时不能与FileChannel一起使用,Selector使用场景是非阻塞的,而FileChannel是阻塞场景下的文件IO,而SocketChannel可以是非阻塞的,所以Selector常与SocketChannel连用。

    以下代码:
    客户端的FileChannel=>SocketChannel=>服务器的FileChannel
    阻塞下Channel+Buffer:

    //这是客户端代码
    public class TestDemo {
    
        
        public static void main(String[] args) throws Exception {
            //1.获取通道,向IP127.0.0.1的9999建立通道
            SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
    
            //2.发送一张图片给服务器
            FileChannel fileChannel=FileChannel.open(Paths.get("E:"+File.separator+"psb.jpg"));
    
            //3.创建Buffer
            ByteBuffer buffer=ByteBuffer.allocate(1024);
    
            //4.fileChannel承载本地图片,SocketChannel读取本地图片,两管道配合,传输给服务器
            while(fileChannel.read(buffer)!=-1){
    
                //Buffer在被socketChannel读取前切换成读模式
                buffer.flip();
    
                socketChannel.write(buffer);
    
                //读完将Buffer切换成写模式,能让管道继续读取文件
                buffer.clear();
            }
    
            //5.关闭流
            fileChannel.close();
            socketChannel.close();
        }
    
    }
    
    //这里是服务器端
    public class Member {
        public static void main(String[] args) throws Exception{
            
    
            //1.获取通道
            ServerSocketChannel server=ServerSocketChannel.open();
    
            //2.得到文件通道,将客户端传递过来的图片写到本地项目下(写模式,没有则创建)
            FileChannel outChannel=FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
    
            //3.绑定连接通道(这里的Socket套接字侧面反映了TCP,UDP)
            server.bind(new InetSocketAddress(9999));//监听服务器的6666端口数据请求
    
            //4.获取客户端的连接(阻塞的)
            SocketChannel client=server.accept();
    
            //5.同样与服务器端的SocketChannel对应的Buffer缓冲区
            ByteBuffer buffer=ByteBuffer.allocate(1024);
    
            //6.将客户端传来的图片保存在本地中
            while(client.read(buffer)!=-1){
                //在读之前都要切换成读模式
                buffer.flip();
    
                outChannel.write(buffer);
    
                //读完切换成写模式,能让管道继续读取文件的数据
                buffer.clear();
            }
    
            //7.关闭通道
            outChannel.close();
            client.close();
            server.close();
        }
    }
    

    然后运行服务器端代码,再运行客户端请求。的确将客户端E盘的图片文件通过Socket管道,以FileChannel阻塞方式,上传到了服务器端。

    image.png

    非阻塞下:SocketChannel+Selector+Buffer:
    以上阻塞态如果不关闭流,则服务器端一直会读取客户端发来的数据,进而阻塞,所以要使用socketChannel

    public class TestDemo {
    
        //这是客户端代码
        public static void main(String[] args) throws Exception {
            //1.获取通道,向IP127.0.0.1的9999建立通道
            SocketChannel socketChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
    
            socketChannel.configureBlocking(false);//非阻塞状态
            //2.发送一张图片给服务器
            FileChannel fileChannel=FileChannel.open(Paths.get("E:"+File.separator+"psb.jpg"));
    
            //3.创建Buffer
            ByteBuffer buffer=ByteBuffer.allocate(1024);
    
            //4.fileChannel承载本地图片,SocketChannel读取本地图片,两管道配合,传输给服务器
            while(fileChannel.read(buffer)!=-1){
    
                //Buffer在被socketChannel读取前切换成读模式
                buffer.flip();
    
                socketChannel.write(buffer);
    
                //读完将Buffer切换成写模式,能让管道继续读取文件
                buffer.clear();
            }
    
            //5.关闭流
            fileChannel.close();
            socketChannel.close();
        }
    
    }
    
    
    public class Member {
        public static void main(String[] args) throws Exception{
            //这里是服务器端
    
            //1.获取通道
            ServerSocketChannel server=ServerSocketChannel.open();
    
            //2.切换成非阻塞模式
            server.configureBlocking(false);
    
        
    
            //3.绑定连接通道(这里的Socket套接字侧面反映了TCP,UDP)
            server.bind(new InetSocketAddress(9999));//监听服务器的6666端口数据请求
    
            //4.获取选择器
            Selector selector=Selector.open();
    
            //4.1将通道注册到选择器上,指定接收监听通道"事件,回调接收就绪事件代码
            server.register(selector, SelectionKey.OP_ACCEPT);
    
            //5.轮询的获取选择器上已 就绪 的事件(只要select()>0,说明已经就绪)(这里非阻塞才真正体现多路复用)
            while(selector.select()>0){
                //6.使用Iterator遍历句柄
                Iterator<SelectionKey>iterator=selector.selectedKeys().iterator();
    
                //7.获取已经就绪的事件
                while(iterator.hasNext()){
                    SelectionKey selectionKey=iterator.next();
    
                    //接收事件就绪
                    if(selectionKey.isAcceptable()){
                        //8.获取客户端连接
                        SocketChannel client=server.accept();
                        //8.1 切换成非阻塞态 这样才能使用FileChannel
                        client.configureBlocking(false);
                        //8.2 注册到选择器上-->拿到客户端的连接为了读取通道的数据(指定监听读就绪事件) 回调读状态代码
                        client.register(selector, SelectionKey.OP_READ);
    
                    }//读事件就绪
                    else if(selectionKey.isReadable()){
                        //9.获取当前选择器读就绪状态的通道
                        SocketChannel client=(SocketChannel)selectionKey.channel();
    
                        //9.1读取数据
                        ByteBuffer buffer=ByteBuffer.allocate(1024);
    
                        //9.2得到文件通道,将客户端传递过来的图片写道服务器本地(写模式,没有则创建)
                        FileChannel outChannel=FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
    
                        while(client.read(buffer)!=-1){
                            //在读之前都要切换成读模式
                            buffer.flip();
                
                            outChannel.write(buffer);
                
                            //读完切换成写模式,能让管道继续读取文件的数据
                            buffer.clear();
                        }
                    }
    
                    //取消选择键(已经处理过的事情,就取消)
                    iterator.remove();
                
                }
            }
    
        
            
    
            //7.关闭通道
            //outChannel.close();
            //client.close();
            server.close();
        }
    }
    
    
    

    同样也上传了图片。这里加入了Selector选择器的NIO非阻塞,才真正实现了IO多路复用,并且通过选择器状态的不同,回调不同的Channel。并且使用Iterator实现了轮询。

    JavaNIO与IO的区别:

    1.传统的IO面向流,一个字节一个字节处理数据,而NIO是面向缓冲区的,面向内存块处理数据。
    2.Java IO的各种流是阻塞的,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取。
    NIO是非阻塞的,使一个线程从某通道发送请求读取数据。
    3.Java NIO的选择器允许一个单独的线程来监视多个输入通道。
    4.传统IO是单向的流。NIO是双向的Channel管道,读写都是双向的。

    分散读取与聚集写入

    分散读取(scatter):将一个Channel数据分散读取到多个Buffer中
    聚集写入(gather):将多个缓冲区数据集中写入到一个通道中

    相关文章

      网友评论

          本文标题:阻塞非阻塞 同步异步 IO模型及其应用 NIO实现原理

          本文链接:https://www.haomeiwen.com/subject/bhryeqtx.html