高性能服务器程序框架

这一章可以说是核心了,因为前面说了一些基础知识,这里将要讲述如何搭建一个高性能服务器。我们将服务器分解为以下三个模块:
I/O处理单元,逻辑单元,存储单元。本章介绍前两个,存储单元是可选的模块,跟网络编程本身无关;

1.服务器模型
C/S模型
虽然TCP/IP协议在设计和实现上没有客户端和服务端的概念,但是资源的垄断意味着我们必须从某台机器获取信息,所以几乎所有的网络应用服务都采取了C/S模型。
这个模型实现也很简单,服务器端创建一个多个socket监听,bind,listen,客户端有请求就通过select系统调用选择一个客户请求,通过创建一个子进程(逻辑单元)处理客户请求,父进程依然在不断监听,这是一个并发的服务器。
服务器模型实现简单,但是缺点明显,如果访问量过大,所有用户将得到很慢的响应。

P2P模型:它比前者更符合网络通信的情况。所有主机都是对等的地位。
缺点是,当用户之间的请求过多时,网络负载很大。
在这里插入图片描述
从编程角度看,P2P仍然是C/S模型的扩展,因为每台主机即是客户端,又是服务端。
所以,我们仍然采用C/S模型讨论网络编程。

2、服务器编程框架
在这里插入图片描述
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复用和SIGIO信号
I/O复用机制指的是:应用程序通过I/O复用函数向内核注册一组事件,内核通过I/O复用函数把就绪的时间通知给应用程序。常用的I/O复用函数有select,pool,epoll_wait。后面会具体说这个。
注意的是, I/O复用函数本身是阻塞的,它提高效率的原因在于可以同时监听多个I/O事件。
SIGIO也可以用来报告I/O事件。我们可以为一个目标文件描述符指定宿主进程,那么被指定的宿主进程将捕获到I/O信号。这样当文件描述符有事发生时,信号处理函数就会触发,对目标文件描述符进行非阻塞的操作。

阻塞I/O,I/O复用,信号驱动I/O都是同步I/O模型。因为I/O读写操作都是在事件发生后,由应用程序完成。而异步I/O模型直接执行读写操作,告诉内核数据缓冲区位置。不过这不是本次的重点,只是简单说明。

4.两种高效的事件处理模式(reactor,proactor)
服务器程序通常需要处理三种事件:I/O事假,信号事件,定时事件。
Reactor:要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生。其余的实质性工作比如,读写数据,接受新连接, 处理用户请求都在工作线程(逻辑单元)完成。
在这里插入图片描述
proactor模式:将所有I/O操作交给主线程和内核处理,工作线程只负责业务逻辑。也就是说主线程执行数据读写操作,将这一完成事件告诉工作进程。工作进程已经得到数据读写的结果,接下来只要对得到的数据进行逻辑处理。
在这里插入图片描述

在这里插入图片描述

5、两种高效的并发模式
并发编程是为了“同时”执行多个程序,如果是计算密集的任务,并发反而没有优势,会因为任务切换降低效率。而对于I/O密集的任务,比如经常读写文件,访问数据库等由于I/O操作远没有CPU速度快,让程序阻塞于I.O操作将浪费CPU资源。
并发模式指的是I/O处理单元和多个逻辑单元协调完成任务的方法。主要有两种:半同步/半异步模式,领导者/追随者模式。
半同步/半异步模式
我们要注意了,这里的同步异步的概念和前面的不一样,前面同步指的是由应用程序完成读写,异步是内核直接完成,然后告诉应用程序这一完成事件。
而在并发模式中,同步指的是程序完全按照代码序列的顺序执行,异步指的是程序执行需要由系统事件驱动,系统事件包括中断,信号等。
在这里插入图片描述
显然,异步进程执行效率高,实时性强,但是结构复杂,不适合调试扩展,且不适合大量的并发。而服务器这种既需要实时性,又需要满足多个用户请求,所以我们需要结合二者的优点。
同步线程用来处理客户逻辑,异步线程用来处理I/O事件。
在这里插入图片描述

这个半同步-半反应堆的缺点有:
1、因为主线程和工作线程共享请求队列,所以需要对队列加锁保护,这会耗费CPU时间;
2、一个工作线程只能处理一个客户连接,一旦请求过多,请求队列就会堆积大量请求,所有用户响应变慢。如果增加工作线程,则工作线程间的切换也会需要CPU时间。
在这里插入图片描述
领导者-追随者模式
这是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。
在任意时刻,程序只有一个领导者线程,用来监听I/O事件,其他线程都是追随者,休眠在线程池中等待成为新的领导者。如果当前领导者监听到了I/O事件,则首先从线程池中选出新的领导者,自己来处理I/O事件,这样就实现了并发。(我第一次看到这个设计时是非常佩服的)
在这里插入图片描述
句柄集:
在这里插入图片描述

线程集:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
6.有限状态机
这是逻辑单元内部一种高效的编程方法。
学过编译原理的都应该知道有限状态机其实不稀奇。就是把事件的每个状态及其转移结果标出来,还可以化繁为简,然后用编程语言写出来。这里就不详细说了。

7.提高服务器性能的一些建议
7.1 池
既然服务器硬件资源充裕,那么提高性能很直接的一个方法就是:空间换时间。这就是池的概念。
池其实很简单,就是一组资源的集合。在服务器启动之初就创建好并初始化,服务器运行时有需求就从池里面获取资源。处理完客户后,还可以把资源放回池中。这意味着资源不用动态分配,因为分配资源的动态调用是很耗时的。避免了服务器对内核的频繁访问。
根据资源类型不同,池可以分为内存池,线程池,进程池,连接池。

7.2数据复制
高性能的服务器应该避免不必要的数据复制,尤其是当数据复制发生在用户代码和内核之间的时候。举个很形象的例子:
ftp服务器,当客户请求一个文件,服务器只需要检测文件是否存在,以及客户是否有读取权限,而不关心文件的内容。那么,ftp服务器就无需把文件内容完整读入应用程序缓冲区并调用send函数发送,而是用零拷贝函数sendfie直接发给客户端。
用户代码内部的数据复制也是要避免的。比如两个工作进程之间要传递大量数据,那么应该考虑共享内存直接共享这些数据。而不是使用管道或消息队列来传递。
还有可以利用指针表示偏移位置,而不是简单复制数据。

7.3上下文切换和锁
并发程序必须考虑剩下文切换带来的时间开销问题。所以我们不能为每个客户都设置一个工作进程,因为这样时间都消耗在进程切换了。
另外并发程序需要考虑对共享资源的加锁保护。但是锁被认为是导致服务器效率低下的原因,因为锁不处理任何逻辑,而且还要访问内核资源。那么最好的解决方案就是避免使用锁。
如何不得不用锁,就要尽量使用小粒度的锁,比如读写锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值