几种经典的网络服务器架构模型的分析与比较_计算机硬件及网络_IT/计算机_专业资料。几种经典的网络服务器架构模型的分析与比较
如对您有帮助,请购买打赏,谢谢您! 几种经典的网络服务器架构模型的分析与比较 华岳 事件驱动为广大的程序员所熟悉, 其最为人津津乐道的是在图形化界面编程中的应用; 事实 上, 在网络编程中事件驱动也被广泛使用, 并大规模部署在高连接数高吞吐量的服务器程序 中,如 http 服务器程序、ftp 服务器程序等。相比于传统的网络编程方式,事件驱动能够 极大的降低资源占用,增大服务接待能力,并提高网络传输效率。 关于本文提及的服务器模型,搜索网络可以查阅到很多的实现代码,所以,本文将不拘泥于 源代码的陈列与分析,而侧重模型的介绍和比较。使用 libev 事件驱动库的服务器模型将 给出实现代码。 本文涉及到线程 / 时间图例,只为表明线程在各个 IO 上确实存在阻塞时延,但并不保证 时延比例的正确性和 IO 执行先后的正确性;另外,本文所提及到的接口也只是笔者熟悉 的 Unix/Linux 接口, 并未推荐 Windows 接口, 读者可以自行查阅对应的 Windows 接口。 阻塞型的网络编程接口 几乎所有的程序员第一次接触到的网络编程都是从 listen()、send()、recv()等接口开始的。 使用这些接口可以很方便的构建服务器/客户机的模型。 我们假设希望建立一个简单的服务器程序,实现向单个客户机提供类似于“一问一答”的内容 服务。 图 1. 简单的一问一答的服务器 /客户机模型 我们注意到,大部分的 socket 接口都是阻塞型的。所谓阻塞型接口是指系统调用(一般是 IO 接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出 错时才返回。 实际上,除非特别指定,几乎所有的 IO 接口 (包括 socket 接口 )都是阻塞型的。这给网 络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程 将无法执行任何运算或响应任何的网络请求。 这给多客户机、 多业务逻辑的网络编程带来了 挑战。这时,很多程序员可能会选择多线程的方式来解决这个问题。 多线程服务器程序 如对您有帮助,请购买打赏,谢谢您! 应对多客户机的网络应用,最简单的解决方式是在服务器端使用多线程(或多进程)。多线 程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻 塞都不会影响其他的连接。 具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于 线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务 执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问, 则进程较为安全。通常,使用 pthread_create () 创建新线程,fork() 创建新进程。 我们假设对上述的服务器 / 客户机模型,提出更高的要求,即让服务器同时为多个客户机 提供一问一答的服务。于是有了如下的模型。 图 2. 多线程服务器模型 如对您有帮助,请购买打赏,谢谢您! 在上述的线程 / 时间图例中,主线程持续等待客户端的连接请求,如果有连接,则创建新 线程,并在新线程中提供为前例同样的问答服务。 很多初学者可能不明白为何一个 socket 可以 accept 多次。实际上,socket 的设计者可 能特意为多客户机的情况留下了伏笔,让 accept() 能够返回一个新的 socket。下面是 accept 接口的原型: 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 接口的原型: FD_ZERO(int fd, fd_set*

