服务器一般可以解构为三个主要模块
- I/O处理单元:四种I/O模型,两种高效时间处理模式
- 逻辑单元:两种高效并发模式,逻辑状态机
- 存储单元:服务器程序可选模块
8.1 服务器模型
8.1.1 C/S模型
- C/S(client/server,客户/服务器模型)。
- 逻辑:服务器启动,创建一个或多个监听socket,调用bind函数绑定到特定端口,调用listen函数等待客户连接。客户端可以调用connect函数像服务器发起连接。
- 因为客户端的连接请求是随机到达的异步事件,需要某种I/O模型来监听这一事件。
8.1.2 P2P模型
- 所有主机地位对等,每台机器在消耗服务的同时,也给其他机器提供服务。
- 传统的P2P模型主机之间很难发现,所以P2P模型通常有一个发现服务器,专门用来提供给查找服务,使得客户能够很快找到自己的资源。
8.2 服务器编程框架
- I/O处理单元:
- 管理客户连接模块。
- 等待并接收新的客户连接
- 接收客户数据
- 将服务器响应返回给客户端
- 数据的收发也可能在逻辑单元中执行。
- 逻辑单元:
- 通常是一个进程或线程
- 分析并处理客户数据,结果传递给I/O处理单元或直接发送给客户端。
- 对于多个逻辑单元的服务器,可以实现并行处理。
- 存储单元:
- 数据库、缓存、文件、独立的服务器
- 并不是必须的。例如ssh和telnet就不需要
- 请求队列:
- 各个单元之间通信方式的抽象。
- I/O处理单元接收到客户请求时,需要以某种方式通知逻辑单元来处理请求。
- 多个逻辑单元访问一个存储单元时,也需要以某种机制协调处理静态条件。
- 请求队列通常被实现为池的一部分。
8.3 I/O模型
- socket创建时默认是阻塞的。
- 阻塞的文件描述符称为阻塞I/O,非阻塞文件描述符称为非阻塞I/O。
- 针对阻塞I/O的系统调用可能因为无法立刻完成被系统挂起,直到等待的事件发送。
- 例如客户端的connect调用,如果服务器的确认文报没有到达客户端,那么connect将会被挂起,直到客户端收到确认文报唤醒。
- accept、send、recv和connect系统调用都可能会被阻塞。
- 对于非阻塞I/O的系统调用会立刻返回,不管事件是否发送,如果没发送,返回-1。
- 在事件已经发生的情况下操作非阻塞I/O才能提高程序效率。因此非阻塞I/O通常要和其他I/O通知机制一起使用。
- I/O复用
- 应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的 + 事件通知给应用程序。
- Linux常用I/O复用函数是select、poll、epoll_wait
- I/O复用本身是阻塞的,因为他们可以同时监听多个I/O事件,所以才提高效率。
- 阻塞I/O,I/O复用,型号驱动I/O都是同步I/O模型。I/O的读写操作,都是在I/O事件发生后,由应用程序来完成。
- 对于异步I/O而言,用户可以直接对I/O执行读写操作。
- 同步I/O向应用程序通知的是I/O就绪事件,异步I/sO向应用程序通知的是I/O完成事件。
8.4 两种高效的事件处理模式
服务器程序主要处理三类事件:
- I/O事件
- 信号事件
- 定时事件
Reactor和Proactor
- 同步I\O模型通常用于事件Reactor模式
- 异步I\O通常用于实现Proactor模式,但是同步I\O也能实现Proactor模式。
8.4.1 Reactor模式
- 主线程(I/O处理单元)只负责监听事件是否发生,事件发生立刻通知到工作线程。
- 读写数据、接收新连接、处理用户请求均在工作线程完成。
- 使用同步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写入服务器处理客户请求的结果。
8.4.2 Proactor模式
- Proator模式把所有I/O操作交给主线程和内核处理,工作线程处理逻辑业务。
- 使用异步I/O(aio_read和aio_write)实现的Proactor模式工作流程:
- 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时候如何通知应用程序。
- 主线程继续处理其他罗i就
- 当socket上的数据被读入用户缓冲区后,内核向应用程序发送信号,通知应用程序数据以及可用。
- 选择一个工作线程处理用户请求。
- 处理完用户请求,使用aio_write函数向内核注册socket写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序。
- 主线程继续处理其他逻辑。
- 当用户缓冲区的数据被写入socket之后,内核向应用程序发送一个信号,通知应用程序数据发送完毕。
- 应用程序预先定义好的信号处理函数选择一个工作线程来做善后工作,例如决定是否关闭socket。
8.4.3 模拟Proactor模式
- 使用同步IO也能模拟Proactor模式,原理在于主线程执行数据读写操作,当读写完成后,主线程向工作线程通知完成事件,所以从工作线程角度看,他直接获得了数据读写的结果,只需要对结果进行逻辑处理。
- 使用同步IO(例如epoll_wait)模拟Proactor模式的工作流程如下:
- 主线程往epoll内核事件表注册socket 读就绪事件
- 主线程调用epoll_wait函数等待socket上有数据可读
- 当socket上有数据可读时,epoll_wait通知主线程,主线程从socket循环读取数据, 将读取到的数据封装成请求并加入请求队列。
- 请求队列某个工作线程被唤醒,获得请求对象并处理客户请求,之后往epoll内核事件表注册socket写就绪事件、
- 主线程调用epoll_wait等待socket可写
- socket可写时,epoll_wait通知主线程,主线程往socket写入处理结果。
8.5 两种高效的并发模式
- 计算密集型,并发编程并无优势,反倒由于任务切换使得效率降低。
- I/O密集型,并发可以提高CPU利用率。
8.5.1 半同步/半异步模式
- 同步:程序按代码顺序执行
- 异步:程序执行需要事件驱动
8.5.1.1 半同步/半反应堆 模式
- 主线程为异步线程,负责监听socket事件
- 采用Reactor模式,工作线程自己从socket中读取客户请求,以及往socket中写入服务器应答。
- 也可以采用Proactor模式
缺点:
- 主线程和工作线程共享请求队列,所以主线程加任务到请求队列,或者工作线程从主线程中取出任务,都要加锁解锁。
- 工作线程只能处理一个客户请求,客户数量多而工作线程少, 客户端响应速度会降低。
相对高效的半同步半异步:
- 主线程监听socket,连接socket有工作线程来处理
- 与上边的半同步半反应堆的区别在于,上边是在主线程的epoll注册事件,这个是在工作线程epoll注册事件
8.5.2 领导者/追随者模式
- 程序只有一个领导者线程,负责监听IO事件,其他线程是追随者,休眠在线程池中等待成为新的领导者。
- 如果当前领导者线程检测到IO事件,需要首先从线程池中选出新的领导者线程,如何处理IO事件,新的领导者线程等待新的IO事件,而原来的领导者则处理IO事件, 实现并发。