本节主要包括三个模块
1、I/O处理单元,I/O的四种模型,两种高效的处理模式
2、逻辑单元:两种高效并发模式,以及高效的逻辑处理方式–有效状态机
3、存储单元
1、服务器模型
1.1 C/S模型
TCP/IP协议里面没有客户端与服务器的概念,通信过程都是对等的,只是由于人们的大部分的访问形式都是:多个用户向资源拥有方提出访问申请,资源拥有方提供资源响应。
C/S模型特别适合资源相对集中的场合,实现也很简单,但是缺点也很明显,就是访问量很大时,可能客户端会得到很慢的响应。p2p刚好就解决了这个问题。
1.2 P2P 模型
P2P让网络中的所有主机又重新回到了对等的地位,每台主机既是资源的提供方也是需求方,这样资源可以得到充分的利用。但是有一个问题:主机之间传输的请求过多时,网络的负载将加重(可能是,对于小型用户而言,它的带宽并不大,而服务器的带宽很大,所以相对的,用户的承受能力弱)还有一个问题就是:主机之间很难相互发现,图a所示,所以一般会有一个服务器提供查找服务,有时候也会提供内容服务。
从编程的角度来看,p2p仍然可以算是CS模型。
2、服务器编程框架
虽然服务器程序种类繁多,但基本框架都一样,不同之处在于逻辑处理。
该图既可以用来描述一台服务器,也可以是一个服务器机群。
- I/O处理单元
I/O处理单元是服务器I管理客户连接的模块,需要完成。等待并且接收新的客户的连接,接收客户数据,将服务器响应数据返回给客户端。但是数据的收发并不一定在I/O处理单元中执行,也可以在逻辑单元中执行,具体在何处取决于事件处理模式。 - 逻辑单元
逻辑单元通常是一个线程或者一个进程,它分析处理数据,将结果传递给I/O处理单元或者直接发送给客户端。(具体使用哪种,取决于事件处理模式) - 网络存储单元
网络存储单元可以是数据库、缓存和文件。甚至可以是一台独立的服务器,但它也不是必须的,比如ssh,telnet 等登录服务就不需要。 - 请求队列
请求队列是各单元之间通信方式的抽象。I/O处理单元收到客户请求时,需要以某种方式通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件。请求队列通常被实现为池的一部分。
3 I/O模型
socket在创建时是阻塞的,可以通过socket系统调用的第二个参数设置为SOCK_NONBLOCK标志,或者通过fctnl系统调用的F_SETFL命令。阻塞与非阻塞的概念适用于所有的文件描述符,不仅仅是socket,我们称阻塞的文件描述符为阻塞I/O,称非阻塞的文件描述符为非阻塞I/O。
在socket 的API里面,accept,send,recv,和connect都可能被阻塞。被阻塞的系统调用,会因为无法执行而被挂起,而对于非阻塞的系统调用则是立即返回。不管事件是否发生。并且返回的值是-1,所以我们有必要对其通过errno进行区分。
很显然只有在事件已经发生的情况下,使用非阻塞I/O操作才能提高程序的效率,因此非阻塞I/O通常和其他I/O通知机制一起使用,比如I/O复用和SIGIO信号。
I/O复用是一种常用的I/O通知机制,意思大概就是向内核注册一组事件,然后通过I/O复用函数将其中就绪的事件通知给应用程序,但I/O复用函数本身是阻塞的,他能提高效率主要是因为它具有同时监听多个I/O事件的能力,而不像最原始的轮询加阻塞机制。
SIGIO信号(占坑)
从理论上说,阻塞I/O、I/O复用和信号驱动I/O都是同步I/O模型。因为在这三种模型中,I/O的读写操作都是在I/O事件发生之后。
4、两种高效的事件处理模式
服务器程序通常需要处理三类事件:I/O事件、信号及定时事件。我们将在后续章节依次讨论这三种类型的事件。
随着网络设计模式的兴起,Reactor和Preactor事件处理模式应运而生。同步I/O模型常用于Reactor,异步I/O模型则用于实现Proactor。但其实也可以通过同步I/O实现Proactor。
4.1 Reactor 模式
Reactor是:主线程(I/O)只负责监听文件描述符上是否有事件发生。有的话就立即将该事件通知工作线程(逻辑单元)。除此之外,主线程不做任何实质性的工作。读写数据,接收新的连接,以及处理客户请求均在工作线程中完成。
使用同步模型(以 epoll_wait)为例,实现Reactor模式的工作流程。
- 主线程往epoll内核时间表中注册socket上的读就绪事件。
- 主线程调用epoll_wait等待socket上有数据可读。
- 当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒。它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。
- 主线程调用epoll_wait等待socket可写。
- 当socket可写时,epoll_watr通知主线程。主线程将socket可写事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
对于上图的工作流程来看,工作线程从请求队列中取出事件出来后,将根据事件的类型来决定如何处理它,并没有区别所谓的读工作线程与写工作线程。
4.2、Proactor 模式
与Reactor不同Proactor模式将所有的I/O操作都交给主线程和内核处理,工作线程仅仅负责业务逻辑。工作模式更加接近于上面的那种服务器编程框架。
使用异步I/O模型(以aio_read和aio_write为例)实现的工作流程。
1.主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序,(这里以信号为例)。
2. 主线程继续处理其他逻辑
3. 当socket上堆数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序的数据已经可以用了。
4. 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序(仍然以信号为例)
5. 主线程继续处理其他逻辑。
6. 当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕。
7. 应用程序预先定义好的信号处理函数选择一个工作线程做善后工作,比如决定是否关闭socket。
连接socket上的读写事件是通过aio_read/aot_write向内核注册的,因此内核将通过信号来向应用程序报告连接socket上的读写事件。所以主线程上的epoll_wait调用仅用来监听socket上的连接请求事件,而不能用来检测连接socket上的读写事件。
4.3 模拟Proactor模式
使用同步I/O方式模拟处Proactor这一种模式。原理是:主线程执行数据读写操作,读写完之后,主线程向工作线程通知这一:完成事件”。那么从工作线程的角度来看,他们就直接获得了数据读写的结果,后面就只需要对读写的结果进行逻辑处理。
使用同步I/O(仍然以epoll_wait为例)模拟出的Proactor模式的工作流程:
1.主线程往epoll内核事件中注册socket上的读就绪事件。
2.主线程调用epoll_wait等待socket等待socket上有数据可读。
3.当socket有数据可以读时,epoll_waie通知主线程。主线程从socket循环读取数据,直到没有更多数据可以读,然后将读到的数据封装成一个请求对象并插入请求对了。
4.睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核时间上注册socket上写就绪事件。
5.主线程调用epoll_wait等待socket可写
6.当socket可写时,epoll_wait通知主线程,主线程往socket上写入服务器处理客户请求的结果
5 两种高效的并发模式
并发编程的目的是为了同时执行多个任务。对于程序密集型的,并发编程没有优势,而对于I/O密集型的,经常读写文件,访问数据库,并发编程可以让cpu做更加有意义的事,效率也就更高。
并发编程主要有多进程与多线程两种方式,本节主要讨论,并发模式。并发模式是指I/O处理单元和多个逻辑单元之间协调完成任务的方法,服务器主要有两种并发编程模式:半同步/半异步模式和领导者/追随者模式
5.1 半同步/半异步模式
半同步/半异步中的“同步”,“异步”与前面讨论的I/O模型中的“同步”,和异步是两个完全不一样的概念。在I/O模型中,“同步”,和“异步”区分的是何种I/O事件(是就绪事件还是完成事件),以及该由谁来完成I/O读写(是应用程序还是内核)。在并发模式中,“同步”指的是程序完全按照代码序列的顺序执行:“异步”指的是程序的执行要由系统事件来驱动。常见的系统事件包括中断、信号。
按照同步方式来定义同步线程与异步线程。
- 异步线程:执行效率高,实时性高,但程序相对复杂,那么调式和扩展,不适合大量的并发。
- 同步线程:效率很低,实时性较差,但逻辑简单。
而对于服务器这种既要求较好的实时性,有要求同时能处理多个客户请求的应用程序。所以使用同步线程与异步线程来实现。即采用 半同步/半异步模式来实现。
半同步/半异步模式中,同步线程用于处理客户逻辑,异步线程用于处理I/O事件。异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式下的工作线程来读取处理该请求对象。具体选择哪个工作线程来为新的客户请求服务,取决于请求队列的设计(如轮询,或者是条件变量,信号量)来随机选择一个工作线程。
将这两种事件处理模式跟几种IO模型结合在一起,存在多种变体。
6、有限状态机
有限状态机:逻辑单元内部一种高效编程方法:
有点应用层的协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态,服务器可以根据它来编写响应的处理逻辑。
这是一个简单的有限状态机,状态之间是相互独立的,状态之间没有相互转移。状态之间的转移时需要状态内部驱动的。
该状态机包含三种状态,A、B、C。A是初始状态,C是结束状态。状态单独拿一个变量用来存储,