1、IO基础原理&Java NIO原理&Reactor反应器模式

业务高并发离不开,Netty\Redis\Zookeeper等分布式高性能工具,涉及到高并发肯定离不开频繁的IO读写操作,IO底层原理是这些高并发工具的一个基石;

IO的底层原理

当我们聊IO底层原理的时候我们应该想到哪些关键问题节点?

  • 用户进程进行IO读写,对应操作系统内核(kernel)底层的IO的read/write必然有区分,是否是物理设备直接的读写?(必然不是)?
  • 用户进程、系统内核 进行read、write操作是缓冲区起到了什么作用?
  • 4种IO模型的流程?(同步阻塞、同步非阻塞、IO多路复用、异步IO)
  • IO阻塞过程,cup执行的内核态用户态是如何进行切换的?(参考:CPU用户态和内核态)
  • Java NIO的IO模型(io多路复用)?三个组件(channel、buffer、selector)?NIO在网络通信中的应用?
  • Reactor反应器主要是用来解决什问题而产生的(Java OIO 网络通信阻塞式的等待连接,进而引入的一连接一线程,但资源消耗严重)?该模型如何应用到Netty框架的?

基于这些问题go on;

内核缓存区和进程缓冲区

首先我们应该明白一点就是在进行一次read/write(后续rw简写)系统调用或socket套接字调用,并不是直接进行物理设备和内存之间的数据置换;他们都是通过一层缓冲区进行数据置换的;

  • 调用操作系统的read,是把数据从系统内核缓冲区复制到进程缓冲区;而w调用是把数据从进程缓冲区复制到内核缓存区;

  • 调用socket套接字的r,是把数据从系统内核缓冲区复制到网卡(这里我理解网卡类似用户进程缓冲区);而w调用是把数据从网卡复制到内核缓存区;
  • 无论是socket的IO,还是文件的IO都是上层应用的开发,他们属于输入Input和输出Output,在编程流程上都是一致的;

为何要设置这么多的缓存区呢?

缓存区的目的是为了减少频繁的与设备之间的物理交换,我们清楚外部物理设备的直接读写,涉及到操作系统的中断。发生中断时要保留进程的数据和状态信息,而结束中断后要恢复。为了减少这种中断带来底层系统的时间和性能损耗,于是出现了内存缓冲区;

有了内核缓冲区,操作系统会对内核缓存区进行监控,等待缓存区数据达到一定大小时,才进行IO设备的中断处理,集中执行物理设备的IO操作,这种机制提升系统性能;用户进程缓存区,则是将用户进程的IO操作集中处理;用户进程的IO操作实际上是用户进程缓存区和内核缓冲区之间的数据置换;

系统调用read&write流程

read一个socket套接字的流程如下:

  • 客户端请求:Linux通过网卡读取客户端的请求数据,将数据读取到内核缓存区
  • 获取数据请求:java服务器通过read调用,从linux内核缓冲区读取数据,再送入到java进程缓冲区。
  • 服务器端业务处理:java服务器在自己的用户空间处理客户端的请求。
  • 服务器端返回数据:java服务器完成处理后,通过系统write调用,将数据从系统内核缓冲区写入到用户缓冲区。
  • 发送数据给客户端:系统内核通过网络IO,将内核缓冲区数据写入到网卡,网卡通过底层通信协议将数据发送给目标客户端;

四种主流的IO通信模型

同步阻塞IO(Blocking IO)

优点:程序开发简单,阻塞等待数据期间,用户线程挂起;阻塞期间,用户线程基本不会占用CPU;

缺点:每个连接会开启一个独立线程,维护一个连接的IO;并发量高的情况下,维护大量的网络连接,内存、线程切换开销巨大;不适合高并发。

同步非租塞IO(None Blocking IO)

优点:每次发起IO系统调用,在内核等待数据过程中可以立即返回。用户线程并不会阻塞,实时性好;

缺点:用户进程要想拿到数据,需要不断轮询,这将占用大量cpu;这里的同步非租塞IO不是java的NIO(NIO采用的IO多路复用);

IO多路复用(IO Multiplexing)

查询IO数据就绪状态的轮询过程交给了select/epoll调用完成,通过这种调用方式,一个进程可以监视多个文件描述符(文件句柄),一旦某个文件描述符数据准备就绪(内核缓存区可读、写),内核就将就绪的状态返回给用户进程,随后用户程序进行就绪状态数据的IO rw系统调用;与NIO模型相比,IO多路复用模型涉及两种系统调用,一种是select/epoll(就绪查询),另一种是IO操作;NIO只有IO操作;

流程:

  • 选择器注册:read操作的目标socket网络连接,提前注册到select/epoll选择器中,Java对应Selector类。
  • 就绪状态轮询:用户进程通过调用选择器的查询方法,查询注册过的所有socket连接的就绪状态。查询方法内核会返回一个socket的就绪列表;当任何一个注册过的socket中的数据准备好了,内核缓存区有数据了,内核将就绪socket加入到就绪列表返回;调用select查询是整个线程是阻塞的;
  • 用户线程就绪socket列表,进行socket的read系统调用,用户线程阻塞,内核复制数据到用户缓存区;
  • 用户线程解除阻塞,执行后续流程;

优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接。系统不必创建大量的线程,也不必维护这些线程,大大减小系统开销;

缺点:本质上select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,这个读写过程是阻塞的。

异步IO(Asynchronous IO)

用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(从网卡读取数据到内核缓冲区、数据复制到用户缓冲区)完成后,通知用户程序,用户程序执行后续业务操作;内核的整个IO操作过程中用户程序不阻塞;

优点:在内核等待数据和复制两个阶段,用户线程都不阻塞。用户线程接收内核IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。吞吐量最大;

缺点:应用程序需要进行事件注册和接收,其余工作交给系统内核,底层系统内核需要支持;Linux 2.6版本才引入异步IO,目前不完善。

Java NIO 通信详解

        之前就说了java NIO是基础IO多路复用模型进行通信的,New IO也有人称为非阻塞IO(Non-Block IO),弥补了老式IO(OIO)面向流阻塞的不足,它是面向缓冲区的;Java NIO被应用到文件读写和网络通信中;

提供了三个核心组件:

  • Channel(通道):同一个网络连接关联的两个流的结合体,既可以从通道读取,也可以向通道写入;4种channel(FileChannel(不能设置非租塞模式),SocketChannel.ServerSocketChannel,DataGrameChannel)
  • Buffer(缓冲区):读取和写入只需要从通道中将数据rw到缓冲区,可以随便一读取buffer的任何位置;8种数据类型(ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedBuffer),3个重要属性(capacity\position\limit);
  • Selector(选择器):实现IO多路复用的关键,首先把通道注册到选择器中,选择器提供select方法进行select/epoll的系统调用,查询注册的通道是否有就绪的IO事件(选择键)(可读、可写、网络连接完成);

与Java OIO相比,java NIO网络通信编程特点大致如下:

(1)、在NIO中,服务器接收新连接的工作,是异步进行的。不像java的OIO那样,服务器监听连接是同步、阻塞的。NIO可以通过选择器(多路复用器)查看IO事件的就绪状态,后续只需不断进行轮询选择器中的选择键集合,选择新到来的连接;

(2)、NIO中SocketChannel通道的读写操作都是异步的。如果没有可读写的数据,负责IO的线程不会阻塞同步等待,线程可以处理其他的连接通道;

(3)、NIO中选择器可以同时处理成千上万个客户端连接,性能会随着客户端的增加而线性下降;

Reactor反应器模式

现状:高并发通信中间件Netty、Nginx、Redis等都是基于Reactor反应器模式实现的;

背景:在Reactor反应器模式出现之前;Java 原始的OIO编程,网络服务器程序,是通过一个while循环,不断的监听端口是否有新的连接。如果有才调用处理函数处理。示例代码

while(true){
    socket=accept(); //阻塞,接收连接
    hadler(socket);  //读取数据,业务处理,写入结果
}

这种方法问题在一个连接handler(socket) 没有处理完,那么后面的连接请求没法被接收,于是后面的请求统统会被阻塞住,服务器的吞吐量就太低了。

进而引入了Connection Per Thread(一线程处理一连接)模式。代码逻辑如下:

class ConnectionPerThread implements Runnable{
    public void run(){
        try{
            ServerSocket serverSocket=new ServerSocket(SOCKET_SERVER_PORT);   
            while(!Thread.interrupted()){
                Socket socket = serverSocket.accept();
                //接收一个连接后,为socket连接,新建一个专属的处理器对象
                Handler handler=new Handler(socket);
                //创建新线程专门处理这个连接
                new Thread(handler).start();
            } 
        }catch(Exception e){
            //异常处理
        }
    }
}

static class Handler implements Runnable{
    final Socket socket;
    Handler(Socket s){
       this.socket=s;
    }
    while(true){
        try{
            byte[] input=new byte[1024];
            //读取数据源
            socket.getInputStream().read(input);
            //业务逻辑处理
            ....
            byte[] output=null;
            //写入结果
            socket.getOutputStream().write(outPut);
        }catch(Exception e){
            //异常处理
        }
    }
}

解决了前面新连接被严重阻塞的问题,在一定程度上,极大提高了服务器的吞吐量;但是对于成千上万的连接,需要耗费大量线程资源(创建、销毁、线程切换代价太高);所以引入了Reactor反应器模式,对线程数据量进行控制,做到一个线程可以处理大量的连接;

反应器模式Reactor由反应器线程、Handler处理器两大角色组成:

  • Reactor反应器职责:负责查询IO事件,就是检查NIO中注册到选择器上的IO事件(读、写、连接建立、连接接受),并分发到Handlers处理器。
  • Handlers处理器的职责:与IO事件(或者叫选择键)绑定,非阻塞的处理IO事件,执行真正建立连接、通道数据读取、业务处理逻辑、写入到通道等。

单线程的Reactor反应器模式

Reactor选择器需要用到SelectionKey提供的两个方法:

  • 一个void attach(Object o) 方法,将处理器作为附件绑定到选择键(或者叫做事件)上;
  • 另一个方法Object attachment() ,可以从选择键上获取到绑定handler处理器;

通信服务端的Reactor反应器和处理器在同一个线程中:

单线程Reactor模型

public class Reactor implements Runnable {  
        private final ServerSocketChannel ssc;  
        private final Selector selector;  
      
        public TCPReactor(int port) throws IOException {  
            selector = Selector.open();  
            ssc = ServerSocketChannel.open();  
            InetSocketAddress addr = new InetSocketAddress(port);  
            // 在ServerSocketChannel绑定监听端口
            ssc.socket().bind(addr); 
            // 设置ServerSocketChannel为非阻塞  
            ssc.configureBlocking(false); 
            // ServerSocketChannel向selector注册一个OP_ACCEPT事件,然后返回通道的key 
            SelectionKey sk = ssc.register(selector, SelectionKey.OP_ACCEPT);
            // 核心1:给选择key绑定一个附加的Acceptor处理器    
            sk.attach(new Acceptor(selector, ssc));
        }  
      
        @Override  
        public void run() {  
            // 在线程被中断前持续运行  
            while (!Thread.interrupted()) {
                System.out.println("Waiting for new event on port:"+ssc.socket().getLocalPort() + "...");  
                try {  
                    // 若沒有事件就就绪则不往下执行 
                    if (selector.select() == 0) 
                        continue;  
                } catch (IOException e) {  
                    // TODO Auto-generated catch block  
                    e.printStackTrace();  
                }  
                // 取得所有已就绪事件的key集合  
                Set<SelectionKey> selectedKeys = selector.selectedKeys(); 
                Iterator<SelectionKey> it = selectedKeys.iterator();  
                while (it.hasNext()) {  
                    // 根據事件的key進行調度  
                    dispatch((SelectionKey) (it.next()));
                    // 处理完后从就绪事件列表中移除
                    it.remove();  
                }  
            }  
        }  
      
        /* 
         * name: dispatch(SelectionKey key) 
         * description: 調度方法,根據事件绑定的对象新开线程 
         */  
        private void dispatch(SelectionKey key) {  
            // 核心2:根据事件之key绑定的对象新开线程
            Runnable r = (Runnable) (key.attachment());   
            if (r != null)  
                r.run();  
       }

       
       class AcceptorHandler implements Runnable {  
      
            private final ServerSocketChannel ssc;  
            private final Selector selector;  
            // 核心3:使用同一个选择器注册其他事件
            public Acceptor(Selector selector, ServerSocketChannel ssc) {  
                this.ssc=ssc;  
                this.selector=selector;  
            }  
          
            @Override  
            public void run() {  
                try { 
                    // 接受client连接请求  
                    SocketChannel sc= ssc.accept();  
                 System.out.println(sc.socket().getRemoteSocketAddress().toString() + " is connected.");  
                  
                if(sc!=null) {  
                    // 设置为非阻塞  
                    sc.configureBlocking(false);
                    // SocketChannel向selector注册一个OP_READ事件,然后返回该通道的key
                    SelectionKey sk = sc.register(selector, SelectionKey.OP_READ);   
                    // 使一个阻塞住的selector操作立即返回
                    selector.wakeup();  
                    // 给定key一个附加的IOHandler处理象  
                    sk.attach(new IOHandler(sk, sc));
                }  
                  
            } catch (IOException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
            }  
        }  
    }



    class IOHandler implements Runnable {  
      
        private final SelectionKey sk;  
        private final SocketChannel sc;  
      
        int state;   
      
        public TCPHandler(SelectionKey sk, SocketChannel sc) {  
            this.sk = sk;  
            this.sc = sc;  
            state = 0; // 初始狀態設定為READING  
        }  
      
        @Override  
        public void run() {  
            try {  
                if (state == 0)  
                    read(); // 讀取網絡數據  
                else  
                    send(); // 發送網絡數據  
      
            } catch (IOException e) {  
                System.out.println("[Warning!] A client has been closed.");  
                closeChannel();  
            }  
        }  
          
        private void closeChannel() {  
            try {  
                sk.cancel();  
                sc.close();  
            } catch (IOException e1) {  
                e1.printStackTrace();  
            }  
        }  
      
        private synchronized void read() throws IOException {  
            // non-blocking下不可用Readers,因為Readers不支援non-blocking  
            byte[] arr = new byte[1024];  
            ByteBuffer buf = ByteBuffer.wrap(arr);  
              
            int numBytes = sc.read(buf); // 讀取字符串  
            if(numBytes == -1)  
            {  
                System.out.println("[Warning!] A client has been closed.");  
                closeChannel();  
                return;  
            }  
            String str = new String(arr); // 將讀取到的byte內容轉為字符串型態  
            if ((str != null) && !str.equals(" ")) {  
                process(str); // 邏輯處理  
                System.out.println(sc.socket().getRemoteSocketAddress().toString()  
                        + " > " + str);  
                state = 1; // 改變狀態  
                sk.interestOps(SelectionKey.OP_WRITE); // 通過key改變通道註冊的事件  
                sk.selector().wakeup(); // 使一個阻塞住的selector操作立即返回  
            }  
        }  
      
        private void send() throws IOException  {  
            // get message from message queue  
              
            String str = "Your message has sent to "  
                    + sc.socket().getLocalSocketAddress().toString() + "\r\n";  
            ByteBuffer buf = ByteBuffer.wrap(str.getBytes()); // wrap自動把buf的position設為0,所以不需要再flip()  
      
            while (buf.hasRemaining()) {  
                sc.write(buf); // 回傳給client回應字符串,發送buf的position位置 到limit位置為止之間的內容  
            }  
              
            state = 0; // 改變狀態  
            sk.interestOps(SelectionKey.OP_READ); // 通過key改變通道註冊的事件  
            sk.selector().wakeup(); // 使一個阻塞住的selector操作立即返回  
        }  
          
        void process(String str) {  
            // do process(decode, logically process, encode)..  
            // ..  
        }  
    }
 
}

通信服务端示例代码的关键:

  • Reactor反应器建立完serverSocket管道后,初始注册一个OP_ACCEPT事件(选择键)到,Selector选择器中;并为该事件(选择键)绑定一个连接建立AccptorHandler处理器;
  • Reactor的服务线程,会循环不断通过Selector选择器的select调用轮询就绪事件;当有就绪事件(选择键)时分发给相应的事件处理器;初始建立连接后会轮询到一个OP_ACCEPT就绪键,分发给AccetorHandler处理器;
  • AccetorHandler处理器的作用两个,一个是接受新连接SocketChannel;另一个改变选择键的类型为OP_READ,并创建一个输入输出的IOHandler处理器,作为附件绑定到OP_READ选择键上,AccetorHandler处理器和Reactor反应器使用同一个选择器;
  • IOHandler,负责数据输入、业务处理、结果输出;业务处理后,改变interestOps改变注册事件为OP_WTRIT,并唤醒阻塞事件(OP_READ);
  • Reactor反应器,selector选择器轮询获取到OP_WRITE事件后,会在分发时获取对应的IOHandler处理器;进行结果返回处理;

优点:相对于传统的一连接一线程,是基于Java NIO实现,反应器模式不用再启动成千上万的线程了。

缺点:当其中某个handler处理器阻塞时,也会导致其他所有handler无法执行;handler不仅仅负责输入、输出业务处理,还包括建             立连接监听的AcceptorHandler处理器,问题很严重;其次也不能充分利用多核心资源;

多线程的Reactor反应器模式

多线程池的Reactor反应器演进在两个方面:

  • 首先升级Handler处理器,既要使用多线程,又要高效率,则可以考虑使用线程池。
  • 其次升级Reactor反应器,考虑引入多个选择器,替身选择大量ServerSocketChannel通道的能力;

总体设计思路如下:

  • 负责输入输出处理的IOHandler处理器的执行,放入独立的线程池中,这样业务处理线程和负责服务监听和IO事件查询的反应器线程相互隔离,避免服务器连接监听收到阻塞。
  • 如果服务器是多核的CPU,可以将反应器线程拆分为多个自反应器(SubReactor)线程;同时引入多个选择器,每个SubReactor子线程负责一个选择器。这样充分释放了系统资源的能力,也提高了反应器管理大量连接,提升选择大量通道的能力;

多线程反应器模式

实例代码参考https://www.cnblogs.com/crazymakercircle/p/9833847.html 5.3. 多线程Reactor的参考代码

发布了42 篇原创文章 · 获赞 6 · 访问量 2630

猜你喜欢

转载自blog.csdn.net/a1290123825/article/details/104719971
今日推荐