几种经典的网络服务器架构模型的分析与比较

原文出处:http://blog.csdn.net/lmh12506/article/details/7753978linux

 

前言程序员

事件驱动为广大的程序员所熟悉,其最为人津津乐道的是在图形化界面编程中的应用;事实上,在网络编程中事件驱动也被普遍使用,并大规模部署在高链接数高吞吐量的服务器程序中,如 http 服务器程序、ftp 服务器程序等。相比于传统的网络编程方式,事件驱动可以极大的下降资源占用,增大服务接待能力,并提升网络传输效率。web

关于本文说起的服务器模型,搜索网络能够查阅到不少的实现代码,因此,本文将不拘泥于源代码的陈列与分析,而侧重模型的介绍和比较。使用 libev 事件驱动库的服务器模型将给出实现代码。数据库

本文涉及到线程 / 时间图例,只为代表线程在各个 IO 上确实存在阻塞时延,但并不保证时延比例的正确性和 IO 执行前后的正确性;另外,本文所说起到的接口也只是笔者熟悉的 Unix/Linux 接口,并未推荐 Windows 接口,读者能够自行查阅对应的 Windows 接口。编程

阻塞型的网络编程接口缓存

几乎全部的程序员第一次接触到的网络编程都是从 listen()、send()、recv()等接口开始的。使用这些接口能够很方便的构建服务器 /客户机的模型。tomcat

咱们假设但愿创建一个简单的服务器程序,实现向单个客户机提供相似于“一问一答”的内容服务。安全

图 1. 简单的一问一答的服务器 /客户机模型服务器

几种经典的网络服务器架构模型的分析与比较

咱们注意到,大部分的 socket接口都是阻塞型的。所谓阻塞型接口是指系统调用(通常是 IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用得到结果或者超时出错时才返回。网络

实际上,除非特别指定,几乎全部的 IO接口 (包括 socket 接口 )都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将没法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。这时,不少程序员可能会选择多线程的方式来解决这个问题。

多线程服务器程序

应对多客户机的网络应用,最简单的解决方式是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每一个链接都拥有独立的线程(或进程),这样任何一个链接的阻塞都不会影响其余的链接。

具体使用多进程仍是多线程,并无一个特定的模式。传统意义上,进程的开销要远远大于线程,因此,若是须要同时为较多的客户机提供服务,则不推荐使用多进程;若是单个服务执行体须要消耗较多的 CPU 资源,譬如须要进行大规模或长时间的数据运算或文件访问,则进程较为安全。一般,使用 pthread_create () 建立新线程,fork() 建立新进程。

咱们假设对上述的服务器 / 客户机模型,提出更高的要求,即让服务器同时为多个客户机提供一问一答的服务。因而有了以下的模型。

图 2. 多线程服务器模型
几种经典的网络服务器架构模型的分析与比较

在上述的线程 / 时间图例中,主线程持续等待客户端的链接请求,若是有链接,则建立新线程,并在新线程中提供为前例一样的问答服务。

不少初学者可能不明白为什么一个 socket 能够 accept 屡次。实际上,socket 的设计者可能特地为多客户机的状况留下了伏笔,让 accept() 可以返回一个新的 socket。下面是 accept 接口的原型:

1
int accept( int s, struct sockaddr *addr, socklen_t *addrlen);

输入参数 s 是从 socket(),bind() 和 listen() 中沿用下来的 socket 句柄值。执行完 bind() 和 listen() 后,操做系统已经开始在指定的端口处监听全部的链接请求,若是有请求,则将该链接请求加入请求队列。调用 accept() 接口正是从 socket s 的请求队列抽取第一个链接信息,建立一个与 s 同类的新的 socket 返回句柄。新的 socket 句柄便是后续 read() 和 recv() 的输入参数。若是请求队列当前没有请求,则 accept() 将进入阻塞状态直到有请求进入队列。

上述多线程的服务器模型彷佛完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。若是要同时响应成百上千路的链接请求,则不管多线程仍是多进程都会严重占据系统资源,下降系统对外界响应效率,而线程与进程自己也更容易进入假死状态。

不少程序员可能会考虑使用“线程池”或“链接池”。“线程池”旨在减小建立和销毁线程的频率,其维持必定合理数量的线程,并让空闲的线程从新承担新的执行任务。“链接池”维持链接的缓存池,尽可能重用已有的链接、减小建立和关闭链接的频率。这两种技术均可以很好的下降系统开销,都被普遍应用不少大型系统,如 websphere、tomcat 和各类数据库等。

可是,“线程池”和“链接池”技术也只是在必定程度上缓解了频繁调用 IO 接口带来的资源占用。并且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。因此使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“链接池”或许能够缓解部分压力,可是不能解决全部问题。

总之,多线程模型能够方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型并非最佳方案。下一章咱们将讨论用非阻塞接口来尝试解决这个问题。

使用select()接口的基于事件驱动的服务器模型

大部分 Unix/Linux 都支持 select 函数,该函数用于探测多个文件句柄的状态变化。下面给出 select 接口的原型:

1
2
3
4
5
6
FD_ZERO( int fd, fd_set* fds)
FD_SET( int fd, fd_set* fds)
FD_ISSET( int fd, fd_set* fds)
FD_CLR( int fd, fd_set* fds)
int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
        struct timeval *timeout)

这里,fd_set 类型能够简单的理解为按 bit 位标记句柄的队列,例如要在某 fd_set 中标记一个值为 16 的句柄,则该 fd_set 的第 16 个 bit 位被标记为 1。具体的置位、验证可以使用 FD_SET、FD_ISSET 等宏实现。在 select() 函数中,readfds、writefds 和 exceptfds 同时做为输入参数和输出参数。若是输入的 readfds 标记了 16 号句柄,则 select() 将检测 16 号句柄是否可读。在 select() 返回后,能够经过检查 readfds 有否标记 16 号句柄,来判断该“可读”事件是否发生。另外,用户能够设置 timeout 时间。

下面将从新模拟上例中从多个客户端接收数据的模型。

图4.使用select()的接收数据模型
几种经典的网络服务器架构模型的分析与比较

上述模型只是描述了使用 select() 接口同时从多个客户端接收数据的过程;因为 select() 接口能够同时对多个句柄进行读状态、写状态和错误状态的探测,因此能够很容易构建为多个客户端提供独立问答服务的服务器系统。

图5.使用select()接口的基于事件驱动的服务器模型

几种经典的网络服务器架构模型的分析与比较

这里须要指出的是,客户端的一个 connect() 操做,将在服务器端激发一个“可读事件”,因此 select() 也能探测来自客户端的 connect() 行为。

上述模型中,最关键的地方是如何动态维护 select() 的三个参数 readfds、writefds 和 exceptfds。做为输入参数,readfds 应该标记全部的须要探测的“可读事件”的句柄,其中永远包括那个探测 connect() 的那个“母”句柄;同时,writefds 和 exceptfds 应该标记全部须要探测的“可写事件”和“错误事件”的句柄 ( 使用 FD_SET() 标记 )。

做为输出参数,readfds、writefds 和 exceptfds 中的保存了 select() 捕捉到的全部事件的句柄值。程序员须要检查的全部的标记位 ( 使用 FD_ISSET() 检查 ),以肯定到底哪些句柄发生了事件。

上述模型主要模拟的是“一问一答”的服务流程,因此,若是 select() 发现某句柄捕捉到了“可读事件”,服务器程序应及时作 recv() 操做,并根据接收到的数据准备好待发送数据,并将对应的句柄值加入 writefds,准备下一次的“可写事件”的 select() 探测。一样,若是 select() 发现某句柄捕捉到“可写事件”,则程序应及时作 send() 操做,并准备好下一次的“可读事件”探测准备。下图描述的是上述模型中的一个执行周期。

图6. 一个执行周期

几种经典的网络服务器架构模型的分析与比较

这种模型的特征在于每个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。咱们能够将这种模型归类为“事件驱动模型”。

相比其余模型,使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时可以为多客户端提供服务。若是试图创建一个简单的事件驱动的服务器程序,这个模型有必定的参考价值。

但这个模型依旧有着不少问题。

首先,select() 接口并非实现“事件驱动”的最好选择。由于当须要探测的句柄值较大时,select() 接口自己须要消耗大量时间去轮询各个句柄。不少操做系统提供了更为高效的接口,如 linux 提供了 epoll,BSD 提供了 kqueue,Solaris 提供了 /dev/poll …。若是须要实现更高效的服务器程序,相似 epoll 这样的接口更被推荐。遗憾的是不一样的操做系统特供的 epoll 接口有很大差别,因此使用相似于 epoll 的接口实现具备较好跨平台能力的服务器会比较困难。

其次,该模型将事件探测和事件响应夹杂在一块儿,一旦事件响应的执行体庞大,则对整个模型是灾难性的。以下例,庞大的执行体 1 的将直接致使响应事件 2 的执行体迟迟得不到执行,并在很大程度上下降了事件探测的及时性。

图7. 庞大的执行体对使用select()的事件驱动模型的影响
几种经典的网络服务器架构模型的分析与比较

幸运的是,有不少高效的事件驱动库能够屏蔽上述的困难,常见的事件驱动库有 libevent 库,还有做为 libevent 替代者的 libev 库。这些库会根据操做系统的特色选择最合适的事件探测接口,而且加入了信号 (signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。下章将介绍如何使用 libev 库替换 select 或 epoll 接口,实现高效稳定的服务器模型。

使用事件驱动库libev的服务器模型

Libev 是一种高性能事件循环 / 事件驱动库。做为 libevent 的替代做品,其第一个版本发布与 2007 年 11 月。Libev 的设计者声称 libev 拥有更快的速度,更小的体积,更多功能等优点,这些优点在不少测评中获得了证实。正由于其良好的性能,不少系统开始使用 libev 库。本章将介绍如何使用 Libev 实现提供问答服务的服务器。

(事实上,现存的事件循环 / 事件驱动库有不少,做者也无心推荐读者必定使用 libev 库,而只是为了说明事件驱动模型给网络服务器编程带来的便利和好处。大部分的事件驱动库都有着与 libev 库相相似的接口,只要明白大体的原理,便可灵活挑选合适的库。)

与前章的模型相似,libev 一样须要循环探测事件是否产生。Libev 的循环体用 ev_loop 结构来表达,并用 ev_loop( ) 来启动。

1
void ev_loop( ev_loop* loop, int flags )

Libev 支持八种事件类型,其中包括 IO 事件。一个 IO 事件用 ev_io 来表征,并用 ev_io_init() 函数来初始化:

1
void ev_io_init(ev_io *io, callback, int fd, int events)

初始化内容包括回调函数 callback,被探测的句柄 fd 和须要探测的事件,EV_READ 表“可读事件”,EV_WRITE 表“可写事件”。

如今,用户须要作的仅仅是在合适的时候,将某些 ev_io 从 ev_loop 加入或剔除。一旦加入,下个循环即会检查 ev_io 所指定的事件有否发生;若是该事件被探测到,则 ev_loop 会自动执行 ev_io 的回调函数 callback();若是 ev_io 被注销,则再也不检测对应事件。

不管某 ev_loop 启动与否,均可以对其添加或删除一个或多个 ev_io,添加删除的接口是 ev_io_start() 和 ev_io_stop()。

1
2
void ev_io_start( ev_loop *loop, ev_io* io )
void ev_io_stop( EV_A_* )

由此,咱们能够容易得出以下的“一问一答”的服务器模型。因为没有考虑服务器端主动终止链接机制,因此各个链接能够维持任意时间,客户端能够自由选择退出时机。

图8. 使用libev库的服务器模型
几种经典的网络服务器架构模型的分析与比较

上述模型能够接受任意多个链接,且为各个链接提供彻底独立的问答服务。借助 libev 提供的事件循环 / 事件驱动接口,上述模型有机会具有其余模型不能提供的高效率、低资源占用、稳定性好和编写简单等特色。

因为传统的 web 服务器,ftp 服务器及其余网络应用程序都具备“一问一答”的通信逻辑,因此上述使用 libev 库的“一问一答”模型对构建相似的服务器程序具备参考价值;另外,对于须要实现远程监视或远程遥控的应用程序,上述模型一样提供了一个可行的实现方案。

总结

本文围绕如何构建一个提供“一问一答”的服务器程序,前后讨论了用阻塞型的 socket 接口实现的模型,使用多线程的模型,使用 select() 接口的基于事件驱动的服务器模型,直到使用 libev 事件驱动库的服务器模型。文章对各类模型的优缺点都作了比较,从比较中得出结论,即便用“事件驱动模型”能够的实现更为高效稳定的服务器程序。文中描述的多种模型能够为读者的网络编程提供参考价值。

相关文章
相关标签/搜索