Java Reactor 同步多路复用模型详解

作者注:说起网络 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 有很多实现类,例如:ByteBufferCharBufferLongBuffer 等,分别用于处理不同的数据类型,以提高性能。

缓冲区对象创建

以上所有类型的 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 多路复用器,比如 selectpollepoll 等,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();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Reactor多线程模型是一种在单个Reactor架构下引入线程池的模式,用于处理一些非IO操作的事件,如计算、编解码任务等。虽然这种模式在效率上有一定提升,但在面对瞬间的高并发连接场景时,性能仍然不佳。\[1\] 主从Reactor多线程模型是一种相对复杂的模式,需要封装多个模块,如主反应器(MainReactor)、子反应器(SubReactor)、接受者(Acceptor)、处理者(Handler)和线程池(Worker)。这种模型的编程实现较为复杂。\[2\] 单个主Reactor多个从Reactor多线程模型是主从Reactor模型的一种变体,它包含了主Reactor和多个从Reactor,以及多个线程。这种模型的元素较为复杂,但可以提高并发处理能力。\[3\] 综上所述,单Reactor多线程模型和主从Reactor多线程模型都是用于提高并发处理能力的模式,但在实现上有一定的差异。 #### 引用[.reference_title] - *1* [IO多路复用Reactor模型](https://blog.csdn.net/qq_42290561/article/details/125859442)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [【Netty】主从反应器 ( Reactor ) 多线程模型](https://blog.csdn.net/han1202012/article/details/106489252)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值