IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> BIO、NIO、AIO -> 正文阅读

[系统运维]BIO、NIO、AIO

同步 VS 异步

同步和异步概念以 “调用者的行为方式” 做区分

同步

调用者发起一个调用后,主动等待被调用者返回的结果。

异步

调用者发起一个调用后,被动接收调用者结果。
回调函数,状态,消息等方式通知调用者。

阻塞 VS 非阻塞

调用者发起一个调用后,在被调用者处理期间,调用者的状态来区分

阻塞

调用者什么都不干,一直等待结果

非阻塞

在被调用者处理期间,调用者可以干其他事情

同步阻塞 BIO

调用者发起一个调用后,调用者其他什么都不干, 直到被调用者的结果返回为止。

BIO(Block-IO)是一种阻塞同步的通信模式。常说的Socket IO 一般指的是BIO。是一个比较传统的通信方式,模式简单,使用方便。但并发处理能力低,通信耗时,依赖网速。

设计原理

服务器通过一个Acceptor线程负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理。典型的一请求一应答模式。若客户端数量增多,频繁地创建和销毁线程会给服务器打开很大的压力。而且操作系统允许的线程数量是有限的,每当有一个请求过来,都会创建新的线程,当线程数达到一定数量,占满了整台机器的资源,那么机器就挂掉了。对于CPU来说也是一个不好的事情,因为会导致频繁的切换上下文。

image

后改良为用线程池的方式代替新增线程,被称为伪异步IO。但是还是有上面的一些问题,仅仅是解决了频繁创建线程带来额外开销的问题。不过由于是同步,如果读写速度慢,那么每个线程进来是会导致阻塞的,性能的高低完全取决于阻塞的时间。这个对于用户的体验也是相当不好的。

image

服务器提供IP地址和监听的端口,客户端通过TCP的三次握手与服务器连接,连接成功后,双方才能通过套接字(Stock)通信。
小结:BIO模型中通过Socket和ServerSocket完成套接字通道的实现。每一个线程在处理请求时(accept,read,write)都是阻塞,同步,建立连接耗时。

具体实现

BIO服务器代码,负责启动服务,阻塞服务,监听客户端请求,新建线程处理任务。

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
 * IO 也称为 BIO,Block IO 阻塞同步的通讯方式
 * 比较传统的技术,实际开发中基本上用Netty或者是AIO。熟悉BIO,NIO,体会其中变化的过程。作为一个web开发人员,stock通讯面试经常问题。
 * BIO最大的问题是:阻塞,同步。
 * BIO通讯方式很依赖于网络,若网速不好,阻塞时间会很长。每次请求都由程序执行并返回,这是同步的缺陷。
 * BIO工作流程:
 * 第一步:server端服务器启动
 * 第二步:server端服务器阻塞监听client请求
 * 第三步:server端服务器接收请求,创建线程实现任务
 */
public class ITDragonBIOServer {

    private static final Integer PORT = 8888; // 服务器对外的端口号  
    public static void main(String[] args) {  
        ServerSocket server = null;  
        Socket socket = null;  
        ThreadPoolExecutor executor = null;  
        try {  
            server = new ServerSocket(PORT); // ServerSocket 启动监听端口  
            System.out.println("BIO Server 服务器启动.........");  
            /*--------------传统的新增线程处理----------------*/
            /*while (true) { 
                // 服务器监听:阻塞,等待Client请求 
                socket = server.accept(); 
                System.out.println("server 服务器确认请求 : " + socket); 
                // 服务器连接确认:确认Client请求后,创建线程执行任务  。很明显的问题,若每接收一次请求就要创建一个线程,显然是不合理的。
                new Thread(new ITDragonBIOServerHandler(socket)).start(); 
            } */
            /*--------------通过线程池处理缓解高并发给程序带来的压力(伪异步IO编程)----------------*/  
            executor = new ThreadPoolExecutor(10, 100, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(50));  
            while (true) {  
                socket = server.accept();  // 服务器监听:阻塞,等待Client请求 
                ITDragonBIOServerHandler serverHandler = new ITDragonBIOServerHandler(socket);  
                executor.execute(serverHandler);  
            }  
        } catch (IOException e) {  
            e.printStackTrace();  
        } finally {  
            try {  
                if (null != socket) {  
                  socket.close(); 
                  socket = null;
                }  
                if (null != server) {  
                    server.close();  
                    server = null;  
                    System.out.println("BIO Server 服务器关闭了!!!!");  
                }  
                executor.shutdown();  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}

BIO服务端处理任务代码,负责处理Stock套接字,返回套接字给客户端,解耦。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import com.itdragon.util.CalculatorUtil;

public class ITDragonBIOServerHandler implements Runnable{  

  private Socket socket;  
  public ITDragonBIOServerHandler(Socket socket) {  
      this.socket = socket;  
  }  
  @Override  
  public void run() {  
      BufferedReader reader = null;  
      PrintWriter writer = null;  
      try {  
          reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));  
          writer = new PrintWriter(this.socket.getOutputStream(), true);  
          String body = null;  
          while (true) {  
              body = reader.readLine(); // 若客户端用的是 writer.print() 传值,那readerLine() 是不能获取值,细节  
              if (null == body) {  
                  break;  
              }  
              System.out.println("server服务端接收参数 : " + body);  
              writer.println(body + " = " + CalculatorUtil.cal(body).toString());
          }  
      } catch (IOException e) {  
          e.printStackTrace();  
      } finally {  
          if (null != writer) {  
              writer.close();  
          }  
          try {  
              if (null != reader) {  
                  reader.close();  
              }  
              if (null != this.socket) {  
                  this.socket.close();  
                  this.socket = null;  
              }  
          } catch (IOException e) {  
              e.printStackTrace();  
          }  
      }  
  }  
}

BIO客户端代码,负责启动客户端,向服务器发送请求,接收服务器返回的Stock套接字。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Random;
/**
 * BIO 客户端
 * Socket :         向服务端发送连接
 * PrintWriter :    向服务端传递参数
 * BufferedReader : 从服务端接收参数
 */
public class ITDragonBIOClient {

    private static Integer PORT = 8888;  
    private static String IP_ADDRESS = "127.0.0.1";  
    public static void main(String[] args) {  
        for (int i = 0; i < 10; i++) {  
            clientReq(i);  
        }  
    }  
    private static void clientReq(int i) {  
        Socket socket = null;  
        BufferedReader reader = null;  
        PrintWriter writer = null;  
        try {  
            socket = new Socket(IP_ADDRESS, PORT); // Socket 发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信  
            reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 获取返回内容  
            writer = new PrintWriter(socket.getOutputStream(), true);  
            String []operators = {"+","-","*","/"};
            Random random = new Random(System.currentTimeMillis());  
            String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
            writer.println(expression); // 向服务器端发送数据  
            System.out.println(i + " 客户端打印返回数据 : " + reader.readLine());  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            try {  
                if (null != reader) {  
                    reader.close();  
                }  
                if (null != socket) {  
                    socket.close();  
                    socket = null;  
                }  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
}

同步非阻塞 NIO

调用者发起一个调用后,调用者转而可以去干其他事,只是需要时不时轮询被调用者获取结果。

NIO(New IO or Non-Block IO)是一种非阻塞同步的通信模式。即 同步是指线程不断轮询 IO 事件是否就绪(主动获取),非阻塞是指线程在等待 IO 的时候,可以同时做其他任务。

设计原理

NIO 相对于BIO来说一大进步。客户端和服务器之间通过Channel通信。NIO可以在Channel进行读写操作。这些Channel都会被注册在Selector多路复用器上。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。
NIO 通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。

同步的核心就是 Selector,Selector 代替了线程本身轮询 IO 事件,避免了阻塞同时减少了不必要的线程消耗;

非阻塞的核心就是通道和缓冲区,当 IO 事件就绪时,可以通过读写缓冲区,保证 IO 的成功,而无需线程阻塞式地等待。

  1. 缓冲区Buffer:它是NIO与BIO的一个重要区别。BIO是将数据直接写入或读取到Stream对象中。而NIO的数据操作都是在缓冲区中进行的。缓冲区实际上是一个数组。Buffer最常见的类型是ByteBuffer,另外还有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。

  2. 通道Channel:和流不同,通道是双向的。NIO可以通过Channel进行数据的读,写和同时读写操作。

    通道分为两大类:一类是网络读写(SelectableChannel),一类是用于文件操作(FileChannel),SocketChannel和ServerSocketChannel都是SelectableChannel的子类。

  3. 多路复用器Selector:NIO编程的基础。多路复用器提供选择已经就绪的任务的能力。就是Selector会不断地轮询注册在其上的通道(Channel),如果某个通道处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。服务器端只要提供一个线程负责Selector的轮询,就可以接入成千上万个客户端,这就是JDK NIO库的巨大进步。

NIO + 单线程Reactor模式

Reactor设计模式是 event-driven architecture (事件驱动架构)的一种实现方式,处理多个客户端并发的向服务端请求服务的场景。每种服务在服务端可能由多个方法组成。Reactor会解耦并发请求的服务并分发给对应的事件处理器来处理。

image

Reactor主要由以下几个角色构成:Handle、Synchronous Event Demultiplexer、Initiation Dispatcher、Event Handler、Concrete Event Handler

  • Handle:Handle在linux中一般称为文件描述符,在window称为句柄,两者的含义一样。Handle是事件的发源地。比如一个网络socket、磁盘文件等。而发生在Handle上的事件可以有connection、ready for read、ready for write等。

  • Synchronous Event Demultiplexer(同步事件分离器):本质上是系统调用,感知并获取事件发生的Handle并通知Initiation Dispatcher。同步事件分离器指的是常用的IO多路复用,比如select、poll、epoll。

    select方法会一直阻塞直到Handle上有事件发生时才会返回。select会轮询所有注册到系统内核的Socket连接,一旦发现某个Socket连接数据准备完毕,select就会返回。用户线程获得了目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。

    poll类似与select,只是poll上注册的socket列表可以无限制,因为他是通过链表实现的。

    epoll系统调用则是对每个注册的Socket连接加入一个回调函数,一旦Socket连接数据准备完毕,Socket连接执行回调函数,然后将该fd放入到就绪链表中。用户线程可以通过访问就绪链表获得准备数据的Socket,然后发起read系统调用获得数据。

    在Java NIO领域中,同步事件分离器对应的组件就是selector,阻塞方法就是select。

  • Event Handler(事件处理器):定义一些回调方法或者称为钩子函数。当Handle上有事件发生时,回调方法便会执行,一种事件处理机制。Java NIO中没有对应的相关类,由开发者自行开发。

  • Concrete Event Handler(具体的事件处理器):实现了Event Handler。在回调方法中会实现具体的业务逻辑。

  • Initiation Dispatcher(初始分发器):也是Reactor角色,提供了注册、删除与转发Event handler的方法。当Synchronous Event Demultiplexer检测到Handle上有事件发生时,便会通知Initiation dispatcher调用特定的Event handler的回调方法。

处理流程:

  1. 当应用向Initiation Dispatcher注册Concrete Event Handler时,应用会标识出该事件处理器希望Initiation Dispatcher在某种类型的事件发生时向其通知,事件与Handle关联;
  2. Initiation Dispatcher要求注册在其上面的Concrete Event Handler传递内部关联的Handle,该Handle会向操作系统标识;
  3. 当所有的Concrete Event Handler都注册到 Initiation Dispatcher上后,应用会调用handle_events方法来启动Initiation Dispatcher的事件循环,这时Initiation Dispatcher会将每个Concrete Event Handler关联的handle合并,并使用Synchronous Event Demultiplexer来等待这些Handle上事件的发生;
  4. 当与某个事件源对应的Handle变为ready时,Synchronous Event Demultiplexer便会通知 Initiation Dispatcher。比如tcp的socket变为ready for read;
  5. Initiation Dispatcher会触发事件处理器的回调方法。当事件发生时, Initiation Dispatcher会将被一个“key”(表示一个激活的Handle)定位和分发给特定的Event Handler的回调方法;
  6. Initiation Dispatcher调用特定的Concrete Event Handler的回调方法来响应其关联的handle上发生的事件。

这种模型情况下,由于 Reactor 是单线程的,既要接受请求,还要去处理事件,如果某一些事件处理请求花费的时间比较长,那么这个请求将会进入等待,整个情况下会同步。基于这种问题下有什么改进措施呢?

NIO + 多线程Reactor模式

可以使用多线程去处理,使用线程池,让Reactor仅仅去接受请求,把事件的处理交给线程池中的线程去处理:

image

将处理器的执行放入线程池,多线程进行业务处理。但Reactor仍为单个线程,无法去并行的去响应多个客户端,那么要怎么处理呢?

NIO + 主从多线程Reactor模式

image

mainReactor负责监听连接(可能包括3次握手,安全接入认证),accept连接给subReactor处理(读写及编解码),具体业务处理由subReactor交给业务线程池处理,为什么要单独分一个Reactor来处理监听呢?因为像TCP这样需要经过3次握手才能建立连接,这个建立连接的过程也是要耗时间和资源的,单独分一个Reactor来处理,可以提高性能。


select、poll、epoll

select、poll、epoll这3个命令是linux中实现IO多路复用(IO multiplexing,也称事件驱动型IO event driven IO)的系统调用。

select、 poll 及 epoll实现的均是同步非阻塞IO。

select

select仅仅知道有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。select创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。

select有以下缺点:

  1. 文件描述符个数受限:单进程能够监控的文件描述符的数量存在最大限制,在Linux上32/64位系统一般为1024/2048,可以通过修改宏定义__FD_SETSIZE增大上限,但同样存在效率低的弱势;
  2. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

poll

poll本质上和select没有区别,依然存在select的2,3两个问题,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它解决了select的第1个问题,没有最大连接数的限制,原因是它是基于链表来存储的。

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

epoll

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时对这些流的操作都是有意义的(复杂度降低到了O(1))。

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

image

epoll第3步就绪队列的返回,文件描述符列表是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。

可以看到select、poll、epoll_wait都需要用户进程主动查询就绪的文件描述符列表,并不是回调等模式(虽然epoll的就绪列表更新是回调的,但是用户进程获取还是epoll_wait查询),因此是同步的,3者都提供了超时时间timeout=0时立即返回不阻塞的模式,因此都是同步非阻塞IO。

注意:虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

异步非阻塞 AIO

调用者发起一个调用后,调用者转而可以去干其他事,当被调用者处理完成后会通知调用者处理结果

NIO是同步的IO,是因为程序需要IO操作时,必须获得了IO权限后亲自进行IO操作才能进行下一步操作。AIO是对NIO的改进(所以AIO又叫NIO.2),它是基于Proactor模型的。每个socket连接在事件分离器注册 IO完成事件 和 IO完成事件处理器。程序需要进行IO时,向分离器发出IO请求并把所用的Buffer区域告知分离器,分离器通知操作系统进行IO操作,操作系统自己不断尝试获取IO权限并进行IO操作(数据保存在Buffer区),操作完成后通知分离器;分离器检测到 IO完成事件,则激活 IO完成事件处理器,处理器会通知程序说“IO已完成”,程序知道后就直接从Buffer区进行数据的读写。

  • 在 Windows 操作系统中,提供了一个叫做 I/O Completion Ports 的方案,通常简称为 IOCP,操作系统负责管理线程池,其性能非常优异,所以在 Windows 中 JDK 直接采用了 IOCP 的支持。
  • 而在 Linux 中其实也是有AIO 的实现的,但是限制比较多,性能也一般,所以 JDK 采用了自建线程池的方式,也就是说JDK并没有用Linux提供的AIO。

参考:

Netty序章之BIO NIO AIO演变

Netty基础-BIO/NIO/AIO

select、poll、epoll之间的区别(搜狗面试)

彻底搞懂epoll高效运行的原理

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-01-24 11:21:19  更:2022-01-24 11:23:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/16 7:37:20-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码