BIO NIO AIO 学习笔记

Posted 活跃的咸鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BIO NIO AIO 学习笔记相关的知识,希望对你有一定的参考价值。

1.同步阻塞的BIO

1.1 BIO介绍

BIO是最传统的网络通信模式,通信模式是首先服务端启动一个ServerSocket,然后在客户端启动Socket来对服务端进行通信,此时就进入了同步阻塞模式的通信,客户端Socket发送一个请求,服务端Socket处理后进行响应,响应必须是处理完以后,在这之前任何事情都干不了,是一个阻塞的过程。通常服务端Socket需要对每个请求建立一个线程来服务这个客户端,服务端启用了多个线程会消耗系统的大量资源。

BIO的应用场景

BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。

BIO通信模式图:
在这里插入图片描述
网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信(绑定IP地址和端口),客户端通过连接操作向服务端监听的端口地址发起连接请求,基于TCP协议下进行三次握手连接,连接成功后,双方通过网络套接字(Socket)进行通信。

在这里插入图片描述
传统的同步阻塞模型开发中,服务端ServerSocket负责绑定IP地址,启动监听端口;客户端Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。 基于BIO模式下的通信,客户端 - 服务端是完全同步,完全耦合的。

1.2 BIO通信案例

1.简单通信

功能要求:客户端发送一个消息,服务端接收一个消息。

public class Server {
    public static void main(String[] args) throws Exception{
        //注册端口
        ServerSocket serverSocket = new ServerSocket(8888);
        //开始等待接收客户端的连接,得到一个端到端的Socket管道
        Socket socket = serverSocket.accept();
        // 从Socket管道中得到一个字节输入流。把字节输入流包装成自己需要的流进行数据的读取。
        BufferedInputStream bis=new BufferedInputStream(socket.getInputStream());
        byte[] bytes=new byte[1024];
        int length=0;
        while((length= bis.read(bytes))>0){
            System.out.println(new String(bytes,0, length));
        }
       //关闭流
        socket.close();
        serverSocket.close();
        bis.close();
    }
}

public class Client {
    public static void main(String[] args) {
        Socket socket=null;
        BufferedOutputStream bos=null;
        try {
            //创建一个Socket的通信管道,请求与服务端的端口连接。
             socket=new Socket(InetAddress.getLocalHost(),8888);
            //从Socket管道中得到一个字节输出流。
            //    把字节流改装成自己需要的流进行数据的发送
            bos=new BufferedOutputStream(socket.getOutputStream());
            bos.write("hello world".getBytes());
            bos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                bos.close();
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

2.多个客户端通信

在上述的案例中,一个服务端只能接收一个客户端的通信请求,那么如果服务端需要处理很多个客户端的消息通信请求应该如何处理呢,此时我们就需要在服务端引入线程了,也就是说客户端每发起一个请求,服务端就创建一个新的线程来处理这个客户端的请求,这样就实现了一个客户端一个线程的模型,图解模式如下:
在这里插入图片描述

public class Server {
        public static void main(String[] args) throws Exception{
            ServerSocket serverSocket = new ServerSocket(9999);
            while (true){
               //循环监听客户端的请求
                Socket socket = serverSocket.accept();
                //收到一个客户端的请求则开启一个线程为其服务
                new ServerSocketThread(socket).start();
            }

    }
}
//服务线程用于处理客户端发来的消息
public class ServerSocketThread extends Thread{
    private Socket socket;

    public ServerSocketThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        BufferedReader reader=null;
            try {
                 reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String msg;
                while ((msg=reader.readLine())!=null){
                    System.out.println("客户端说:"+msg);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    socket.close();
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }


    }
}


public class Client {
    public static void main(String[] args) throws Exception{
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        PrintStream ps=new PrintStream(socket.getOutputStream());
        Scanner scanner = new Scanner(System.in);
        while (true){
            System.out.println("请说:");
            String msg=scanner.nextLine();
            ps.println(msg);
            ps.flush();
        }
    }
}

缺点分析

  • 1.每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能;
  • 2.每个线程都会占用栈空间和CPU资源;
  • 3.并不是每个socket都进行IO操作,无意义的线程处理;
  • 4.客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

3.伪异步I/O通信

在多用户客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。
​ 接下来我们采用一个伪异步I/O的通信框架,采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。图示如下:
在这里插入图片描述

public class Server {
    public static void main(String[] args) {
        try {
            // 1、注册端口
            ServerSocket ss = new ServerSocket(9999);
            // 2、定义一个循环接收客户端的Socket链接请求
            // 初始化一个线程池对象
            HandlerSocketServerPool pool = new HandlerSocketServerPool(3,10);
            while(true){
                Socket socket = ss.accept();
                // 3、把socket对象交给一个线程池进行处理,
                // 把socket封装成一个任务对象交给线程池处理
                Runnable target = new ServerRunnableTarget(socket);
                pool.execute(target);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}


public class HandlerSocketServerPool {
    // 1、创建一个线程池的成员变量用于存储一个线程池对象
    private ExecutorService executorService;

    /**
     * 2、创建这个类的对象的时候就需要初始化线程池对象
     *    public ThreadPoolExecutor(int corePoolSize,
     *                               int maximumPoolSize,
     *                               long keepAliveTime,
     *                               TimeUnit unit,
     *                               BlockingQueue<Runnable> workQueue)
     */
    public HandlerSocketServerPool(int maxThreadNum , int queueSize){
        executorService = new ThreadPoolExecutor(3,maxThreadNum,120
                , TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
    }

    /**
     * 3、提供一个方法来提交任务给线程池的任务队列来暂存,等着线程池来处理
     */
    public void execute(Runnable target){
        executorService.execute(target);
    }
}


public class ServerRunnableTarget implements Runnable{
    private Socket socket;
    public ServerRunnableTarget(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 1、从socket管道中得到一个字节输入流对象
            InputStream is = socket.getInputStream();
            // 2、把字节输入流包装成一个缓冲字符输入流
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String msg;
            while ((msg = br.readLine()) != null) {
                System.out.println("服务端接收到:"+ msg);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class Client {
    public static void main(String[] args) {
        try {
            // 1、请求与服务端的Socket对象链接
            Socket socket = new Socket("127.0.0.1" , 9999);
            // 2、得到一个打印流
            PrintStream ps = new PrintStream(socket.getOutputStream());
            // 3、使用循环不断的发送消息给服务端接收
            Scanner sc = new Scanner(System.in);
            while(true){
                System.out.print("请说:");
                String msg = sc.nextLine();
                ps.println(msg);
                ps.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4.BIO模式下端口转发思想

需求分析:要求实现客户端与客户端之间交流,或者一个客户端可以把消息转发给除自己之外的其他在线客户端。这个时候我们就需要有端口转发的思想,一个客户端把消息发送给服务端,服务端再把消息转发给另一个客户端或者其他所有的在线客户端。
在这里插入图片描述

public class Server {
    //用于管理在线socket
    public static ArrayList<Socket> list=new ArrayList<>();

    public static void main(String[] args) throws Exception{
        ServerSocket serverSocket = new ServerSocket(8989);
        while (true){
            Socket socket = serverSocket.accept();
            //放入集合管理
            list.add(socket);
            //创建线程与客户端通信
            new ServerThread(socket).start();
        }
    }
}

与客户端通信的线程

public class ServerThread extends Thread{

    private Socket socket;

    public ServerThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        //循环一直与客户端通信,有消息就读
        while (true){
            try {
                DataInputStream dis=new DataInputStream(socket.getInputStream());
                String msg = dis.readUTF();
                System.out.println("客户端发来的消息:"+msg);
                //将客户端的消息发送给其他客户端
                sendToAll(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    //用于将客户端的消息发送给其他客户端
    public void sendToAll(String msg){
        ArrayList<Socket> list = Server.list;
        for (Socket sk : list) {
                try {
                    DataOutputStream dos=new DataOutputStream(socket.getOutputStream());
                    dos.writeUTF(msg);
                } catch (IOException e) {
                    //出异常了说明当前线程出问题了
                    System.out.println("有客户端下线");
                    Server.list.remove(socket);
                    e.printStackTrace();
                }
        }
    }
}

BIO的缺点

服务端每增加一个连接便要开启一个线程与客户端通信当面对几万甚至成千上百的请求时,该模式就显得效率低下,会导致服务器负载过高,最后崩溃等。服务端的许多线程不仅难管理而且耗费系统的资源,如果这个连接不做任何事情会造成不必要的线程开销,虽然可以通过线程池机制改善(实现多个客户连接服务器),但是线程池的做法依旧不能好好的改善。因此我们需要更加高效的网络通信模式也就是NIO模型。

2.同步非阻塞的NIO

2.1 NIO的介绍

为什么要有NIO,NIO能解决什么问题?

我们知道传统的BIO网络通信模式服务端每增加一个连接便要开启一个线程与客户端通信当面对几万甚至成千上百的请求时,该模式就显得效率低下,会导致服务器负载过高,最后崩溃等。服务端的许多线程不仅难管理而且耗费系统的资源,如果这个连接不做任何事情会造成不必要的线程开销,虽然可以通过线程池机制改善(实现多个客户连接服务器),但是线程池的做法依旧不能好好的改善。因此我们需要更加高效的网络通信模式也就是NIO模型。

NIO采用的是一种多路复用的机制,利用单线程轮询事件,高效定位就绪的Channel来决定做什么,只是Select阶段是阻塞式的,能有效避免大量连接数时,频繁线程的切换带来的性能或各种问题。

NIO到底是什么

Java NIO(New IO)也有人称之为 java non-blocking IO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。

什么是NIO 的非阻塞模式

Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况,可以分配20 或者 80个线程来处理。不像之前的阻塞 IO 那样,非得分配 1000 个。

NIO使用场景

1 有很大的数据需要存储,它的生命周期又很长
2 适合频繁的IO操作,比如网络并发场景

NIO和BIO之间的比较

  • BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
  • BIO 是阻塞的,NIO 则是非阻塞的
  • BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道 读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIOBIO
Channel(通道) 和 面向缓冲区(Buffer)面向流(Stream)
非阻塞(Non Blocking IO)阻塞IO(Blocking IO)
选择器(Selectors)

NIO 三大核心

Buffer( 缓冲区) :缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理。

Channel(通道):Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。 通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。

Selector( 选择器):Selector是 一个Java NIO组件,可以能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率。

Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。如图所示:
在这里插入图片描述

  • 每个 channel 都会对应一个 Buffer
  • 一个线程对应Selector , 一个Selector对应多个 channel(连接)
  • 程序切换到哪个 channel 是由事件决定的
  • Selector 会根据不同的事件,在各个通道上切换
  • Buffer 就是一个内存块 , 底层是一个数组
  • 数据的读取写入是通过 Buffer完成的 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写。
  • Java NIO系统的核心在于:通道(Channel)和缓冲区 (Buffer)。通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,需要获取 用于连接 IO 设备的通道以及用于容纳数据的缓冲 区。然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据

2.2 NIO核心一Buffer缓冲区

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理。
在这里插入图片描述

Buffer的子类继承关系如下:
在这里插入图片描述
其中用的最多的是ByteBuffer和CharBuffer。

缓冲区的基本属性:

capacity容量:是指缓冲区可以具体存储多少个数据.容量在创建Buffer缓冲区时指定大小不能为负数,且创建后不能再修改如果缓冲区满了,需要清空后才能继续写数据。

position表示当前位置:即缓冲区写入读取的位置.刚刚创建Buffer对象后,positin初始化为0,写入一个数据,position就向后移动一个单元,它的最大值是capacity-1.当Buffer从写模式切换到读模式position会被重置为0.从Buffer的开始位置读取数据,每读-一个数据postion.就向后移动一个单元。

limit上限:表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。 写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。

mark标记:设置一个标记位置,可以调用mark0方法,把标记就设置在position位置,当调用reset)方法时,就把postion设置为mark标记的位置。

标记、位置、限制、容量遵守以下不变式:

0<= mark <= position <= limit capacity

图示:
在这里插入图片描述
常用API

方法名功能
array()array() 返回支持此缓冲区的数组 (可选操作) 。
capacity()返回此缓冲区的容量。
clear()清除此缓冲区。
flip()翻转这个缓冲区。
hasArray()告诉这个缓冲区是否由可访问的数组支持。
hasRemaining()告诉当前位置和极限之间是否存在任何元素。
isDirect()告诉这个缓冲区是否为 direct 。
isReadOnly()告知这个缓冲区是否是只读的。
limit()返回此缓冲区的限制。
limit(int newLimit)设置此缓冲区的限制。
position()返回此缓冲区的位置。
position(int newPosition)设置这个缓冲区的位置。

以上是关于BIO NIO AIO 学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

bio,nio,aio学习

java BIO/NIO/AIO 学习

一站式学习Java网络编程 全面理解BIO/NIO/AIO

bio aio nio

bio aio nio

Netty学习(源码分析)

(c)2006-2019 SYSTEM All Rights Reserved IT常识