目录
作者注:说起网络 I/O 模型,人们往往脱口而出阻塞、非阻塞、同步、异步等概念,甚至有人还把异步与非阻塞混淆。实际上阻塞、非阻塞跟同步、异步是两个完全不同的概念,它们之间没有任何关联。
- 所谓阻塞、非阻塞在操作系统看来仅仅是一个标志位的事,同步模型可以非阻塞,异步模型也可以阻塞,而且在 I/O 没有数据需要处理的时候,阻塞是最佳的选择,所以一定要摒弃“非阻塞一定更牛逼”这种错误思想!
- I/O 领域所谓的同步和异步,是内核中的概念,这里要与我们常说的“异步业务”区分开,它是需要内核通过大量的编码提供支持的(实现起来比非阻塞复杂的多),比如 io_uring 要在 linux 平台实现异步操作,给内核提交了大量的补丁,且需要通过编译选项启用功能。)
网络 I/O 模型是计算机网络编程中用于描述网络通信过程的一种抽象概念,它定义了在进行网络数据传输时,网卡与 Socket 监听线程之间的交互方式,不同的网络 I/O 模型适用于不同的应用场景和需求。
同步多路复用 I/O 模型
同步多路复用 I/O 是从 JDK 1.4 开始支持的新型同步 I/O 类库 (java.nio)。其核心架构如下:
它引入了诸如 Channel、Selector 和 Buffer 等概念,这些概念有效地封装了底层操作系统的多路复用 I/O 模型。java.nio 使用 select/poll 模型,通过 Selector
对象批量监控多个 I/O 事件,从而避免了每个线程独自处理 Socket 所带来的问题。而且自 JDK 1.5 起还支持了 epoll,在 Linux 系统上进一步提升了 I/O 性能和效率。
Buffer (缓冲区)
Buffer
本质上是可读可写的内存块,它提供了简化内存操作的方法,并通过属性记录缓冲区的状态变化。
Buffer 类及其子类
由图可知,Buffer
有很多实现类,例如:ByteBuffer
、CharBuffer
、LongBuffer
等,分别用于处理不同的数据类型,以提高性能。
缓冲区对象创建
以上所有类型的 Buffer
都支持以下两种方法创建:
方法名 | 说明 |
---|---|
allocate() | 创建一个新 buffer |
wrap(double[] array, ...) | 根据现有内容创建一个缓冲区 |
其中 ByteBuffer
比较特殊,它有两种不同的缓冲区:
- 直接缓冲区:在系统内核缓冲中分配的缓冲区,通过
allocateDirect()
方法分配,可以直接操作 JVM 堆外内存; - 非直接缓冲区:普通的 JVM 堆内缓冲区,通过
allocate()
方法分配。
向缓冲区添加数据
方法名 | 说明 |
---|---|
XxxBuffer put(..) | 向各类 Buffer 中添加数据 |
int position() /Buffer position(int newPosition) | Buffer 基类规定的方法,用于获得当前要操作的索引/修改当前要操作的索引位置 |
int limit() /Buffer limit(int newLimit) | Buffer 基类规定的方法,用于查询最多能操作到哪个索引/修改最多能操作的索引位置 |
int capacity() | Buffer 基类规定的方法,返回缓冲区的总长度 |
int remaining() /boolean hasRemaining() | Buffer 基类规定的方法,查询还有多少能操作的索引/查询是否还能操作 |
读取缓冲区数据
方法名 | 说明 |
---|---|
get() | 读取一个单位类型数据 |
flip() | 反转缓冲区,将 limit 设置为 position ,再将 position 设为 0 常用于写入数据后将 Buffer 切换为读模式 |
get(int index) | 读指定索引处的单位数据 |
rewind() | 将 position 置为 0,用于重复读取 |
clear() | 初始化缓冲区,将 position 设为 0,limit 设置为最大容量capacity ,同时保留 Buffer 内的数据 常用于读取数据后将 Buffer 切换为写模式 |
array() | 将缓冲区转换成数组 char[] 返回 |
Channel (通道)
Channel
是一个全双工读写通道,同时支持阻塞和非阻塞模式。它类似于 I/O 流,但也有一些不同之处:
Channel
可读可写全双工,而流一般来说是单向的,需要区分输入流和输出流;Channel
支持异步读写;Channel
总是基于 Buffer 读写。
java.nio 提供了四类 Channel
,分别是:
XxxFileChannel
:用于文件操作;XxxSocketChannel
:用于客户端 TCP 操作;XxxServerSocketChannel
:用于服务端 TCP 操作;DatagramChannel
:用于 UDP 操作。
Selector (选择器)
Selector
用于持续轮询注册在其上的 Channel
,以选择并分发已处理的就绪事件。同步多路复用 I/O 模型里的事件有以下四种:
- 连接事件;
- 接收事件;
- 可读事件;
- 可写事件。
Selector
可以同时轮训和监控多个 Channel
,当 Selector
发现某个 Channel
的数据状态发生变化时,会通过 SelectorKey
触发相关事件,并由监听此事件的事件处理器来执行相关逻辑。其常用 API 如下:
-
Selector
抽象类:方法名 说明 Selector open()
获取一个 Selector 对象 int select()
阻塞监控所有注册的 Channel,当有对应事件发生,会将 SelectorKey
放入集合内部并返回事件数量int select(long timeout)
带超时的阻塞监听 selectedKeys()
返回存有 SelectorKey
的集合 -
SelectionKey
抽象类方法名 说明 对应事件属性 isAcceptable()
是否是连接继续事件 SelectionKey.OP_ACCEPT
isConnectable()
是否是连接就绪事件 SelectionKey.OP_CONNECT
isReadable()
是否是可读事件 SelectionKey.OP_READ
isWritable()
是否是可写事件 SelectionKey.OP_WRITE
Reactor 同步多路复用模型
Reactor 即“反应器”,是应用最广泛的一种 I/O 多路复用技术。当需要等待 I/O 操作时,首先释放资源。一旦等待完成,便通过事件驱动的方式继续进行后续工作。以下是 Reactor 模型中的五个重要角色:
- Handle (句柄或描述符):它是资源在操作系统层面的一种抽象,表示与事件绑定了的资源,即各种
SocketChannel
。 - Synchronous Event Demultiplexer (同步事件分发器):Handle 代表的事件会被注册到同步事件分发器上,当事件就绪时,Demultiplexer 会将就绪的事件提交给 Reactor。
- Demultiplexer 的本质是一个系统调用,用于等待事件的发生。调用方在调用它后会被阻塞,一直阻塞到 Demultiplexer 上有事件就绪为止。
- 在 Linux 中,同步事件分发器指的就是 I/O 多路复用器,比如
select
、poll
、epoll
等,Java NIO 中的Selector
就是对多路复用器的封装。
- Reactor (反应器):事件管理的接口,内部使用 Synchronous Event Demultiplexer 注册、注销 Event Handler,当有事件进入"就绪"状态时,调用注册事件的回调函数处理事件。
- Event Handler (事件处理器接口):事件处理程序提供了一组接口,在 Reactor 监听到相应的事件发生时调用,执行相应的事件处理。
- 比如当 Channel 被注册到 Selector 时的回调方法、连接事件发生时的回调方法、写事件发生时的回调方法等都是事件处理器,我们可以实现这些回调来达到对某一事件进行特定反馈的目的。
- 原生的 Java 并不支持 Event Handler,实际业务中需要自己实现,或使用 Netty 等网络框架。
- Concrete Event Handler (事件处理器实现):它是 Event Handler 的实现类,用于实现回调方法指定的业务逻辑。
Reactor 模型有三种模型,分别是:单 Reactor 单线程模型、单 Reactor 多线程模型和主从 Reactor 多线程模型。
单 Reactor 单线程模型
单 Reactor 单线程模型指设计中只有一个 Reactor,无论是与 I/O 读写相关,还是与 I/O 无关的编解码和计算,都在一个线程上完成。其架构图如下所示:
在上图中:
- Acceptor 专门处理连接事件,而 Selector 则充当同步事件分发器。
- 客户端的请求可以分为连接请求和其他事件请求两种。
- Selector 上注册了一系列的 Channel,它不断监听这些 Channel。
- 一旦某个 Channel 上的事件处理器就绪,Selector 就会将该事件分发给事件处理器。
该模型仅依靠单线程处理请求,主循环承担了太多的任务,容易在高并发情境下造成请求积压甚至超时。此外,单线程无法有效利用多核资源。因此,更合适的做法是为解码、计算和编码操作引入额外的线程,并使用线程池进行管理。
单 Reactor 多线程模型
单 Reactor 多线程模型是指仅有一个线程负责执行 I/O 操作和处理连接请求,其他逻辑均由 Worker 线程执行。其架构图如下:
与第一种模型相比,单 Reactor 多线程模型将业务逻辑委托给线程池来处理,从而可以更有效地利用多核 CPU 资源。然而,单个线程的 Reactor 仍负责监听和响应所有事件,这在高并发环境下仍可能产生性能瓶颈。因此,主从 Reactor 多线程模型应运而生。
主从 Reactor 多线程模型
在客户端连接众多且频繁进行 I/O 操作的情况下,单 Reactor 模型就会暴露出问题。因为 Reactor 不支持异步 I/O 操作,这意味着当 Reactor 处理读写事件时,其他客户端的连接操作可能无法得到及时处理。主从 Reactor 多线程模型就是专门用来解决这个问题的:
该模型将处理连接事件的 Reactor 与处理读写事件的 Reactor 分离,避免了读写事件较为频繁的情况下影响新客户端连接。
主从 Reactor 多线程模型中存在多个 Reactor,Main-Reactor 一般只有一个,它负责监听和处理连接请求;而 Sub-Reactor 可以有多个,用线程池进行管理,主要负责监听和处理读写事件等。当然 Main Reactor 也可以多个,也通过线程池管理,但是这样会增加系统复杂度,需要合理规划调度,否则反而会拖累性能。
单 Reactor 单线程模型代码示例
服务端代码:
public class ReactorServer {
private final Selector selector;
public ReactorServer() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(1234));
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
public class Reactor {
public void run() {
try {
// Reactor 循环
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
// 处理连接事件
handleAcceptEvent(selectionKey);
} else if (selectionKey.isReadable()) {
// 处理可读事件
handleReadEvent(selectionKey);
} else if (selectionKey.isWritable()) {
// 处理可写事件
handleWriteEvent(selectionKey);
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 连接事件处理器
private void handleAcceptEvent(SelectionKey selectionKey) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel clientChannel = serverSocketChannel.accept();
if (clientChannel != null) {
clientChannel.configureBlocking(false);// 设置非阻塞
// 监听可读事件
clientChannel.register(selector, SelectionKey.OP_READ);
}
}
// 可读事件处理器
private void handleReadEvent(SelectionKey selectionKey) throws IOException {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
int count = clientChannel.read(receiveBuffer);
if (count > 0) {
String context = new String(receiveBuffer.array(), 0, count);
System.out.println("Received from client: " + context);
// 读取成功后监听可写事件
selectionKey.interestOps(SelectionKey.OP_WRITE);
}
}
// 可写事件处理器
private void handleWriteEvent(SelectionKey selectionKey) throws IOException {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
ByteBuffer sendBuffer = ByteBuffer.wrap(("Hello client!").getBytes());
clientChannel.write(sendBuffer);
// 写回后,继续监听可读事件
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
public static void main(String[] args) throws IOException {
ReactorServer server = new ReactorServer();
System.out.println("Server start ...");
Reactor reactor = server.new Reactor();
reactor.run();
}
}
客户端代码:
public class TestReactorClient {
private final String serverHost;
private final int serverPort;
private Selector selector;
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private SocketChannel clientChannel;
public TestReactorClient(String serverHost, int serverPort) {
this.serverHost = serverHost;
this.serverPort = serverPort;
}
public void run() throws IOException, InterruptedException {
connect();
Thread.sleep(1000);
sendMsg();
executorService.shutdown();
}
private void connect() throws IOException {
clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
selector = Selector.open();
clientChannel.register(selector, SelectionKey.OP_CONNECT);
clientChannel.connect(new InetSocketAddress(serverHost, serverPort));
selector.select();
clientChannel.finishConnect();
selector.selectedKeys().clear();
clientChannel.register(selector, SelectionKey.OP_READ);
executorService.execute(this::handleEvent);
}
private void handleEvent() {
try {
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isReadable()) {
ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
int count = clientChannel.read(receiveBuffer);
if (count > 0) {
String context = new String(receiveBuffer.array(), 0, count);
System.out.println("Received from server: " + context);
}
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void sendMsg() throws IOException {
ByteBuffer sendBuffer = ByteBuffer.wrap("Hello server!".getBytes());
clientChannel.write(sendBuffer);
System.out.println("Sent to server: Hello server!");
}
public static void main(String[] args) throws IOException, InterruptedException {
TestReactorClient client = new TestReactorClient("127.0.0.1", 1234);
client.run();
}
}
- 参考资料: Java I/O 模型详解。