Мы не приводим код для последовательного сервера, так как он представляет собой тривиальную модификацию параллельного сервера, показанного в следующем разделе.
30.5. Параллельный сервер TCP: один дочерний процесс для каждого клиента
Традиционно параллельный сервер TCP вызывает функцию
fork
для порождения нового дочернего процесса, который будет выполнять обработку очередного клиентского запроса. Это позволяет серверу обрабатывать несколько запросов одновременно, выделяя по одному дочернему процессу для каждого клиента. Единственным ограничением
на количество одновременно обрабатываемых клиентских запросов является ограничение операционной системы на количество дочерних процессов, допустимое для пользователя, в сеансе которого работает сервер. Листинг 5.9 содержит пример параллельного сервера, и большинство серверов TCP написаны в том же стиле.
Проблема с параллельными серверами заключается в количестве времени, которое тратит центральный процессор на выполнение функции
fork
для порождения нового дочернего процесса для каждого клиента. Давным-давно, в конце 80-х годов XX века, когда наиболее загруженные серверы обрабатывали сотни или тысячи клиентов за день, это было приемлемо. Но расширение Сети изменило требования. Теперь загруженными считаются серверы, обрабатывающие миллионы соединений TCP в день. Сказанное относится лишь к одиночным узлам, но наиболее загруженные сайты используют несколько узлов, распределяя нагрузку между ними (в разделе 14.2 [112] рассказывается об общепринятом способе распределения этой нагрузки, называемом циклическим обслуживанием DNS — DNS round robin). В последующих разделах описаны различные способы, позволяющие избежать вызова функции
fork
для каждого клиентского запроса, но тем не менее параллельные серверы остаются широко распространенными.
В листинге 30.2 показана функция
main
для нашего параллельного сервера TCP.
Листинг 30.2. Функция main для параллельного сервера TCP
32 Close(connfd); /* родительский процесс закрывает
присоединенный сокет */
33 }
34 }
Эта функция аналогична функции, показанной в листинге 5.9: она вызывает функцию
fork
для каждого клиентского соединения и обрабатывает сигналы
SIGCHLD
, приходящие от закончивших свое выполнение дочерних процессов. Тем не менее мы сделали эту функцию не зависящей от протокола за счет вызова функции
tcp_listen
. Мы не показываем обработчик сигнала
sig_chld
: он совпадает с показанным в листинге 5.8, но только без функции
printf
.
Мы также перехватываем сигнал
SIGINT
, который генерируется при вводе символа прерывания. Мы вводим этот символ после завершения работы клиента, чтобы было выведено время, потраченное центральным процессором на выполнение данной программы. В листинге 30.3 показан обработчик сигнала. Это пример обработчика сигнала, который никогда не возвращает управление.
Листинг 30.3. Обработчик сигнала SIGINT
//server/serv01.c
35 void
36 sig_int(int signo)
37 {
38 void pr_cpu_time(void);
39 pr_cpu_time;
40 exit(0);
41 }
В листинге 30.4 показана функция
pr_cpu_time
, вызываемая из обработчика сигнала.
Листинг 30.4. Функция pr_cpu_time: вывод полного времени центрального процессора