JAVA-IO 模型

阻塞I/O模型

我们在钓鱼时,有一种方式比较惬意、轻松,那就是坐在鱼竿面前,在这个过程中我们什么也不做,双手一直握着鱼竿,静静地等着鱼咬钩。一旦手上感受到鱼拉扯鱼竿,就把鱼钓起来放人鱼篓,再钓下一条鱼。

  1. 应用进程通过系统调用recvfrom接收数据,但由于内核还未准备好数据报,应用进程就会阻塞,直到内核准备好数据报,recvfrom完成数据报的复制工作,应用进程才能结束阻塞状态。
  2. 这种最简单的“钓鱼”方式对于钓鱼的人来说,不需要特制的鱼竿,拿一根够长的木棍就可以悠闲地钓鱼了(实现简单)
  3. 缺点是比较耗费时间
  4. 适合需求量小的场景(并发低,时效性要求低)

singThread BIO

如果客户端与服务端建立了连接,如果这个连接的容户端迟迟不发数据,程就会一直堵塞在read()方法上,这样其他客户端也不能进行连接,也就是一次只能处理一个客户端,对客户很不友好

public class RedisServerBIO {
  public static void main(String[] args) throws IOException {

    ServerSocket serverSocket = new ServerSocket(6379);

    while (true) {
      System.out.println("-----111 等待连接");
      Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接 一次 只能连接一个客户端
      System.out.println("-----222 成功连接");

      InputStream inputStream = socket.getInputStream();
      int length = -1;
      byte[] bytes = new byte[1024];
      System.out.println("-----333 等待读取");
      while ((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
      {
        System.out.println("-----444 成功读取" + new String(bytes, 0, length));
        System.out.println("====================");
        System.out.println();
      }
      inputStream.close();
      socket.close();
    }
  }
}
public class RedisClient01 {
  public static void main(String[] args) throws IOException {
    Socket socket = new Socket("127.0.0.1", 6379);
    OutputStream outputStream = socket.getOutputStream();

    //socket.getOutputStream().write("RedisClient01".getBytes());

    while (true) {
      Scanner scanner = new Scanner(System.in);
      String string = scanner.next();
      if (string.equalsIgnoreCase("quit")) {
        break;
      }
      socket.getOutputStream().write(string.getBytes());
      System.out.println("------input quit keyword to finish......");
    }
    outputStream.close();
    socket.close();
  }
}
public class RedisClient02 {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 6379);
        OutputStream outputStream = socket.getOutputStream();

        //socket.getOutputStream().write("RedisClient01".getBytes());

        while (true) {
            Scanner scanner = new Scanner(System.in);
            String string = scanner.next();
            if (string.equalsIgnoreCase("quit")) {
                break;
            }
            socket.getOutputStream().write(string.getBytes());
            System.out.println("------input quit keyword to finish......");
        }
        outputStream.close();
        socket.close();
    }
}

manyThread

  1. 只要连接了一个socket,操作系统分配一个线来处理,这样read()方法堵塞在每个具体线程上而不堵塞主线程,就能操作多个socket了,哪个线程中的socket有数据,就读哪个socket,各取所需,灵活统一。
  2. 程序服务端只负责监听是否有客户端连接,使用accept)阻塞 客户端1连接服务端,就开辟一个线程(thread1)来执行read0方法,程序服务端继续监听 客户端2连接服务端,也开辟一个线程(thread2)来执行read()方法,程序服务端继续监听 客户端3连接服务端,也开辟一个线程(thread3)来执行read)方法,程序服务瑞继续监听 。。。。。。。。。
  3. 任何一个线程上的socket有数据发送过来,read()就能立马读到,cpu就能进行处理。
  4. 问题:每来一个客户端,就要开辟一个线程,如果来1万个客户端,那就要开辟1万个线程。在操作系统中用户态不能直接开辟线程,需要调用内核来创建的一个线程,这其中还涉及到用户状态的切换(上下文的切换),十分耗资源。
  5. 解决

使用线程池: 这个在客户端连接少的情况下可以使用,但是用户量大的情况下,你不知道线程池要多大,太大了内存可能不够,也不可行。

NIO(非阻塞式IO)方式

tomcat7之前就是用BlO多线程来解决多连接

public class RedisServerBIOMultiThread {
  public static void main(String[] args) throws IOException {
    ServerSocket serverSocket = new ServerSocket(6379);

    while (true) {
      //System.out.println("-----111 等待连接");
      Socket socket = serverSocket.accept();//阻塞1 ,等待客户端连接
      //System.out.println("-----222 成功连接");

      new Thread(() -> {
        try {
          InputStream inputStream = socket.getInputStream();
          int length = -1;
          byte[] bytes = new byte[1024];
          System.out.println("-----333 等待读取");
          while ((length = inputStream.read(bytes)) != -1)//阻塞2 ,等待客户端发送数据
          {
            System.out.println("-----444 成功读取" + new String(bytes, 0, length));
            System.out.println("====================");
            System.out.println();
          }
          inputStream.close();
          socket.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }, Thread.currentThread().getName()).start();

      System.out.println(Thread.currentThread().getName());

    }
  }
}
public class RedisClient01{
  public static void main(String[] args) throws IOException{
    Socket socket = new Socket("127.0.0.1",6379);
    OutputStream outputStream = socket.getOutputStream();

    //socket.getOutputStream().write("RedisClient01".getBytes());

    while(true)
    {
      Scanner scanner = new Scanner(System.in);
      String string = scanner.next();
      if (string.equalsIgnoreCase("quit")) {
        break;
      }
      socket.getOutputStream().write(string.getBytes());
      System.out.println("------input quit keyword to finish......");
    }
    outputStream.close();
    socket.close();
  }
}
public class RedisClient02 {
  public static void main(String[] args) throws IOException {
    Socket socket = new Socket("127.0.0.1", 6379);
    OutputStream outputStream = socket.getOutputStream();

    //socket.getOutputStream().write("RedisClient01".getBytes());

    while (true) {
      Scanner scanner = new Scanner(System.in);
      String string = scanner.next();
      if (string.equalsIgnoreCase("quit")) {
        break;
      }
      socket.getOutputStream().write(string.getBytes());
      System.out.println("------input quit keyword to finish......");
    }
    outputStream.close();
    socket.close();
  }
}

非阻塞I/O模型

钓鱼的时候,在等待鱼咬钩的过程中,我们可以做一些别的事情,比如玩一会儿手机游戏等。但是,我们要时不时地看一下鱼竿,一旦发现有鱼上钩了,就把鱼钓上来

  1. accept()方法是非阻塞的,如果没有客户端连接,就返回error read()方法是非阻塞的,如果read()方法读取不到数据就返回error,如果读取到数据时只阻塞read)方法读数据的时间
  2. 在NIO模式中,只有一个线程: 当一个客户端与服务端进行连接,这个socket就会加入到一个数组中,隔一段时间遍历一次, 看这个socket的read方法能否读到数据,这样一个线程就能处理多个客户端的连接和读取了
  3. 在非阻塞式I/O模型中,应用程序把一个套接口设置为非阻塞,就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠而是返回一个“错误”,应用程序基于I/O操作函数将不断的轮询数据是否己经准备好,如果没有准备好,继续轮询,直到数据准备好为止。
  4. 让Liunx内核搞定上述需求,我们将一批文件描述符通过一次系统调用传给内核由内核层去遍历,才能真正解决这个问题。IO多路复用应运而生,也即将上述工作直接放进Liux内核,不再两态转换而是直接从内核获得结果,因为内核是非阻塞的。

问题

客户端如果很多:

比如有1万个客户端进行连接,那么每次循环就要遍历1万个socket,如果一万个socket中只有10个socket有数据,也会遍历一万个socket,就会做很多无用功,每次遍历遇到read返回-1时仍然是一次浪费资源的系统调用。

遍历过程是在用户态进行的:

用户态判断socket是否有数据还是调用内核的read()方法实现的,这就涉及到用户态和内核态的切换,每遍历一个就要切换一次,开销很大因为这些问题的存在。

ServerSocketChannel

public class RedisServerNIO {
  static ArrayList<SocketChannel> socketList = new ArrayList<>();
  static ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

  public static void main(String[] args) throws IOException {
    System.out.println("---------RedisServerNIO 启动等待中......");
    ServerSocketChannel serverSocket = ServerSocketChannel.open();
    serverSocket.bind(new InetSocketAddress("127.0.0.1", 6379));
    serverSocket.configureBlocking(false);//设置为非阻塞模式

    while (true) {
      for (SocketChannel element : socketList) {
        int read = element.read(byteBuffer);
        if (read > 0) {
          System.out.println("-----读取数据: " + read);
          byteBuffer.flip();
          byte[] bytes = new byte[read];
          byteBuffer.get(bytes);
          System.out.println(new String(bytes));
          byteBuffer.clear();
        }
      }

      SocketChannel socketChannel = serverSocket.accept();
      if (socketChannel != null) {
        System.out.println("-----成功连接: ");
        socketChannel.configureBlocking(false);//设置为非阻塞模式
        socketList.add(socketChannel);
        System.out.println("-----socketList size: " + socketList.size());
      }
    }
  }
}
public class RedisClient01 {
  public static void main(String[] args) throws IOException {
    System.out.println("------RedisClient01 start");
    Socket socket = new Socket("127.0.0.1", 6379);
    OutputStream outputStream = socket.getOutputStream();
    while (true) {
      Scanner scanner = new Scanner(System.in);
      String string = scanner.next();
      if (string.equalsIgnoreCase("quit")) {
        break;
      }
      socket.getOutputStream().write(string.getBytes());
      System.out.println("------input quit keyword to finish......");
    }
    outputStream.close();
    socket.close();
  }
}
public class RedisClient02 {
  public static void main(String[] args) throws IOException {
    System.out.println("------RedisClient02 start");


    Socket socket = new Socket("127.0.0.1", 6379);
    OutputStream outputStream = socket.getOutputStream();

    while (true) {
      Scanner scanner = new Scanner(System.in);
      String string = scanner.next();
      if (string.equalsIgnoreCase("quit")) {
        break;
      }
      socket.getOutputStream().write(string.getBytes());
      System.out.println("------input quit keyword to finish......");
    }
    outputStream.close();
    socket.close();
  }
}

信号驱动式IO模型

  1. 钓鱼时,为了避免自己一遍遍地查看鱼竿,我们可以给鱼竿安装一个报警器,当有鱼咬钩时立刻报警,我们在收到报警后,就把鱼钓起来。
  2. 应用进程预先向内核注册一个信号处理函数,然后用户进程不阻塞直接返回,当内核数据准备就绪时会发送一个信号给进程,用户进程在信号处理函数中把数据复制到用户空间。

 IO复用模型

介绍

  1. 避免非阻塞IO 中的空转
  2. 我们钓鱼时,为了保证可以在最短的时间钓到最多的鱼,会同时摆放多个鱼竿,哪个鱼竿有鱼咬钩了,我们就把哪个鱼竿上面的鱼钓起来。
  3. 多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据复制到用户空间
  4. IO multiplexing.就是我们说的select,poll,epol,有些地方也称这种IO方式为event driven IO事件驱动lO。就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程,每次一个线程),这样可以大大节省系统资源。所以,I/O多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select)函数就可以返回。

模型

多个Sock复用一根网线这个功能是在内核+驱动层实现的

I/O multiplexing这里面的multiplexing指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态来同时管理多个I/O流.目的是尽量多的提高服务器的吞吐能力。

  1. IO多路转接是多了一个select函数,进程调用该select时,select会监听所有注册好的I0,如果所有被监听的IO需要的数据都没有准备好,那么select调用进程会阻塞。当任意一个I/0所需的数据准备好之后,select调用就会返回,然后进程通过recvfrom实现数据复制。
  2. 这里并没有向内核注册信号处理函数,所以,I/O复用模型并不是非阻塞的。进程在发出select后,要等select监听的所有I/O操作中至少有一个需要的数据准备好,才会有返回值并且需要再次发送请求去执行文件的复制。

Reactor模式

  1. 基于IO复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
  2. Reacto模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式。即I/O多了复用统一监听事件收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术。

  1. Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人:
  2. Handlers:处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际办理人。Reactor通过调度适当的处理程序来响应/O事件,处理程序执行非阻塞操作。

为什么以上四种模式都是同步的?

  1. 真正的数据复制过程都是同步进行的。
  2. 信号驱动难道不是异步的吗?

基于信号驱动I/O模型,内核是在数据准备好之后通知进程的,然后进程再通过recvfrom操作进行数据复制。我们可以认为数据准备阶段是异步的,但数据复制操作是同步的。所以,整个I/O过程也不能认为是异步的。

  1. 还是以钓鱼为例,钓鱼过程可以拆分为以下两个步骤:
    1.  鱼咬钩(数据准备)。
    2.  把鱼钓起来放进中(数据复制)
  2. 无论以上提到的哪种钓鱼方式,第二步都是需要人主动去做的,并不是鱼竿自己完成的所以,这个钓鱼过程其实还是同步进行的。

异步IO模型

  1. 我们钓鱼时使用了一种高科技钓鱼竿,即全自动钓鱼竿,可以自动感应鱼上钩、自动收竿,更厉害的是可以自动把鱼放进鱼塞中,然后通知我们鱼已经钓到了,可以继续钓下一条鱼了。
  2. 应用进程把I/O请求传给内核后,完全由内核去完成文件的复制。内核完成相关操作后,会发送信号告诉应用进程本次IO操作已经完成

用户进程发起aio_read操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了。当内核收到aio read后,会立刻返回,然后开始等待数据准备,数据准备好以后,直接把数据复制到用户控件,然后通知进程本次IO操作已经完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值