写点什么

【后端开发】Reactor 模型详解

作者:C++后台开发
  • 2022 年 7 月 28 日
  • 本文字数:4469 字

    阅读完需:约 15 分钟

研究背景

其实我们在研究 netty 的时候我们必定绕不过 NIO 的,也必定必须研究一下这个 Reactor 模型的,如果不进行这个 Reactor 模型和 NIO 知识点的研究,那么我们必定掌握不了 Netty 的精髓,为什么呢?

  1. 因为 Netty 底层封装的就是 NIO 的代码,如果 NIO 的三大组件比如 channel、buffer、以及 selector 不搞清楚的话那么指定是搞不懂 Netty 的,即使掌握了也是 API 层面的

  2. Reactor 模型简直是太经典了,Netty 的模型是三种经典的 Reactor 模型演化过来的,而且不仅仅是 Netty 有这个模型,Redis、Nginx 等有名的中间件都是借鉴了这个模型的思想

Reactor 模型

核心思想

Reactor 模型的核心是 Reactor 加上对应的处理器 Handler,Reactor 在一个单独的线程中运行,负责监听和分发事件,将接收到的事件交给不同的 Handler 来处理,Handler 是处理程序执行 I/O 事件的实际操作

基础类型

我们先说说基础的客户端服务端传统模型,这里 BIO 是最原生的代表,也是因为效率比较低下之后衍生出来了 NIO 的模型

BIO 模型

经典的类型就是 BIO 模型,一个客户端过来进行请求连接,那么服务端就需要进行创建一个线程进行处理链接请求,这种就是少量的客户端的话还可以,如果当大量的客户端如果进行连接请求的话,那么就会造成服务端的线程资源紧缺,而且这个过程服务器和客户端两边都是阻塞的状态,而且传统的 BIO 模式还存在同步效率低的问题,如果建立了链接,服务端就傻等着客户端发来请求,如果没有请求过来,那么这个线程一直在阻塞着,就造成了资源的浪费

图解

案例代码

public class BIOServer {    public static void main(String[] args) {        try {            // 服务端监听端口8080            ServerSocket serverSocket = new ServerSocket(8080);            // 服务端接收客户端链接请求            Socket socket = serverSocket.accept();            new Thread(() -> {                try {                    byte[] bytes = new byte[1024];                    // 将信息从输入流读取到创建的byte数组中                    socket.getInputStream().read(bytes);                    String message = new String(bytes, CharsetUtil.UTF_8);                    System.out.println("客户端发送过来的信息是:" + message);                    byte[] byteWrite = "Hello Client".getBytes(CharsetUtil.UTF_8);                    // 返回信息给客户端                    socket.getOutputStream().write(byteWrite);                } catch (IOException e) {                    e.printStackTrace();                }            }).start();        } catch (IOException e) {            e.printStackTrace();        }    }}
复制代码

NIO 模型

上面的 BIO 模式就是效率低下的阻塞 IO,而 NIO 是基于事件驱动的 IO 模型,他这种方式就好很多了,他不会进行线程的阻塞,因为他是有一个专门负责事件轮询的 selector 选择器进行 channel 通道监听,如果有事件发生那么就进行相应的事件处理就可以了, 更多详情可以阅读我之前写的 NIO 系列的文章

【文章福利】另外小编还整理了一些 C++后台开发教学视频,相关面试题,后台学习路线图免费分享,需要的可以自行添加:Q群:720209036 点击加入~ 群文件共享

小编强力推荐 C++后台开发免费学习地址:C/C++Linux服务器开发高级架构师/C++后台开发架构师​

​图解

​案例代码

public class ChatServer {    public static void main(String[] args) throws Exception {        // 1. 创建选择器        Selector selector = Selector.open();        // 2. 创建服务端 channel        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();        // 3. 创建服务端的监听端口        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 9000));        // 4.设置serversocketchannel 是非阻塞的        serverSocketChannel.configureBlocking(false);        // 5. 将serversocketchannel注册到selector选择器上面,并将事件设置成连接事件        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);        // 监听就绪事件        while (true) {            System.out.println("等待......");            // 休眠1秒  无论是否有读写事件发生 selector每隔1秒被唤醒            int selected = selector.select(1000);            if (selected > 0) { // 证明有事件已经准备就绪                // 返回已经就绪的事件                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();                if (iterator.hasNext()) {                    SelectionKey key = iterator.next();                    // 获取socketChannel                    if (key.isAcceptable()) {                         // 连接事件就绪,将其感兴趣的事件设置成已读事件                        // 处理接入的新请求                        handleAccept(selector, key);                    }                    if (key.isReadable()) { // 已读事件就绪                        // 处理通道的读请求                        handleRead(key);                    }                    iterator.remove();// 将处理完的数据进行了移除                }            }        }
}
/** * 处理客户端读操作请求 */ private static void handleRead(SelectionKey key) { SocketChannel socketChannel = (SocketChannel) key.channel(); // 申请一个buffer ByteBuffer buffer = ByteBuffer.allocate(1024); // 将通道的数据读入到buffer中 try { socketChannel.read(buffer); } catch (IOException e) { e.printStackTrace(); } System.out.println("客户端发来消息: " + new String(buffer.array(), CharsetUtil.UTF_8)); }
/** * 处理连接操作 */ private static void handleAccept(Selector selector, SelectionKey key) { // 通过 ServerSocketChannel 监听过来连接的客户端事件 ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); //通过调用 accept 方法,返回一个具体的客户端连接管道 try { SocketChannel socketChannel = serverChannel.accept(); System.out.println("客户端 " + socketChannel.getRemoteAddress() + "已上线......"); // 将channel 注册到selector 上面,而且需要设置成是非阻塞的 socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } }}
复制代码

这种方式就可以通过一个线程来进行接收客户端的所有链接请求,之后监听所有的链接通道 channel,如果有相应事件发生那么就进行对应的相应事件处理,比如读事件、连接请求事件等等

单 Reactor 单线程模型

生活中的例子

酒店的前台,当前的这种情况就是前台和服务员是同一个人,全程一个人进行服务,效率会非常的低下,后面新来的客人只能在大厅等待了,客户的体验也不好

模型详解

上面的 NIO 代码就是单 Reactor 单线程模型的,确实是一个 selector 监听轮询所有的 channel 不假,但是如果真正的多数据量处理读写请求的时候他也是堵塞在那里等待着 handler 处理完才能进行处理下一个请求,所以这种场景只适合小数据量的处理,瞬间完成或者是毫秒级完成才能达到高效率缺点:

  1. 高并发复杂数据处理的时候效率不高性能低下,容易造成堵塞效果

  2. 由于是单线程所以发挥不出来多核心的效果

优点:

  1. 模型简单、不存在线程并发的时候造成数据不安全的问题

图解

单 Reactor 多线程模型

生活中的例子

此时就是一个前台接待员对应多个前台的服务员了,这样的话前台的接待员专门对接就是接待客人的任务,后面的工作任务都是其他服务员的,这样其他的客人来了能进行及时的接待,即使间隔比较短的来人,那么也是稍等一小会儿就可以了

模型详解

单线程模型其实就是进行数据逻辑处理的时候效率比较低下,那我们可以将单线程改成多线程,那么就是还是一个 Reactor 中的 selector 进行事件监听,之后 Acceptor 进行处理客户端的连接请求,创建一个 Handler 进行该连接请求的后续处理工作,但是这个 Hanlder 只是负责事件的响应操作,真正的业务逻辑处理还是直接交给了后续的线程池去处理,线程池将任务完成后返回给 Handler,之后 Handler 将处理好的结果返回给客户端缺点:

  1. 大并发上来的时候还是会存在性能瓶颈的问题

  2. 在并发场景下会存在数据安全性的问题

优点:

  1. 多线程可以充分的利用了系统的 CPU 资源

图解

主从 Reactor 多线程模型

生活中的例子

这种就是接待员只负责类似喊句话的操作,欢迎光临这种,之后就将其交给了其他的接待员进行处理了,比如订房间等等、之后剩下的工作任务交给其他的服务员,比如端茶倒水带领客户去对应的房间,这样客户体验感会更好,能处理客户的需求更快

模型详解

主从模式就是,Reactor 的主线程模型通过 selector 进行连接事件监听,收到的如果是连接事件的话,那么用 Acceptor 进行连接事件处理,之后将创建好的连接事件交给 Reactor 子线程进行处理【Reactor 主线程和 Reactor 子线程是一对多的关系】,此时子线程将连接加入到连接队列进行事件监听,如果发生了其他事件比如读事件,那么 Reactor 子线程就会调用相应的 Handler 进行事件处理,handler 进行数据读取后复杂的业务也是交给后面的线程池进行业务处理并返回结果,Handler 接收到处理结果后返回给客户端缺点:

  1. 高并发的时候依旧存在数据安全性问题

  2. 编码起来比较繁琐

优点:

  1. 能够处理高并发、吞吐量大、效率高、结构之间分工明确 netty 其实就是这种场景的演化

图解

​总结

Reactor 模型具有如下优点

  1. 响应速度快,不必为单个同步事件所阻塞,因为是事件轮询机制

  2. 可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销

  3. 扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源

  4. 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性

Reactor 模型具有如下缺点

  1. 相比传统的简单模型,Reactor 增加了一定的复杂性,因而有一定的门槛,并且不易于调试。想要掌握 netty 那么就必须掌握这个模型的机制。

参考资料

推荐一个零声教育 C/C++后台开发的免费公开课程,个人觉得老师讲得不错,分享给大家:C/C++后台开发高级架构师,内容包括Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,立即学习


原文:Reactor 模型详解 - 掘金

用户头像

还未添加个人签名 2022.05.06 加入

还未添加个人简介

评论

发布
暂无评论
【后端开发】Reactor 模型详解_reactor_C++后台开发_InfoQ写作社区