zoukankan      html  css  js  c++  java
  • NIO浅谈

    NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

    那么NIO的本质是什么样的呢?它是怎样与事件模型结合来解放线程、提高系统吞吐的呢?

    1、阻塞与非阻塞

    阻塞与非阻塞是描述进程在访问某个资源时,数据是否准备就绪的的一种处理方式。当数据没有准备就绪时:

    • 阻塞:线程持续等待资源中数据准备完成,直到返回响应结果。
    • 非阻塞:线程直接返回结果,不会持续等待资源准备数据结束后才响应结果。

    2、同步与异步

    • 同步与异步是指访问数据的机制,同步一般指主动请求并等待IO操作完成的方式。
    • 异步则指主动请求数据后便可以继续处理其它任务,随后等待IO操作完毕的通知。

    老王烧开水:
    1、普通水壶煮水,站在旁边,主动的看水开了没有?同步的阻塞
    2、普通水壶煮水,去干点别的事,每过一段时间去看看水开了没有,水没开就走人。 同步非阻塞
    3、响水壶煮水,站在旁边,不会每过一段时间主动看水开了没有。如果水开了,水壶自动通知他。 异步阻塞
    4、响水壶煮水,去干点别的事,如果水开了,水壶自动通知他。异步非阻塞

    让我们先回忆一下传统的服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O)的经典编程模型:

    {
     ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
    
     ServerSocket serverSocket = new ServerSocket();
     serverSocket.bind(8088);
     while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
     Socket socket = serverSocket.accept();
     executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
    }
    
    class ConnectIOnHandler extends Thread{
        private Socket socket;
        public ConnectIOnHandler(Socket socket){
           this.socket = socket;
        }
        public void run(){
          while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
              String someThing = socket.read()....//读取数据
              if(someThing!=null){
                 ......//处理数据
                 socket.write()....//写数据
              }
    
          }
        }
    }

    这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情;这种模式的弊端也是显而易见的,一个连接就需要一个线程去处理,我们都知道线程是很贵的,所以BIO只适用于少量连接数的场景。

    基础概念

    1、传统BIO模型

    传统BIO是一种同步的阻塞IO,IO在进行读写时,该线程将被阻塞,线程无法进行其它操作。
    IO流在读取时,会阻塞。直到发生以下情况:1、有数据可以读取。2、数据读取完成。3、发生异常

    2、伪异步IO模型

    以传统BIO模型为基础,通过线程池的方式维护所有的IO线程,实现相对高效的线程开销及管理。

    3、NIO模型

    NIO(JDK1.4)模型是一种同步非阻塞IO,主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(多路复用器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(多路复用器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
    NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
    IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

    NIO三大核心示意图:

                                                                                                   

    NIO优点:

    1. 通过Channel注册到Selector上的状态来实现一种客户端与服务端的通信。
    2. Channel中数据的读取是通过Buffer , 一种非阻塞的读取方式。
    3. Selector 多路复用器 单线程模型, 线程的资源开销相对比较小。

    Channel(通道)

    传统IO操作对read()或write()方法的调用,可能会因为没有数据可读/可写而阻塞,直到有数据响应。也就是说读写数据的IO调用,可能会无限期的阻塞等待,效率依赖网络传输的速度。最重要的是在调用一个方法前,无法知道是否会被阻塞。

    NIO的Channel抽象了一个重要特征就是可以通过配置它的阻塞行为,来实现非阻塞式的通道。

    Channel是一个双向通道,与传统IO操作只允许单向的读写不同的是,NIO的Channel允许在一个通道上进行读和写的操作。

    FileChannel:文件

    SocketChannel:

    ServerSocketChannel:

    DatagramChannel: UDP

    Buffer(缓冲区)

    Bufer顾名思义,它是一个缓冲区,实际上是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。


     
    Buffer(缓冲区)

    Buffer缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该模块内存。为了理解Buffer的工作原理,需要熟悉它的三个属性:capacity、position和limit。

    position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。见下图:


     
    capacity、position和limit
    • capacity:作为一个内存块,Buffer有固定的大小值,也叫作“capacity”,只能往其中写入capacity个byte、long、char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清楚数据)才能继续写数据。
    • position:当你写数据到Buffer中时,position表示当前的位置。出事的position值为0,当写入一个字节数据到Buffer中后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1。当读取数据时,也是从某个特定位置读,讲Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取一个字节数据后,position向前移动到下一个可读的位置。
    • limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

    Buffer的分配:

    对Buffer对象的操作必须首先进行分配,Buffer提供一个allocate(int capacity)方法分配一个指定字节大小的对象。
    向Buffer中写数据:写数据到Buffer中有两种方式:
    1.从channel写到Buffer

    int bytes = channel.read(buf); //将channel中的数据读取到buf中
    

    2.通过Buffer的put()方法写到Buffer

    buf.put(byte); //将数据通过put()方法写入到buf中
    
    • flip()方法:将Buffer从写模式切换到读模式,调用flip()方法会将position设置为0,并将limit设置为之前的position的值。
      从Buffer中读数据:从Buffer中读数据有两种方式:
      1.从Buffer读取数据到Channel
    int bytes = channel.write(buf); //将buf中的数据读取到channel中
    

    2.通过Buffer的get()方法读取数据

    byte bt = buf.get(); //从buf中读取一个byte
    
    • rewind()方法:Buffer.rewind()方法将position设置为0,使得可以重读Buffer中的所有数据,limit保持不变。
    • clear()与compact()方法:一旦读完Buffer中的数据,需要让Buffer准备好再次被写入,可以通过clear()或compact()方法完成。如果调用的是clear()方法,position将被设置为0,limit设置为capacity的值。但是Buffer并未被清空,只是通过这些标记告诉我们可以从哪里开始往Buffer中写入多少数据。如果Buffer中还有一些未读的数据,调用clear()方法将被"遗忘 "。compact()方法将所有未读的数据拷贝到Buffer起始处,然后将position设置到最后一个未读元素的后面,limit属性依然设置为capacity。可以使得Buffer中的未读数据还可以在后续中被使用。
    • mark()与reset()方法:通过调用Buffer.mark()方法可以标记一个特定的position,之后可以通过调用Buffer.reset()恢复到这个position上。

    Selector(多路复用器)

    1. Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)。
    2. Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。【示意图】
    3. 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
    4. 避免了多线程之间的上下文切换导致的开销。

    Selector 示意图和特点说明

                                                          

    说明如下:

    1. Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
    2. 当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
    3. 线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
    4. 由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
    5. 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
    下面我们从代码看看NIO是怎么使用的:

    /**
     * 带多路复用器
     */
    public class NioDemo1 {
    
    
        public static void main(String[] args) throws IOException {
            //创建NIOServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //serverSocketChannel绑定9000端口
            serverSocketChannel.socket().bind(new InetSocketAddress(9000));
            //设置serverSocketChannel为非阻塞
            serverSocketChannel.configureBlocking(false);
            System.out.println("服务启动成功!");
            //打开Selector处理Channel
            Selector selector = Selector.open();
            //serverSocketChannel注册到selector上,并且selector对客户端的ACCEPT操作感兴趣
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                //阻塞等待事件发生
                selector.select();
                //获取selector中注册的所有ServerSocketChannel的SelectionKey
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    //如果key是OP_ACCEPT事件,则进行连接获取和事件注册
                    if (key.isAcceptable()) {
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = channel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ, SelectionKey.OP_WRITE);
                        System.out.println("客户端连接成功!");
                    } else if (key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int read = socketChannel.read(byteBuffer);
                        if (read > 0) {
                            System.out.println("发消息的地址是:" + socketChannel.getRemoteAddress());
                            System.out.println("接收到的消息是:" + new String(byteBuffer.array()));
                        } else if (read == -1) {
                            socketChannel.close();
                            System.out.println("客户端断开消息");
                        }
                    }
                    //从事件集合里删除本次处理的key,防止下次selector重复处理
                    iterator.remove();
                }
            }
    
    
        }
    }

    下面看一下NIO中都使用了些什么:

    1.java NIO使用的是 ServerSocketChannel 相当于IO的ServerSoket, SocketChannel相当于IO的socket;

    2.serverSocketChannel.configureBlocking(false);将管道设置为非阻塞模式,这个很重要;

    3.Selector 多路复用器,这是NIO三大核心组件之一,这里我们追述源码的话会发现,Selector是调用了Linux操作系统的Epoll模型;

    4.selector = Selector.open();底层是调用了Linux操作系统的Epoll的Epoll_create方法,创建了一个Epoll实例;

    5.serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);底层是调用了Linux操作系统的Epoll的Epoll_ctl方法,向Epoll实例注册了一个channel;

    6.selector.select();底层是调用了Linux操作系统的Epoll的Epoll_wait方法,等待事件的发生;

    7.Set<SelectionKey> selectionKeys = selector.selectedKeys();SelectionKey,表示 Selector 和网络通道的注册关系,此方法就是获取Epoll实例中注册的所有发生事件的通道的集合。




  • 相关阅读:
    【尺取】HDU Problem Killer
    【尺取或dp】codeforces C. An impassioned circulation of affection
    【搜索】codeforces C. The Tag Game
    【数学】codeforces B. The Golden Age
    【dfs+理解题意+构造】【待重做】codeforces E. Ice cream coloring
    【贪心】codeforces D. Minimum number of steps
    【数学】codeforces A. Success Rate
    【二分+交互】codeforces B. Glad to see you!
    【组合 数学】codeforces C. Do you want a date?
    JavaScript实现快速排序
  • 原文地址:https://www.cnblogs.com/wiliamzhao/p/14779651.html
Copyright © 2011-2022 走看看