客户端/服务器程序设计模式
- 0. 迭代服务器
- 1. 为每个连接创建子进程
- 2. 创建多个子进程,在子进程中
accept
- 3. 预先派生子进程,accept 使用锁保护
- 4. 每个客户一个线程
- 5. 预先创建线程
- 6. IO 多路复用
阅读 UNP 的时候了解到了多种客户端/服务器程序设计的范式,这些模式很容易理解。由于本书写于20多年前,目前大部分模式都已经过时了。不过现在流行的模式自然是在此基础上演化而来的,因此有必要了解一下这些模式。
所谓客户/服务器模式,就是我们常说的 C/S 模式,客户端和服务器基于网络进行通信,通常基于 TCP 协议。本文中总结了常见的客户端/服务器程序设计模式。
0. 迭代服务器
收到一个请求后就同步地处理它,在未处理完之前不能处理其他的请求。这就是最简单的迭代服务器,这种模式只适用于单个请求耗时很少的场景,比如下面的例子中,接收到请求后服务器返回当前的世界,而后立刻断开连接。
1. 为每个连接创建子进程
如果每个请求都需要消耗不少时间才能处理完,那么在处理一个请求的时候,其他客户的请求就被会阻塞。可以为每个请求创建一个进程,用该进程来处理该请求。为每个新的连接创建进程,连接完成后会销毁进程,这导致进程频繁地创建与销毁,存在不少系统开销。
2. 创建多个子进程,在子进程中 accept
在多个进程中进行 accept
操作,新来一个请求最终只会被单个进程成功 accept
,这样就能够把多个请求分散到不同的进程中。
在主进程中创建多个进程,各个进程都执行 accept
操作,新的连接到来后,所有子进程在同一个 listen_fd
上执行 accept
操作的子进程均被唤醒,但只有最快运行的那个子进程能够 accept
成功,该子进程就负责处理此次连接。
缺点是单个连接会导致多个子进程被唤醒,如果子进程较多的时候,这种做法会导致性能受损。
3. 预先派生子进程,accept 使用锁保护
为了避免多个子进程同时阻塞在 accept
调用上,可以在 accept
前面那使用某种锁,让多个子进程阻塞在锁上,其中只有一个能够拿到锁,进而只有单个进程能够阻塞在 accept
调用上。
4. 每个客户一个线程
accept
到新的客户连接后,就创建一个线程,在此线程中处理用户请求。由于处理请求过程中往往涉及到读取文件、数据库,这类 IO 操作都相当耗时,当执行这些 IO 操作的时候,操作系统可以调度其他线程。这样可以让计算资源得到更高效的利用。
5. 预先创建线程
尽管创建和销毁线程的代价较进程的创建与销毁要少,但是毕竟存在消耗。另外如果请求量很大,那么就会创建大量的进程,可能会耗尽内存。因此,可以使用线程池。同样可以在各个线程上做 accept
也可以又主线程来 accept
而后交给子线程。
后者可以使用队列来实现。主线程 accept
之后,把文件描述符等信息放入队列,其他子线程从队列中取出文件描述符,并完成后续的服务。这是一个典型的生产者消费者的模型,在实现的时候使用一个互斥锁和条件变量即可轻松实现。
6. IO 多路复用
前面提到的方法中,任何时刻,进程和线程都是在为一个连接服务。如果该连接的处理需要做耗时的 IO 操作,则需要操作系统切换进程或者线程,以此保存不浪费系统资源。而基于 select、poll、epoll 这样的系统调用,可以订阅多个描述符上的事件,一个进程/线程可以同时处理多个连接。
这种模式下,当监听的文件描述符上有事件发生时,根据发生的事件类型和对应的文件描述符,可以决定该如何采取行动。但是采取行动往往是在当前线程中进行的,即在处理某事件的时候,其他事件不能得到处理。
因此,这种模式常常和多线程搭配使用,在工作线程来处理事件。