目录
0x01 服务器编程基本框架
0x02 两种高效的事件处理模式
Reactor 模式
Proactor 模式
模拟Proactor 模式
0x01 服务器编程基本框架
虽然服务器程序的种类繁多,但是其基本框架都是一样的,不同之处是在于处理逻辑。对于我们在这个服务器的搭建可以分为解析-读取-响应三个阶段。
-
I/O处理单元:处理客户连接,读写网络数据。它通常要完成以下工作:等待并接受新的客户连接,接收客户数据,将服务器响应数据返回给客户端。但是数据的收发不一定在 I/O 处理单元中执行,也可能在逻辑单元中执行,具体在何处执行取决于事件处理模式。
-
逻辑单元:业务进程或线程。通常是进程或线程。它分析并处理客户数据,然后将结果传递给 I/O 处理单元或者直 接发送给客户端(具体使用哪种方式取决于事件处理模式)。服务器通常拥有多个逻辑单元,以实现对多个客户任务的并发处理。
-
网络存储单元:可以是数据库、缓存和文件,但不是必须的。
-
请求队列:是各单元之间的通信方式的抽象。I/O 处理单元接收到客户请求时,需要以某种方式通知一个 逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件。请求队列通常被实现为池的一部分。(进程池、线程池)
对于服务器模型,常见的有C/S模型,也就是常见的TCP服务器与客户端的工作流程,适合在资源相对集中的场合,服务器是通信中心,当访问量过大的适合,可能所有的客户都将面对很慢的响应。还有一种P2P模型,这种模型的每台机器既是主机也是服务器,当用户之间传输请求过多时,网络的负载将加重。
对于两种高效的并发模式:并发模式是指I/O处理单元和多个逻辑单元之间协调完成任务的方法。两种并发编程模式:半同步/半异步模式、领跑者/追随者模式。
半同步/半异步模式
-
同步:程序完全按照代码序列的顺序执行——同步线程
-
异步:程序的执行需要有系统事件来驱动,如中断、信号等——异步线程
在半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理I/O事件。异步线程监听到客户请求后,将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理请求对象。
如果结合考虑两种事件处理模式和几种I/O模型,半同步/半异步模式就存在多种变体:
-
半同步/半反应堆模式:异步线程只有主线程,同步线程为多个工作线程
-
高效的半同步/半异步模式:主线程只负责监听socket,连接socket由工作线程来管理
领跑者/追随者模式
-
领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件。
-
在任意的时间点,程序都仅有一个领导者进程,它负责监听I/O事件;其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者。
-
当前的领导者如果检测到I/O事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件。此时,新的领导者等待新的I/O事件而原来的领导者则处理I/O事件,二者实现了并发。
需要的组件有如下:
-
句柄集(HandleSet):句柄I/O资源,通常为一个文件描述符;句柄集使用wait_for_event监听I/O事件等
-
线程集(ThreadSet):所有工作进程的管理者,负责各线程之间的同步,以及新领导者的推选;三种状态:Leader,Processing 和Follower
-
事件处理器(EventHander)和 (ConcreteEventHandler):包含一个或多个回调函数handle_event
缺点:领导者/追随者模式仅支持一个事件源集合,无法让每个工作线程独立的管理多个客户连接
0x02 两种高效的事件处理模式
服务器程序通常需要处理三类事件:I/O 事件、信号及定时事件。有两种高效的事件处理模式:Reactor 和 Proactor:
-
同步 I/O 模型通常用于实现 Reactor 模式。
-
异步 I/O 模型通常用于实现 Proactor 模式。
其中异步IO的模式可以使用同步IO来进行模拟得出。
Reactor 模式
在这里只考虑线程,在这里要使用多线程进行并发处理,在这里有主线程以及子线程,要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元),将 socket 可读可写事件放入请求队列,交给工作线程处理(子线程)。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。(只负责监听)
主要的工作流程如下:
使用同步 I/O(以 epoll_wait 为例)实现的 Reactor 模式的工作流程是:
-
主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
-
主线程调用 epoll_wait 等待 socket 上有数据可读。
-
当 socket 上有数据可读时, epoll_wait 通知主线程。主线程则将 socket 可读事件放入请求队列。
-
睡眠在请求队列上的某个工作线程被唤醒,它从 socket 读取数据,并处理客户请求,然后往 epoll内核事件表中注册该 socket 上的写就绪事件。
-
当主线程调用 epoll_wait 等待 socket 可写。
-
当 socket 可写时,epoll_wait 通知主线程。主线程将 socket 可写事件放入请求队列。
-
睡眠在请求队列上的某个工作线程被唤醒,它往 socket 上写入服务器处理客户请求的结果。
在这里,主进程只负责监听以及工作交互,子线程负责对事件进行读写。
Proactor 模式
Proactor 模式将所有 I/O 操作都交给主线程和内核来处理(进行读、写),工作线程仅仅负责业务逻辑。使用异步 I/O 模型(以 aio_read 和 aio_write 为例)实现的 Proactor 模式的工作流程是:
区别在于数据是否在主线程中进行处理,也就是同步与异步的区别,直接得到了数据的读写结果。
-
主线程调用 aio_read 函数向内核注册 socket 上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例)。
-
主线程继续处理其他逻辑。
-
当 socket 上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
-
应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求后,调用 aio_write 函数向内核注册 socket 上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序。
-
主线程继续处理其他逻辑。
-
当用户缓冲区的数据被写入 socket 之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
-
应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。
模拟Proactor 模式
使用同步 I/O 方式模拟出 Proactor 模式。原理是:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一”完成事件“。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。
使用同步 I/O 模型(以 epoll_wait为例)模拟出的 Proactor 模式的工作流程如下:
-
主线程往 epoll 内核事件表中注册 socket 上的读就绪事件。
-
主线程调用 epoll_wait 等待 socket 上有数据可读。
-
当 socket 上有数据可读时,epoll_wait 通知主线程。主线程从 socket 循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
-
睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事件表中注册 socket 上的写就绪事件。
-
主线程调用 epoll_wait 等待 socket 可写。
-
当 socket 可写时,epoll_wait 通知主线程。主线程往 socket 上写入服务器处理客户请求的结果。
其Reactor模式与Proactor模式做个简单的区分主要在于如下:
Reactor模式——当事件就绪,通知工作线程。
Proactor模式——当事件就绪,内核完成读写后,通知工作线程。