UNIX: разработка сетевых приложений
Листинг 30.5. Функция web_child: обработка каждого клиентского запроса
//server/web_child.c1 #include "unp.h"2 #define MAXN 16384 /* максимальное количество байтов, которое клиентможет запросить */3 void4 web_child(int sockfd)5 {6 int ntowrite;7 ssize_t nread;8 char line[MAXLINE], result[MAXN];9 for (;;) {10 if ((nread = Readline(sockfd, line, MAXLINE)) == 0)11 return; /* соединение закрыто другим концом */12 /* line задает, сколько байтов следует отправлять обратно */13 ntowrite = atol(line);14 if ((ntowrite <= 0) || (ntowrite > MAXN))15 err_quit("client request for bytes", ntowrite);16 Writen(sockfd, result, ntowrite);17 }18 }Установив соединение с сервером, клиент записывает одну строку, задающую количество байтов, которое сервер должен вернуть. Это отчасти похоже на HTTP: клиент отправляет небольшой запрос, а сервер в ответ отправляет требуемую информацию (часто это файл HTML или изображение GIF). В случае HTTP сервер обычно закрывает соединение после отправки клиенту затребованных данных, хотя более новые версии используют постоянные соединения (persistent connection), оставляя соединения TCP открытыми для дополнительных клиентских запросов. В нашей функции
сервер допускает дополнительные запросы от клиента, но, как мы видели в листинге 24.1, клиент посылает серверу только по одному запросу на каждое соединение, а по получении ответа от сервера это соединение закрывается.web_childВ строке 1 табл. 30.1 показаны результаты измерения времени, затраченного параллельным сервером. При сравнении со следующими строками этой таблицы видно, что параллельный сервер тратит больше процессорного времени, чем все другие типы серверов — то, что мы и ожидали при вызове функции
.forkПРИМЕЧАНИЕОдин из способов устройства сервера, который мы не рассматриваем в этой главе, — это сервер, инициируемый демоном inetd (см. раздел 13.5). С точки зрения управления процессами такой сервер подразумевает использование функций fork и exec, так что затраты времени центрального процессора будут еще больше, чем показанные в строке 1 для параллельного сервера.
30.6. Сервер TCP с предварительным порождением процессов без блокировки для вызова accept
В первом из рассматриваемых нами «усовершенствованных» серверов используется технология, называемая предварительным созданием процессов (preforking). Вместо того чтобы вызывать функцию
каждый раз при поступлении очередного запроса от клиента, сервер создает при запуске некоторое количество дочерних процессов, и впоследствии они обслуживают клиентские запросы по мере установления соединений с клиентами. На рис. 30.1 показан сценарий, при котором родитель предварительно создал N дочерних процессов, и в настоящий момент имеется два соединения с клиентами.forkРис. 30.1. Предварительное создание дочерних процессов сервером
Преимущество этой технологии заключается в том, что обслуживание нового клиента не требует вызова функции
родительским процессом, тем самым стоимость этого обслуживания понижается. Недостатком же является необходимость угадать, сколько дочерних процессов нужно создать при запуске. Если в некоторый момент времени количество имеющихся дочерних процессов будет равно количеству обслуживаемых клиентов, то дополнительные клиентские запросы будут игнорироваться до того момента, когда освободится какой-либо дочерний процесс. Но, как сказано в разделе 4.5, клиентские запросы в такой ситуации игнорируются не полностью. Для каждого из этих дополнительных клиентов ядро выполнит трехэтапное рукопожатие (при этом общее количество соединений не может превышать значения аргументаforkфункцииbacklog), и при вызове функцииlistenустановленные соединения будут переданы серверу. При этом, однако, приложение-клиент может заметить некоторое ухудшение в скорости получения ответа, так как, хотя функцияacceptможет быть выполнена сразу же, запрос может не поступать на обработку еще некоторое время.connectЗа счет некоторого дополнительного усложнения кода всегда можно добиться того, что сервер справится со всеми клиентскими запросами. От родительского процесса требуется постоянно отслеживать количество свободных дочерних процессов, и если это количество падает ниже некоторого минимального предела, родитель должен вызвать функцию
и создать недостающее количество дочерних процессов. Аналогично, если количество свободных дочерних процессов превосходит некоторую максимальную величину, некоторые из этих процессов могут быть завершены родителем, так как излишнее количество свободных дочерних процессов тоже отрицательно влияет на производительность (об этом мы поговорим чуть позже).forkНо прежде чем углубляться в детали, исследуем основную структуру этого типа сервера. В листинге 30.6 показана функция
для первой версии нашего сервера с предварительным порождением дочерних процессов.mainЛистинг 30.6. Функция main сервера с предварительным порождением дочерних процессов
//server/serv02.c1 #include "unp.h"2 static int nchildren;3 static pid_t *pids;4 int5 main(int argc, char **argv)6 {7 int listenfd, i;8 socklen_t addrlen;9 void sig_int(int);10 pid_t child_make(int, int, int);11 if (argc == 3)12 listenfd = Tcp_listen(NULL, argv[1], &addrlen);13 else if (argc == 4)14 listenfd = Tcp_listen(argv[1], argv[2], &addrlen);15 else
