- 我们在需要开发一个服务器程序时,有较多的的程序设计范式可供选择,不同范式有其自身的特点和实用范围,明了不同范式的特性有助于我们服务器程序的开发。
- 客户端程序通常比服务器容易些,当然客户端程序也可以使用这些程序设计范式,因为它们蕴含的道理都是想通的。
- 本文所设计的服务器主要是指基于TCP的服务器
迭代服务器
迭代TCP服务器 总是在完全处理某个客户的请求之后才开始下一个客户的请求处理。
这样的服务器实际中比较少见。
基于UDP的大多服务器却是这样实现。
并发服务器,每个客户请求fork一个子进程
传统并发服务器 调用fork派生一个子进程来处理每个客户,这使得服务器能够同时为多个客户服务,每个进程一个客户。
客户数目的唯一限制是操作系统对其能够同时拥有多少子进程的限制。
绝大多数TCP服务器程序都是按这个范式编写。
并发服务器的问题在于为每个客户现场fork一个子进程比较耗费CPU时间。
预先派生子进程,每个子进程无保护地调用accept
不同于传统意义的并发服务器那样为每个客户现场派生一个子进程,而是 在启动阶段预先派生一定数量的子进程,当有客户连接到达时,这些子进程就能立即为它提供服务。
这种技术的有点在于无需引入父进程执行fork的开销就能处理新到来的客户。缺点是父进程必须在服务启动阶段猜测需要预先派生多少子进程。如果某个时刻客户数恰好等于子进程总数,那么新到的客户将被忽略,直到至少有一个子进程完成处理重新可用。
预先派生子进程,使用文件上锁保护accept
在多个进程中引用同一个监听套接字的描述符上调用accept的做法在某些系统实现是不被支持的,那么解决办法是让应用进程在调用accept前后安装某种形式的锁(lock),这样任意时刻只有一个子进程阻塞在accept调用中,其他子进程则阻塞在获取保护accept的锁上。
这里使用文件锁来保护,文件锁涉及到文件系统的操作,可能比较耗时。
预先派生子进程,使用线程互斥锁上锁保护accept
相比于 预先派生子进程,使用文件上锁保护accept,使用线程锁保护accept,这种方法 不仅适用于同一进程内各个线程间的锁保护,而且能够用于不同进程之间的锁保护。
在不同进程间的锁保护需要注意的是
- 互斥锁变量必须存放在由所有进程共享的内存区中
- 必须告知线程函数库这个锁是用于不同进程间共享的互斥锁
预先派生子进程,父进程向子进程传递套接字描述符
只让父进程嗲用accept,然后把所接受的已经连接的套接字 传递 给某个子进程。
这样绕过了为所有子进程的accept调用提供上锁保护的需求,但是需要从父进程到子进程进行某种形式的描述符传递。
这种技术会上代码比较复杂,父进程必须跟踪子进程的闲忙状态,以便于给空闲的子进程传递新的套接字。
并发服务器,每个客户端请求创建一个线程
相比于多进程模型,如果服务器主机提供支持线程,我们可以改用线程以取代进程。线程相比于进程的优势这里不再累述。
预先创建线程服务器,使用互斥锁上锁保护accept
相比预先派生一个子进程池快于为每个客户线程fork一个子进程池类似的道理,在有线程支持的系统上,预先创建的线程池取代为每个客户现场创建一个线程的做法有类似的性能提升。
这种模式的基本设计是预先创建一个线程,并让每个线程各自调用accept,取代让每个线程都阻塞在accept调用中的做法,使用互斥锁保证任何时刻只有一个线程在调用accept。
预先创建线程服务器,由主线程调用accept
程序启动阶段创建一个线程池后让主线程调用accept;
主线程把每个客户连接传递给池中某个可用的线程,类似于进程版本的做法。
这样的设计问题在于主线程如何将一个已连接套接字传递给线程池中某个可用线程
我们有很多实现手段,本可用如前面一样使用描述符传递,但是既然所有线程和所有描述符都在同一个进程中,那么也就没有必要把一个描述符从一个线程传递到另一个线程。接收线程只需要知道这个已连接套接字描述符的值(传递描述符可不只是传递这个值,事实上是需要传递这个套接字的引用,因此也将返回一个不同于原值的描述符,该套接字的引用计数也会增加)
总结
- 当 系统负载较轻时,每来一个客户请求现场派生一个子进程为之服务的传统并发服务器程序模型就足够了
- 相比传统的每个客户fork一次设计范式,预先创建一个子进程池或一个线程池的范式能够把进程控制CPU时间降低10倍或以上。编写这些范式的程序并不会复杂,不过会有额外的工作,比如监视现在子进程数,随着所服务客户数的动态变化而增加或减少这个数目
- 某些实现允许多个子进程或线程阻塞在同一个accept调用中,另外的实现却要求对accept调用需要某种类型的锁加以保(文件锁或者互斥锁等)
- 让所有子进程或线程自行调用accept通常比让父进程或主线程独自调用accept并把描述符传递个子进程或线程来的简单和快捷
- 由于潜在select冲突的原因,让所有子进程或线程阻塞在同一accept调用中比让他们阻塞在同一个select调用中更可取。
- 使用 线程通常远快于使用进程,不过选择每个客户一个子进程还是每个客户一个线程取决于操作系统提供什么支持(某些系统不提供线程支持),还可能取决于为服务每个客户需要激活其他什么程序。例如,如果accept客户连接的服务器调用fork和exec,那么fork一个单线程的进程可能快于fork一个多线程的进程,另外还有资源等方面的综合考虑。