服务器模型
C/S模型
- TCP/IP协议在设计和实现上并没有客户端和服务器的概念,在通信过程中所有机器都是对等的。
- C/S模型的逻辑很简单。
- 服务器启动后,首先创建一个或多个监听socket,并调用bind函数将其绑定到服务器感兴趣的端口上。
- 然后调用listen函数等待客户连接。
- 服务器稳定运行之后,客户端就可以调用connect函数向服务器发起连接了。
- 由于客户端连接请求是随机到达的异步事件,服务器需要使用某种I/O模型来监听这一事件。
- 服务器处理完请求,会把处理结果返回给客户端。客户端接收到服务器反馈的结果之后,可以继续向服务器发送请求,也可以立即主动关闭连接。
如果客户端主动关闭连接,则服务器执行被动关闭连接。至此,双方的通信结束。
P2P模型
- P2P(
Peer to Peer
),点对点模型比C/S模型更符合网络通信的实际情况。它摒弃了以服务器为中心的格局,让网络上所有主机重新回归对等的地位。 - P2P模型使得每台机器在消耗服务的同时也能给别人提供服务,这样资源能够充分、自由地共享。如下图a:
云计算群可以看作P2P模型的一个典范。但P2P模型的缺点也很明显:当用户之间的请求过多时,网络的负载将加重。
- 上图的P2P模型存在一个显著的问题,就是主机之间很难互相发现。所以实际上使用的P2P模型通常带有一个专门的发现服务器,如下图b:
这个发现服务器通常还提供查找服务(甚至还可以提供内容服务),使每个客户端都能尽快找到自己需要的资源。
服务器编程框架
- 虽然服务器种类繁多,但其基本框架都一样,不同之处在于逻辑处理。
模块 | 单个服务器程序 | 服务器集群 |
I/O处理单元 | 处理客户连接,读写网络数据 | 作为接入服务器,实现负载均衡 |
逻辑单元 | 业务进程或现场 | 逻辑处理服务器 |
网络存储单元 | 本地数据库,文件或缓存 | 数据库服务器 |
请求队列 | 各单元之间的通信方式 | 各服务器之间的永久TCP连接 |
- I/O处理单元服务器管理客户端连接的模块。
- 等待并接收新的客户端连接
- 接收客户端数据,将服务器响应数据返回给客户端
但是,数据的收发不一定在I/O处理单元中执行,也可能在逻辑单元中执行,具体在哪里执行取决于事件处理模式。 - 对于一个服务器机群来说,I/O处理单元是一个专门的接入服务器。它实现负载均衡,从所有逻辑服务器中选取负荷最小的一台来为新客户服务。
- 一个逻辑单元通常是一个进程或线程。
- 它分析并处理客户端数据,然后将结果传递给I/O处理单元或者直接发送给客户端(具体在哪里执行取决于事件处理模式)。
- 对服务机群而言,一个逻辑单元本身就是一台逻辑服务器。服务器通常拥有多个逻辑单元,以实现对多个客户端任务的并行处理。
- 网络存储单元可以是数据库、缓存或文件,甚至可以是一台独立的服务器。
但它不是必须的,比如ssh、telnet等登录服务器就不需要这个单元。 - 请求队列是各单元之间通信方式的抽象。
- I/O处理单元接收到客户端请求时,需要以某种方式通知一个逻辑单元来处理该请求。
- 同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件。
- 请求队列通常被实现为池的一部分。
- 对于服务器机群而言,请求队列是各台服务器之间预先建立的、静态的、永久的TCP连接。
这种TCP连接能提高服务器之间交换数据的效率,因为它避免了动态建立TCP连接导致的额外的系统开销。
I/O模型
- socket在创建的时候默认是阻塞的。可以给socket系统调用的第2个参数传递SOCK+NOBLOCK标志,或者通过fcntl系统调用的F_SETEL命令,将其设置为非阻塞。
- 阻塞和非阻塞的概念能应用于所有文件描述符,而不仅仅是socket。
称阻塞的文件描述符为阻塞I/O,非阻塞的文件描述符为非阻塞I/O。 - 针对阻塞I/O执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发送为止。
socket的基础API中,可能被阻塞的系统调用包括accept,send,recv和connect。- 比如客户端通过connect向服务器发起连接时,connect首先将发送同步报文段给服务器,然后等待服务器返回确认报文段。如果服务器的确认报文段没有立即到达客户端,则connect调用将被挂起,直到客户端收到确认报文段并唤醒connect调用。
- 针对非阻塞I/O执行的系统调用则总是立即返回,而不管事件是否已经发送。
如果事件没有立即发送,这些系统调用就返回-1,和出错的情况一样。此时必须根据errno来区分这两种情况。- 对accept,send和recv而言,事件未发送时errno通常被设置成EAGAIN(再来一次)或者EWOULDBLOCK(期望阻塞);
- 对connect而言,errno则被设置成EINPROGRESS(在处理中)。
- 很显然,只有在事件已经发送的情况下,操作非阻塞I/O(比如读写等),才能提高程序的效率。因此,非阻塞I/O通常要和其他I/O通知机制一起使用,比如I/O复用和SIGIO信号。
- I/O复用是最常使用的I/O通知机制。它指的是,应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把其中就绪的事件通知给应用程序。
Linux上常用的I/O复用函数是select,poll,epoll_wait。- 需要注意的是,I/O复用函数本身是阻塞的,它们能提高程序效率的原因在于它们具有同时监听多个I/O事件的能力。
- SIGIO信号也可以用来报告I/O事件。
- 我们可以为一个目标文件描述符指定宿主进程,那么被指定的宿主进程将捕获到SIGIO信号。
- 这样,当目标文件描述符上有事件发生时,SIGIO信号的信号处理函数将被触发,我们也就可以在该信号处理函数中对目标文件描述符指向非阻塞I/O操作了。
- 理论上说,阻塞I/O,I/O复用和信号驱动I/O都是同步I/O模型。因为这三种I/O模型中,I/O的读写操作,都是在I/O事件发生之后,由应用程序来完成的。
而POSIX规范中所定义的异步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/O向应用程序通知的是I/O完成事件。
1 2 |
// 异步io #include <aio.h> |
两种高效的事件处理模式
- 服务器程序通常需要处理三类事件:
- I/O事件
- 信号
- 定时事件
- 随着网络设计模式的兴起,Reactor和Proactor事件处理模式应运而生:
- 同步I/O模型通常用于实现Reactor模式
- 异步I/O模型则用于实现Proactor模式
Reactor模式
- 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上写入服务器处理客户请求的结果。
- 如上图,工作线程从请求队列中取出事件后,将根据事件的类型来决定如何处理它:
- 对于可读事件,执行读数据和处理请求的操作
- 对于可写事件,执行写数据的操作
Proactor模式
- 与Reactor模型不同,Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅负责业务逻辑。
- 使用异步I/O模型(以aio_read和aio_write为例)实现的Proactor模式的工作流程是:
- 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序。
- 主线程继续处理其他逻辑。
- 当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用。
- 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上写完成事件,并告诉内核用户缓冲区的位置,以及写操作完成时如何通知应用程序。
- 主线程继续处理其他逻辑。
- 当用户缓冲区的数据被写入socket之后,内核向应用程序发送一个信号,已通知应用程序数据已经发送完毕。
- 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket。
- 如上图,连接上socket的读写事件是通过aio_read或aio_write向内核注册的,因此内核将通过信号来向应用报告连接socket上的读写事件。
所以,主线程上的epoll_wait调用仅用来检测监听socket上的连接请求事件,而不能用来检测连接socket上的读写事件。
同步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上写入服务器处理客户请求的结果。
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Linux 高性能服务器编程:高性能服务器架构二12/05
- ♥ Linux 线程概述&&创建03/31
- ♥ Linux高性能服务器编程:TCP/IP协议族09/02
- ♥ Linux_ 命令大全 文档编辑03/16
- ♥ Linux_命令大全 文件传输03/16
- ♥ Linux_ 命令大全 Windows System03/16