1总述
客户程序一般比较简单,而服务器程序就比较复杂了,因为对服务器程序的设计,必须考虑到其响应速度和响应能力等服务性能因数。本文主要讨论的是面向连接的服务器程序设计方法。
总体上服务器程序可分为两类:并发服务器(Concurrent Server)和串行服务器(Iterative Server)。前者主要针对实时性的客户/服务器模式,后者主要针对服务量小的客户/服务器模式。
2 TCP串行服务器程序
串行服务器程序是这样的:每次它只能为一个连接过来的客户程序提供服务,只有在完全处理了一个客户的请求后,才能响应下一个客户的请求,即按照FIFO的原则响应请求。一般很少使用串行服务器程序,不过诸如时间/日期等服务量小的且实时性要求不高的服务器程序可以使用该方式。从进程控制的角度来讲,该方式的速度是最快的,因为它不进行进程控制,系统开销小。
3 传统的TCP进程并发服务器程序
在这种方式下,并发服务器程序在收到客户程序请求后,派生出一个子进程来为该客户程序服务,自己则回到等待状态,准备接收下一个客户程序的请求,子进程在服务完成后退出。其中,作为父进程的并发服务器程序成为主服务器(master),具体处理客户请求的子进程成为从服务器(slave)。
图4 传统的TCP进程并发服务器程序框架
并发服务器的问题在于派生子进程(fork()操作)时会消耗CPU的很多时间,这对需要响应数目众多的客户进程的服务器进程所在的系统是极为不利的,例如对于Web服务器就是这样。
3.4 TCP预先派生子进程并发服务器程序
在传统的TCP进程并发服务器程序的基础上,可以对响应方式进行一些改造。传统的TCP进程并发服务器程序的响应方式是即响应即派生子进程。现在将这种方式改变为:服务器程序启动后就先生成若干子进程以备响应,这些子进程构成服务子进程组,而父进程则成了监控进程。
fork
可用子进程组
...
父进程
子进程1
子进程2
子进程3
客户2
客户1
子进程N
图5 TCP预先派生子进程
这里需要解决的问题是:怎样保持一定量的可用子进程;服务请求到达时,唤醒子进程的机制应该怎样以及父进程怎样将必要的信息传递给子进程。
父进程监视可用子进程的数量,当数量低于某个阈值时就再派生一些子进程,当数量高于某个阈值时就终止一些可用子进程。当然,总的子进程数量也应当有个上限值,以防止系统资源消耗完。这样就使得可用子进程数及总的子进程数保持在一定范围之内了。
预先生成得子进程在各自调用accept()后进入睡眠状态。由于这些子进程共用一个socket结构,当一个可户请求到达时,就会造成惊群(thundering herd)——唤醒所有的子进程。当然,只有最先被调度的子进程才会获得客户的连接,其他的子进程会再次进入睡眠状态。这种情况会导致系统性能的下降。解决这个问题的方法是给accept上锁,即保证accept操作的原子性。有些UNIX系统在内核已经解决了这一问题,就无须再给accept上锁了。
如果子进程只是父进程的副本,基本上就不用额外考虑进程通讯的问题了。如果将父进程改造成类似于inetd的守护进程(启动后先调用fork()生成子进程,再通过exec系统调用执行服务处理程序),就必须解决父进程同子进程之间的通讯问题。UNIX下进程通讯的机制有多种,如管道(pipe),具名管道(named pipe),IPC消息(InterProcess Communication Message)等。这里我们使用IPC消息队列来向子进程传递socket描述字。
图6 使用IPC消息机制的TCP预先派生子进程方法
5 TCP线程并发服务器程序
线程的执行效率比进程高10到100倍。编写线程函数时必须注意函数的线程安全性。并非所有的UNIX系统或同种UNIX的不同版本都支持线程。由于在方法上线程服务器程序同进程服务器程序的设计差不多,这里就不作多的描述了。
(忘记从哪看到的了,放在MyBASE里感觉不错就贴上来了,感谢原作者)