【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式

【0】传统阻塞 I/O 服务模型

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第1张图片

特点

1. 采用阻塞式 I/O 模型获取输入数据;

2. 每个连接都需要独立的线程完成数据输入,业务处理,数据返回的完整操作;

存在问题

1. 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大;

2. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费;

【1】Reactor 模式

I/O 复用结合线程池

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第2张图片

Reactor 模式中有 2 个关键组成 :

1. Reactor : Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应;

2. Handlers : 处理程序执行 I/O 事件要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作;

Reactor 模式构成示意图

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第3张图片

EventHandler 抽象类表示 IO 事件处理器,它拥有 IO 文件句柄 Handle(通过get_handle获取),以及对 Handle 的操作handle_event(读/写等)方法;

Concrete EventHandler 继承于 EventHandler 可以对事件处理器的行为进行定制处理;

Reactor 类用于管理 EventHandler(注册、删除等),并使用 handle_events 实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数 select,只要某个文件句柄被激活(可读/写等),select 就返回(阻塞),handle_events 就会调用与文件句柄关联的事件处理器的 handle_event 进行相关操作;

Reactor 处理时序示意图

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第4张图片

通过 Reactor 的方式,可以将用户线程轮询 IO 操作状态的工作统一交给 handle_events 事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而 Reactor 线程负责调用内核的 select 函数检查 socket 状态。当有 socket 被激活时,则通知相应的用户线程(或执行用户线程的回调函数),执行 handle_event 进行数据读取等处理的工作。由于 select 函数是阻塞的,因此多路 I/O 复用模型也被称为异步阻塞 I/O 模型。注意,这里的所说的阻塞是指 select 函数执行时线程被阻塞,而不是指 socket。一般在使用 I/O 多路复用模型时,socket 都是设置为 NONBLOCK 的,不过这并不会产生影响,因为用户发起 I/O 请求时,数据已经到达了,用户线程一定不会被阻塞。

Reactor 模式的工作流程

  1. 主线程往 epoll 内核事件表中注册socket上的读就绪事件;
  2. 主线程调用 epoll_wait 等待 socket 上有数据可读;
  3. 当 socket 上有数据可读时,epoll_wait 通知主线程,主线程则将 socket 可读事件放入请求队列;
  4. 睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll 内核事件表中注册该 socket 上的写就绪事件;
  5. 主线程调用 epoll_wait 等待 socket 可写;
  6. 当 socket 可写时,epoll_wait 通知主线程,主线程将 socket 可写事件放入请求队列;
  7. 睡眠在请求队列上的某个工作线程(工作线程从请求队列读取事件后,根据事件的类型来决定如何处理它,没有必要区分读工作线程和写工作线程)被唤醒,它往 socket 上写入服务器处理客户请求的结果;

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第5张图片

根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现 :

1. 单 Reactor 单线程

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第6张图片

2. 单 Reactor 多线程

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第7张图片

3. 主从 Reactor 多线程

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第8张图片

【2】Proactor 模式

Proactor 模型

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第9张图片

详细方案

1. Proactor Initiator 创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 AsyOptProcessor (Asynchronous Operation Processor) 注册到内核;

2. AsyOptProcessor 处理注册请求,并处理 I/O 操作;

3. AsyOptProcessor 完成 I/O 操作后通知 Proactor;

4. Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理;

5. Handler 完成业务处理;

Proactor 模式构成示意图

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第10张图片

Proactor 模式中,用户线程将 AsynchronousOperation(读/写等)、Proactor 以及操作完成时的 CompletionHandler 注册到 AsynchronousOperationProcessor;
AsynchronousOperationProcessor 使用 Facade 模式提供了一组异步操作 API(读/写等)供用户使用,当用户线程调用异步 API后,便继续执行自己的任务,AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,从而实现真正的异步;
当异步IO操作完成时,AsynchronousOperationProcessor 将用户线程与 AsynchronousOperation 一起注册的 Proactor 和 CompletionHandler 取出,然后将 CompletionHandler 与 IO 操作的结果数据一起转发给 Proactor,Proactor 负责回调每一个异步操作的事件完成处理函数 handle_event;虽然 Proactor 模式中每个异步操作都可以绑定一个 Proactor 对象,但是一般在操作系统中,Proactor 被实现为 Singleton 模式,以便于集中分发操作完成事件;

Proactor 处理时序示意图

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第11张图片

异步 I/O 模型中,用户线程直接使用内核提供的异步 I/O  API 发起 read 请求,且发起后立即返回,继续执行用户线程代码;此时用户线程已经将调用的 AsynchronousOperation 和 CompletionHandler 注册到内核;
操作系统开启独立的内核线程去处理 I/O 操作,当 read 请求的数据到达时,由内核负责读取 socket 中的数据,并写入用户指定的缓冲区中;
内核将 read 的数据和用户线程注册的 CompletionHandler 分发给内部 Proactor,Proactor 将 I/O 完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数),完成异步 I/O;

Procator 模式的工作流程

  1. 主线程调用 aio_read 向内核注册 socket 上的读完成事件,并告诉内核用户缓冲区的位置,以及读操作完成时如何通知应用程序(可以用信号);
  2. 主线程继续处理其他逻辑;
  3. 当 socket 上的读数据被读入用户缓冲区后,内核向应用进程发送一个信号,已通知应用程序数据已经可用;
  4. 应用进程预先定义好的信号处理函数选择一个工作线程来处理处理客户请求,工作线程处理完客户请求之后,调用 aio_write 向内核注册 socket 的完成写事件,并告诉内核用户写缓冲区的位置,以及操作完成时如何通知应用程序(可以用信号);
  5. 主线程继续处理其他逻辑;
  6. 当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,已通知应用程序数据已经发送完毕;
  7. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket;

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第12张图片

模拟 Proactor 模式

目前操作系统对异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式;

  1. 主线程往 epoll 内核事件表上注册socket上的读就绪;
  2. 主线程调用 epoll_wait 等待 socket 上有数据可读;
  3. 当 socket 上有数据可读时,epoll_wait 通知主线程,主线程从 socket 上循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入到请求队列;
  4. 睡眠在请求队列上的某个工作线程被唤醒,他获得请求对象并处理客户请求,然后往 epoll 内核事件表中注册 socket 上的写就绪;
  5. 主线程调用 epoll_wait 等待 socket 可写;
  6. 当 socket 可写时,epoll_wait 通知主线程,主线程往 socket 上写入服务器处理客户请求结果;

【Linux网络编程】高并发服务器编程 -- Reactor模式与Proactor模式_第13张图片

参考
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。

【1】UNIX网络编程

【2】Linux 高性能服务器编程

【3】Proactor模式&Reactor模式详解

【4】高性能网络编程(六):一文读懂高性能网络编程中的线程模型

你可能感兴趣的:(网络编程)