IO 模型和Java 中的 IO 体系

0 I/O 模型

  • 传统的阻塞 I/O(Blocking I/O,BIO):进程调用读取指令后阻塞直至数据复制到内存完毕,一个进程或线程对应一个连接。
  • 非阻塞 I/O(Non-blocking I/O,NIO):进程调用读取指令后进程可以处理其他任务后再查看是否数据准备完毕,准备完毕后阻塞至数据从IO设备复制至内存中。
  • I/O 多路复用(I/O multiplexing):改进的阻塞IO,可以处理多个连接。IO 多路复用是阻塞在 select epoll 这样的系统调用之上,而没有阻塞在真正的 IO 系统调用上。
  • 异步 I/O(Asynchronous I/O,AIO):进程调用读取指令后内核负责处理数据从 IO 设备直至复制至内存后再回调进程函数。

1 BIO

1.1 I/O分类

从数据传输方式角度看,I/O 可以分为:

  1. 字节流:字节流是以一个字节单位来传输数据,读取单个字节,用来处理二进制文件,字节是给计算机看的。
    • InputStream
      • ByteArrayInputStream
      • PipedInputStream
      • FilterInputStream
        • BufferedInputStream
        • DataInputStream
      • FileInputStream
      • ObjectInputStream
    • OutputStream
      • ByteArrayOutputStream
      • PipedOutputStream
      • FilterOutputStream
        • BufferedOutputStream
        • DataOutputStream
        • PrintStream
      • FileOutputStream
      • ObjectOutputStream
  2. 字符流:字符流是以多个字节来传输数据,读取单个字符,用来处理文本文件,字符是给人看的。
    • Reader
      • CharArrayReader
      • PipedReader
      • FilterReader
      • BufferedReader
      • InputStreamReader
        • FileReader
    • Writer
      • CharArrayWriter
      • PipedWriter
      • FilterWriter
      • BufferedWriter
      • OutputStreamWriter
        • FileWriter
      • PrintWriter

从操作对象角度看,I/O 可以分为:

  1. 文件(file):FileInputStreamFileOutputStreamFileReaderFileWriter
  2. 缓冲操作:BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
  3. 字符串(String):StringBufferInputStreamStringBufferOutputStreamStringReaderStringWriter
  4. 数组([]):
    1. 字节数组(byte[]):ByteArrayInputStreamByteArrayOutputStream
    2. 字符数组(char[]):CharArrayReaderCharArrayWriter
  5. 管道操作:PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
  6. 基本数据类型:DataInputStreamDataOutputStream
  7. 打印:PrintStreamPrintWriter
  8. 对象序列化反序列化:ObjectInputStreamObjectOutputStream
  9. 转换:InputStreamReader 字节流解码成字符流、OutputStreamWriter 字符流编码成为字节流

1.2 编码与解码

编码就是把字符转换为字节,而解码是把字节重新组合成字符。不管是磁盘还是网络传输,最小的存储单元都是字节。

  • GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
  • UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
  • UTF-16be 编码中,中文字符和英文字符都占 2 个字节。Java 的内存编码使用双字节编码 UTF-16be。

1.3 序列化

序列化就是将一个对象转换成字节序列,方便存储和传输。不会对静态变量进行序列化。核心作用是对象状态的保存与重建。

  • 序列化:将对象写入到 IO 流中。ObjectOutputStream.writeObject()
  • 反序列化:从 IO 流中恢复对象。ObjectInputStream.readObject()

序列化方法

  • 序列化的类需要实现 Serializable 接口
    • 成员必须是可序列化的对象
    • transient 关键字可以使一些属性不会被序列化
      • 序列化时会忽略掉此字段,反序列化后的属性是默认值(null / 0 / false)
    • private static final long serialVersionUID:序列化版本号,方便项目升级。
  • 强制自定义序列化实现 Externalizable 接口,必须实现 writeExternal、readExternal 方法。

Java 序列化算法

  1. 所有保存到磁盘的对象都有一个序列化编码号
  2. 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
  3. 如果此对象已经序列化过,则直接输出编号即可。

序列化优势:

  1. 对象序列化可以实现分布式对象。
  2. Java 对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。
  3. 序列化可以将内存中的类写入文件或数据库中。
  4. 对象、文件、数据,有许多不同的格式,很难统一传输和保存。

1.4 网络支持

Java 提供了专门的网络开发程序包 java.net

  • InetAddress 处理 IP 地址
  • URL 处理 URL 对象
  • URLConnection 建立与远程服务器的连接,检查远程资源的一些属性
  • URLEncoderURLDecoder 编码与解码

Sockets:使用 TCP 协议实现网络通信

  • 服务端
    1. 创建 ServerSocket 对象,绑定监听端口
    2. 通过 accept() 方法监听客户端请求
    3. 连接建立后,通过输入流读取客户端发送的请求信息
    4. 通过输出流向客户端发送响应信息
  • 客户端
    1. 创建 Socket 对象,指明需要连接的服务器的地址和端口号
    2. 连接建立后,通过输出流向服务器发送请求信息
    3. 通过输入流获取服务器响应的信息

Datagram:使用 UDP 协议实现网络通信

  • 服务端
    1. 创建数据报套接字 DatagramSocket
    2. 创建一个数据报包 DatagramPacket
    3. 调用 receive() 方法接收数据包
    4. 从数据报包中获取数据
  • 客户端
    1. 创建数据报套接字 DatagramSocket
    2. 创建数据报包 DatagramPacket 用于封装数据和目标地址 InetAddress
    3. 调用 send() 方法进行发送数据

2 NIO

同步非阻塞 IO

  • BIO 数据处理以流(字节)为单位,面向流;
  • NIO 数据处理以块为单位,面向缓冲区。

NIO 三大核心

  • Channel(通道)
    • 类似 IO 中的 Stream,但是 Stream 是单向的,Channel 是双向的。
    • 通过 open() 静态方法打开一个通道。
    • 主要实现:FileChannelDatagramChannelSocketChannelServerSocketChannel
  • Buffer(缓冲区)
    • 使用堆外内存,不受 GC 管理,非线程安全。
    • 主要实现:ByteBufferCharBufferDoubleBufferFloatBufferIntBufferLongBufferShortBuffer
  • Selector
    • 检测并处理 Channel 上注册的事件。
    • Selector 类是 NIO 的核心类,通过 Selector.open() 静态方法选择一个事件进行处理。
    • 运行单线程处理多个 Channel。

BIO 会一直阻塞对应的进程直到操作完成,而 NIO 在内核准备数据的时候会返回。

3 I/O多路复用

主要函数

  • select 函数监视的文件描述符分 3 类,分别是 writefds、readfds、和exceptfds。调用后 select 函数后会阻塞,直到有描述符就绪(有数据 可读、可写、或者有异常)或超时,函数返回。当 select 函数返回后,可以通过遍历 fdset,来找到就绪的描述符。
  • poll 和 select 类似,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果没有发现就绪设备则挂起当前进程直到设备就绪或者主动超时,被唤醒后它又要再次遍历 fd。
  • epoll 是 select 和 poll 的增强版本。epoll 支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些 fd 刚刚变为就绪态并且只会通知一次。

阻塞式 I/O 和 I/O 多路复用的两个阶段都阻塞

  • BIO 只能阻塞一个 IO 操作
  • I/O 多路复用的 Selector 复用器可以同时阻塞多个 IO
⤧  Next post 【Tools】管理工具 Git ⤧  Previous post 【计算机科学】《编程珠玑》书摘