TCP-серверы
Для TCP-серверов inetd прослушивает хорошо известные порты, ожидая запроса на соединение, затем принимает соединение, ассоциирует с ним файловые Дескрипторы stdin, stdout и stderr, после чего запускает приложение. Таким образом, сервер может работать с соединением через дескрипторы 0, 1 и 2. Если это допускается конфигурационным файлом inetd (/etc/ inetd.conf), то inetd продолжает прослушивать тот же порт. Когда в этот порт поступает запрос на новое соединение, запускается новый экземпляр сервера, даже если первый еще не завершил сеанс. Это показано на рис. 3.2. Обратите внимание, что серверу не нужно обслуживать нескольких клиентов. Он просто выполняет запросы одного клиента, а потом завершается. Остальные клиенты обслуживаются дополнительными экземплярами сервера.
Рис. 3.2. Действия inetd при запуске TCP-сервера
Применение inetd освобождает от необходимости самостоятельно устанавливать TCP или UDP-соединение и позволяет писать сетевое приложение почти так же, как обычный фильтр. Простой, хотя и не очень интересный пример приведен в листинге 3.3.
Листинг 3.3. Программа rlnumd для подсчета строк
1 #include <stdio.h>
2 void main( void )
3 {
4 int cnt = 0;
5 char line[ 1024 ];
6 /*
7 *Мы должны явно установить режим построчной буферизации,
8 *так как функции из библиотеки стандартного ввода/вывод
9 *не считают сокет терминалом. */
10 setvbuf( stdout, NULL, _IOLBF, 0 );
11 while ( fgets ( line, sizeof( line ) , stdin ) != NULL )
12 printf( "%3i: %s", ++cnt, line );
13 }
По поводу этой программы стоит сделать несколько замечаний:
в тексте программы не упоминается ни о TCP, ни вообще о сети. Это не значит, что нельзя выполнять связанные с сокетами вызовы (getpeername, [gs ] etsockopt и т.д.), просто в этом не всегда есть необходимость. Нет никаких ограничений и на использование read и write. Кроме того, можно пользоваться вызовами send, recv, sendto и recvfrom, как если бы inetd не было.
режим буферизации строк приходится устанавливать самостоятельно, поскольку стандартная библиотека ввода/вывода автоматически устанавливает подобный режим только в том случае, если считает, что вывод производится на терминал. Это позволяет обеспечить быстрое время реакции для интерактивных приложений;
стандартная библиотека берет на себя разбиение входного потока на строки. Об этом уже говорилось в совете 6;
предполагаем, что не будет строк длиннее 1023 байт. Более длинные строки будут разбиты на несколько частей, и у каждой будет свой номер;
Примечание: Этот факт, который указан в книге [Oliver 2000], служит еще одним примером того, как можно легко допустить ошибку переполнения буфера. Подробнее этот вопрос обсуждался в совете 11.
хотя это приложение тривиально, но во многих «настоящих» TCP-приложениях, например telnet, rlogin и ftp, используется такая же техника.
Программа в листинге 3.3 может работать и как «нормальный» фильтр, и как Удаленный сервис подсчета строк. Чтобы превратить ее в удаленный сервис, нужно только выбрать номер порта, добавить в файл /etc/ services строку с именем сервиса и номером порта и включить в файл /etc/inetd.conf строку, описывающую этот сервис и путь к исполняемой программе. Например, если вы назовете сервис rlnum, исполняемую программу для него –
rlnumd и назначите ему порт 8000, то надо будет добавить в /etc/services строку
rlnum 8000/tcp # удаленный сервис подсчета строк,
а в /etc/inetd.conf - строку
rlnum stream tcp nowait jcs /usr/home/jcs/rlnumd rlnumd.
Добавленная в /etc/services строка означает, что сервис rlnum использует протокол TCP по порту 8000. Смысл же полей в строке, добавленной в /etc/inetd.conf, таков:
имя сервиса, как он назван в /etc/services. Это имя хорошо известного порта, к которому подсоединяются клиенты данного сервера. В вашем примере - rlnum;
тип сокета, который нужен серверу. Для TCP-серверов это stream, a для UDP-серверов - dgram. Поскольку здесь сервер пользуется протоколом ТCP указан stream;
протокол, применяемый с сервером, - tcp или udp. В данном примере это tср;
флаг wait/nowait. Для UDP- серверов его значение всегда wait, а для ТСР-серверов - почти всегда nowait. Если задан флаг nowait, то inetd сразу после запуска сервера возобновляет прослушивание связанного с ним хорошо известного порта. Если же задан флаг wait, то inetd не производит никакой работы с этим сокетом, пока сервер не завершится. А затем он возобновляет прослушивание порта в ожидании запросов на новые соединения (для stream-серверов) или новых датаграмм (для dgram-серверов). Если для stream-сервера задан флаг wait, то inetd не вызывает accept для соединения, а передает сокет, находящийся в режиме прослушивания, самому серверу, который должен принять хотя бы одно соединение перед завершением. Как отмечено в сообщении [Kacker 1998], задание флага wait для TCP-приложения - это мощная, но редко используемая возможность. Здесь приводится несколько применений флага wait для TCP-соединений:
- в качестве механизма рестарта для ненадежных сетевых программ-демонов. Пока демон работает корректно, он принимает соединения от клиентов, но если по какой-то причине демон «падает», то при следующей попытке соединения inetd его рестартует;
- как способ гарантировать одновременное подключение только одного клиента;
- как способ управления многопоточным или многопроцессным приложением, зависящим от нагрузки. В этом случае начальный процесс запускается inetd, а затем он динамически балансирует нагрузку, создавая по мере необходимости дополнительные процессы или потоки. При уменьшении нагрузки, потоки уничтожаются, а в случае длительного простоя завершает работу и сам процесс, освобождая ресурсы и возвращая прослушивающий сокет inetd.
В данном примере задан флаг nowait, как и обычно для TCP-серверов.
- имя пользователя, с правами которого будет запущен сервер. Это имя должно присутствовать в файле /etc/passwd. Большинство стандартных серверов, прописанных в inetd. conf, запускаются от имени root, но это совершенно необязательно. Здесь в качестве имени пользователя выбрано jcs;
- полный путь к файлу исполняемой программы. Поскольку rlnumd находится в каталоге пользователя jcs, задан путь /usr/home/ jcs/rlnumd;
- до пяти аргументов (начиная с argv [ 0 ]), которые будут переданы серверу. Поскольку в этом примере у сервера нет аргументов, оставлен только argv [ 0 ]
Чтобы протестировать сервер, необходимо заставить inetd перечитать свой конфигурационный файл (в большинстве реализаций для этого нужно послать ему сигнал SIGHUP) и соединиться с помощью telnet:
bsd: $ telnet localhost rlnum
Trying 127.0.0.1. . .
Connected to localhost
Escape character is "^]".
hello
1: hello
world
2: world
^]
telnet> quit
Connection closed.
bsd: $