Java IO编程总结(BIO、NIO、AIO)

本文包括以下内容:

  • IO介绍

传统的 IO 大致可以分为4种类型:

  • InputStream、OutputStream 基于字节操作的 IO
  • Writer、Reader 基于字符操作的 IO
  • File 基于磁盘操作的 IO
  • Socket 基于网络操作的 IO

java.net 下提供的 Scoket 很多时候人们也把它归为 同步阻塞 IO,因为网络通讯同样是 IO 行为。

因项目数传服务器是基于 socket 的 BIO 和 NIO 实现,所以 下面讲述概念以及实现demo时,均以 socket 编程为例。

参考:

  • https://www.jianshu.com/p/ddf1165e618a

1. IO介绍

我们通常所说的 BIO 是相对于 NIO 来说的,BIO 也就是 Java 开始之初推出的 IO 操作模块,BIO 是 BlockingIO 的缩写,顾名思义就是阻塞 IO 的意思。

1.1 BIO、NIO、AIO的区别

  1. BIO

    传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。

    它的优点就是代码比较简单、直观;

    缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。

  2. NIO

    Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序

    同时提供了更接近操作系统底层高性能的数据操作方式。

  3. AIO

    Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO)

    异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

1.2 传统的 IO 的4种类型

传统的 IO 大致可以分为4种类型:

  1. InputStream、OutputStream 基于字节操作的
  2. IO Writer、Reader 基于字符操作的 IO
  3. File 基于磁盘操作的 IO
  4. Socket 基于网络操作的 IO

java.net 下提供的 Scoket 很多时候人们也把它归为 同步阻塞 IO ,因为网络通讯同样是 IO 行为。

2. BIO编程

2.1 传统的BIO编程

网络编程的基本模型是C/S模型,即两个进程间的通信。

服务端提供IP和监听端口,客户端通过连接操作想服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。

传统的同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

简单的描述一下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理没处理完成后,通过输出流返回应答给客户端,线程销毁。即典型的一请求一应答通宵模型。

传统BIO通信模型图:
Java IO编程总结(BIO、NIO、AIO)_第1张图片
该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,Java中的线程也是比较宝贵的系统资源,线程数量快速膨胀后,系统的性能将急剧下降,随着访问量的继续增大,系统最终就死-掉-了。

2.2 伪异步I/O编程

为了改进这种一连接一线程的模型,我们可以使用线程池来管理这些线程(需要了解更多请参考前面提供的文章),实现1个或多个线程处理N个客户端的模型(但是底层还是使用的同步阻塞I/O),通常被称为“伪异步I/O模型“。

伪异步I/O模型图:
Java IO编程总结(BIO、NIO、AIO)_第2张图片
如果使用 CachedThreadPool线程池(不限制线程数量),其实除了能自动帮我们管理线程(复用),看起来也就像是1:1的客户端:线程数模型

而使用 FixedThreadPool 我们就有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N:M的伪异步I/O模型。

但是,正因为限制了线程数量,如果发生大量并发请求,超过最大数量的线程就只能等待,直到线程池中的有空闲的线程可以被复用。

而对Socket的输入流就行读取时,会一直阻塞,直到以下情况发生:

  • 有数据可读
  • 可用数据以及读取完毕
  • 发生空指针或I/O异常

所以在读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他接入的消息,只能一直等待,这就是最大的弊端。

后面即将介绍的NIO,就能解决这个难题。

3. NIO 编程

JDK 1.4中的java.nio.*包中引入新的Java I/O库,其目的是提高速度。实际上,“旧”的I/O包已经使用NIO重新实现过,即使我们不显式的使用NIO编程,也能从中受益。

速度的提高在文件I/O和网络I/O中都可能会发生,但本文只讨论后者。

Java IO编程总结(BIO、NIO、AIO)_第3张图片

3.1 简介

NIO我们一般认为是New I/O(也是官方的叫法),因为它是相对于老的I/O类库新增的,做了很大的改变。

但跟多人称之为Non-block I/O,即非阻塞I/O,因为这样叫,更能体现它的特点。

而下文中的 NIO,不是指整个新的 I/O库,而是非阻塞 I/O。

NIO 提供了与传统 BIO模型 中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。

新增的着两种通道都支持阻塞和非阻塞两种模式。

阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。

对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。

下面会先对基础知识进行介绍。

3.2 缓冲区 Buffer

Buffer是一个对象,包含一些要写入或者读出的数据。

在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。

具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。

3.3 通道 Channel

我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。

底层的操作系统的通道一般都是全双工的,所以全双工的Channel 比流能更好的映射底层操作系统的API。

Channel主要分两大类:

  • SelectableChannel:用户网络读写
  • FileChannel:用于文件操作

后面代码会涉及的 ServerSocketChannel 和 SocketChannel 都是 SelectableChannel 的子类。

3.4 多路复用器 Selector

Selector是Java NIO 编程的基础。

Selector 提供选择已经就绪的任务的能力:Selector 会不断轮询注册在其上的 Channel ,如果某个 Channel 上面发生读或者写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的I/O操作。

一个 Selector 可以同时轮询多个 Channel,因为JDK使用了 epoll() 代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。

3.5 NIO服务端

创建NIO服务端的主要步骤如下:

  1. 打开ServerSocketChannel,监听客户端连接
  2. 绑定监听端口,设置连接为非阻塞模式
  3. 创建Reactor线程,创建多路复用器并启动线程
  4. 将ServerSocketChannel注册到Reactor线程中的Selector上,监听ACCEPT事件
  5. Selector轮询准备就绪的key
  6. Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,简历物理链路
  7. 设置客户端链路为非阻塞模式
  8. 将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息
  9. 异步读取客户端消息到缓冲区
  10. 对Buffer编解码,处理半包消息,将解码成功的消息封装成Task
  11. 将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端

因为应答消息的发送,SocketChannel也是异步非阻塞的,所以不能保证一次能吧需要发送的数据发送完,此时就会出现写半包的问题。我们需要注册写操作,不断轮询Selector将没有发送完的消息发送完毕,然后通过Buffer的hasRemain()方法判断消息是否发送完成。

4 AIO编程

NIO 2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。

异步的套接字通道时真正的异步非阻塞I/O,对应于UNIX网络编程中的事件驱动 I/O(AIO)。

不需要过多的 Selector 对注册的通道进行轮询即可实现异步读写,从而简化了 NIO 的编程模型。

5 各种I/O的对比

Java IO编程总结(BIO、NIO、AIO)_第4张图片

具体选择什么样的模型或者NIO框架,完全基于业务的实际应用场景和性能需求,如果客户端很少,服务器负荷不重,就没有必要选择开发起来相对不那么简单的NIO做服务端;相反,就应考虑使用NIO或者相关的框架了。

你可能感兴趣的:(#,base,JAVA)