UDP-серверы
Поскольку в протоколе UDP соединения не устанавливаются (совет 1), inetd нечего слушать. При этом inetd запрашивает операционную систему (с помощью вызова select) о приходе новых датаграмм в порт UDP-сервера. Получив извещение, inetd дублирует дескриптор сокета на stdin, stdout и stderr и запускает UDP-сервер. В отличие от работы с TCP-серверами при наличии флага nowait, inetd больше не предпринимает с этим портом никаких действий, пока сервер не завершит сеанс. В этот момент он снова предлагает системе извещать его о новых датаграммах. Прежде чем закончить работу, серверу нужно прочесть хотя бы одну датаграмму из сокета, чтобы inetd не «увидел» то же самое сообщение, что и раньше. В противном случае он опять запустит сервер, войдя в бесконечный цикл.
Пример простого UDP-сервера, запускаемого через inetd, приведен в листинге 3.4. Этот сервер возвращает то, что получил, добавляя идентификатор своего процесса.
Листинг 3.4. Простой сервер, реализующий протокол запрос-ответ
udpecho1.с
1 ttinclude "etcp.h"
2 int main( int argc, char **argv )
3 {
4 struct sockaddr_in peer;
5 int rc;
6 int len;
7 int pidsz;
8 char buf[ 120 ] ;
9 pidsz = sprintf( buf, "%d: ", getpid () ) ;
10 len = sizeof( peer );
11 rc = recvfromt 0, buf + pidsz, sizeof( buf ) - pidsz, 0,
12 ( struct sockaddr * )&peer, &len);
13 if ( rc <= 0 )
14 exit ( 1 ) ;
15 sendto( 1, buf, re + pidsz, 0,
16 (struct sockaddr * )&peer, len);
17 exit( 0 );
18 }
updecho1
9 Получаем идентификатор процесса сервера (PID) от операционной системы, преобразуем его в код ASCII и помещаем в начало буфера ввода/вывода.
10-14 Читаем датаграмму от клиента и размещаем ее в буфере после идентификатора процесса. 15-17 Возвращаем клиенту ответ и завершаем сеанс.
Для экспериментов с этим сервером воспользуемся простым клиентом, код которого приведен в листинге 3.5. Он читает запросы из стандартного ввода, отсылает их серверу и печатает ответы на стандартном выводе.
Листинг 3.5. Простой UDP-клиент
1 #include "etcp.h"
2 int main( int argc, char **argv )
з {
4 struct sockaddr_in peer;
5 SOCKET s;
6 int rc = 0;
7 int len;
8 char buf[ 120 ];
9 INIT();
10 s = udp_client( argv[ 1 ], argvf 2 ], &peer );
11 while ( fgets( buf, sizeof'( buf ), stdin ) != NULL )
12 {
13 rc = sendto( s, buf, strlenf buf ), 0,
14 (struct sockaddr * )&peer, sizeof( peer ) );
15 if ( rc < 0 )
16 error( 1, errno, "ошибка вызова sendto" );
17 len = sizeof( peer );
18 rc = recvfrom( s, buf, sizeof( buf ) - 1, 0,
19 (struct sockaddr * )&peer, &len );
20 if ( rc < 0 )
21 error( 1, errno, "ошибка вызова recvfrom" );
22 buff [rc ] = '\0';
23 fputsf (buf, stdout);
24 }
25 EXIT( 0 ) ;
26 }
10 Вызываем функцию udp_client, чтобы она поместила в структуру peer адрес сервера и получила UDP-сокет.
11-16 Читаем строку из стандартного ввода и посылаем ее в виде UDP-датаграммы хосту и в порт, указанные в командной строке.
17-21 Вызываем recvfrom для чтения ответа сервера и в случае ошибки завершаем сеанс.
22-23 Добавляем в конец ответа двоичный нуль и записываем строку на стандартный вывод.
В отношении программы udpclient можно сделать два замечания:
Примечание: В сервере udpechol об этом не нужно беспокоиться, так как точно известно, что датаграмма уже пришла (иначе inetd не запустил бы сервер). Однако уже в следующем примере (листинг 3.6) приходится думать о потере датаграмм, так что таймер ассоциирован с recvfrom.
rc = recvfrom( s, buf, sizeof( buf ) - 1, 0, NULL, NULL );
Но, как показано в следующем примере, иногда клиенту необходимо иметь информацию, с какого адреса сервер послал ответ, поэтому приведенные здесь UDP-клиенты всегда извлекают адрес.
Для тестирования сервера добавьте в файл /etc/inetd.conf на машине bsd строку
udpecho dgram udp wait jcs /usr/home/jcs/udpechod udpechod,
а в файл /etc/services – строку
udpecho 8001/udp
Затем переименуйте udpechol в udpechod и заставьте программу inetd перечитать свой конфигурационный файл. При запуске клиента udpclient на машине sparc получается:
sparc: $ udpclient bed udpeoho
one
28685: one
two
28686: two
three
28687: three
^C
spare: $
Этот результат демонстрирует важную особенность UDP-серверов: они обычно ведут диалог с клиентом. Иными словами, сервер получает один запрос и посылает один ответ. Для UDP-серверов, запускаемых через inetd, типичными будут следующие действия: получить запрос, отправить ответ, выйти. Выходить нужно как можно скорее, поскольку inetd не будет ждать других запросов, направленных в порт этого сервера, пока тот не завершит сеанс.
Из предыдущей распечатки видно, что, хотя складывается впечатление, будто udpclient ведет с udpechol диалог, в действительности каждый раз вызывается новый экземпляр сервера. Конечно, это неэффективно, но важнее то, что сервер не запоминает информации о состоянии диалога. Для udpechol это несущественно так как каждое сообщение - это, по сути, отдельная транзакция. Но так бывает не всегда. Один из способов решения этой проблемы таков: сервер принимает сообщение от клиента (чтобы избежать бесконечного цикла), затем соединяется с ним, получая тем самым новый (эфемерный) порт, создает новый процесс и завершает работу. Диалог с клиентом продолжает созданный вновь процесс.
Примечание: Есть и другие возможности. Например, сервер мог бы обслуживать нескольких клиентов. Принимая датаграммы от нескольких клиентов, сервер амортизирует накладные расходы на свой запуск и не завершает сеанс, пока не обнаружит, что долго простаивает без дела. Преимущество этого метода в некотором упрощении клиентов за счет усложнения сервера.
Чтобы понять, как это работает, внесите в код udpechol изменения, представленные в листинге 3.6.
Листинг 3.6. Вторая версия udpechod
1 #include "etcp.h"
2 int main( int argc, char **argv )
3 {
4 struct sockaddr_in peer;
5 int s;
6 int rc;
7 int len;
8 int pidsz;
9 char buf[ 120 ] ;
10 pidsz = sprintf( buf, "%d: ", getpid() );
11 len = sizeof( peer );
12 rc = recvfrom( 0, buf + pidsz, sizeof( buf ) - pidsz,
13 0, ( struct sockaddr * )&peer, &len );
14 if ( rc < 0 )
15 exit ( 1 );
16 s = socket( AF_INET, SOCK_DGRAM, 0 );
17 if ( s < 0 )
18 exit( 1 ) ;
19 if ( connect( s, ( struct sockaddr * )&peer, len ) < 0)
20 exit (1);
21 if ( fork() != 0 ) /* Ошибка или родительский процесс? */
22 exit( 0 ) ;
23 /* Порожденный процесс. */
24 while ( strncmp( buf + pidsz, "done", 4 ) != 0 )
25 {
26 if ( write( s, buf, re + pidsz ) < 0 )
27 break;
28 pidsz = sprintf( buf, "%d: ", getpid() );
29 alarm( 30 );
30 rc = read( s, buf + pidsz, sizeof( buf ) - pidsz );
31 alarm( 0 );
32 if ( re < 0)
33 break;
34 }
35 exit( 0 );
36 }
udpecho2
10-15 Получаем идентификатор процесса, записываем его в начало буфера и читаем первое сообщение так же, как в udpechol.
16-20 Получаем новый сокет и подсоединяем его к клиенту, пользуясь адресом в структуре peer, которая была заполнена при вызове recvfrom.
21-22 Родительский процесс разветвляется и завершается. В этот момент inetd может возобновить прослушивание хорошо известного порта сервера в ожидании новых сообщений. Важно отметить, что потомок использует номер порта new, привязанный к сокету s в результате вызова connect.
24-35 Затем посылаем клиенту полученное от него сообщение, только с добавленным в начало идентификатором процесса. Продолжаем читать сообщения от клиента, добавлять к ним идентификатор процесса-потомка и отправлять их назад, пока не получим сообщение, начинающееся со строки done. В этот момент сервер завершает работу. Вызовы alarm, окружающие операцию чтения на строке 30, - это защита от клиента, который закончил сеанс, не послав done. В противном случае сервер мог бы «зависнуть» навсегда. Поскольку установлен обработчик сигнала SIGALRM, UNIX завершает программу при срабатывании таймера.
Переименовав новую версию исполняемой программы в udpechod и запустив ее. вы получили следующие результаты:
sparc: $ udpclient bad udpecho
one
28743: one
two
28744: two
three
28744: three
done
^C
sparc: $
На этот раз, как видите, в первом сообщении пришел идентификатор родительского процесса (сервера, запущенного inetd), а в остальных - один и тот же идентификатор (потомка). Теперь вы понимаете, почему udpclient всякий раз извлекает адрес сервера: ему нужно знать новый номер порта (а возможно, и новый IP-адрес если сервер работает на машине с несколькими сетевыми интерфейсами), в который посылать следующее сообщение. Разумеется, это необходимо делать только для первого вызова recvfrom, но для упрощения здесь не выделяется особый случай.