Linux高性能服务器编程学习笔记(五)

1、服务器模型

1)C/S模型

采用C/S模型的TCP服务器和TCP客户端的工作流程如下图所示:
在这里插入图片描述
C/S模型的逻辑很简单。服务器启动后,首先创建一个(或多个)监听 socket,并调用bind函数将其绑定到服务器感兴趣的端口上,然后调用 listen函数等待客户连接。服务器稳定运行之后,客户端就可以调用 connect函数向服务器发起连接了。

由于客户连接请求是随机到达的异步事件,服务器需要使用某种I/O模型来监听这一事件。I/O模型有多种,下图中,服务器使用的是I/O复用技术之一的 select系统调用。当监听到连接请求后,服务器就调用 accept函数接受它,并分配一个逻辑单元为新的连接服务。逻辑单元可以是新创建的子进程、子线程或者其他。下图中,服务器给客户端分配的逻辑单元是由fork系统调用创建的子进程。逻辑单元读取客户请求,处理该请求,然后将处理结果返回给客户端。客户端接收到服务器反馈的结果之后,可以继续向服务器发送请求,也可以立即主动关闭连接。如果客户端主动关闭连接,则服务器执行被动关闭连接。至此,双方的通信结束。需要注意的是,服务器在处理一个客户请求的同时还会继续监听其他客户请求,否则就变成了效率低下的串行服务器了(必须先处理完前一个客户的请求,才能继续处理下一个客户请求)。下图中,服务器同时监听多个客户请求是通过 select系统调用实现的。
在这里插入图片描述

2)P2P模型

P2P( Peer to Peer,点对点)模型比CS模型更符合网络通信的实际情况。它摒弃了以服务器为中心的格局,让网络上所有主机重新回归对等的地位。P2P模型如下图a所示。
在这里插入图片描述
图a所示的P2P模型存在一个显著的问题,即主机之间很难互相发现。所以实际使用的P2P模型通常带有一个专门的发现服务器,如图b所示。这个发现服务器通常还提供查找服务(甚至还可以提供内容服务),使每个客户都能尽快地找到自己需要的资源

2、服务器编程框架

在这里插入图片描述

模块单个服务器程序服务器机群
I/O处理单元处理客户连接,读写网络数据作为接入服务器,实现负载均衡
逻辑单元业务进程或线程逻辑服务器
网络存储单元本地数据库、文件或缓存数据库服务器
请求队列各单元之间的通信方式各服务器之间的永久TCP连接

I/O处理单元是服务器管理客户连接的模块。它通常要完成以下工作:
等待并接受新的客户连接,接收客户数据,将服务器响应数据返回给客户端。但是,数据的收发不一定在I/O处理单元中执行,也可能在逻辑单元中执行,具体在何处执行取决于事件处理模式。对于一个服务器机群来说,I/O处理单元是一个专门的接入服务器。它实现负载均衡从所有逻辑服务器中选取负荷最小的一台来为新客户服务。

一个逻辑单元通常是一个进程或线程。它分析并处理客户数据,然后将结果传递给IO处理单元或者直接发送给客户端(具体使用哪种方式取决于事件处理模式)。对服务器机群而言,一个逻辑单元本身就是一台逻辑服务器。服务器通常拥有多个逻辑单元,以实现对多个客户任务的并行处理。

网络存储单元可以是数据库、缓存和文件,甚至是一台独立的服务器,但它不是必须的。

请求队列是各单元之间的通信方式的抽象。IO处理单元接收到客户请求时,需要以某种方式通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件,请求队列通常被实现为池的一部分。对于服务器机群而言,请求队列是各台服务器之间预先建立的、静态的、永久的TCP连接。这种TCP连接能提高服务器之间交换数据的效率,因为它避免了动态建立ICP连接导致的额外的系统开销。

3、两种高效的事件处理模式

1)Reactor模式(同步I/O)

Reactor是这样一种模式,它要求主线程(l/O处理单元,下同)只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元,下同)。除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。

使用同步I/O模型(以 epoll wait为例)实现的 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上写入服务器处理客户请求的结果。
在这里插入图片描述

2)Proactor模式(异步I/O)

与 Reactor模式不同, Proactor模式将所有Io操作都交绐主线程和内核来处理,工作线程仅仅负责业务逻辑。

使用异步I/O模型(以 aio_read和 aio_write为例)实现的 Proactor模式的工作流程是
1)主线程调用 aio_read函数向内核注册 socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序。
2)主线程继续处理其他逻辑。
3)当 socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
4)应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用 alo write函数向内核注册 socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序
5)主线程继续处理其他逻辑。
6)当用户缓冲区的数据被写入 socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
7)应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭 socket。
在这里插入图片描述

3、模拟Proactor模式

1)同步和异步I/O

同步I/O模型要求用户代码自行执行I/O操作(将数据从内核缓冲区读人用户缓冲区,或将数据从用户缓冲区写入内核缓冲区)。
异步I/O机制则由内核来执行I/O操作(数据在内核缓冲区和用户缓冲区之间的移动是由内核在“后台”完成的)。
也可以这样认为,同步I/O向应用程序通知的是I/O就绪事件,而异步I/O向应用程序通知的是I/O完成事件。 Linux环境下,aio.h头文件中定义的函数提供了对异步I/O的支持。

2)使用同步I/O模拟Proactor模式

其原理是:主线程执行数据读写操作,读写完成之后,主线程向工作线程通知这一“完成事件”。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。
使用同步I/O模型(以 epoll_wait为例)模拟出的 Proactor模式的工作流程如下:
1)主线程往epoll内核事件表中注册 socket上的读就绪事件。
2)主线程调用 epoll_wait等待 socket上有数据可读。
3)当 socket上有数据可读时, epoll wait通知主线程。主线程从 socket 循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列。
4)睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll内核事件表中注册 socket上的写就绪事件。
5)主线程调用 epoll_wait等待 socket可写。
6)当 socket可写时, epoll_wait通知主线程。主线程往 socket上写人服务器处理客户请求的结果。
在这里插入图片描述

4、两种高效的并发模式

1)半同步/半异步模式

原型

半同步/半异步模式中的“同步”和“异步”与前面讨论的IO模型中的“同步”和“异步”是完全不同的概念。在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行:“异步指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。比如,下图中a描述了同步的读操作,而图b则描述了异步的读操作。
在这里插入图片描述
**半同步/半异步模式中,同步线程用于处理客户逻辑,相当于逻辑单元;异步线程用于处理I/O事件,相当于I/O处理单元。异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。**具体选择哪个工作线程来为新的客户请求服务,则取决于请求队列的设计。
在这里插入图片描述

半同步/半反应堆模式

下图中,异步线程只有一个,由主线程来充当。它负责监听所有 socket上的事件如果监听 socket上有可读事件发生,即有新的连接请求到来,主线程就接受之以得到新的连接 socket,然后往epoll内核事件表中注册该 socket上的读写事件。如果连接 socket上有读写事件发生,即有新的客户请求到来或有数据要发送至客户端,主线程就将该连接 socket插入请求队列中。所有工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争(比如申请互斥锁)获得任务的接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务。
在这里插入图片描述半同步/半反应堆模式存在如下缺点:
①主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费CPU时间。
②每个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量CPU时间。

高效半同步/半异步模式

下图中,主线程只管理监听 socket,连接 socket由工作线程来管理。当有新的连接到来时,主线程就接受之并将新返回的连接 socket派发给某个工作线程,此后该新 socket上的任何I/O操作都由被选中的工作线程来处理,直到客户关闭连接。主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新 socket上的读写事件注册到自己的 epoll内核事件表中。
在这里插入图片描述

2)领导者/追随者模式

领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任意时间点,程序都仅有一个领导者线程,它负责监听I/O事件。而其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到I/O事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件。此时,新的领导者等待新的I/O事件,而原来的领导者则处理I/O事件,二者实现了并发。
领导者/追随者模式包含如下几个组件:句柄集( HandleSet)、线程集( ThreadSet)、事件处理器( Eventhandler)和具体的事件处理器( Concrete Eventhandler)。它们的关系如下图所示:
在这里插入图片描述

句柄集

句柄( Handle)用于表示I/O资源,在 Linux下通常就是一个文件描述符。句柄集管理众多句柄,它使用 wait_for_event方法来监听这些句柄上的I/O事件,并将其中的就绪事件通知给领导者线程。领导者则调用绑定到 Handle上的事件处理器来处理事件。领导者将Handle和事件处理器绑定是通过调用句柄集中的 register_handle方法实现的。

线程集

这个组件是所有工作线程(包括领导者线程和追随者线程)的管理者。它负责各线程之间的同步,以及新领导者线程的推选。线程集中的线程在任一时间必处于如下三种状态之一:
Leader:线程当前处于领导者身份,负责等待旬柄集上的I/O事件。
Processing:线程正在处理事件。领导者检测到I/O事件之后,可以转移到 Processing状态来处理该事件,并调用 promote_new_leader方法推选新的领导者;也可以指定其他追随者来处理事件( Event handoff),此时领导者的地位不变。当处于 Processing状态的线程处理完事件之后,如果当前线程集中没有领导者,则它将成为新的领导者,否则它就直接转变为追随者。
Follower:线程当前处于追随者身份,通过调用线程集的join方法等待成为新的领导者,也可能被当前的领导者指定来处理新的任务。
在这里插入图片描述

事件处理器和具体的事件处理器

事件处理器通常包含一个或多个回调函数 handle_event。这些回调函数用于处理事件对应的业务逻辑。事件处理器在使用前需要被绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中的回调函数。具体的事件处理器是事件处理器的派生类。它们必须重新实现基类的 handle_event方法,以处理特定的任务。

工作流程

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值