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

       

Подумайте о том, чтобы


| | |

Проектировщик сетевого сервера сталкивается с проблемой выбора номера для хорошо известного порта. Агентство по выделению имен и уникальных параметров протоколов Internet (Internet Assigned Numbers Authority - IANА) подразделяет все номера портов на три группы: «официальные» (хорошо известные), зарегистрированные и динамические, или частные.

Примечание: Термин «хорошо известный порт» используется в общем смысле — как номер порта доступа к серверу. Строго говоря, хорошо известные порты контролируются агентством IANA.

Хорошо известные - это номера портов в диапазоне от 0 до 1023. Они контролируются агентством IANA. Зарегистрированные номера портов находятся в диапазоне от 1024 до 49151. IANA не контролирует их, но регистрирует и публикует в качестве услуги сетевому сообществу. Динамические или частные порты имею номера от 49152 до 65535. Предполагается, что эти порты будут использоваться как эфемерные, но многие системы не следуют этому соглашению. Так, системы, производные от BSD, традиционно выбирают номера эфемерных портов из диапазона от 1024-5000. Полный список всех присвоенных IANA и зарегистриро­ванных номеров портов можно найти на сайте http://www.isi.edu/in-notes/iana/ assignment/port-numbers/.

Проектировщик сервера может получить от IANA зарегистрированный номер порта.

Примечание: Чтобы подать заявку на получение хорошо известного или зарегистрированного номера порта, зайдите на Web-страницуhttp://www.isi.edu/cgi-bin/iana/port-numbers.pl.

Это, конечно, не помешает другим использовать тот же номер, и рано или поздно два сервера, работающие с одним и тем же номером порта, окажутся на одной машине. Обычно эту проблему решают, выделяя некоторый номер порта по умолчанию, но позволяя задать другой в командной строке.

Другое более гибкое решение, но применяемое реже, состоит в том, чтобы ис­пользовать возможность inetd (совет 17), которая называется мультиплексором портов TCP (TCP Port Service Multiplexor - TCPMUX). Сервис TCPMUX описан в RFC 1078 [Letter 1988]. Мультиплексор прослушивает порт 1 в ожидании TCP-соединений. Клиент соединяется с TCPMUX и посылает ему строку с име­нем сервиса, который он хочет запустить. Строка должна завершаться символа­ми возврата каретки и перевода строки (<CR><LF>). Сервер или, возможно, TCPMUX посылает клиенту один символ: + (подтверждение) или - (отказ), за которым следует необязательное пояснительное сообщение, завершаемое после­довательностью <CR><LF>. Имена сервисов (без учета регистра) также хранятся в файле inetd. conf, но начинаются со строки tcpmux/, чтобы отличить их от обычных сервисов. Если имя сервиса начинаются со знака +, то подтверждение посылает TCPMUX, а не сервер. Это позволяет таким серверам, как rlnumd (листинг 3.3), которые проектировались без учета TCPMUX, все же воспользоваться предоставляемым им сервисом.


Например, если вы захотите запустить сервис подсчета строк из совета 17 в качестве TCPMUX-сервера, то надо добавить в файл inetd. conf строку
tcpmux/+rlnumd stream tcp nowait jcs /usr/jome/jcs/rlnumd rlnumd
Для тестирования заставьте inetd перечитать свой конфигурационный файл, а затем подсоединитесь к нему с помощью telnet, указав имя сервиса TCPMUX:
bsd: $ telnet localhost tcpmux
Trying 127.0.0.1 ...
Connected to localhost
Escape character is "^]".


rlnumd
+Go
hello
 1: hello
world
 2: world А]
telnet> quit
Connection closed
bsd: $
К сожалению, сервис TCPMUX поддерживается не всеми операционными системами и даже не всеми UNIX-системами. Но, с другой стороны, его реализация настолько проста, что возможно написать собственную версию. Поскольку TCPMUX должен делать почти то же, что и inetd (за исключением мониторинга нескольких шкетов), заодно будут проиллюстрированы те идеи, которые лежат в основе inetd. Начнем с определения констант, глобальных переменных и функции main (листинг 3.7).
Листинг 3.7. tcpmux - константы, глобальные переменные и main
1    #include"etcp.h"
2    #define MAXARGS 10 /*Максималиное число аргументов сервера.*/
3    #define MAXLINE 256 /*Максимальная длина строки в tcpmux.conf.*/
4    #define NSERVTAB 10 /*Число элементов в таблице service_table.*/
5    #define CONFIG “tcpmux.conf”
6    typedef  struct
7    {
8    int flag;
9    char *service;
10   char *path;
11   char *args[ MAXARGS + 1 ];
12   } servtab_t;
13   int ls; /* Прослушиваемый сокет. */
14   servtab_t service_table[ NSERVTAB + 1 ];
15   int main( int argc, char **argv )
16   {
17   struct sockaddr_in peer;
18   int s;
19   int peerlen;
20   /* Инициализировать и запустить сервер tcpmux. */
21   INIT ();
22   parsetab ();
23   switch ( argc }
24   {
25     case 1: /* Все по умолчанию. */
26      ls = tcp_server( NULL, "tcpmux" );
27      break;
28     case 2  /* Задан интерфейс и номер порта. */
29      ls = tcp_server( argv[ 1 ], "tcpmux" );


30      break;
31     case 3: /* Заданы все параметры. */
32      ls = tcp_server( argv[ 1 ], argv[ 2 ] );
33      break;
34     default:
35      error( 1, 0, "Вызов: %s [ интерфейс [ порт ] ]\n",
36       program_name );
37   }
38   daemon( 0, 0 );
39   signal( SIGCHLD, reaper ) ;
40   /* Принять соединения с портом tcpmux. */
41   for ( ; ; )
42   {
43     peerlen  =  sizeof(  peer   );
44     s  =  accept( ls, (struct  sockaddr  * )&peer, &peerlen ) ;
45     if   ( s  <  0 }
46      continue;
47     start_server( s );
48     CLOSE( s );
49   }
50   }
main
6- 12 Структура servtab_t определяет тип элементов в таблице service_table. Поле flag устанавливается в TRUE, если подтверждение должен посылать tcpmux, а не сам сервер.
22 В начале вызываем функцию parsetab, которая читает и разбирает файл tcpmux. conf и строит таблицу service_table. Текст процедуры parsetab приведен в листинге 3.9.
23-37 Данная версия tcpmux позволяет пользователю задать интерфейс или порт, который будет прослушиваться. Этот код инициализирует сер­вер с учетом заданных параметров, а остальным присваивает значения по умолчанию.
38 Вызываем функцию daemon, чтобы перевести процесс tcpmux в фоновый режим и разорвать его связь с терминалом.
39 Устанавливаем обработчик сигнала SIGCHLD. Это не дает запускаемым серверам превратиться в «зомби» (и зря расходовать системные ресурсы) при завершении.
Примечание: В некоторых системах функция signal - это интерфейс к сигналам со старой «ненадежной» семантикой. В этом случае надо пользоваться функцией sigaction, которая обеспечивает семантику надежных сигналов. Обычно эту проблему решают путем создания собственной функции signal, которая вызываетиз себя sigaction. Такая реализация приведена в приложении 1.
41-49 В этом цикле принимаются соединения с tcpmux и вызывается функция start_server, которая создает новый процесс с помощью fork и запускает запрошенный сервер с помощью ехес.
Теперь надо познакомимся с функцией start_server (листинг 3.8). Именно здесь выполняются основные действия.


Листинг 3.8. Функция start_server
1    static void start_server( int s )
2    {
3    char line[ MAXLINE ];
4    servtab_t *stp;
5    int re;
6    static char errl[] = "- не могу прочесть имя сервиса \r\n";
7    static char err2[ ] = "-неизвестный сервис\г\п";
8    static char еrrЗ[] = "-не могу запустить сервис\г\п";
9    static char ok [ ] = "+OK\r\n";
10   rc = fork();
11   if(rc<0) /* Ошибка вызова fork. */
12   {
13     write( s, еrrЗ, sizeof( еrrЗ ) - 1 ) ;
14     return;'
15   }
16   if ( rc != 0 )  /* Родитель. */
17     return;
18   /* Процесс-потомок. */
19   CLOSE( ls );    /* Закрыть прослушивающий сокет. */
20   alarm( 10 );
21   rc = readcrlf( s, line, sizeof( line ) );
22   alarm( 0 );
23   if ( rc <= 0 )
24   {
25     write( s, errl, sizeoff errl ) - 1 );
26     EXIT( 1 ) ;
27   }
28   for ( stp = service_table; stp->service; stp+ + )
29     if ( strcasecmp( line, stp->service ) == 0 )
30      break;
31   if ( !stp->service )
32   {
33     write( s, err2, sizeof( err2 ) - 1 );
34     EXIT( 1 ) ;
35   }
36   if ( stp->flag )
37     if ( write( s, ok, sizeof( ok } - 1 ) < 0 )
38      EXIT( 1 );
39   dup2 ( s , 0 ) ;
40   dup2( s, 1 } ;
41   dup2( s, 2 ) ;
42   CLOSE( s ) ;
43   execv( stp->path, stp->args );
44   write( 1, еrrЗ, sizeof ( еrrЗ ) - 1 );
45   EXIT( 1 );
46   }
start_server
10-17 Сначала с помощью системного вызова fork создаем новый процесс, идентичный своему родителю. Если fork завершился неудачно, то посылаем клиенту сообщение об ошибке и возвращаемся (раз fork не отработал, то процесса-потомка нет, и управление возвращается в функцию main родительского процесса). Если fork завершился нормально, то это родительский процесс, и управление возвращается.
19-27 В созданном процессе закрываем прослушивающий сокет и из подсоединенного сокета читаем имя сервиса, которому нужно запустить клиент. Окружаем операцию чтения вызовами alarm, чтобы завер­шить работу, если клиент так и не пришлет имя сервиса. Если функция reader If возвращает ошибку, посылаем клиенту сообщение и за­канчиваем сеанс. Текст readcrlf приведен ниже в листинге 3.10.


28-35 Ищем в таблице service_table имя запрошенного сервиса. Если оно отсутствует, то посылаем клиенту сообщение об ошибке и завершаем работу.
36-38 Если имя сервиса начинается со знака +, посылаем клиенту подтверждение. В противном случае даем возможность сделать это серверу.
39-45 С помощью системного вызова dup дублируем дескриптор сокета на stdin, stdout и stderr, после чего закрываем исходный сокет. И, наконец, подменяем процесс процессом сервера с помощью вызова execv. После этого запрошенный клиентом сервер - это процесс-потомок. Если execv возвращает управление, то сообщаем клиенту, что не смогли запустить запрошенный сервер, и завершаем сеанс.
В листинге 3.9 приведен текст подпрограммы parsetab. Она выполняет простой, но несколько утомительный разбор файла tcpmux. conf. Файл имеет следующий формат:
имя_сервиса путь аргументы ...
Листинг 3.9. Функция parsetab
1    static void parsetab( void )
2    {
3    FILE *fp;
4    servtab_t *stp = service_table;
5    char *cp;
6    int i;
7    int lineno;
8    char line[ MAXLINE ];
9    fp = fopen( CONFIG, "r" );
10   if ( fp == NULL )
11     error( 1, errno, "не могу открыть %s", CONFIG );
12   lineno = 0;
13   while ( fgets( line, sizeof( line ), fp ) != NULL )
14   {
15     lineno++;
16     if ( line[ strlen( line ) - 1 ] != '\n' )
17      error( 1, 0, "строка %d слишком длинная\п", lineno );
18     if ( stp >= service_table + NSERVTAB )
19      error( 1, 0, "слишком много строк в tcpmux.conf\n" );
20     cp = strchr( line, '#' );
21     if ( cp != NULL )
22      *cp = '\0';
23     cp = strtok( line, " \t\n" ) ;
24     if ( cp == NULL )
25      continue;
26     if ( *cp =='+')
28      stp->flag = TRUE;
29     cp++;
30     if ( *cp == '\0' strchrf " \t\n", *cp ) != NULL )
31      error( 1, 0, "строка %d: пробел после ‘+’'\n",
32       lineno );
34     stp->service = strdup( cp );
35     if ( stp->service == NULL )


36      error( 1, 0, "не хватило памяти\n" );
37     cp = strtok( NULL, " \t\n" );
38     if ( cp == NULL)
39      error( 1, 0, "строка %d: не задан путь (%s)\n",
40     lineno, stp->service );
41     stp->path = strdup( cp );
42     if ( stp->path == NULL )
43      error( 1, 0, "не хватило памяти\n" );
44     for ( i = 0; i < MAXARGS; i++ )
45     {
46      cp = strtok( NULL, " \t\n" );
47      if ( cp == NULL )
48       break;
49      stp->args[ i ] = strdup( cp );
50      if ( stp->args[ i ] == NULL )
51       error( 1, 0, "не хватило памяти\n" );
53      if ( i >= MAXARGS && strtok( NULL, " \t\n" ) != NULL)
54       error( 1, 0, "строка %d: слишком много аргументов (%s) \n,
55        lineno, stp->service );
56      stp->args[ i ] = NULL;
57      stp++;
58     }
59     stp->service = NULL;
60   fclose ( fp );
61   }
Показанная в листинге 3. 10 функция readcrlf читает из сокета по одному байту. Хотя это и неэффективно, но гарантирует, что будет прочитана только пер­вая строка данных, полученных от клиента. Все данные, кроме первой строки, предназначены серверу. Если бы вы буферизовали ввод, а клиент послал бы боль­ше одной строки, то часть данных, адресованных серверу, считывал бы tcpmux, и они были бы потеряны.
Обратите внимание, что readcrlf принимает также и строку, завершающую­ся только символом новой строки. Это находится в полном соответствии с прин­ципом устойчивости [Postel 1981a], который гласит: «Подходите не слишком стро­го к тому, что принимаете, но очень строго - к тому, что посылаете». В любом случае как <CR><LF>, так и одиночный <LF> отбрасываются.
Определение функции readcrlf такое же, как функций read, readline, readn и readvrec:
#include "etcp.h"
int readcrlf( SOCKET s, char *buf, size_t len );
Возвращаемое значение: число прочитанных байт или -1 в случае ошибки.


Листинг 3.10. Функция readcrlf
1    int readcrlf( SOCKET s, char *buf, size_t len )
2    {
3    char *bufx = buf;
4    int rc;
5    char с;
6    char lastc = 0;
7    while ( len > 0 )
8    {
9      if ( ( rc = recv( s, &c, 1, 0 ) ) !=1)
10     {
11      /*
12       *Если нас прервали, повторим,
13       *иначе вернем EOF или код ошибки.
14       */
15      if ( гс < 0 && errno = EINTR )
16       continue;
17      return  rc;
18     }
19     if ( с = '\n' )
20     {
21      if ( lastc   ==   '\r' )
22       buf--;
23      *buf   =   '\0';  /* He  включать  <CR><LF>. */
24      return  buf - bufx;
25     }
26     *buf++ = c;
27     lastc = c;
28     len--;
29   }
30   set_errno( EMSGSIZE );
31   return -1;
32   }
И наконец рассмотрим функцию reaper (листинг 3.11). Когда сервер, запу­щенный с помощью tcpmux, завершает сеанс, UNIX посылает родителю (то есть tcpmux) сигнал SIGCHLD. При этом вызывается обработчик сигнала reaper, ко­торый, в свою очередь, вызывает waitpid для получения статуса любого из за­вершившихся потомков. В системе UNIX это необходимо, поскольку процесс-потомок может возвращать родителю свой статус завершения (например, аргумент функции exit).
Примечание: В некоторых вариантах UNIX потомок возвращает и другую информацию. Так, в системах, производных от BSD, возвращается сводная информация о количестве ресурсов, потребленных завершившимся процессом и всеми его потомками. Во всех системах UNIX, no меньшей мере, возвращается указание на то, как завершился процесс: из-за вызова exit (передается также код возврата) или из-за прерывания сигналом (указывается номер сигнала).
Пока родительский процесс не заберет информацию о завершении потомка с помощью вызова wait или waitpid, система UNIX должна удерживать ту часть ресурсов, занятых процессом-потомком, в которой хранится информация о состоянии. Потомки, которые уже завершились, но еще не передали родителю инфор­мацию о состоянии, называются мертвыми (defunct) или «зомби».
Листинг 3.11. Функция reaper
1    void reaper( int sig )
2    {
3    int waitstatus;
4    while ( waitpid( -1, &waitstatus, WNOHANG ) > 0 ) {;}
5    }
Протестируйте tcpmux, создав файл tcpmux.conf из одной строки:
+rlnum  /usr/hone/jcs/rlnumd rlnumd
Затем запустите tcpmux на машине spare, которая не поддерживает сервиса TCPMUX, и соединитесь с ним, запустив telnet на машине bsd.
spare: # tcpmux
bsd: $ telnet spare tcpmux
Trying 127.0.0.1 ...
Connected to spare
Escape character is ‘^]’.
rlnumd
+OK
hello
 1: hello
world
 2: world
^]
telnet> quit
Connection closed
bsd: $

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