Dawn's Blogs

分享技术 记录成长

0%

Java面试之IO (3) IO模型

当应用程序发起 IO 系统调用后,会经历两个阶段:

  • 内核等待 I/O 设备准备好数据。
  • 内核将数据从内核空间拷贝到用户空间。

BIO

BIO(Blocking IO)属于同步阻塞 IO,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。BIO 因为应用程序一直阻塞,直到系统调用返回,所以效率不高。

图源:《深入拆解Tomcat & Jetty》

NIO

NIO(Non-blocking/New IO),可以看作是多路复用 IO,Java 在 1.4 中引入 java.nio 包,提供了 Channel , SelectorBuffer 等抽象。

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

img

NIO 核心组件

Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。

Channel(通道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。

Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。

Buffer、Channel和Selector三者之间的关系

类比于 Golang:

  • Channel 和 Buffer:可以看作是 channel 和 channel 的缓冲区。
  • Selecter:go select

Buffer

在 Java nio 库中,所有数据都是用缓冲区处理的。使用 NIO 在读写数据时,都是通过缓冲区进行操作。

Buffer 的子类如下图所示。其中,最常用的是 ByteBuffer,它可以用来存储和操作字节数据。

Buffer 的子类

Buffer 有四个成员变量:

  • 容量 capacity:Buffer 可以存储的最大容量,创建时设置且不可改变。
  • 界限 limit:Buffer 中可以读/写数据的边界。写模式下,limit 表示最多能写入的数据;读模式下,limit 表示最多能读取的数据。
  • 位置 position:下一个可以被读写的数据的位置。从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。从读模式到写模式(clear),position 会归零,limit 设置为 capacity。
  • 标记 mark:Buffer 允许将位置直接定位到该标记处,这是一个可选属性

Buffer 有读模式和写模式这两种模式,分别用于从 Buffer 中读取数据或者向 Buffer 中写入数据。Buffer 被创建之后默认是写模式,调用 flip 可以切换到读模式。如果要再次切换回写模式,可以调用 clear 或者 compact 方法。

position 、limit 和 capacity 之前的关系

Buffer 对象不能通过 new 调用构造方法创建对象 ,只能通过静态方法实例化 Buffer

1
2
3
4
// 分配堆内存
public static ByteBuffer allocate(int capacity);
// 分配直接内存
public static ByteBuffer allocateDirect(int capacity);

Channel

Channel 是一个通道,它建立了与数据源(如文件、网络套接字等)之间的连接。Channel 和 Buffer 交互,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。

通道与流的不同之处在于通道是双向的,它可以用于读、写或者同时用于读写。

Channel 的子类如下所示:

Channel 的子类

其中,最常用的是以下几种类型的通道:

  • FileChannel:文件访问通道;
  • SocketChannelServerSocketChannel:TCP 通信通道;
  • DatagramChannel:UDP 通信通道;

Channel 最核心的两个方法:

  1. read :读取数据并写入到 Buffer 中。
  2. write :将 Buffer 中的数据写入到 Channel 中。
1
2
3
4
RandomAccessFile reader = new RandomAccessFile("/Users/guide/Documents/test_read.in", "r"))
FileChannel channel = reader.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);

Selector

Selector(选择器) 是 NIO 中的一个关键组件,它允许一个线程处理多个 Channel

Selector 是基于事件驱动的 I/O 多路复用模型,主要运作原理是:通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。

Selector 选择器工作示意图

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

Selector 可以监听以下四种事件类型:

  1. SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于 ServerSocketChannel
  2. SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于 SocketChannel
  3. SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读。
  4. SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。

Selector 是抽象类,可以通过调用此类的 open() 静态方法来创建 Selector 实例。Selector 可以同时监控多个 SelectableChannelIO 状况,是非阻塞 IO 的核心。

一个 Selector 实例有三个 SelectionKey 集合:

  1. 所有的 SelectionKey 集合:代表了注册在该 Selector 上的 Channel,这个集合可以通过 keys() 方法返回。
  2. 被选择的 SelectionKey 集合:代表了所有可通过 select() 方法获取的、需要进行 IO 处理的 Channel,这个集合可以通过 selectedKeys() 返回。
  3. 被取消的 SelectionKey 集合:代表了所有被取消注册关系的 Channel,在下一次执行 select() 方法时,这些 Channel 对应的 SelectionKey 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。

Selector 还提供了一系列和 select() 相关的方法:

  • int select():监控所有注册的 Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应的 SelectionKey 加入被选择的 SelectionKey 集合中,该方法返回这些 Channel 的数量。
  • int select(long timeout):可以设置超时时长的 select() 操作。
  • int selectNow():执行一个立即返回的 select() 操作,相对于无参数的 select() 方法而言,该方法不会阻塞线程。
  • Selector wakeup():使一个还未返回的 select() 方法立刻返回.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioSelectorExample {

public static void main(String[] args) {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));

Selector selector = Selector.open();
// 将 ServerSocketChannel 注册到 Selector 并监听 OP_ACCEPT 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
int readyChannels = selector.select();

if (readyChannels == 0) {
continue;
}

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();

if (key.isAcceptable()) {
// 处理连接事件
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);

// 将客户端通道注册到 Selector 并监听 OP_READ 事件
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);

if (bytesRead > 0) {
buffer.flip();
System.out.println("收到数据:" +new String(buffer.array(), 0, bytesRead));
// 将客户端通道注册到 Selector 并监听 OP_WRITE 事件
client.register(selector, SelectionKey.OP_WRITE);
} else if (bytesRead < 0) {
// 客户端断开连接
client.close();
}
} else if (key.isWritable()) {
// 处理写事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());
client.write(buffer);

// 将客户端通道注册到 Selector 并监听 OP_READ 事件
client.register(selector, SelectionKey.OP_READ);
}

keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

NIO 直接读取 零拷贝

直接读取

MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的⼀种实现,底层实际是调用了 Linux 内核的 mmap 系统调用。

它可以将一个文件或者文件的一部分映射到内存中,形成一个虚拟内存文件,这样就可以直接操作内存中的数据,而不需要通过系统调用来读写文件。

1
2
3
4
5
6
7
8
9
10
private void loadFileIntoMemory(File xmlFile) throws IOException {
FileInputStream fis = new FileInputStream(xmlFile);
// 创建 FileChannel 对象
FileChannel fc = fis.getChannel();
// FileChannel.map() 将文件映射到直接内存并返回 MappedByteBuffer 对象
MappedByteBuffer mmb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
xmlFileBuffer = new byte[(int)fc.size()];
mmb.get(xmlFileBuffer);
fis.close();
}

零拷贝

FileChanneltransferTo()/transferFrom()是 NIO 基于发送文件(sendfile)这种零拷贝方式的提供的一种实现,底层实际是调用了 Linux 内核的 sendfile系统调用。

它可以直接将文件数据从磁盘发送到网络,而不需要经过用户空间的缓冲区

AIO

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

img