BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)、字节流字符流的区别

看到一个讲的很详细的BIO、NIO
https://blog.csdn.net/u010310183/article/details/81700405

与Java关联起来的BIO、NIO
http://www.imooc.com/article/265871

BIO(同步阻塞)

针对每一个套接字,都新建一个线程处理其数据读取。

在BIO工作模式下,服务端程序要想同时处理多个套接字的数据读取,在等待接收连接请求的主线程之外,还要为每一个建立好的连接分配一个新的线程进行处理。

在java中

BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
在这里插入图片描述

代码示例(转载):

int port = 4343; //端口号
// Socket 服务器端(简单的发送信息)
Thread sThread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while (true) {
                // 等待连接
                Socket socket = serverSocket.accept();
                Thread sHandlerThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try (PrintWriter printWriter = new PrintWriter(socket.getOutputStream())) {
                            printWriter.println("hello world!");
                            printWriter.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
                sHandlerThread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
sThread.start();

// Socket 客户端(接收信息并打印)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("客户端:" + s));
} catch (UnknownHostException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

NIO(同步非阻塞)

在java中

NIO 是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
在这里插入图片描述

它有两个最重要的特点:

  1. 同步非阻塞
  2. I/O多路复用

同步非阻塞

如果将套接字读操作换成非阻塞的,那么只需要一个线程就可以同时处理套接字,它扮演着调度员的角色(也就是上图中的Selector)。每当链接请求来的时候,便创建一个ServerSocketChannel,并向Selector注册。

Selector 管理这些ServerSocketChannel最简单的方式是轮询:每次检查一个ServerSocketChannel,有数据则读取,没有则检查下一个,因为是非阻塞的,所以执行read操作时若没有数据准备好则立即返回,不会发生阻塞。这种轮询的方式缺点是浪费CPU资源,大部分时间可能都是无数据可读的,不必仍不间断的反复执行read操作。

I/O多路复用(IOmultiplexing)是一种更好的方法

I/O多路复用

I/O多路复用(IOmultiplexing)是一种更好的方法,Selector 会阻塞在 select 操作,当有 Channel 发生接入请求,才会被唤醒。也就是说其内部会维护一张监听的套接字的列表,会一直阻塞直到其中某一个套接字有数据准备好才返回,并告诉是哪个套接字可读,这时再调用该套接字的read函数效率更高。

综上所述:
基本可以认为 “NIO = I/O多路复用 + 非阻塞式I/O”,大部分情况下是单线程,但也有超过一个线程实现NIO的情况

核心类

  1. Channel:数据的传输管道
  2. Buffer:数据缓冲区
  3. Selector:根据key处理对应的channel

核心流程

  1. 打开serversocketchannel
  2. 绑定监听地址ip
  3. 创建selector启动线程
  4. 把serversocketchannel注册到selector 监听
  5. selector轮询就绪的key
  6. handleAccept处理新的客户端接入
  7. 设置新建客户端连接的socket参数
  8. 向selector注册监听读操作
  9. handleRead异步读请求消息到bytebuffer
  10. decode请求消息
  11. 异步写bytebuffer到socketchannel

代码示例(转载):

// NIO 多路复用
public static void testServer() throws IOException{

   // 1、获取Selector选择器
   Selector selector = Selector.open();

   // 2、获取通道
   ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
   // 3.设置为非阻塞
   serverSocketChannel.configureBlocking(false);
   // 4、绑定连接
   serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));

   // 5、将通道注册到选择器上,并注册的操作为:“接收”操作
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

   // 6、采用轮询的方式,查询获取“准备就绪”的注册过的操作
   while (selector.select() > 0)
   {
       // 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
       Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
       while (selectedKeys.hasNext())
       {
           // 8、获取“准备就绪”的时间
           SelectionKey selectedKey = selectedKeys.next();

           // 9、判断key是具体的什么事件
           if (selectedKey.isAcceptable())
           {
               // 10、若接受的事件是“接收就绪” 操作,就获取客户端连接
               SocketChannel socketChannel = serverSocketChannel.accept();
               // 11、切换为非阻塞模式
               socketChannel.configureBlocking(false);
               // 12、将该通道注册到selector选择器上
               socketChannel.register(selector, SelectionKey.OP_READ);
            }
            else if (selectedKey.isReadable())
            {
               // 13、获取该选择器上的“读就绪”状态的通道
               SocketChannel socketChannel = (SocketChannel) selectedKey.channel();

               // 14、读取数据,读是socketChannel.read,写是socketChannel.write
               ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
               int length = 0;
               while ((length = socketChannel.read(byteBuffer)) != -1)
               {
                   byteBuffer.flip();
                   System.out.println(new String(byteBuffer.array(), 0, length));
                    byteBuffer.clear();
                }
                socketChannel.close();
           }

           // 15、移除选择键
           selectedKeys.remove();
        }
   }

   // 7、关闭连接
   serverSocketChannel.close();
}

// Socket 客户端(接收信息并打印)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("NIO 客户端:" + s));
} catch (IOException e) {
    e.printStackTrace();
}

再来看看I/O多路复用的三种形式

  1. select:知道了有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长
  2. poll:本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的
  3. epoll(Linux内核所特有):可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))(Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll)

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

AIO(异步非阻塞)

线程发起io请求后,立即返回(非阻塞io),当数据读写完成后,OS通知用户线程(异步)。这里数据写入socket空间,或从socket空间读取数据到用户空间由OS完成,用户线程无需介入,所以也就不会阻塞用户线程,即异步。

在java中

AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

深入了解:NIO三种模型

上面所讲到的只需要一个线程就可以同时处理多个套接字,这只是其中的一种单线程模型,是一种较为极端的情况,NIO主要包含三种线程模型:
1、Reactor单线程模型

2、Reactor多线程模型

3、主从Reactor多线程模型

Reactor单线程模型:

单个线程完成所有事情包括接收客户端的TCP连接请求,读取和写入套接字数据等。

对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用却不合适,主要原因如下:

  1. 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送;

  2. 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,NIO线程会成为系统的性能瓶颈;

  3. 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

为了解决这些问题,演进出了Reactor多线程模型。

Reactor多线程模型:

Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理真实的IO操作。

Reactor多线程模型的特点:

  1. 有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP连接请求;

  2. 网络IO操作-读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;

  3. 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。

在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是,在极特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型。

即从单线程中由一个线程即监听连接事件、读写事件、由完成数据读写,拆分为由一个线程专门监听各种事件,再由专门的线程池负责处理真正的IO数据读写。

主从Reactor多线程模型

主从Reactor线程模型与Reactor多线程模型的最大区别就是有一组NIO线程处理连接、读写事件。

主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。

即从多线程模型中由一个线程来监听连接事件和数据读写事件,拆分为一个线程监听连接事件,线程池的多个线程监听已经建立连接的套接字的数据读写事件,另外和多线程模型一样有专门的线程池处理真正的IO操作。

各自适用场景

NIO适用场景

服务器需要支持超大量的长时间连接。比如10000个连接以上,并且每个客户端并不会频繁地发送太多数据。
例如聊天服务器,连接数目多且连接比较短(轻操作)的架构,只需要少量线程按需处理维护的大量长期连接。

Jetty、Mina、Netty、ZooKeeper等都是基于NIO方式实现。

BIO适用场景

适用于连接数目比较小,并且一次发送大量数据的场景,这种方式对服务器资源要求比较高,并发局限于应用中。

例子

下面一个例子是我看过的一个讲述的很贴切的例子:

一辆从 A 开往 B 的公共汽车上,路上有很多点可能会有人下车。司机不知道哪些点会有哪些人会下车,对于需要下车的人,如何处理更好?

  1. 司机过程中定时询问每个乘客是否到达目的地,若有人说到了,那么司机停车,乘客下车。 ( 类似阻塞式 )

  2. 每个人告诉售票员自己的目的地,然后睡觉,司机只和售票员交互,到了某个点由售票员通知乘客下车。 ( 类似非阻塞 )

很显然,每个人要到达某个目的地可以认为是一个线程,司机可以认为是 CPU 。在阻塞式里面,每个线程需要不断的轮询,上下文切换,以达到找到目的地的结果。而在非阻塞方式里,每个乘客 ( 线程 ) 都在睡觉 ( 休眠 ) ,只在真正外部环境准备好了才唤醒,这样的唤醒肯定不会阻塞。

同步/异步

主要针对客户端:

同步:就是当客户端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是说必须一件一件的的事情去做,等一件做完了才能去做下一件。

异步:就是当客户端发出一个功能调用时,调用者不用等接收方发出响应。实际处理这个调用的部件在完成后,会通过状态,通知和回调来通知调用者。客户端可以接着去做后面的事情。

虽然主要是针对客户端,但是服务器端不是完全没有关系的,同步/异步必须配合服务器端才能实现。同步/异步是由客户端自己控制,但是服务器端是否阻塞/非阻塞,客户端完全不用关心。

阻塞/非阻塞

主要针对服务器端:

阻塞:阻塞调用是指服务器端被调用者调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞:指不能立即得到结果之前,该调用不会阻塞当前线程。

阻塞和非阻塞区别:当采用BIO时,如果没有收到信息,则会一直处于等待状态,线程不休眠;而采用NIO时,当有消息到达时才进行处理,没有消息到达时就干别的事。

同步和异步IO:当进行IO操作时(例如复制文件),若采用同步IO,那么程序会等IO完毕才往下执行;而异步IO会讲IO操作交给操作系统来完成,程序继续往下执行,当操作系统完成后会做出通知。

字节流字符流的区别

  • 字节流和字符流都有输入和输出方式
  • 字节输入流和输出流的祖先:InputStream和OutputStream
  • 字符输入流和输出流的祖先:Reader和Writer
  • 以上这些类都是abstract修饰的抽象类,不能直接实例化对象
  • 字节流可以处理所有文件类型的数据(图片,视频,文本·····)
  • 字符流只能处理纯文本数据(txt文本文档)

在这里插入图片描述

字节流使用场景

字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位,因为在通常情况下一个ACSII码就是一个字节的空间来存放。

字符流使用场景

字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。

https://blog.csdn.net/weixin_43986427/article/details/100726574

发布了67 篇原创文章 · 获赞 32 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/weixin_43751710/article/details/98751786