Эффективное программирование TCP-IP

       

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 можно сделать два замечания:

  • в реализации клиента предполагается, что он всегда получит ответ от серве­ра. Как было сказано в совете 1, нет гарантии, что посланная сервером датаграмма будет доставлена. Поскольку udpclient - это интерактивная про­грамма, ее всегда можно прервать и запустить заново, если она «зависнет» в вызове recvfrom. Но если бы клиент не был интерактивным, нужно было бы взвести таймер, чтобы предотвратить потери датаграмм;


  • Примечание: В сервере udpechol об этом не нужно беспокоиться, так как точно известно, что датаграмма уже пришла (иначе inetd не запустил бы сервер). Однако уже в следующем примере (листинг 3.6) приходится думать о потере датаграмм, так что таймер ассоциирован с recvfrom.



  • при работе с сервером udpechol не нужно получать адрес и порт отправителя, так как они уже известны. Поэтому строки 18 и 19 можно было бы заменить на:


  • 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, но для упрощения здесь не выделяется особый случай.


    Содержание раздела