15 nbytes = atoi(argv[5]);
16 snprintf(request, sizeof(request), "%d\n", nbytes); /* в конце
символ новой строки */
17 for (i = 0; i < nchildren; i++) {
18 if ((pid = Fork()) == 0) { /* дочерний процесс */
19 for (j = 0; j < nloops; j++) {
20 fd = Tcp_connect(argv[1], argv[2]);
21 Write(fd, request, strlen(request));
22 if ((n = Readn(fd, reply, nbytes)) != nbytes)
23 err_quit("server returned %d bytes", n);
24 Close(fd); /* состояние TIME_WAIT на стороне клиента,
а не сервера */
25 }
26 printf("child %d done\n", i);
27 exit(0);
28 }
29 /* родительский процесс снова вызывает функцию fork */
30 }
31 while (wait(NULL) > 0) /* теперь родитель ждет завершения всех
дочерних процессов */
32 ;
33 if (errno != ECHILD)
34 err_sys("wait error");
35 exit(0);
36 }
10-12
Каждый раз при запуске клиента мы задаем имя узла или IP-адрес сервера, порт сервера, количество дочерних процессов, порождаемых функцией
fork
(что позволяет нам инициировать несколько одновременных соединений с сервером), количество запросов, которое каждый дочерний процесс должен посылать серверу, и количество байтов, отправляемых сервером в ответ на каждый запрос.
17-30
Родительский процесс вызывает функцию
fork
для порождения каждого дочернего процесса, и каждый дочерний процесс устанавливает указанное количество соединений с сервером. По каждому соединению дочерний процесс посылает запрос, задавая количество байтов, которое должен вернуть сервер, а затем дочерний процесс считывает это количество данных с сервера. Родительский процесс просто ждет завершения выполнения всех дочерних процессов. Обратите внимание, что клиент закрывает каждое соединение TCP, таким образом состояние TCP TIME_WAIT имеет место на стороне клиента, а не на стороне сервера. Это отличает наше клиент-серверное соединение от обычного соединения HTTP.
При тестировании различных серверов из этой главы мы запускали клиент следующим образом:
% <b>client 192.168.1.20 8888 5 500 4000</b>
Таким образом создается 2500 соединений TCP с сервером: по 500 соединений от каждого из 5 дочерних процессов. По каждому соединению от клиента к серверу посылается 5 байт (
"4000\n"
), а от сервера клиенту передается 4000 байт. Мы запускаем клиент на двух различных узлах, соединяясь с одним и тем же сервером, что дает в сумме 5000 соединений TCP, причем максимальное количество одновременных соединений с сервером в любой момент времени равно 10.
ПРИМЕЧАНИЕ
Для проверки различных веб-серверов существуют изощренные контрольные тесты. Один из них называется WebStone. Информация о нем находится в свободном доступе по адресу http://www.mindcraft.com/webstone. Для общего сравнения различных альтернативных устройств сервера, которые мы рассматриваем в этой главе, нам не нужны столь сложные тесты.
Теперь мы представим девять различных вариантов устройства сервера.
30.4. Последовательный сервер TCP
Последовательный сервер TCP полностью обрабатывает запрос каждого клиента, прежде чем перейти к следующему клиенту. Последовательные серверы редко используются, но один из них, простой сервер времени и даты, мы показали в листинге 1.5.
Тем не менее у нас имеется область, в которой желательно применение именно последовательного сервера — это сравнение характеристик других серверов. Если мы запустим клиент следующим образом:
% <b>client 192.168.1.20 8888 1 5000 4000</b>
и соединимся с последовательным сервером, то получим такое же количество соединений TCP (5000) и такое же количество данных, передаваемых по одному соединению. Но поскольку сервер является последовательным, на нем не осуществляется никакого управления процессами. Это дает нам возможность получить базовое значение времени, затрачиваемого центральным процессором на обработку указанного количества запросов, которое потом мы можем вычесть из результатов измерений для других серверов. С точки зрения управления процессами последовательный сервер является самым быстрым, поскольку он вовсе не занимается этим управлением. Взяв последовательный сервер за точку отсчета, мы можем сравнивать результаты измерений быстродействия других серверов, показанные в табл. 30.1.
Мы не приводим код для последовательного сервера, так как он представляет собой тривиальную модификацию параллельного сервера, показанного в следующем разделе.
30.5. Параллельный сервер TCP: один дочерний процесс для каждого клиента
Традиционно параллельный сервер TCP вызывает функцию
fork
для порождения нового дочернего процесса, который будет выполнять обработку очередного клиентского запроса. Это позволяет серверу обрабатывать несколько запросов одновременно, выделяя по одному дочернему процессу для каждого клиента. Единственным ограничением на количество одновременно обрабатываемых клиентских запросов является ограничение операционной системы на количество дочерних процессов, допустимое для пользователя, в сеансе которого работает сервер. Листинг 5.9 содержит пример параллельного сервера, и большинство серверов TCP написаны в том же стиле.
Проблема с параллельными серверами заключается в количестве времени, которое тратит центральный процессор на выполнение функции
fork
для порождения нового дочернего процесса для каждого клиента. Давным-давно, в конце 80-х годов XX века, когда наиболее загруженные серверы обрабатывали сотни или тысячи клиентов за день, это было приемлемо. Но расширение Сети изменило требования. Теперь загруженными считаются серверы, обрабатывающие миллионы соединений TCP в день. Сказанное относится лишь к одиночным узлам, но наиболее загруженные сайты используют несколько узлов, распределяя нагрузку между ними (в разделе 14.2 [112] рассказывается об общепринятом способе распределения этой нагрузки, называемом
циклическим обслуживанием DNS — DNS round robin). В последующих разделах описаны различные способы, позволяющие избежать вызова функции
fork
для каждого клиентского запроса, но тем не менее параллельные серверы остаются широко распространенными.