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

         

Основной цикл


8-13 Так же, как в udpclient, читаем строки из стандартного ввода, отправляем их удаленному хосту, читаем от него ответ и записываем его на стандартный вывод.

Хотя знакомые сетевые функции иногда принимают несколько иные аргументы и могут возвращать результат непривычным образом, но, в общем, программа в листинге 3.36 кажется знакомой и понятной. Тот, кто знаком с основами сетевого программирования и хотя бы чуть-чуть разбирается в Perl, может добиться высокой производительности.

Для сравнения в листинге 3.37 представлен TCP-сервер эхо-контроля. Вы можете соединиться с этим сервером с помощью программы telnet или любого другого TCP-приложения, способного вести себя как клиент эхо-сервера.

Здесь также видна знакомая последовательность обращений к API сокетов и, даже не зная языка Perl, можно проследить за ходом выполнения программы. Следует отметить две особенности, присущие Perl:

вызов accept на строке 11 возвращает TRUE, если все хорошо, а новый со-кет возвращается во втором параметре (S1). В результате естественно выглядит цикл for, в котором принимаются соединения;

поскольку recv возвращает адрес отправителя (или специальное значение undef), а не число прочитанных байт, получая длину строки $line (строка 16), следует явно проверять, не пришел ли признак конца файла. Оператор last выполняет те же действия, что break в языке С.

Листинг 3.37. Версия эхо-сервера на языке Perl

pechos

1    #! /usr/bin/perl5

2    use Socket;

3    $port = shift;

4    $port = getservbyname( $port, 'tcp' ) if $port =~ /\D/;

5    die "Invalid port" unless $port;

6    socket( S, PF_INET, SOCK_STREAM, 0 ) die "socket: $!";

7    setsockopt( S,SOL_SOCKET, SO_REUSEADDR, pack( '1' , 1 ) )

8    die "setsockopt: $!";

9    bindf S, sockaddr_in( $port, INADDR_ANY ) ) die "bind: $!"

10   listen ( S, SOMAXCONN );

11   for( ; accept( SI, S ); close( SI ) )

12   {

13   while ( TRUE )

14   {

15     definedf recv( SI, $line, 120, 0 ) ) die "recv: $!"


16     last if length( $line ) == 0;

17     definedt send( SI, $line, 0 ) ) II die "send: $!";

18   }

19   }

Как видно из этих двух примеров, языки сценариев вообще и Perl в частности -это отличный инструмент для написания небольших тестовых программ, создания прототипов более крупных систем и утилит. Perl и другие языки сценариев активно применяются при разработке Web-серверов и специализированных Web-клиентов. Примеры рассматриваются в книгах [Castro 1998] и [Patchett and Wright 1998].

Помимо простоты и скорости разработки прототипа, есть и другие причины для использования языков сценариев. Одна из них - наличие в таких языках спе­циальных возможностей. Например, Perl обладает прекрасными средствами для манипулирования данными и работы с регулярными выражениями. Поэтому во многих случаях Perl оказывается удобнее таких традиционных языков, как С.

Предположим, что каждое утро вам надо проверять, не появились ли в конференции comp.protocols.tcp-ip новые сообщения о протоколах TCP и UDP. В листинге 3.38 приведен каркас Peri-сценария для автоматизации решения этой задачи. В таком виде сценарий не очень полезен, так как он показывает все сообщения от сервера новостей, даже старые; отбор сообщений осуществляется довольно грубо. Можно было бы без труда модифицировать сценарий, ужесточив критерий отбора, но лучше оставить его таким, как есть, чтобы не запутаться в деталях языка Perl. Подробнее протокол передачи сетевых новостей (NNTP) рассматривается в RFC 977 [Каntor and Lapsley 1986].

Листинг 3.38. Peri-сценарий для формирования дайджеста из сетевых конференций

tcpnews

1    #' /usr/bin/perl5

2    use Socket;

3    $host = inet_aton( 'nntp.ix.netcom.com') die "хост: $!";

4    $port = getservbyname('nntp1, 'tcp') die "некорректный порт";

5    socket( S, PF_INET, SOCK_STREAM, 0 ) die "socket: $!";

6    connect! S, sockaddr_in( $port, $host ) ) die "connect: $!";

7    select( S ) ;

8    $1 = 1;

9    select( STDOUT );



10   print S "group сотр.protocols.tcp-ip\r\n";

11   while ( $line = <S> )

12   {

13   last if $line =~ /^211/;

14   }

15   ($rc, $total, $start, $end ) = split( /\s/, $line );

16   print S "xover $start-$end\nguit\r\n" ;

17   while ( $line = <S> )

18   {

19   ( $no, $sub, $auth, $date ) = split( /\t/, $line );

20   print   "$no,     $sub,   $date\n"   if   $sub  =~   /TCPIUDP/;

21   }

22   close(   S   );

Инициализация и соединение с сервером новостей

2- 6 Это написанный на Perl аналог логики инициализации стандартного TCP-клиента.

Установить режим небуферизованного ввода/вывода

7-9 В Perl функция print вызывает стандартную библиотеку ввода/вывода, а та, как упоминалось в совете 17, буферизует вывод в сокет. Эти три строки отключают буферизацию. Хотя по виду оператор select напоминает системный вызов select, который рассматривался ранее, в действительности он просто указывает, какой файловый дескриптор будет использоваться по умолчанию. Выбрав дескриптор, вы можете отменить буферизацию вывода в сокет S, задав ненулевое значение спе­циальной переменной $ |, используемой в Perl.

Примечание: Строго говоря, это не совсем так. Эти действия приводят к тому, что после каждого вызова wri te или print для данного дескриптора автоматически выполняется функция fflush. Но результат оказывается таким же, как если бы вывод в сокет был не буферизован.

В строке 9 stdout восстанавливается как дескриптор по умолчанию.

Выбрать группу comp.protocols. tcp-ip

10-14 Посылаем серверу новостей команду group, которая означает, что те­кущей группой следует сделать comp. protocols. tcp-ip. Сервер от­вечает строкой вида

211 total_articles first_article# last_article# group_namespace

В строке 13 вы ищете именно такой ответ, отбрасывая все строки, которые начинаются не с кода ответа 211. Обратите внимание, что оператор <. . . > сам разбивает на строки входной поток, поступающий от TCP.

15-16 Обнаружив ответ на команду group, нужно послать серверу строки



xover  first_article#-last_article#

quit

Команда xover запрашивает сервер, заголовки всех статей с номерами из за­данного диапазона. Заголовок содержит список данных, разделенных символами табуляции: номер статьи, тема, автор, дата и время, идентификатор сообщения, идентификаторы сообщений для статей, на которую ссылается данная, число баи тов и число строк. Команда quit приказывает серверу разорвать соединение, та как запросов больше не будет.

Отбор заголовков статей

17-20 Читаем каждый заголовок, выделяем из него интересующие нас поля и оставляем только те заголовки, для которых в теме присутствует стро­ка «TCP» или «UDP».

Запуск tcpnews дает следующий результат:

bsd: $ tcpnews

74179, Re: UDP multicast, Thu, 22 Jul 1999 21:06:47 GMT

74181, Re: UDP multicast, Thu, 22 Jul 1999 21:10:45 -0500

74187, Re: UDP multicast, Thu, 22 Jul 1999 23:23:00 +0200

74202, Re: NT 4.0 Server and TCP/IP, Fri, 23 Jul 1999 11:56:07 GMT

74227, New Seiko TCP/IP Chip, Thu, 22 Jul 1999 08:39:09 -0500

74267, WATTCP problems, Mon, 26 Jul 1999 13:18:14 -0500

74277, Re: New Seiko TCP/IP Chip, Thu, 26 Jul 1999 23:33:42 GMT

74305, TCP Petri Net model, Wed, 28 Jul 1999 02:27:20 +0200

bsd: $

Помимо языка Perl, есть и другие языки сценариев, пригодные для сетевого программирования, например:

TCL/Expect;

Python;

JavaScript;

Visual Basic (для Windows).

Их можно использовать для автоматизации решения простых сетевых задач, построения прототипов и быстрого создания удобных утилит или тестовых примеров. Как вы видели, языки сценариев часто проще применять, чем традицион­ные компилируемые языки программирования, поскольку интерпретатор берет на себя многие технические детали (конечно, расплачиваясь эффективностью). Усилия, потраченные на овладение хотя бы одним из таких языков, окупятся ростом производительности труда.


Отказ приложения


А теперь разберемся, что происходит, когда аварийно или как-либо иначе завершается приложение на другом конце соединения. Прежде всего следует понимать, что с точки зрения вашего приложения аварийное завершение другого конца отличается от ситуации, когда приложение на том конце вызывает функцию close (или closesocket, если речь идет о Windows), а затем exit. В обоих случаях TCP на другом конце посылает вашему TCP сегмент FIN. FIN выступает в роли признака конца файла и означает, что у отправившего его приложения нет больше данных для вас. Это не значит, что приложение на другом конце завершилось или не хочет принимать данные. Подробнее это рассмотрено в совете 16. Как приложение уведомляется о приходе FIN (и уведомляется ли вообще), зависит от его действий в этот момент. Для проработки возможных ситуаций напишем небольшую клиентскую программу, которая читает строку из стандартного входа, посылает ее серверу, читает ответ сервера и записывает его на стандартный выход. Исходный текст клиента приведен в листинге 2.21.

Листинг 2.21. TCP-клиент, который читает и выводит строки

tcprw.с

1    #include "etcp.h"

2    int main( int argc, char **argv )

3    {

4    SOCKET s;

5    int rc;

6    int len;

7    char buf[ 120 ] ;

8    INIT();

9    s = tcp_client(  argv[ 1 ],   argv[ 2 ] );

10   while ( fgets( buf, sizeof( buf ), stdin ) != NULL )

11   {

12     len = strlen ( buf );

13     rc = send( s, buf, len, 0 );

14     if ( rc < 0 )

15      error( 1, errno, "ошибка вызова send" );

16     rc = readline( s, buf, sizeof( buf ) );

17     if ( rc < 0 )

18      error( 1, errno, "ошибка вызова readline" );

19     else if ( rc == 0 )

20      error( 1, 0, "сервер завершил работу\n" );

21     else

22      fputs( buf, stdout );

23   }

24   EXIT( 0 ) ;

25   }

8-9 Инициализируем приложение как TCP-клиент и соединяемся с указанными в командной строке сервером и портом.

10-15 Читаем строки из стандартного входа и посылаем их серверу, пока не встретится конец файла.


16- 20 После отправки данных серверу читается строка ответа. Функция гeadline получает строку, считывая данные из сокета до символа новой строки. Текст этой функции приведен в листинге 2.32 в совете 11. Если readline обнаруживает ошибку или возвращает признак конца файла (совет 16), то печатаем диагностическое сообщение и завершаем работу

22 В противном случае выводим строку на stdout.

Для тестирования клиента напишем сервер, который читает в цикле строки поступающие от клиента, и возвращает сообщения о количестве полученных строк. Для имитации задержки между приемом сообщения и отправкой ответа сервер пять секунд «спит». Код сервера приведен в листинге 2.22.

Листинг 2.22. Сервер, подсчитывающий сообщения

count.c

1    #include "etcp.h"

2    int main( int argc, char **argv )

3    {

4    SOCKET s;

5    SOCKET s1;

6    int rc;

7    int len;

8    int counter = 1;

9    char buf [ 120 ];

10   INIT();

11   s = tcp_server( NULL, argv[ 1 ] );

12   s1 = accept ( s, NULL, NULL );

13   if ( !isvalidsock( s1 ) )

14     error( 1, errno, "ошибка вызова accept" );

15   while ( ( rc = readline( s1, buf, sizeof( buf ) ) ) > 0)

16   {

17     sleep ( 5 ) ;

18     len=sprintf(buf, "получено сообщение %d\n", counter++ );

19     rc = send( s1, buf, len, 0 );

20     if ( rc < 0 )

21      error( 1, errno, "ошибка вызова send" );

22   }

23   EXIT ( 0 );

24   }

Чтобы увидеть, что происходит при крахе сервера, сначала запустим сервер и клиент в различных окнах на машине bsd.

bsd: $ tcprw localhost 9000

hello

получено сообщение 1 Это печатается после пятисекундной задержки

                     Здесь сервер был остановлен.

hello again

tcprw: ошибка вызова readline: Connection reset by peer (54)

bsd: $

Серверу посылается одно сообщение, и через 5 с приходит ожидаемый ответ. Останавливаете серверный процесс, моделируя аварийный отказ. На стороне клиента ничего не происходит. Клиент блокирован в вызове fgets, а протокол TCP не может передать клиенту информацию о том, что от другого конца получен конец файла (FIN). Если ничего не делать, то клиент так и останется блокированным в ожидании ввода и не узнает о завершении сеанса сервера.



Затем вводите новую строку. Клиент немедленно завершает работу с сообщением о том' что хост сервера сбросил соединение. Вот что произошло: функция fgets вернула управление клиенту, которому все еще неизвестно о приходе признака конца файла от сервера. Поскольку ничто не мешает приложению посылать данные после прихода FIN, TCP клиента попытался послать серверу вторую строку. Когда TCP сервера получил эту строку, он послал в ответ сегмент RST (сброс), поскольку соединения уже не существует, - сервер завершил сеанс. Когда клиент вызывает readline, ядро возвращает ему код ошибки ECONNRESET, сообщая тем самым о получении извещения о сбросе. На рис. 2.19 показана хронологическая последовательность этих событий.



Рис. 2.19. Хронологическая последовательность событий при крахе сервера

А теперь рассмотрим ситуацию, когда сервер «падает», не успев закончить обработку запроса и ответить. Снова запустите сервер и клиент в разных окнах на машине bsd.

bsd = $ tcprw  localhoBt 9000

hello

Здесь сервер был остановлен.

tcprw: сервер завершил работу

bsd: $

Посылаете строку серверу, а затем прерываете его работу до завершения вызова sleep. Тем самым имитируется крах сервера до завершения обработки запроса. На этот раз клиент немедленно получает сообщение об ошибке, говорящее о завершении сервера. В этом примере клиент в момент прихода FIN блокирован в вызове readline и TCP может уведомить readline сразу, как только будет получен конец файла. Хронологическая последовательность этих событий изображена на рис. 2.20



Рис. 2.20. Крах сервера в момент, когда в клиенте происходит чтение

Ошибка также может произойти, если игнорировать извещение о сбросе соединения и продолжать посылать данные. Чтобы промоделировать эту ситуацию, следует изменить обращение к функции error после readline - вывести диагностическое сообщение, но не завершаться. Для этого достаточно вместо строки 17 в листинге 2.21 написать

error( 0, errno, "ошибка при вызове readline" );

Теперь еще раз надо прогнать тест:



bsd: $ tcprw localhost 9000

hello.

получено сообщение 1

Здесь сервер был остановлен.

hello again

tcprw: ошибка вызова readline: Connection reset by peer (54)

Клиент игнорирует ошибку, но

TCP уже разорвал соединение.

hello for the last time

Broken pipe   Клиент получает сигнал SIGPlPE

и завершает работу.

bsd: $

Когда вводится вторая строка, клиент, как и раньше, немедленно извещает ошибке (соединение сброшено сервером), но не завершает сеанс. Он еще раз обращается к fgets, чтобы получить очередную строку для отправки серверу стоит внести эту строку, как клиент тут же прекращает работу, и командный интерпретатор сообщает, что выполнение было прервано сигналом SIGPIPE. В этом случае при втором обращении к send, как и прежде, TCP послал RST, но вы не обратили на него внимания. Однако после получения RST клиентский ТСP разорвал соединение, поэтому при попытке отправить третью строку он немедленно завершает клиента, посылая ему сигнал SIGPIPE. Хронология такая же как на рис. 2.19. Разница лишь в том, что клиент «падает» при попытке записи, а не чтения.

Правильно спроектированное приложение, конечно, не игнорирует ошибки, такая ситуация может иметь место и в корректно написанных программах. Предположим, что приложение выполняет подряд несколько операций записи без промежуточного чтения- Типичный пример - FTP. Если приложение на другом конце «падает», то TCP посылает сегмент FIN. Поскольку данная программа только пишет, но не читает, в ней не содержится информация о получении этого FIN. При отправке следующего сегмента TCP на другом конце вернет RST. А в программе опять не будет никаких сведений об этом, так как ожидающей операции чтения нет. При второй попытке записи после краха отвечающего конца программа получит сигнал SIGPIPE, если этот сигнал перехвачен или игнорируется - код ошибки EPIPE.

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

Поведение зависит от соотношения времен. Например, если снова прогнать первый тест, запустив сервер на машине spare, а клиента - на машине bsd, то получается следующее:

bsd: $ tcprw localhost 9000

hello

получено сообщение 1    Это печатается после пятисекундной

задержки.

Здесь сервер был остановлен.

hello again

tcprw: сервер завершил работу

bsd: $

На этот раз клиент обнаружил конец файла, посланный в результате остановки сервера. RST по-прежнему генерируется при отправке второй строки, но из-за задержек в сети клиент успевает вызвать readline и обнаружить конец файла еще до того, как хост bsd получит RST. Если вставить между строками 14 и 15 в листинге 2.21 строчку

sleep( 1 );

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


Отключение алгоритма Нейгла


Поскольку сервер из последнего примера записывал в журнал полученные данные, взаимодействие алгоритмов Нейгла и отложенного подтверждение не принесло вреда и, по сути, уменьшило общее число пакетов в четыре раза. Допустим что клиент посылает серверу серию результатов измерения температуры, и сервер должен отреагировать в течение 100 мс, если показания выходят за пределы допустимого диапазона. В этом случае задержка на 200 мс, вызванная интерференцией алгоритмов, уже нетерпима, и ее надо устранить.

Хорошо, что RFC 1122 требует наличия метода, отключающего алгоритм Нейгла. Пример с клиентом, следящим за температурой, - это один из случаев, когда такое отключение необходимо. Менее драматичный, но более реалистичный пример относится к системе X-Window, работающей на платформе UNIX. Поскольку X использует протокол TCP для общения между дисплеем (сервером) и приложением (клиентом), Х-сервер должен доставлять информацию о действиях пользо­вателя (перемещении курсора мыши) Х-клиенту без задержек, вносимых алгоритмом Нейгла.

В API сокетов можно отключить алгоритм Нейгла с помощью установки опции сокета TCP_NODELAY.

const  int  on =  1

setsockopt( s, IPPROTO_TCP, TCP_NODELAY, &on, sizeof( on ) );

Но возможность отключения алгоритма Нейгла вовсе не означает, что это обязательно делать. Приложений, имеющих реальные основания отключать алгоритм, значительно меньше, чем тех, которые это делают без причины. Причина, по которой программисты с пугающей регулярностью сталкиваются с классической проблемой интерференции между алгоритмами Нейгла и отложенного подтверждения, в том, что делают много мелких операций записи вместо одной большой. Потом они замечают, что производительность приложений намного хуже, чем ожи­далось, и спрашивают, что делать. И кто-нибудь обязательно ответит: «Это все алгоритм Нейгла. Отключите его!». Нет нужды говорить, что после отключения этого алгоритма проблема производительности действительно исчезает. Только за это приходится расплачиваться увеличением числа крохотных пакетов в сети. Если так работают многие приложения или, что еще хуже, алгоритм Нейгла отключен по умолчанию, то возрастет нагрузка сети. В худшем случае это может привести к полному затору.



Печать ICMP-сообщений


Далее рассмотрим форматирование и печать ICMP-сообщений. Это делает функция print_dg, показанная в листинге 4.4. Передаваемый этой функции буфер имеет структуру, показанную на рис. 4.21.

Из рис. 4.21 видно, что буфер содержит IP-заголовок, за которым идет собственно ICMP-сообщение.

Рис.4.21. ICMP-сообщение, передаваемое функции print_dg

Листинг 4.4. Функция printjdg

1    static void print_dg( char *dg, int len )

2    {

3    struct ip *ip;

4    struct icmp *icmp;

5    struct hostent *hp;

6    char *hname;

7    int hl;

8    static char *redirect_code[] =

9    {

10     "сеть", "хост",

11     "тип сервиса и сеть", "тип сервиса и хост"

12   };

13   static char *timexceed_code [ ] =

14   {

15     "транзите", "сборке"

16   }

17   static char *param_code[] =

18   {

19     "Плохой IP-заголовок", "Нет обязательной опции"

20   };

21   ip = ( struct ip * )dg;

22   if ( ip->ip_v !=4)

23   {

24     error( 0, 0, "IP-датаграмма не версии 4\n" );

25     return;

26   }

27   hl = ip->ip_hl « 2;  /* Длина IP-заголовка в байтах. */

28   if ( len < hl + ICMP_MINLEN )

29   {

30     error( 0, 0, "short datagram (%d bytes) from %s\n",

31     len, inet_ntoa( ip->ip_src ) );

32     return;

33   }

34   hp = gethostbyaddr( ( char * )&ip->ip_src, 4, AF_INET );

35   if ( hp == NULL )

36     hname = "";

37   else

38     hname = hp->h_name;

39   icmp = ( struct icmp * }( dg + hl );  /* ICMP-пакет. */

40   printf( "ICMP %s (%d) от %s (%s)\n",

41      get_type( icmp->icmp_type ),

42      icmp->icmp_type, hname, inet_ntoa( ip->ip_src ) );

43   if ( icmp->icmp_type == ICMP_UNREACH )

44     print_unreachable( icmp );

45   else if ( icmp->icmp_type == ICMP_REDIRECT )

46     printf( "\tПеренаправление на %s\n", icmp->icmp_code <=

47      redirect_code[ icmp->icmp_code ] : "Некорректный код" );


48   else if ( icmp->icmp_type == ICMP_TIMXCEED )

49     printf( "\tTTL == 0 при %s\n", icmp->icmp_code <= 1 ?

50      timexceed_code[ icmp->icmp_code] : "Некорректный код" );

51   else if ( icmp->icmp_type == ICMP_PARAMPROB )

52     printf ( "\t%s\n", icmp->icmp_code <= 1 ?

53      param_code[ icmp->icmp_code ] : "Некорректный код" );

54   }

Получение указателя на IP-заголовок и проверка корректности пакета

21 Записываем в переменную ip указатель на только что прочитанную датаграмму, приведенный к типу struct ip *.

22-26 Поле ip_v - это версия протокола IP. Если протокол не совпадает с IPv4, то печатаем сообщение об ошибке и выходим.

27-33 Поле ip_hl содержит длину заголовка в 32-байтных словах. Умножаем его на 4, чтобы получить длину в байтах, и сохраняем результат в переменной hl. Затем проверяем, что длина ICMP-сообщения не меньше минимально допустимой величины.

Получение имени хоста отправителя

34-38 Используем адрес источника в ICMP-сообщении, чтобы найти имя хоста отправителя. Если gethostbyaddr вернет NULL, то записываем в hname пустую строку, в обратном случае - имя хоста.

Пропуск IP-заголовка и печать отправителя и типа

39-42 Устанавливаем указатель icmp на первый байт, следующий за IP-заголовком. Этот указатель используется далее для получения типа ICMP-сообщения (icmp_type) и печати типа, адреса и имени хоста отправителя. Для получения ASCII-представления типа ICMP вызываем функцию get_type, текст которой приведен в листинге 4.5.

Печать информации, соответствующей типу

43-44 Если это одно из ICMP-сообщений о недоступности, то вызываем функцию print_unreachable (листинг 4.6) для печати дополнительной информации.

45-47 Если это сообщение о перенаправлении, то получаем тип перенаправления из поля icmp_code и печатаем его.

48-50 Если это сообщение об истечении времени существования, из поля icmp_code узнаем, произошло ли это во время транзита или сборки датаграммы, и печатаем результат.



51-53 Если это сообщение о некорректном параметре, из поля icmp_code определяем, в чем ошибка, и печатаем результат.

Функция get_type очевидна. Вы проверяете допустимость кода типа и возвращаете указатель на соответствующую строку (листинг 4.5).

Листинг 4.5. Функция getjype

1    static char *get_type( unsigned icmptype )

2    {

3    static  char  *type[] =

4    {

5      "Эхо-ответ",                        /* 0*/

6      "ICMP  Тип  1",                     /* 1*/

7      "ICMP  Тип  2",                     /* 2*/

8      "Пункт назначения недоступен",      /* 3*/

9      "Источник приостановлен",           /* 4*/

10     "Перенаправление",                  /* 5*/

11     "ICMP Тип  6",                      /* 6*/

12     "ICMP  Тип  7",                     /* 7*/

13     "Эхо-запрос",                       /* 8*/

14     "Отклик маршрутизатора",            /* 9*/

15     "Поиск  маршрутизаторов",           /* 10*/

16     "Истекло время существования",      /* 11*/

17     "Неверный  параметр",               /* 12*/

18     "Запрос  временного штампа",        /* 13*/

19     "Ответ на запрос временного штампа", /* 14*/

20     "Запрос информации",                /* 15*/

21     "Ответ  на  запрос  информации",    /* 16*/

22     "Запрос  маски  адреса",            /* 17*/

23     "Ответ  на  запрос маски  адреса"   /* 18*/

24   }

25   if ( icmptype < ( sizeof( type ) / sizeof ( type[ 0 ]) ) )

26   return type[ icmptype ];

27   return "НЕИЗВЕСТНЫЙ ТИП";

28   }

Последняя функция - это print_unreachable. ICMP-сообщения о недоступности содержат IP-заголовок и первые восемь байт той IP-датаграммы, из-за которой было сгенерировано сообщение о недоступности. Это позволяет узнать адреса и номера портов отправителя и предполагаемого получателя недоставленного сообщения.



Структура IP-датаграммы, прочитанной из простого сокета в составе ICMP-сообщения о недоступности, показана на рис. 4.22. Та часть, которую уже обработала функция print_dg, заштрихована, она не передается в print_unreachable. Приведены также входной параметр функции print_unreachable - icmp и локальные переменные ip и udp.



Рис. 4.22. ICMP-сообщение о недоступности

Функция print_unreachable извлекает информацию из заголовка и первых восьми байт включенной IP-датаграммы. Хотя вы пометили байты как UDP-заголовок, это мог быть и заголовок TCP: номера портов в обоих случаях находятся в одной и той же позиции. Формат UDP-заголовка показан на рис. 4.23.



Рис. 4.23. UDP-заголовок

Текст функции print_unreachable приведен в листинге 4.6.

Листинг4.6. Функцияprint_unreachable

1    static void print_unreachable( struct icmp *icmp )

2    {

3    struct ip *ip;

4    struct udphdr *udp;

5    char laddr[ 15 + 1 ] ;

6    static char *unreach[] =

7    {

8      "Сеть недоступна",                        /* 0 */

9      "Хост недоступен",                        /* 1 */

10     "Протокол недоступен",                    /* 2 */

11     "Порт недоступен",                        /* 3 */

12     "Нужна фрагментация, поднят бит DF",      /* 4 */

13     "Ошибка маршрутизации от источника",      /* 5 */

14     "Сеть назначения неизвестна",             /* 6 */

15     "Хост назначения неизвестен",             /* 7 */

16     "Хост источника изолирован",              /* 8 */

17     "Сеть назначения закрыта администратором ",    /* 9 */

18     "Хост назначения закрыт администратором ",     /* 10 */

19     "Сеть недоступна для типа сервиса",       /* 11 */

20     "Хост недоступен для типа сервиса",       /* 12 */

21     "Связь запрещена администратором",        /* 13 */

22     "Нарушение предшествования хостов",       /* 14 */

23     "Действует отсечка предшествования"       /* 15 */



24   };

25   ip = ( struct ip * )( ( char * )icmp + 8 );

26   udp = ( struct udphdr *)((char *)ip + (ip->ip_hl « 2 ) );

27   strcpy( laddr, inet_ntoa( ip->ip_src ) );

28   printf( "\t%s\n\tИст.: %s.%d, Назн.: %s.%d\n",

29   icmp->icmp_code < ( sizeof( unreach ) /

30   sizeof( unreach[ 0 ] ) )?

31   unreach[ icmp->icmp_code ] : "Некорректный код",

32   laddr, ntohs( udp->uh_sport ),

33   inet_ntoa( ip->ip_dst ), ntohs( udp->uh_dport ) );

34   }

Установка указателей и получение адреса источника

25-26 Начинаем с установки указателей ip и udp соответственно на IP-заголовок и первые восемь байт вложенной IP-датаграммы.

27 Копируем адрес источника из IP-заголовка в локальную переменную laddr.

Печать адресов, портов и типа сообщения

28-33 Печатаем адреса и номера портов источника и назначения, а также уточненный тип сообщения о недоступности.

В качестве примера использования программы ICMP приведено несколько юследних ICMP-сообщений, полученных при запуске traceroute (совет 35).

traceroute -q 1 netcom4.netcom.com

Опция -q 1 означает, что traceroute должна посылать пробный запрос только один раз, а не три, как принято по умолчанию.

ICMP Истекло время существования (11) от hl-0.mig-fl-gwl.icg.net

(165.236.144.110)

 TTL == 0 во время транзита

ICMP Истекло время существования (11) от sl0-0-0.dfw-tx-

gwl.icg.net (165.236.32.74)

 TTL == 0 во время транзита

ICMP Истекло время существования (11) от dfw-tx-gw2.icg.net

(163.179.1.133)

 TTL == 0 во время транзита

ICMP Пункт назначения недоступен (3) от netcom4.netcom.com

(199.183.9.104)

 Порт недоступен

 Ист. 205.184.-142.71.45935, Назн. 199.183.9.104.33441

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


Подсети


Мне хотелось найти решение, сочетающее два достоинства: во-первых, небольшие маршрутные таблицы и эффективное использование адресного пространства, обеспечиваемые единым идентификатором сети, во-вторых, простота маршрутизации, характерная для сетей, имеющих сегменты с разными идентификаторами сети. Желательно, чтобы внешние хосты «видели» только одну сеть, а внутрен­ние - несколько сетей, по одной для каждого сегмента.

Это достигается с помощью механизма подсетей. Идея очень проста. Поскольку внешние хосты для принятия решения о выборе маршрута используют только идентификатор сети, администратор может распределять идентификаторы хостов по своему усмотрению. Таким образом, идентификатор хоста - это закрытая структура, не имеющая вне данной сети интерпретации.

Разделение на подсети осуществляется по следующему принципу. Одна часть идентификатора хоста служит для определения сегмента (то есть подсети), в состав которого входит хост, а другая - для идентификации конкретного хоста. Рассмотрим, например, сеть класса B с адресом 190.50.0.0. Можно считать, что третий байт адреса - это идентификатор подсети, а четвертый байт - номер хоста в этой подсети. На рис. 2.6а приведена структура адреса с точки зрения внешнего компьютера. Идентификатор хоста- это поле с заранее неизвестной структурой. На рис. 2.6б показано, как эта структура выглядит изнутри сети. Вы видите, что она состоит из идентификатора подсети и номера хоста.

Рис. 2.6. Два взгляда на адрес сети класса B с подсетями

В приведенном примере взят адрес класса В, и поле номера хоста выделено по границе байта. Но это необязательно. На подсети можно разбивать сети классов А, В и С и часто не по границе байта. С каждой подсетью ассоциируется маска подсети, которой определяется, какая часть адреса отведена под идентификаторы сети I и подсети, а какая - под номер хоста. Так, маска подсети для примера, показанного на рис. 2.6б, будет 0xffffff00. В основном маска записывается в десятичной нотации (255.255.255.0), но если разбивка проходит не по границе байта, то удобнее первая форма.


Примечание: Обратите внимание, что, хотя говорится о маске подсети, фактически она выделяет части, относящиеся как к сети, так и к подсети, то есть все, кроме номера хоста.

Предположим, что для иде                         нтификатора подсети отведено 10 бит, а для номера хоста - 6 бит. Тогда маска подсети будет 255.255.255.192 (0xffffffc0). Как следует из рис. 2.7, в результате наложения этой маски на адрес 190.50.7.75 получается номер сети/подсети, равный 190.70.7.64.

Для проверки убедитесь, что адрес 190.50.7.75 принадлежит хосту 11 в подсети 29 сети 190.50.0.0. Важно не забывать, что эта интерпретация имеет смысл только внутри сети. Для внешнего мира адрес интерпретируется как хост 1867 в сети 190.50.0.0.

Теперь следует выяснить, как маршрутизаторы на рис. 2.5 могут воспользоваться структурой идентификатора хоста для рассылки датаграмм внутри сети. Предположим, что есть сеть класса В с адресом 190.5.0.0 и маска подсети равна 255.255.255.0. Такая структура показана на рис. 2.6б.



Рис. 2.7. Наложение маски подсети с помощью операции AND для выделения сетевой части IP - адреса

На рис. 2.8 первому сегменту назначен идентификатор подсети 1, а второму - идентификатор подсети 2. Рядом с сетевым интерфейсом каждого хоста указан его IP -адрес. Обратите внимание, что третий байт каждого адреса - это номер подсети, которой принадлежит интерфейс. Однако внешнему компьютеру эта интерпретация неизвестна.



Рис. 2.8. Сеть с подсетями

Возвращаясь к вышесказанному, следует выяснить, что происходит, когда хосту H1 нужно обратиться к хосту Н3. H1 берет адрес Н3 (190.50.2.1) и накладывает на него маску подсети (255.255.255.0), получая в результате 190.5.2.0. Поскольку H1 находится в подсети 190.5.1.0, то Н3 напрямую недоступен, поэтому он сверяется со своей маршрутной таблицей и обнаруживает, что следующий адрес на пути к Н3 - это R1.

Примечание: Во многих реализациях эти два шага объединены за счет помещения в маршрутную таблицу обеих подсетей. При поиске маршрута IP выявляет одно из двух: либо целевая сеть доступна непосредственно, либо датаграмму надо отослать промежуточному маршрутизатору.



Затем H1 отображает IP - адрес R1 на его физический адрес (например, с помощью протокола ARP) и посылает R1 датаграмму. R1 ищет адрес назначения в своей маршрутной таблице, пользуясь той же маской подсети, и определяет местонахождение НЗ в подсети, соединенной с его интерфейсом 190.50.2.4. После чего R1 доставляет датаграмму хосту НЗ, получив предварительно его физический адрес по IP-адресу, - для этого достаточно передать датаграмму сетевому интерфейсу 190.50.2.4.

А теперь предположим, что H1 необходимо отправить датаграмму Н2. При наложении маски подсети на адрес Н2 (190.5.1.2) получается 190.50.1.0, то есть та же подсеть, в которой находится сам хост H1. Поэтому H1 нужно только получить физический адрес Н2 и отправить ему датаграмму напрямую.

Далее разберемся, что происходит, когда хосту Е из внешней сети нужно отправить датаграмму Н3. Поскольку 190.50.2.1 - адрес класса В, то маршрутизатору на границе сети хоста Е известно, что Н3 находится в сети 190.50.0.0. Так как шлюзом в эту сеть является R2, рано или поздно датаграмма от хоста Е дойдет до этого маршрутизатора. С этого момента все совершается так же, как при отправке датаграммы хостом H1: R2 накладывает маску, выделяет адрес подсети 190.50.2.0, определяет R1 в качестве следующего узла на пути к Н3 и посылает R1 датаграмму, которую тот переправляет Н3. Заметьте, что хосту Е неизвестна внутренняя топология сети 190.50.0.0. Он просто посылает датаграмму шлюзу R2. Только R2 и другие хосты внутри сети определяют существование подсетей и маршруты доступа к ним.

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

Предположим, что адрес класса В 190.50.0.0 принадлежит университетской сети, а каждому факультету выделена подсеть с маской 255.255.255.0 (на рис. 2.8 показана только часть всей сети). Администратор факультета информатики, которому назначена подсеть 5, решает выделить один сегмент сети Ethernet компьютерному классу, а другой - всем остальным факультетским компьютерам. Он мог бы потребовать у администрации университета еще один номер подсети, но в компьютер­ном классе всего несколько машин, так что нет смысла выделять ему адресное про­странство, эквивалентное целой подсети класса С. Вместо этого он предпочел разбить свою подсеть на два сегмента, то есть создать подсеть внутри подсети.



Для этого он увеличивает длину поля подсети до 10 бит и использует маску 255.255.255.192. В результате структура адреса выглядит, как показано на рис. 2.9.

Старшие 8 бит идентификатора подсети всегда равны 0000 0101 (5), поскольку основная сеть адресует всю подсеть как подсеть 5. Биты X и Y определяют, какой Ethernet-сегмент внутри подсети 190.50.5.0 адресуется. Из рис. 2.10 видно, что если XY = 10, то адресуется подсеть в компьютерном классе, а если XY = 01 - оставшаяся часть сети. Частично топология подсети 190.50.5.0 изображена на рис. 2.10.



Рис. 2.9. Структура адреса для подсети 190.50.5.0

В верхнем сегменте (подсеть 190.50.1.0) на рис. 2.10 расположен маршрутизатор R2, обеспечивающий выход во внешний мир, такой же, как на рис. 2.8. Под сеть 190.50.2.0 здесь не показана. Средний сегмент (подсеть 190.50.5.128) - это локальная сеть Ethernet в компьютерном классе. Нижний сегмент (подсеть 190.50.5.64) - это сеть Ethernet, объединяющая остальные факультетские компьютеры. Для упрощения номер хоста каждой машины один и тот же для всех ее сетевых интерфейсов и совпадает с числом внутри прямоугольника, представляющего хост или маршрутизатор.



Рис. 2.10. Подсеть внутри подсети

Маска подсети для интерфейсов, подсоединенных к подсетям 190.50.5.64 и 190.50.5.128, равна 255.255.255.192, а к подсети 190.50.1.0 - 255.255.255.0.

Эта ситуация в точности аналогична предыдущей, которая рассматривалась для рис. 2.8. Так же, как хостам вне сети 190.50.0.0 неизвестно то, что третий байт адреса определяет подсеть, так и хосты в сети 190.50.0.0, но вне подсети 190.50.5.0, не могут определить, что первые два бита четвертого байта задают подсеть подсети 190.50.5.0.

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


Потенциальные ошибки


Вы уже видели одну из потенциальных ошибок при работе с TCP: не исключено, что подтвержденные TCP данные не дошли до приложения-получателя. Как и большинство других таких ошибок, это довольно редкая ситуация, и, даже если она встречается, последствия могут быть не очень печальными. Важно, чтобы программист знал об этой неприятности и предусматривал защиту при возможном нежелательном результате. Не думайте, что TCP обо всем позаботится сам, следует подумать об устойчивости приложения.

Защита от упомянутой ошибки очевидна. Если приложению-отправителю важно иметь информацию, что сообщение дошло до приложения-получателя, то получатель должен сам подтвердить факт приема. Часто такое подтверждение присутствует неявно. Например, если клиент запрашивает у сервера некоторые данные и сервер отвечает, то сам ответ - это подтверждение получения запроса. Один из возможных способов организации явных подтверждений обсуждается в совете 19.

Более сложный для клиента вопрос - что делать, если сервер не подтверждает приема? Это в основном зависит от конкретного приложения, поэтому готового Решения не существует. Однако стоит отметить, что повторная посылка запроса не всегда годится; как говорилось в совете 8, вряд ли будет правильно дважды переводить одну сумму со счета на счет. В системах управления базами данных для решения такого рода проблем применяется протокол трехфазной фиксации. Полный подход приемлем и для других приложений, гарантирующих, что операция выполняется «не более одного раза». Один из примеров - службы параллельности, фиксации и восстановления (concurrency, commitment, recovery - CCR) – это элемент прикладного сервиса в протоколах OSI. Протокол CCR обсуждается в работе [Jain and Agrawala 1993].

TCP - протокол сквозной передачи (end-to-end protocol), то есть он стремится обеспечить надежный транспортный механизм между двумя хостами одного ранга. Важно, однако, понимать, что конечные точки - это уровни TCP на обоих хостах, а не приложения. Программы, которым нужны подтверждения на прикладном уровне, должны самостоятельно это определить.

Рассмотрим некоторые типичные ошибки. Пока между двумя хостами существует связь, TCP гарантирует доставку данных по порядку и без искажений Ошибка может произойти только при разрыве связи. Из-за чего же связь может разорваться? Есть три причины:

постоянный или временный сбой в сети;

отказ принимающего приложения;

аварийный сбой самого хоста на принимающем конце.

Каждое из этих событий по-разному отражается на приложении-отправителе.



Преждевременное завершение


Первый пример - это вариация на тему первой версии программы shutdownc (листинг 3.1), которая разработана в совете 16. Идея программ badclient и shutdownc та же: читаются данные из стандартного ввода, пока не будет получен признак конца файла. В этот момент вы вызываете shutdown для отправки FIN-сегмента удаленному хосту, а затем продолжаете читать от него данные, пока не получите EOF, что служит признаком прекращения передачи удаленным хостом. Текст программы badclient приведен в листинге 4.2.

Листинг 4.2. Некорректный эхо-клиент

1    #include "etcp.h"

2    int main( int argc, char **argv )

3    {

4    SOCKET s;

5    fd_set readmask;

6    fd_set allreads;

7    int rc;

8    int len;

9    char lin[ 1024 ] ;

10   char lout[ 1024 ] ;

11   INIT();

12   s = tcp_client( argv[ optind ], argv[ optind + 1 ] ) ;

13   FD_ZERO( &allreads );

14   FD_SET( 0, &allreads );

15   FD_SET( s, &allreads );

16   for ( ;; )

17   {

18     readmask = allreads;

19     rc = select( s + 1, &readmask, NULL, NULL, NULL };

20     if ( rc <= 0 )

21      error( 1, errno, "select вернула (%d)", rc );

22     if ( FD_ISSET( s, kreadmask ) )

23     {

24      rc = recv( s, lin, sizeof( lin ) - 1, 0 );

25      if ( rc < 0 )

26       error( 1, errno, "ошибка вызова recv" );

27      if ( rc == 0 )

28       error( 1, 0, "сервер отсоединился\n" );

29      lin[ rc] = '\0';

30      if ( fputst lin, stdout ) )

31       error( 1, errno, "ошибка вызова fputs" );

32     }

33     if ( FD_ISSET( 0, &readmask ) )

34     {

35      if ( fgets( lout, sizeof( lout ), stdin ) == NULL )

36      {

37       if ( shutdown( s, 1 ) )

38        error( 1, errno, "ошибка вызова shutdown" );

39      }

40      else

41      {

42       len =  strlen( lout );

43       rc  =  send( s, lout, len, 0 );

44       if ( rc< 0 )

45        error( 1, errno, "ошибка вызова send" );

46      }

47     }


48   }

49   }

22- 32 Если select показывает, что произошло событие чтения на соединении, пытаемся читать данные. Если получен признак конца файла, то удаленный хост прекратил передачу, поэтому завершаем работу. В про­тивном случае выводим только что прочитанные данные на stdout.

33-47 Если select показывает, что произошло событие чтения на стандартном вводе, вызываем f gets для чтения данных. Если f gets возвращает NULL, что является признаком ошибки или конца файла, то вызываем shutdown, чтобы сообщить удаленному хосту о прекращении передачи. В противном случае посылаем только что прочитанные данные.

А теперь посмотрим, что произойдет при запуске программы badcl lent. В качестве сервера в этом эксперименте будет использоваться программа tcpecho (листинг 3.2). Следует напомнить (совет 16), что вы можете задать число секунд, на которое tcpecho должна задержать отправку ответа на запрос. Установите задержку в 30 с. Запустив клиент, напечатайте hello и сразу нажмите Ctrl+D, таким образом посылается fgets признак конца файла.

bsd: $ tcpecho 9000 30

 спустя 30 с

tcpecho: ошибка вызова recv:

 Connection reset by peer (54)

bsd: $

bsd: $ badclient bad 9000

hello

^D

badclient: сервер отсоединился

bsd: $

Как видите, badclient завершает сеанс сразу же с сообщением о том, что сервер отсоединился. Но tcpecho продолжает работать и «спит», пока не истечет 30 с таим-аута. После этого программа получает от своего партнера ошибку Connection reset by peer.

Это удивительно. Ожидалось, что tcpecho через 30 с пошлет эхо-ответ, а затем завершит сеанс, прочтя признак конца файла. Вместо этого badclient завершает работу немедленно, a tcpecho получает ошибку чтения.

Правильнее начать исследование проблемы с использования tcpdump (совет 34), чтобы понять, что же на самом деле посылают и принимают обе программы. Выдача tcpdump приведена на рис. 4.16. Здесь опущены строки, относящиеся к фазе установления соединения, и разбиты длинные строки.

1 18:39:48.535212 bsd.2027 > bsd.9000:



    Р 1:7(6) ack 1 win 17376 <nop,nop,timestamp 742414 742400> (DF)

2 18:39:48.546773 bsd.9000 > bsd.2027:

    . ack 7 win 17376 <nop,пор,timestamp 742414 742414> (DF)

3 18:39:49.413285 bsd.2027 > bsd.9000:

    F 7:7(0) ack 1 win 17376 <nop, пор, timestamp 742415 742414> (DF)

4 18:39:49.413311 bsd.9000 > bsd.2027:

    . ack 8 win 17376 <nop,пор,timestamp 742415 742415> (DF)

5 18:40:18.537119 bsd.9000 > bsd.2027:

    P 1:7(6) ack 8 win 17376 <nop,пор,timestamp 742474 742415> (DF)

6 18:40:18.537180 bsd.2027 > bsd.9000:

    R 2059690956:2059690956(0) win 0

Рис. 4.16. Текст, выведенный tcpdump для программы badclient

Все выглядит нормально, кроме последней строки. Программа badclient посылает tcpecho строку hello (строка 1), а спустя секунду появляется сегмент FIN, посланный в результате shutdown (строка 3). Программа tcpecho в обоих случаях отвечает сегментом АСК (строки 2 и 4). Через 30 с после того, как badclient отправила hello, tcpecho отсылает эту строку назад (строка 5), но другая сторона вместо того, чтобы послать АСК, возвращает RST (строка б), что и приводит к печати сообщения Connection reset by peer. RST был послан, поскольку программа badcl ient уже завершила сеанс.

Но все же видно, что tcpecho ничего не сделала для преждевременного завершения работы клиента, так что вся вина целиком лежит на badclient. Посмотрим, что же происходит внутри badclient, поможет в этом трассировка систем­ных вызовов.

Повторим эксперимент, только на этот раз следует запустить программу так:

bsd: $ ktrace badclient bed 9000

При этом badclient работает, как и раньше, но дополнительно вы получаете трассу выполняемых системных вызовов. По умолчанию трасса записывается в файл ktrace. out. Для печати содержимого этого файла надо воспользоваться программой kdump. Результаты показаны на рис. 4.17, в котором опущено несколько начальных вызовов, относящихся к запуску приложения и установлению соединения.

Первые два поля в каждой строке - это идентификатор процесса и имя исполняемой программы. В строке 1 вы видите вызов read с дескриптором fd, равным (stdin). В строке 2 читается шесть байт (GIO- сокращение от general I/O - общий ввод/вывод), содержащих hello\n. В строке 3 показано, что вызов re вернул 6 - число прочитанных байтов. Аналогично из строк 4-6 видно, программа badclient писала в дескриптор 3, который соответствует сокету, соединному с tcpecho. Далее, в строках 7 и 8 показан вызов select, вернувший едини



 1 4692 badclient CALL      read(0,0x804e000,0x10000)

 2 4692 badclient GIO fd    0 read 6 bytes

   "hello

   "

 3 4692 badclient RET       read 6

 4 4692 badclient CALL      sendto(0x3,0xefbfce68,0x6,0,0,0)

 5 4692 badclient GIO       fd 3 wrote 6 bytes

   "hello

   "

 6 4692 badclient RET       sendto 6

 7 4692 badclient CALL      select(0x4,0xefbfd6f0,0 , 0, 0)

 8 4692 badclient RET       select 1

 9 4692 badclient CALL      read(0,0x804e000,0x10000)

10 4692 badclient GIO fd 0  read 0 bytes

   ""

11 4692 badclient RET       read 0

12 4692 badclient CALL      shutdown(0x3,0xl)

13 4692 badclient RET       shutdown 0

14 4692 badclient CALL      select(0x4,0xefbfd6fO,0,0,0)

15 4692 badclient RET       select 1

16 4692 badclient CALL      shutdown(0x3,0xl)

17 4692 badclient RET       shutdown 0

18 4692 badclient CALL      select(0x4,0xefbfd6fO,0,0,0)

19 4692 badclient RET       select 2

20 4692 badclient CALL      recvfrom(0x3,0xefbfd268,0x3ff,0,0,0)

21 4692 badclient GIO       fd 3 read 0 bytes

   ""

22 4692 badclient RET       recvfrom 0

23 4692 badclient CALL      write(0x2,0xefbfc6f4,0xb)

24 4692 badclient GIO       fd 2 wrote 11 bytes

   "badclient: "

25 4692 badclient RET       write 11/0xb

26 4692 badclient CALL      write(0x2,Oxefbfc700,0x14)

27 4692 badclient GIO       fd 2 wrote 20 bytes

   "server disconnected

   "

28 4692 badclient RET       write 20/0x14

29 4692 badclient CALL      exit(0xl)

Рис. 4.17. Результаты прогона badclient под управлением ktrace

Это означает, что произошло одно событие. В строках 9-11 badclient прочитала EOF из stdin и вызвала shutdown (строки 12 и 13).

До сих пор все шло нормально, но вот в строках 14-17 вас поджидает сюрприз: select возвращает одиночное событие, и снова вызывается shutdown. Ознакомившись с листингом 4.2, вы видите, что такое возможно только при условии, если дескриптор 0 снова готов для чтения. Но read не вызывается, как можно было бы ожидать, ибо fgets в момент нажатия Ctrl+D отметила, что поток находится в конце файла, поэтому она возвращается, не выполняя чтения.



Примечание: Вы можете убедиться в этом, познакомившись с эталонной реализацией fgets (на основе fgetc) в книге [Kemighan andRitchie 19881

В строках 18 и 19 select возвращает информацию о событиях на обоих дескрипторах stdin и сокете. В строках 20-22 видно, что recvfrom возвращает нуль (конец файла), а оставшаяся часть трассы показывает, как badclient выводит сообщение об ошибке и завершает сеанс.

Теперь ясно, что произошло: select показывает, что стандартный ввод готов для чтения в строке 15, поскольку вы забыли вызвать FD_CLR для stdin после первого обращения к shutdown. А следующий (уже второй) вызов shutdown вынуждает TCP закрыть соединение.

Примечание: В этом можно убедиться, посмотрев код на странице 1014 книги [Wright and Stevens 1995], где показано, что в результате обращения к shutdown вызывается функция tcp_usrclosee. Если shutdown уже вызывался раньше, то соединение находится в состоянии FIN-WAIT-2 и tcp_usrclosed вызывает функцию soisdisconnected (строка 444 на странице 1021). Этот вызов окончательно закрывает сокет и заставляет select вернуть событие чтения. А в результате будет прочитан EOF.

Поскольку соединение закрыто, recvf rom возвращает нуль, то есть признак конца файла, и badclient выводит сообщение «сервер отсоединился» и завершает сеанс.

Ключ к пониманию событий в этом примере дал второй вызов shutdown. Легко обнаружилось отсутствующее обращение к FD_CLR.


Принудительная отмена состояния TIME-WAIT


К сожалению, иногда можно досрочно выйти из состояния TIM Е-WAIT. Это называется принудительной отменой (TIME-WAIT assassination) и бывает случай но или намеренно.

Сначала посмотрим, как это может произойти случайно. По стандарта RFC 793, если соединение находится в состоянии TIME-WAIT и приходит RST то соединение должно быть немедленно закрыто. Предположим, что имеет единение в состоянии TIME-WAIT и приходит старый сегмент-дубликат, который TCP не принимает (например, потому, что порядковый номер оказался вне окна приема). TCP посылает в ответ АСК, в котором указано, какой порядковый номер он ожидает (следующий за номером сегмента FIN, посланного другой стороной). Но у хоста на другой стороне уже нет информации о соединении, поэтому этот АСК он отвечает сегментом RST. Когда этот RST приходит хосту, у которого соединение находится в состоянии TIME-WAIT, тот немедленно закрывает соединение, - состояние TIME-WAIT принудительно отменено.

Эта ситуация описана в RFC 1337 [Braden 1992b], где также рассматриваются трудности, сопряженные с принудительной отменой состояния TIME-WAIT. Опасность состоит в возможности «воскрешения» старого соединения (то есть появления соединения с теми же двумя сокетами), что может привести к подтверждению старых данных, десинхронизации соединения с входом в бесконечный цикл и к ошибочному завершению нового соединения.

Это легко предотвратить, изменив протокол TCP так, чтобы в состоянии TIME-WAIT было разрешено игнорировать RST Хотя такое изменение, рекомендованное в RFC 1337, официально не одобрено, тем не менее в некоторых стеках оно реализовано.

Принудительно отменить состояние TIME-WAIT можно и намеренно. С помощью опции сокета SO_LINGER программист требует немедленного закрытия соединения даже в том случае, когда приложение выполняет «активное закрытие. Этот сомнительный прием иногда рекомендуют применять, чтобы вывести «упавший» сервер из состояния TIME-WAIT и запустить его заново. Подробнее об этой проблеме и более правильном способе ее решения будет рассказано в совете 23. Корректно написанное приложение никогда не должно манипулировать состоянием TIME-WAIT, поскольку это неотъемлемая часть механизма обеспечения надежности TCP.



Программа netstat в Windows


Выше рассмотрено, как работает программа netstat в системе UNIX. В Windows тоже есть аналогичная программа, принимающая в основном те же опции и выдающая такие же данные. Формат выдачи очень напоминает то, что вы видели, хотя состав информации не такой полный.



Программа tracert в системе Windows


До сих пор описывалась UNIX-версия программы traceroute. Очень похожее средство - tracert - есть и в различных версиях операционной системы Windows. Программа tracert работает аналогично traceroute, но для определения маршрута используются не UDP-датаграммы, а эхо-запросы протокола ICMP (как в программе ping). В результате хост-получатель возвращает эхо-ответ ICMP, а не сообщение о недоступности порта. Промежуточные маршрутизаторы по-прежнему возвращают сообщение «истекло время в пути».

Примечание: В последних версиях traсеrоutе есть опция -1, имитирующая такое же поведение. Подобную версию можно получить на сайте ftp://ftp.ee.lbl.gov/traceroute.tar.Z.

Наверное, это изменение сделано исходя из соображения о том, что UDP-датаграммы часто отфильтровываются маршрутизаторами, тогда как эхо-запросы и эхо-ответы ICMP, используемые программой ping, менее подвержены этому.Исходная версия traceroute также применяла эхо-запросы для определения маршрута, но потом они были заменены UDP-датаграммами, поскольку многие маршрутизаторы строго следовали предписанию RFC 792 [Postel 1981], требую­щему не посылать ICMP-сообщения в ответ на ICMP-сообщения [Jacobson 1999]. Действующее ныне RFC 1122 [Braden 1989] указывает, что ICMP-сообщение не должно посылаться в ответ на ICMP-сообщение об ошибке, но tracert по-прежнему встречает трудности в старых моделях маршрутизаторов.

В RFC 1393 [Malkin 1993] предложено добавить новую опцию в протокол IP и отдельное ICMP-сообщение, чтобы гарантировать надежность traceroute (а заодно и решить некоторые другие задачи), но, так как в маршрутизаторы и программное обеспечение хостов пришлось бы вносить изменения, этот метод не по­лучил распространения.



Проверка корректности входной информации


Что бы вы ни программировали, не думайте, что приложение будет получать только те данные, на которые рассчитывает. Пренебрежение этим принципом- пример отсутствия защитного программирования. Хочется надеяться, что профессиональный программист, разрабатывающий коммерческую программу, всегда ему следует. Однако часто это правило игнорируют. В работе [Miller et al. 1995] описывается, как авторы генерировали случайный набор входных данных и подавали его на вход всевозможных стандартных утилит UNIX от разных производителей. При этом им удалось «сломать» (с дампом памяти) или «подвесить» (в бесконечном цикле) от 6 до 43% тестируемых программ (в зависимости от производителя;. В семи исследованных коммерческих системах частота отказов составила 23%

Вывод ясен: если такие результаты получены при тестировании зрелых программ, которые принято считать программами «промышленного качества», то те более необходимо защищаться и подвергать сомнению все места в программе, где неожиданные входные данные могут привести к нежелательным результатам. Рассмотрим несколько примеров, когда неожиданные данные оказываются источником ошибок.

Две самые распространенные причины краха приложений - это переполнение буфера и сбитые указатели. В вышеупомянутом исследовании именно эти две ошибки послужили причиной большинства сбоев. Можно сказать, что в сетевых программах переполнение буфера должно быть редким явлением, так как при обращении к системным вызовам, выполняющим чтение (read, recv, recvfrom, readv и readmsg), всегда необходимо указывать размер буфера. Но вы увидите далее как легко допустить такую ошибку. (Это рассмотрено в замечании к строке 42 программы shutdown.с в совете 16.)

Чтобы понять, как это происходит, разработаем функцию readline, использовавшуюся в совете 9. Поставленная задача - написать функцию, которая считывает из сокета в буфер одну строку, заканчивающуюся символом новой строки, и дописывает в конец двоичный нуль. На начало буфера указывает параметр buf.

#include "etcp.h"


int readline( SOCKET s, char *buf, size_t len );

Возвращаемое значение: число прочитанных байтов или -1 в случае ошибки.

Первая попытка реализации, которую надо отбросить сразу, похожа на следу­ющий код:

while ( recv( fd, , &с, 1, 0 ) == 1 )

{

 *bufptr++ = с;

 if ( с == "\n" )

  break;

}

/* Проверка ошибок, добавление  завершающего нуля и т.д. */

Прежде всего, многократные вызовы recv совсем неэффективны, поскольку при каждом вызове нужно два переключения - в режим ядра и обратно.

Примечание: Но иногда приходится писать и такой код - смотрите, например, функцию readcrlf в листинге 3.10.

Важнее, однако, то, что нет контроля переполнения буфера, чтобы понять, как аналогичная ошибка может вкрасться и в более рациональную реализацию, следует рассмотреть такой фрагмент:

static char *bp;

static int cnt = 0;

static char b[ 1500 ];

char c;

for (  ; ;  )

{

 if (cnt-- <= 0)

 {

  cnt = recv( fd, b, sizeof( b ), 0 );

  if ( cnt < 0 )

   return -1;

  if ( cnt == 0 )

   return 0;

  bp = b;

 }

 c = *bp++;

 *bufptr++ = c;

 if ( c ==”\n” )

 {

   *bufptr = “\0”;

   break;

 }

}

В этой реализации нет неэффективности первого решения. Теперь считывается большой блок данных в промежуточный буфер, а затем по одному копируются байты в окончательный буфер; по ходу производится поиск символа новой строки. Но при этом в коде присутствует та же ошибка, что и раньше. Не проверяется переполнение буфера, на который указывает переменная bufptr. Можно было бы и не писать универсальную функцию чтения строки; такой код - вместе с ошибкой - легко мог бы быть частью какой-то большей функции.

А теперь напишем настоящую реализацию (листинг 2.31).

Листинг 2.31. Неправильная реализация readline

readline.с

1    int readline( SOCKET fd, char *bufptr, size_t len )

2    {

3    char *bufx = bufptr;

4    static char *bp;

5    static int cnt = 0;

6    static char b[ 1500 ];

7    char c;

8    while ( --len > 0 )

9    {

10     if ( --cnt <= 0 )



11     {

12      cnt = recv( fd, b, sizeof( b ), 0 );

13      if ( cnt < 0 )

14       return -1;

15      if ( cnt == 0 )

16       return 0;

17      bp = b;

18     }

19     с = *bp++;

20     *bufptr++ = c;

21     if ( с == "\n" )

22     {

23      *bufptr = "\

24      return bufptr - bufx;

25     )

26   }

27   set_errno( EMSGSIZE ) ;

28   return -1;

29   }

На первый взгляд, все хорошо. Размер буфера передается readline и во внешнем цикле проверяется, не превышен ли он. Если размер превышен, то переменной errno присваивается значение EMSGSIZE и возвращается -1.

Чтобы понять, в чем ошибка, представьте, что функция вызывается так:

rc = readline( s, buffer, 10 );

и при этом из сокета читается строка

123456789<nl>

Когда в c записывается символ новой строки, значение len равно нулю. Это означает, что данный байт последний из тех, что готовы принять. В строке 20 помещаете символ новой строки в буфер и продвигаете указатель bufptr за конец буфера. Ошибка возникает в строке 23, где записывается нулевой байт за границу буфера.

Заметим, что похожая ошибка имеет место и во внутреннем цикле. Чтобы увидеть ее, представьте, что при входе в функцию readline значение cnt равно нулю и recv возвращает один байт. Что происходит дальше? Можно назвать это «опустошением» (underflow) буфера.

Этот пример показывает, как легко допустить ошибки, связанные с переполнением буфера, даже предполагая, что все контролируется. В листинге 2.32 приведена окончательная, правильная версия readline.

Листинг 2.32. Окончательная версия readline

1    int readline( SOCKET fd, char *bufptr, size_t len )

2    {

3    char *bufx = bufptr;

4    static char *bp;

5    static int cnt = 0;

6    static char b[ 1500 ];

7    char c;

8    while ( --len > 0 )

9    {

10     if ( --cnt <= 0 )

11     {

12      cnt = recv( fd, b, sizeof ( b ), 0 );

13      if ( cnt < 0 )

14      {

15       if ( errno == EINTR )

16       {

17        len++; /*Уменьшим на 1 в заголовке while.*/

18        continue;

19       }

20       return –1;

21      }

22      if ( cnt == 0)

23       return 0;

24      bp = b;

25     }

26     с = *bp++;

27     *bufptr++ = с;

28     if ( с == "\n" )

29     {

30      *bufptr = "\0";

31      return bufptr - bufx;

32     }

33   }

34   set_errno( EMSGSIZE ) ;

35   return -1;

36   }

Единственная разница между этой и предыдущей версиями в том, что уменьшаются значения len и cnt до проверки, а не после. Также проверяется, не вернула ли recv значение EINTR. Если это так, то вызов следует повторить. При уменьшении len до использования появляется гарантия, что для нулевого байта всегда останется место. А, уменьшая cnt, можно получить некоторую уверенность, что данные не будут читаться из пустого буфера.


Проверка завершения работы клиента


Предположим, что клиент извещает о желании завершить работу, посыла серверу запрос из одной строки, в которой есть только слово quit. Допустим далее, что сервер читает строки из входного потока с помощью функции геаdline (ее текст приведен в листинге 2.32), которая была описана в совете 9. Что произойдет, если клиент завершится (аварийно или нормально) раньше, чем пошлет команду quit? TCP на стороне клиента отправит сегмент FIN, после чего операция чтения на сервере вернет признак конца файла. Конечно, это просто обнаружить, только сервер должен обязательно это сделать. Легко представить себе такой код, предполагая правильное поведение клиента:

for ( ; ; )

{

 if ( readline( s, buf, sizeof( buf ) ) < 0 )

  error( 1, errno, "ошибка вызова readline" );

 if ( strcmp( buf, "quit\n" ) == 0)

  /* Выполнить функцию завершения клиента. */

 else

  /* Обработать  запрос. */

}

Хотя код выглядит правильным, он не работает, поскольку будет повторно обрабатывать последний запрос, если клиент завершился, не послав команду quit.

Предположим, что вы увидели ошибку в предыдущем фрагменте (или нашли ее после долгих часов отладки) и изменили код так, чтобы явно обнаруживался признак конца файла:

for ( ;; )

{

 rc = readline( s, buf, sizeof( buf ) );

 if ( rc < 0 )

  error( 1, errno, "ошибка вызова readline" );

 if ( rc == 0 strcmp( buf, "quit\n" ) == 0)

  /* Выполнить функцию завершения клиента. */

 else

  /* Обработать запрос. */

}

И этот код тоже неправилен, так как в нем не учитывается случай, когда хост клиента «падает» до того, как клиент послал команду quit или завершил работу. В этом месте легко принять неверное решение, даже осознавая проблему. Для проверки краха клиентского хоста надо ассоциировать таймер с вызовом readline. Потребуется примерно в два раза больше кода, если нужно организовать обработку «безвременной кончины» клиента. Представив себе, сколько придется писать, вы решаете, что шансов «грохнуться» хосту клиента мало.


Но проблема в том, что хосту клиента и необязательно завершаться. Если это ПК, то пользователю достаточно выключить его, не выйдя из программы. А это очень легко, поскольку клиент мог исполняться в свернутом окне или в окне, закрытом другими, так что пользователь про него, вероятно, забыл. Есть и другие возможности. Если соединение между хостами установлено с помощью модема на клиентском конце (так сегодня выполняется большинство подключений к Internet), то пользователь может просто выключить модем. Шум в линии также может привести к обрыву соединения. И все это с точки зрения сервера неотличимо от краха хоста клиента.

Примечание: При некоторых обстоятельствах ошибку, связанную с модемом, можно исправить, повторно набрав номер (помните, что TCP способен восстанавливаться после временных сбоев в сети), но зачастую IP-адреса обоих оконечных абонентов назначаются динамически сервис - провайдером при у становлении соединения. В таком случае маловероятно, что будет задан тот же адрес, и поэтому клиент не сможет оживить соединение.

Для обнаружения потери связи с клиентом необязательно реализовывать пульсацию, как это делалось в совете 10. Нужно всего лишь установить тайм-аут для операции чтения. Тогда, если от клиента в течение определенного времени не поступает запросов, то сервер может предположить, что клиента больше нет, и разорвать соединение. Так поступают многие FTP-серверы. Это легко сделать, либо явно установив таймер, либо воспользовавшись возможностями системного вызова select, как было сделано при реализации пульсации.

Если вы хотите, чтобы сервер не «зависал» навечно, то можете воспользоваться Механизмом контролеров для разрыва соединения по истечении контрольного тайм-аута. В листинге 2.30 приведен простой TCP-сервер, который принимает сообщение от клиента, читает из сокета и пишет результат на стандартный вывод. Чтобы сервер не «завис», следует задать для сокета опцию SO_KEEPALIVE с помо­щью вызова setsockopt. Четвертый аргумент setsockopt должен указывать на ненулевое целое число, если надо активировать посылку контролеров, или на нулевое целое, чтобы ее отменить.



Запустите этот сервер на машине bsd, а на другой машине - программу telnet в качестве клиента. Соединитесь с сервером, отправьте ему строку «hello», чтобы соединение точно установилось, а затем отключите клиентскую систему от сети. Сервер напечатает следующее:

bsd: $ keep 9000

hello

Клиент отключился от сети.



Спустя 2 ч 11 мин 15 с.

кеер: ошибка вызова recv: Operation timed out (60)

bsd: $

Как и следовало ожидать, TCP на машине bsd разорвал соединение и вернул серверу код ошибки ETIMEDOUT. В этот момент сервер завершает работу и освобождает все ресурсы.

Листинг 2.30. Сервер, использующий механизм контролеров

1    #include "etcp.h"

2    int main( int argc, char **argv )

3    {

4    SOCKET s;

5    SOCKET s1;

6    int on = 1;

7    int rc;

8    char buf[ 128 ] ;

9    INIT();

10   s = tcp_server( NULL, argv[ 1 ] );

11   s1 = accept ( s, NULL, NULL );

12   if ( !isvalidsock( s1 ) )

13     error( 1, errno, "ошибка вызова accept\n" );

14   if ( setsockopt( si, SOL_SOCKET, SO_KEEPALIVE,

15     ( char * )&on, sizeof ( on ) ) )

16     error( 1, errno, "ошибка вызова setsockopt" );

17   for ( ;; )

18   {

19     rc = readline( si, buf, sizeof( buf ) );

20     if ( rc == 0 )

21      error( 1, 0, "другой конец отключился\n" );

22     if ( rc < 0 )

23      error( 1, errno, "ошибка вызова recv" );

24     write( 1, buf, rc );

25   }

26   }


Пульсация


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

клиент и сервер обмениваются сообщениями разных типов, каждое из которых имеет заголовок, идентифицирующий тип сообщения;

приложение передает данные в виде потока байтов без разбиения на записи.

Первый случай сравнительно несложен. Вводится новый тип сообщения MSG_HEARTBEAT. Получив такое сообщение, приложение возвращает его отправителю. Такой способ предоставляет большую свободу. Проверять наличие связи могут одна или обе стороны, причем только одна действительно посылает контрольное сообщение-пульс.

Сначала рассмотрим заголовочный файл (листинг 2.23), который используют как клиент, так и сервер.

Листинг 2.23. Заголовочный файл для реализации механизма пульсации

1          #ifndef _HEARTBEAT

2          #define _HEARTBEAT

3          #efine MSG_TYPE1 1 /* Сообщение прикладного уровня. */

4          #efine MSG_TYPE2 2/* Еще одно. */

5          #efine MSG_HEARTBEAT3 /* Сообщение-пульс. */

6          typedef struct/* Структура сообщения. */

7          {

8          u_int32_t type; /* MSG_TYPE1, ... */

9          char data[ 2000 ] ;

10        } msg_t;

11        #define Tl 60 /* Время простоя перед отправкой пульса. */

12        #define T2 10 /* Время ожидания ответа. */

13        #endif /* _HEARTBEAT_H_ */

3-5 С помощью этих констант определяются различные типы сообщений, которыми обмениваются клиент и сервер. Для данного примера нужно только сообщение MSG_HEARTBEAT.

6-10 Здесь определяется структура сообщений, которыми обмениваются клиент и сервер. Здесь представляет интерес только поле type. Реальное приложение могло бы подстроить эту структуру под свои возможности. Подробнее это рассматривается в замечаниях к листингу 2.15 о смысле типа u_int32_t и об опасности предположений о способе упаковки структур.


11 Данная константа определяет, сколько времени может простаивать соединение, прежде чем приложение начнет посылать контрольные сообщения-пульсы. Здесь произвольно выбрано 60 с, реальное же приложение должно подобрать значение, наиболее соответствующее потребностям и виду сети.

12 Эта константа определяет, сколько времени клиент будет ждать ответа на контрольное сообщение.

В листинге 2.24 приведен текст клиента, который инициирует посылку контрольных сообщений. Такой выбор абсолютно произволен, в качестве инициатора можно было выбрать сервер.

Листинг 2.24. Клиент, посылающий контрольные сообщения-пульсы

1    #include "etcp.h"

2    #include "heartbeat.h"

3    int main( int  argc, char **argv )

4    {

5    fd_set allfd;

6    fd_set readfd;

7    msg_t msg;

8    struct timeval tv;

9    SOCKET s;

10   int rc;

11   int heartbeats =0;

12   int cnt = sizeof( msg );

13   INIT();

14   s = tcp_client( argv[ 1 ], argv[ 2 ] );

15   FD_ZERO( &allfd } ;

16   FD_SET( s, uallfd );

17   tv.tv_sec = T1;

18   tv.tv_usec =0;

19   for ( ;; )

20   {

21     readfd = allfd;

22     rc = select( s + 1, &readfd, NULL, NULL, &tv );

23     if ( rc < 0 )

24      error( 1, errno, "ошибка вызова select" );

25     if ( rc == 0 )  /* Произошел тайм-аут. */

26     {

27      if ( ++ heartbeats > 3 )

28       error( 1, 0, "соединения нет\n" );

29      error( 0, 0, "посылаю пульс #%d\n" , heartbeats ) ;

30      msg.type = htonl( MSG_HEARTBEAT );

3!     rc = send( s, ( char * )&msg, sizeofl msg ), 0 );

32      if ( rc < 0 )

33       error( 1, errno, "ошибка вызова send" ) ;

34      tv.tv_sec = T2;

35      continue;

36     )

37     if ( !FD_ISSET( s, &readfd ) )

38      error( 1, 0, "select вернул некорректный сокет\n" );

39     rc = recv( s, ( char * )&msg + sizeof( msg ) - cnt,

40      cnt, 0 ) ;

41     if ( rc == 0 )

42      error ( 1, 0, "сервер закончил работу\n" ) ,-



43     if ( rc < 0 )

44      error( 1, errno, "ошибка вызова recv" );

45     heartbeats = 0;

46     tv.tv_sec = T1;

47     cnt -= rc; /* Встроенный readn. */

48     if ( cnt > 0 )

49      continue;

50     cnt = sizeof( msg );

51     /* Обработка сообщения. */

52   }

53   }

Инициализация

13-14 Выполняем стандартную инициализацию и соединяемся с сервером, адрес и номер порта которого заданы в командной строке.

15-16 Задаем маску для системного вызова select, в которой выбран ваш сокет.

17-18 Взводим таймер на Т1 секунд. Если за это время не было получено никакого сообщения, то select вернет управление с индикацией срабатывания таймера.

21-22 Устанавливаем маску, выбирающую сокет, из которого читаем, после чего система блокирует программу в вызове select, пока не поступят данные либо не сработает таймер.

Обработка тайм-аута

27-28 Если послано подряд более трех контрольных пульсов и не получено ответа, то считается, что соединение «мертво». В этом примере просто завершаем работу, но реальное приложение могло бы предпринять более осмысленные действия.

29-33 Если максимальное число последовательных контрольных пульсов не достигнуто, посылается новый пульс.

34 -35 Устанавливаем таймер на Т2 секунд. Если за это время не получен ответ, то либо отправляется новый пульс, либо соединение признается «мертвым» в зависимости от значения переменной heartbeats.

Обработка сообщения

37-38 Если select вернул сокет, отличный от соединенного с сервером, to завершаемся с сообщением о фатальной ошибке.

39-40 Вызываем recv для чтения одного сообщения. Эти строки, а также следующий за ними код, изменяющий значение переменной cnt, - не что иное, как встроенная версия функции readn. Она не может быть вызвана напрямую, поскольку заблокировала бы весь процесс на неопределенное время, нарушив тем самым работу механизма пульсации

41-44 Если получаем признак конца файла или ошибку чтения, выводим диагностическое сообщение и завершаем сеанс.

45-46 Поскольку только что получен ответ от сервера, сбрасывается счетчик пульсов в 0 и переустанавливается таймер на Т1 секунд.



47- 50 Эти строки завершают встроенный вариант readn. Уменьшаем переменную cnt на число, равное количеству только что прочитанных байт. Если прочитано не все, то следует повторить цикл с вызова select. В противном случае заносится в cnt полная длина сообщения и завершается обработка только что принятого сообщения.

Листинг 2.25 содержит текст сервера для этого примера. Здесь предполагается, что сервер также будет следить за состоянием соединения, но это не обязательно.

Листинг 2.25. Сервер, отвечающий на контрольные сообщения-пульсы

1    #include "etcp.h"

2    #include "heartbeat.h"

3    int main( int argc, char **argv )

4    {

5    fd_set allfd;

6    fd_set readfd;

7    msg_t msg;

8    struct timeval tv;

9    SOCKET s;

10   SOCKET s1;

11   int rc;

12   int missed_heartbeats = 0;

13   int cnt = sizeof( msg );

14   INIT();

15   s = tcp_server( NULL, argv[ 1 ] );

16   s1 = accept( s, NULL, NULL ) ;

17   if ( !isvalidsock( s1 ) )

18     error( 1, errno, "ошибка вызова accept" );

19   tv.tv_sec = T1 + T2;

20   tv.tv_usec = 0;

21   FD_ZERO( fcallfd );

22   FD_SET( si, fiallfd ) ;

23   for ( ;; )

24   {

25     readfd = allfd;

26     rc = select( s1 + 1, &readfd, NULL, NULL, &tv );

2.7    if ( rc < 0 }

28      error( 1, errno, "ошибка вызова select" );

29     if ( rc == 0 )  /* Произошел тайм-аут. */

30     {

31      if ( ++missed_heartbeats > 3 )

32       errorf 1, 0, "соединение умерло\n" );

33      error( 0, 0, "пропущен пульс #%d\n",

34       missed_heartbeats );

35      tv.tv_sec = T2;

35      continue;

37     }

38     if ( !FD_ISSET( s1, &readfd ) )

39      error( 1, 0, "select вернул некорректный сокет\n" );

40     rc = recv( si, ( char * )&msg + sizeof( msg ) - cnt,

41      cnt, 0 );

42     if ( rc == 0 )

43      errorf 1, 0, "клиент завершил работу\n" );

44     if { rc < 0 )

45      error( 1, errno, "ошибка вызова recv" );



46     missed_heartbeats = 0;

47     tv.tv_sec = T1 + T2;

48     cnt -= rc;  /* Встроенный readn. */

49     if ( cnt > 0 )

50      continue;

51     cnt = sizeof ( msg );

52     switch ( ntohl( msg.type ) )

53     {

54      case MSG_TYPE1 :

55       /* обработать сообщение типа TYPE1. */

56       break;

57      case MSG_TYPE2 :

58       /* Обработать сообщение типа TYPE2. */

59       break;

60      case MSG_HEARTBEAT :

61       rc = send( si, ( char * )&msg, sizeof( msg ), 0 );

62       if ( rc < 0 )

63        error( 1, errno, "ошибка вызова send" );

64       break;

65      default :

66       error ( 1, 0, "неизвестный тип сообщения (%d)\n"',

67        ntohl( msg.type ) );

68     }

69   }

70   EXIT( 0 ) ;

71   }

Инициализация

14-18 Выполняем стандартную инициализацию и принимаем соединение от клиента.

19- 20 Взводим таймер на Т1 + Т2 секунд. Поскольку клиент посылает пульс после Т1 секунд неактивности, следует подождать немного больше- на Т2 секунд.

21-22 Инициализируем маску для select, указывая в ней соединенный сокет, из которого происходит чтение.

25-28 Вызываем select и проверяем возвращенное значение.

Обработка тайм-аута

31-32 Если пропущено более трех пульсов подряд, то соединение считаете «мертвым» - работа завершается. Как и клиент, реальный сервер мог бы предпринять в этом случае более осмысленные действия.

35 Взводим таймер на Т2 секунд. К этому моменту клиент должен был бы посылать пульсы каждые Т2 секунд, так что если за это время ничего не получено, то необходимо увеличить счетчик пропущенных пульсов.

Обработка сообщения

38-39 Производим ту же проверку корректности сокета, что и в клиенте.

40-41 Как и в клиенте, встраиваем код функции readn.

42-45 Если recv возвращает признак конца файла или код ошибки, то печатаем диагностическое сообщение и выходим.

46-47 Поскольку только что получено сообщение от клиента, соединение все еще живо, так что сбрасываем счетчик пропущенных пульсов в нуль и взводим таймер на Т1 + Т2 секунд.



48- 51 Этот код, такой же, как в клиенте, завершает встроенную версию readn.

60-64 Если это сообщение-пульс, то возвращаем его клиенту. Когда клиент получит сообщение, обе стороны будут знать, что соединение еще есть.

Для тестирования этих программ запустим программу hb_server на машине spare, а программу hb_client - на машине bsd. После того как клиент соединится с сервером, отключим spare от сети. Вот что при этом будет напечатано.

spare: $ hb_server 9000

hb_server: пропущен пульс #1

hb_server: пропущен пульс #2

hb_server: пропущен пульс #3

hb_server: соединения нет

spare: $

bsd: $ hb_client spare 9000

hb_client: посылаю пульс #1

hb_client: посылаю пульс #2

hb_client: посылаю пульс #3

hb_client: соединения нет

bsd: $


Путеводитель по книге


Ниже будут рассмотрены основы API сокетов и архитектура клиент-сервер, свойственная приложениям, в которых используется TCP/IP. Это тот фундамент, на котором вы станете возводить здание своего мастерства.

В главе 2 обсуждаются некоторые заблуждения по поводу TCP/IP и сетей вообще. В частности, вы узнаете, в чем разница между протоколами, требующими логического соединения, и протоколами, не нуждающимися в нем. Здесь будет рассказано об IP-адресации и подсетях (эта концепция часто вызывает недоумение), о бесклассовой междоменной маршрутизации (Classless Interdomain Routing — CIDR) и преобразовании сетевых адресов (Network Address Translation - NAT). Вы увидите, что TCP в действительности не гарантирует доставку данных. И нужно быть готовым к некорректным действиям как пользователя, так и программы на другом конце соединения. Кроме того, приложения будут по-разному работать в глобальной (WAN) и локальной (LAN) сетях.

Следует напомнить, что TCP - это потоковый протокол, и разъяснить его значение для программистов. Вы также узнаете, что TCP автоматически не обнаруживает потерю связи, почему это хорошо и что делать в этой ситуации.

Вам будет понятно, почему API сокетов всегда следует предпочитать API на основе интерфейса транспортного уровня (Transport Layer Interface - TLI) и транспортному интерфейсу X/Open (X/Open Transport Interface - XTI). Кроме того, |я объясню, почему не стоит слишком уж серьезно воспринимать модель открытого взаимодействия систем (Open Systems Interconnection - OSI). TCP - очень эффективный протокол с отличной производительностью, так что обычно не нужно дублировать его функциональность с помощью протокола UDP.

В главе 2 разработаны каркасы для нескольких видов приложений TCP/IP и на их основе построена библиотека часто используемых функций. Каркасы и библио­тека позволяют писать приложения, не заботясь о преобразовании адресов, управлении соединением и т.п. Если каркас готов, то вряд ли следует срезать себе путь, например, «зашив» в код адреса и номера портов или опустив проверку ошибок.


Каркасы и библиотека используются в книге для построения тестов, небольших примеров и автономных приложений. Часто требуется всего лишь добавить пару строк в один из каркасов, чтобы создать специализированную программу или тестовый пример.

В главе 3 подробно рассмотрены некоторые вопросы, на первый взгляд кажущиеся тривиальными. Например, что делает операция записи в контексте TCP. Вроде бы все очевидно: записывается в сокет n байт, a TCP отсылает их на другой конец соединения. Но вы увидите, что иногда это происходит не так. В протоколе TCP есть сложный свод правил, определяющих, можно ли посылать данные немедленно и, если да, то сколько. Чтобы создавать устойчивые и эффективные программы, необходимо усвоить эти правила и их влияние на приложения.

То же относится к чтению данных и завершению соединения. Вы изучите эти операции и разберетесь, как нужно правильно завершать соединение, чтобы не потерять информацию. Здесь будет рассмотрена и операция установления соединения connect: когда при ее выполнении возникает тайм-аут и как она использу­ется в протоколе UDP.

Будет рассказано об имеющимся в системе UNIX суперсервере inetd, упрощающим написание сетевых приложений. Вы научитесь пользоваться программой tcpmux, которая избавляет от необходимости назначать серверам хорошо известные порты. Узнаете, как работает tcpmux, и сможете создать собственную версию для систем, где это средство отсутствует.

Кроме того, здесь подробно обсуждаются такие вопросы, как состояние TIME-WAIT, алгоритм Нейгла, выбор размеров буферов и правильное применение опции SO_REUSEADDR. Вы поймете, как сделать свои приложения событийно- управляемыми и создать отдельный таймер для каждого события. Будут описаны некоторые типичные ошибки, которые допускают даже опытные программисты, и приемы повышения производительности.

Наконец, вы познакомитесь с некоторыми языками сценариев, используемыми при программировании сетей. С их помощью можно легко и быстро создавать полезные сетевые утилиты.

Глава 4 посвящена двум темам. Сначала будет рассмотрено несколько инструментальных средств, необходимых каждому сетевому программисту. Показано, как использовать утилиту ping для диагностики простейших неисправностей. Затем рассказывается о сетевых анализаторах пакетов (sniffer) вообще и программе tcpdump в частности. В этой главе дано несколько примеров применения tcpdump для диагностики сетевых проблем. С помощью программы traceroute исследуется маленькая часть Internet.



Утилита ttcp, в создании которой принимал участие Майк Муусс (Mike Muuss) -автор программы ping, является полезным инструментом для изучения производительности сети и влияния на нее тех или иных параметров TCP. Будут продемонстрированы некоторые методы диагностики. Еще одна бесплатная инструментальная программа Isof необходима в ситуации, когда нужно сопоставить сетевые соединения с процессами, которые их открыли. Очень часто Isof предоставляет информацию, получение которой иным способом потребовало бы поистине героических усилий.

Много внимания уделено утилите netstat и той разнообразной информации, которую можно получить с ее помощью, а также программам трассировки системных вызовов, таким как ktrace и truss.

Обсуждение инструментов диагностики сетей завершается построением утилиты для перехвата и отображения датаграмм протокола ICMP (протокол контроля сообщений в сети Internet). Она не только вносит полезный вклад в ваш набор инструментальных средств, но и иллюстрирует использование простых сокетов (raw sockets).

Во второй части главы 4 описаны дополнительные ресурсы для пополнения знаний о TCP/IP и сетях. Я познакомлю вас с замечательными книгами Ричарда Стивенса, источниками исходных текстов, и собранием документов RFC (предложений для обсуждения), размещенных на сервере проблемной группы проектирования Internet (Internet Engineering Task Force - IETF) и в конференциях Usenet.


Реализация в UNIX


Для завершения реализации системы буферов в разделяемой памяти нужны еще два компонента. Это способ выделения блока разделяемой памяти и отображения его на адресное пространство процесса, а также механизм синхронизации для предотвращения одновременного доступа к списку свободных. Для работы с разделяемой памятью следует воспользоваться механизмом, разработанным в свое время для версии SysV. Можно было бы вместо него применить отображенный на память файл, как в Windows. Кроме того, есть еще разделяемая память в стандарте POSIX - для систем, которые ее поддерживают.

Для работы с разделяемой памятью SysV понадобятся только два системных вызова:

#include <sys/shm.h>

int shmget( key_t key, size_t size, int flags );

Возвращаемое значение: идентификатор сегмента разделяемой памяти в случае успеха, -1 - в случае ошибки.

void shmat( int segid, const void *baseaddr, int flags );

Возвращаемое значение: базовый адрес сегмента в случае успеха, -1 - в случае ошибки.

Системный вызов shmget применяется для выделения сегмента разделяемой памяти. Первый параметр, key, - это глобальный для всей системы уникальный идентификатор, сегмента. Сегмент будет идентифицироваться целым числом, пред­ставление которого в коде ASCII равно SMBM.

Примечание: Использование пространства имен, отличного от файловой системы, считается одним из основных недостатков механизмов IPC, появившихся еще в системе SysV. Для отображения имени файла на ключ IPС можно применить функцию ft ok, но это отображение не будет уникальным. Кроме того, как отмечается в книге [Stevens 1999], описанная в стандарте SVR4 функцияft ok дает коллизию (то есть два имени файла отображаю на один и тот же ключ) с вероятностью 75%.

Параметр size задает размер сегмента в байтах. Во многих UNIX-систем его значение округляется до величины, кратной размеру страницы. Параметру flags задает права доступа и другие атрибуты сегмента. Значения SHM_R и SHM определяют соответственно права на чтение и на запись для владельца. Права для группы и для всех получают путем сдвига этих значений вправо на три (для группы) или шесть (для всех) бит. Иными словами, право на запись для группы - это SHM_W » 3, а право на чтение для всех - SHM_R >> 6. Когда в параметр flags с помощью побитовой операции OR включается флаг IPC_CREATE, создается сегмент, если раньше его не было. При дополнительном включении флага IPC_EXCL ghmget вернет код ошибки EEXIST, если сегмент уже существует.


Вызов shmget только создает сегмент в разделяемой памяти. Для отображения его в адресное пространство процесса нужно вызвать shmat. Параметр segid- это идентификатор сегмента, который вернул вызов shmget. При желании можно ука­зать адрес baseaddr, на который ядро должно отобразить сегмент, но обычно этот параметр оставляют равным NULL, позволяя ядру самостоятельно выбрать адрес. Параметр flags используется, если значение baseaddr не равно NULL, - он управляет выравниваем заданного адреса на приемлемую для ядра границу.

Для построения механизма взаимного исключения следует воспользоваться SysV-семафорами. Хотя они небезупречны (в частности, им присуща та же про­блема нового пространства имен, что и разделяемой памяти), SysV-семафоры ши­роко используются в современных UNIX-системах и, следовательно, обеспечивают максимальную переносимость. Как и в случае разделяемой памяти, сначала надо по­лучить и инициализировать семафор, а потом уже его применять. В данной ситу­ации понадобятся три относящихся к семафорам системных вызова.

Вызов semget аналогичен shmget: он получает у операционной системы семафор и возвращает его идентификатор. Параметр key имеет тот же смысл, что и для shmget - он именует семафор. В SysV-семафоры выделяются группами, и параметр nsems означает, сколько семафоров должно быть в запрашиваемой группе. Параметр flags такой же, как для shmget.

#include <sys/sem.h>

int semget( key_t key, int nsems, int flags );

Возвращаемое значение: идентификатор семафора в случае успеха, -1 - в случае ошибки.

int semctl( int semid, int semnum, int cmd, ... );

Возвращаемое значение: неотрицательное число в случае успеха, -1 - в случае ошибки.

int semop( int semid, struct sembuf *oparray, size_t nops ); Возвращаемое значение: 0 в случае успеха, -1 - в случае ошибки.

Здесь использована semctl для задания начального значения семафора. Этот вызов служит также для установки и получения различных управляющих параметров, связанных с семафором. Параметр semid - это идентификатор семафора, ращенный вызовом semget. Параметр semnum означает конкретный семафор из группы. Поскольку будет выделяться только один семафор, значение этого параметра всегда равно нулю. Параметр cmd- это код выполняемой операции.



У вызова semget могут быть и дополнительные параметры, о чем свидетельствует многоточие в прототипе.

Вызов semop используется для увеличения или уменьшения значения семафора. Когда процесс пытается уменьшить семафор до отрицательного значения, он переводится в состояние ожидания, пока другой процесс не увеличит семафор до значения, большего или равного тому, на которое первый процесс пытался его уменьшить. Поскольку надо использовать семафоры в качестве мьютексов, следует уменьшать значение на единицу для блокировки списка свободных и увеличивать на единицу - для разблокировки. Так как начальное значение семафора равно единице, в результате процесс, пытающийся заблокировать уже блокирован­ный список свободных, будет приостановлен.

Параметр semid- это идентификатор семафора, возвращенный semget. Параметр ораrrау указывает на массив структур sembuf, в котором заданы операции над одним или несколькими семафорами из группы. Параметр nops задает число элементов в массиве ораггау.

Показанная ниже структура sembuf содержит информацию о том, к какому семафору применить операцию (sem_num), увеличить или уменьшить значение семафора (sem_op), а также флаг для двух специальных действий (sem_f lg):

struct sembuf {

 u_short sem__num;   /* Номер семафора. */

 short sem_op;     /* Операция над семафором. */

 short sem_flg;     /* Флаги операций. */

};

В поле sem_f lg могут быть подняты два бита флагов:

IPC_NOWAIT - означает, что semop должна вернуть код EAGAIN, а не приостанавливать процесс, если в результате операции значение семафора окажется отрицательным;

SEMJJNDO - означает, что semop должна отменить действие всех операций над семафором, если процесс завершается, то есть мьютекс будет освобожден.

Теперь рассмотрим UNIX-зависимую часть кода системы буферов в разделяемой памяти (листинг 3.30).

Листинг 3.30. Функция init_smb для UNIX

1    #include <sys/shm.h>

2    #include <sys/sem.h>

3    #define MUTEX_KEY Ox534d4253  /* SMBS */

4    #define SM_KEY Ox534d424d     /* SMBM */



5    #define lock_buf() if ( semop( mutex, &lkbuf, 1 ) < 0 ) \

6    error( 1, errno, "ошибка вызова semop" )

7    #define unlock_buf ()  if ( semop ( mutex, unlkbuf, 1 )<0) \

8    error( 1, errno, "ошибка вызова semop" )

9    int mutex;

10   struct sembuf lkbuf;

11   struct sembuf unlkbuf;

12   void init_smb( int init_freelist )

13   {

14   union semun arg;

15   int smid;

16   int i;

17   int rc;

18   Ikbuf.sem_op = -1;

19   Ikbuf.sem_flg = SEM_UNDO;

20   unlkbuf.sem_op = 1;

21   unlkbuf.sem_flg = SEM_UNDO;

22   mutex = semget( MUTEX_KEY, 1,

23   IPC_EXCL | IPC_CREAT | SEM_R | SEM_A );

24   if ( mutex >= 0 )

25   {

26     arg.val = 1;

27     rc = semctl ( mutex, 0, SETVAL, arg );

28     if ( rc < 0 )

29      error( 1, errno, "semctl failed" );

30   }

31   else if ( errno == EEXIST )

32   {

33     mutex = semget( MUTEX_KEY, 1, SEM_R I SEM_A );

34     if ( mutex < 0 )

35      error( 1, errno, "ошибка вызова semctl" );

36   }

37   else

38     error(   1,   errno,   "ошибка вызова  semctl"   );

39   smid = shmget( SM_KEY,  NSMB * sizeof( smb_t )+sizeof(int ),

40   SHM_R   |   SHM_W   |   IPC_CREAT   );

41   if   (   smid <  0   )

42     error( 1, errno, "ошибка вызова shmget" );

43   smbarray = ( smb_t * )shmat( smid, NULL, 0 );

44   if ( smbarray == ( void * )-1 )

45     error( 1, errno, "ошибка вызова shmat" );

46   if ( init_freelist )

47   {

48     for ( i = 0; i < NSMB - 1; i++ )

49      smbarray[ i ].nexti = i + 1;

50     smbarray[ NSMB - 1 ].nexti = -1;

51     FREE_LIST = 0;

52   }

53   }

Макросы и глобальные переменные

3- 4 Определяем ключи сегмента разделяемой памяти (SMBM) и семафЛpa (SMBS).

5-8 Определяем примитивы блокировки и разблокировки в терминах операций над семафорами.

9-11 Объявляем переменные для семафоров, используемых для реализащЯ мьютекса.

Получение и инициализация семафора

18-21 Инициализируем операции над семафорами, которыми будем пользо­ваться для блокировки и разблокировки списка свободных.



22-38 Этот код создает и инициализирует семафор. Вызываем semget с флагами IPC_EXCL и IPC_CREAT. В результате семафор будет создан, если он еще не существует, и в этом случае semget вернет идентификатор семафора, который инициализируем единицей (разблокированное состояние). Если же семафор уже есть, то снова вызываем semget, уже не задавая флагов IPC_EXCL и IPC_CREAT, для получения идентификатора этого семафора. Как отмечено в книге [Stevens 1999], теоретически здесь возможна гонка, но не в данном случае, поскольку сервер вызывает init_smb перед вызовом listen, а клиент не сможет обратиться к нему, пока вызов connect не вернет управление.

Примечание: В книге [Stevens 1999] рассматриваются условия, при которых возможна гонка, и показывается, как ее избежать.

Получение, отображение и инициализация буферов в разделяемой памяти

39-45 Выделяем сегмент разделяемой памяти и отображаем его на свое адресное пространство. Если сегмент уже существует, то shmget возвра­щает его идентификатор.

46-53 Если init_smb была вызвана с параметром init_freelist, равным TRUE, то помещаем все выделенные буферы в список свободных и возвращаем управление.


Реализация в Windows


Прежде чем демонстрировать систему в действии, рассмотрим реализацию для Windows. Как было упомянуто выше, весь системно-зависимый код сосредточен в функции init_smb. В Windows мьютекс создается очень просто - дост точно вызвать функцию CreateMutex.

#include <windows.h>

HANDLE  CreateMutex( LPSECURITY_ATTRIBUTES  Ipsa,

BOOL  flnitialOwner,   LPTSTR  IpszMutexName  );

Возвращаемое значение: описание мьютекса в случае успеха, NULL - в случае ошибки.

Параметр lpsa - это указатель на структуру с атрибутами защиты. Здесь эта возможность не нужна, так что вместо этого аргумента передадим NULL. Параметр flnitialOwner означает, будет ли создатель мьютекса его начальным владельцем, то есть следует ли сразу заблокировать мьютекс. Параметр lpszMutexName -эхо имя мьютекса, по которому к нему могут обратиться другие процессы. Если мьютекс уже существует, то CreateMutex просто вернет его описание.

Блокировка и разблокировка мьютекса выполняются соответственно с помо­щью функций WaitForSingleObject и ReleaseMutex.

#include  <windows.h>

DWORD WaitForSingleObject( HANDLE hObject, DWORD dwTimeout );

Возвращаемое значение: WAIT_OBJECT_0 (0) в случае успеха, ненулевое значение - в случае ошибки.

BOOL  ReleaseMutex(   HANDLE  hMutex );

Возвращаемое значение: TRUE в случае успеха, FALSE - в случает ошибки.

Параметр hObject функции WaitForSingleObject - это описание ожидаемого объекта (в данном случае мьютекса). Если объект, заданный с помощью hObject, не занят (signaled), то WaitForSingleObject занимает его и возвращает управление. Если же объект занят (not signaled), то обратившийся поток переводится в состояние ожидания до тех пор, пока объект не освободится. После этого WaitForSingleObject переведет объект в занятое состояние и вернет в работу «спящий» поток. Параметр dwTimeout задает время (в миллисекундах), в течение которого потоком ожидается освобождение объекта. Если тайм-аут истечет прежде, чем объект освободится, то WaitForSingleObject вернет код WAIT_TIMEOUT. Таймер можно подавить, задав в качестве dwTimeout значение INFINITE.


Когда поток заканчивает работу с критической областью, охраняемой мыотексом, он разблокирует его вызовом ReleaseMutex, передавая описание мьютекса hMutex в качестве параметра.

В Windows вы получаете сегмент разделяемой памяти, отображая файл на па­мять каждого процесса, которому нужен доступ к разделяемой памяти (в UNIX есть аналогичный системный вызов mmap). Для этого сначала создается обычный файл с помощью функции CreateFile, затем - отображение файла посредством вызова CreateFileMapping, а уже потом оно отображается на ваше адресное про-странство вызовом MapViewOf File.

Параметр hFile в вызове CreateFileMapping - это описание отображаемого файла. Параметр lpsa указывает на структуру с атрибутами безопасности, которые в данном случае не нужны. Параметр fdwProtect определяет права доступа к объекту в памяти. Он может принимать значения PAGE_READONLY, PAGE_READWRITE или E_WRITECOPY. Последнее значение заставляет ядро сделать отдельную копию данных, если процесс пытается записывать в страницу памяти. Здесь используется PAGE_READWRITE, так как будет производится и чтение, и запись в разделяемую память. Существуют также дополнительные флаги, объединяемые операцией побитового OR, которые служат для управления кэшированием страниц памяти но они не понадобятся. Параметры dwMaximumSizeHigh и dwMaximumSizeLowB совокупности дают 64-разрядный размер объекта в памяти. Параметр lpszMapNaitie -это имя объекта. Под данным именем объект известен другим процессам.

#include <windows,h>

HANDLE CreateFileMapping( HANDLE hFile, LPSECURITY_ATTRIBUTES lpsa/ DWORD  fdwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPSTR IpszMapName );

Возвращаемое значение: описатель отображения файла в случае успеха, null -в случае ошибки.

LPVOID MapViewOfFile (   HANDLE hFileMapObject,   DWORD dwDesiredAccess, DWORD  dwFileOffsetHigh,   DWORD dwFileOffsetLow, DWORD  dwBytesToMap );

Возвращаемое значение: адрес, на который отображена память, в случае успеха, NULL - в случае ошибки.



После создания объект в памяти отображается на адресное пространство каж­дого процесса с помощью функции MapViewOfFile. Параметр hFi1eMapObj - это описание, возвращенное после вызова CreateFileMapping. Требуемый уровень доступа следует задать с помощью dwDesiredAccess. Этот параметр может принимать следующие значения: FILE_MAP_WRITE (доступ на чтение и запись), FILE_MAP_READ (доступ только на чтение), FILE_MAP_ALL_ACCESS (то же, что FILE_MAP_WRITE) и FILE_MAP_COPY. Если присвоено последнее значение, то при попытке записи создается отдельная копия данных. Параметры dwFileOffsetHigh и dwFileOffsetLow задают смещение от начала файла, с которого следует начинать отображение. Нужно отобразить файл целиком, поэтому оба параметра будут равны 0. Размер отображаемой области памяти задается с помощью параметра dwBytesToMap.

Подробнее использование мьютексов и отображение памяти в Windows рассматриваются в книге [Richter 1997].

Теперь можно представить версию init_smb для Windows. Как видно из листинга 3.31, она очень напоминает версию для UNIX.

Листинг 3.31. Функция init_smb для Windows

1     #define FILENAME  "./smbfile"

2     #define lock_buf () if ( WaitForSingleObject ( mutex, INFINITE

3     ! = WAIT_OBJECT_0 )

4     error ( 1, errno, "ошибка вызова lock_buf " )

5     #define unlock_buf()  if ( !ReleaseMutex( mutex ) }

6     error( 1, errno, "ошибка вызова unlock_buf" )

7     HANDLE mutex;

8     void init_smb( int init_freelist )

9     {

10    HANDLE hfile;

11    HANDLE hmap;

12    int i;

13    mutex = CreateMutex ( NULL, FALSE, "smbmutex" );

14    if ( mutex == NULL )

15      error( 1, errno, "ошибка вызова CreateMutex" );

16    hfile = CreateFile( FILENAME,

17    GENERIC_READ | GENERIC_WRITE,

18    FILE_SHARE_READ | FILE_SHARE_WRITE,

19    NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL };

20    if ( hfile == INVALID_HANDLE_VALUE )

21      error( 1, errno, "ошибка вызова CreateFile" );



22    hmap = CreateFileMapping( hfile, NULL, PAGE_READWRITE,

23      0, NSMB * sizeof( smb_t ) + sizeof( int ), "smbarray" );

24    smbarray = MapViewOfFile( hmap, FILE_MAP_WRITE, 0, 0, 0 );

25    if ( smbarray == NULL )

26      error( 1, errno, "ошибка вызова MapViewOfFile" );

27

28    if   (   init_freelist   )

29    {

30      for    (   i   =   0;    i   <  NSMB   -   1;   i++   )

31       smbarrayt   i   ].nexti  =  i  +  1;

32      smbarray [ NSMB  -   1 ].nexti  =  -1;

33      FREE_LIST  =   0;

34    }

35    }

Для тестирования всей системы следует написать небольшие программы клиентской (листинг 3.32) и серверной (листинг 3.33) частей.

Листинг 3.32. Клиент, использующий систему буферов в разделяемой памяти

1    #include   "etcp.h"

2    int main(   int  argc,   char  **argv  )

3    {

4    char  *bp;

5    SOCKET  s;

6    INIT();

7    s  =  tcp_client(   argv[   1   ],   argv[   2   ]   );

8    init_smb(   FALSE   );

9    bp = smballoc();

10   while ( fgets( bp, SMBUFSZ, stdin ) != NULL  )

11   {

12     smbsend( s, bp );

13     bp = smballocO;

14   }

15   EXIT(   0   ) ;

16   }

Листинг 3.33. Сервер, использующий систему буферов в разделяемой памяти

1    #include   "etcp.h"

2    int main(   int  argc,   char  **argv  )

3    {

4    char  *bp;

5    SOCKET  s;

6    SOCKET  s1;

7    INIT();

8    init_smb( TRUE );

9    s   =   tcp_server(   NULL, argv[   1   ]    );

10   s1   =   accept(   s,   NULL,   NULL   );

11   if   (   !isvalidsock(   s1 )   )

12     error (   1,   errno,   "ошибка вызова accept"   )

13   for    (    ;;    )

14   {

15     bp = smbrecv( s1 );

16     fputs( bp, stdout );

17     smbfree( bp );

18   }

19   EXIT( 0 );

20   }

Запустив эти программы, получите ожидаемый результат:

bsd: $ smbc  localhost  9000

Hello

Wolds!

^C

bsd:    $

bsd: $ smbs  9000

Hello

Wolds!

^C

bsd:    $

Обратите внимание, что smbc читает каждую строку из стандартного ввода прямо в буфер в разделяемой памяти, a smbs копирует каждую строку из буфер3 сразу на стандартный вывод, поэтому не возникает лишнего копирования данных.


В этой главе приведен краткий


В этой главе приведен краткий обзор последующих глав и рассмотрены эле­менты API сокетов. Теперь можно перейти к более сложному материалу.
| | |


В этом разделе обсуждены различия между протоколами, которые требуют и не требуют установления логического соединения. Вы узнали, что ненадежные протоколы, в которых происходит обмен датаграммами без установления соединения, - это фундамент, на котором строятся надежные протоколы на базе соединений. Попутно было кратко изложено, как надежный протокол TCP строится на основе ненадежного протокола IP.
Также отмечалось, что понятие «соединение» в TCP носит умозрительный характер. Оно состоит из хранящейся информации о состоянии на обоих концах; никакого «физического» соединения, как при телефонном разговоре, не существует.
| | |


В этом разделе рассмотрены подсети и бесклассовая междоменная маршрутизация (CIDR). Вы узнали, как они применяются для решения двух проблем, свойственных адресации на основе классов. Подсети позволяют предотвратить рост маршрутных таблиц, обеспечивая в то же время гибкую адресацию. CIDR служит эффективного выделения IP-адресов и способствует их иерархическому назначению.
| | |


В этом разделе показано, как схема NAT позволяет использовать один из блоков частных сетевых адресов для внутренних хостов, сохраняя при этом возможность выхода в Internet. Метод PAT, в частности, особенно полезен для небольших сетей, у которых есть только один глобально выделенный IP-адрес. К сожалению, поскольку PAT изменяет номер порта в исходящих пакетах, он может оказаться несовместимым с нестандартными протоколами, которые передают информацию о номерах портов в теле сообщения.
| | |


Прочитав данный раздел, вы узнали, как просто создать целый арсенал каркасов и библиотечных функций. Все построенные каркасы очень похожи и различаются только несколькими строками в стартовом коде внутри функции main. Таким образом, после написания первого каркаса пришлось лишь скопировать код и подправить эти несколько строк. Эта методика очень проста. Поэтому, чтобы со­здать несколько элементарных клиентов и серверов, потребовалось только вставить содержательный код вместо заглушек.
Использование каркасов и написание библиотечных функций закладывает тот фундамент, на котором далее легко строить приложения и небольшие тестовые программки для их проверки.
| | |


В обычных ситуациях нет смысла использовать интерфейс XTI/TLI при программировании TCP/IP. Интерфейс сокетов проще и обладает большей переносимостью, а возможности обоих интерфейсов почти одинаковы.
| | |


Типичная ошибка, допускаемая начинающими сетевыми программистами, - в непонимании того, что TCP доставляет поток байтов, в котором нет понятия «границы записей». В TCP нет видимой пользователю концепции «пакета». Он просто передает поток байтов, и нельзя точно предсказать, сколько байтов будет возвращено при очередном чтении. В этом разделе рассмотрено несколько способов работы в таких условиях.
| | |


UDP не всегда быстрее, чем TCP. На сравнительную производительность обоих протоколов влияют разные факторы, и для каждого конкретного случая желательно проверять быстродействие на контрольных задачах.
| | |


Здесь рассмотрены шаги, необходимые для построения надежного протокола поверх UDP. Хотя и существуют приложения, например, DNS, в которых это сделано, но для корректного решения такой задачи необходимо практически заново реализовать TCP. Поскольку маловероятно, что реализованный на базе UDP протокол будет так же эффективен, как TCP, смысла в этом, как правило, нет.
В этом разделе также кратко обсуждается протокол Т/ТСР - модификация TCP для оптимизации транзакционных приложений. Хотя Т/ТСР решает многие проблемы, возникающие при использовании TCP для реализации транзакций, он пока не получил широкого распространения.
| | |


В этом разделе дано объяснение понятию «надежность TCP». Вы узнали, что не существует гарантированной доставки, и при работе с TCP могут встретиться разнообразные ошибки. Ни одна из этих ошибок не фатальна, но вы должны быть готовы к их обработке.
| | |


Хотя TCP и не предоставляет средств для немедленного уведомления клиента о потере связи, тем не менее несложно самостоятельно встроить такой механизм в приложение. Здесь рассмотрены две модели реализации контрольных сообщений-пульсов. На первый взгляд, это может показаться избыточным, но одна модель не подходит для всех случаев.
Первый способ применяется, когда приложения обмениваются между собой сообщениями, содержащими поле идентификатора типа. В этом случае все очень просто: достаточно добавить еще один тип для сообщений-пульсов. «Родители» могут спокойно работать - их «дети» под надежным присмотром.
Второй способ применим в ситуации, когда приложения обмениваются потоком байтов без явно выраженных границ сообщений. В качестве примера можно назвать передачу последовательности нажатий клавиш. В данном примере использовано отдельное соединение для приема и передачи пульсов. Разумеется, тот метод можно было бы применить и в первом случае, но он несколько сложнее, чем простое добавление нового типа сообщения.
В книге «UNIX Network Programming» [Stevens 1998] описан еще один метод организации пульсации с помощью механизма срочных данных, имеющегося в TCP. Это лишний раз демонстрирует, какие разнообразные возможности иметь в распоряжении прикладного программиста для организации уведомления приложения о потере связи.
Наконец, следует напомнить, что хотя было сказано только о протоколе TCP, то же самое верно и в отношении UDP. Представим сервер, который посылает широковещательные сообщения нескольким клиентам в локальной сети или организует групповое вещание на глобальную сеть. Поскольку соединения нет, клиенты имеют информации о крахе сервера, хоста или сбое в сети. Если в датаграммах есть поле типа, то серверу нужно лишь определить тип для датаграммы-пульса и посылать ее, когда в сети какое-то время не было других сообщений. Вместо этого он мог бы рассылать широковещательные датаграммы на отдельный порт, который клиенты прослушивают.
| | |


Вы всегда должны быть готовы к неожиданным действиям со стороны пользователей и хостов на другом конце соединения. В этом разделе рассмотрено два примера некорректного поведения другой стороны. Во-первых, нельзя надеяться на то, что партнер обязательно сообщит вам о прекращении передачи данных. Во-вторых, продемонстрирована важность проверки правильности входных данных и разработана функция readline, устойчивая к ошибкам.
| | |


Локальная сеть, которая представляет собой почти идеальную среду, может маскировать проблемы производительности и даже ошибки. Не думайте, что приложение, работающее в локальной сети, будет также хорошо работать и в глобальной.
Из-за сетевых задержек приложение, производительность которого в локальной сети была удовлетворительной, в глобальной сети может работать неприемлемо медленно. В результате иногда приходится перепроектировать программу.
Из-за перегрузок в интенсивно используемой глобальной сети, особенно в Internet, данные могут доставляться как внезапно, так и пакетами неожиданного размера. Это требует от вас особой осторожности в допущениях о том, сколько данных может прийти в определенный момент и с какой частотой они поступают.
Хотя в этом разделе говорилось исключительно о протоколе TCP, то же относится и к UDP, поскольку он не обладает встроенной надежностью, чтобы противостоять тяжелым условиям в Internet.
| | |


В этом разделе обсуждалось, насколько важно разбираться в функционировании протоколов. Отмечено, что официальной спецификацией TCP/IP являются RFC и рекомендованы книги Комера и Стивенса в качестве дополнительного источника информации о протоколах и их работе.
| | |


В этом разделе дано сравнение моделей OSI и TCP/IP. Вы узнали, что семиуровневая модель OSI нужна как средство описания сетевой архитектуры, но созданные на ее базе реализации почти не имеют успеха.
| | |


В этом разделе подробно рассмотрена операция записи. С точки зрения прило­жения операцию записи проще всего представлять как копирование из адресного пространства пользователя в буферы ядра и последующий возврат. Срок передачи данных TCP и их объем зависят от состояния соединения, а приложение не умеет подобной информации.
Проанализированы стратегия отправки, принятая в BSD TCP, а также влия­ние на нее объема буферной памяти у получателя (представлен окном передачи), оценки загрузки сети (представлена окном перегрузки), объема данных, готовых для передачи, попытки избежать синдрома безумного окна и стратегии повторной передачи.
| | |


Вы изучили системный вызов shutdown и сравнили его с вызовом close. Так­же рассказывалось, что с помощью shutdown можно закрыть только отправляю­щую, принимающую или обе стороны соединения; и счетчик ссылок на сокет при этом изменяется иначе, чем при закрытии с помощью close.
Затем было показано, как использовать shutdown для аккуратного размыка­ния соединения. Аккуратное размыкание - это последовательность разрыва соеди­нения, при которой данные не теряются.
| | |


В этом разделе показано, как заставить приложение работать в сети, приложив совсем немного усилий. Демон inetd берет на себя ожидание соединений или датаграмм, дублирует дескриптор сокета на stdin, stdout и stderr и запускает приложение. После этого приложение может просто читать из stdin или писать в stdout либо stderr, не имея информации о том, что оно работает в сети. Рассмотрен пример простого фильтра, в котором вообще нет кода, имеющего отноше­ние к сети. Но этот фильтр тем не менее прекрасно работает в качестве сетевого сервиса, если запустить его через inetd.
Здесь также приведен пример UDP-сервера, который способен вести продолжительный диалог с клиентами. Для этого серверу пришлось получить новый сокет и номер порта, а затем создать новый процесс и выйти.
| | |


Сервис TCPMUX, имеющийся на очень многих системах, помогает решить проблему выбора хорошо известного номера порта сервера. Здесь реализована собственная версия демона tcpmux, так что если в какой-то системе его нет, то им можно воспользоваться.
| | |


В этом разделе обсуждалась идея об использовании двух соединений между приложениями. Это позволяет отслеживать состояние соединения даже тогда, когда чтение и запись производятся в разных процессах.
| | |


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


В этом и предыдущем разделах говорилось о событийно-управляемом программировании и о том, как использовать вызов select для реагирования на события по мере их поступления. В совете 20 разработана функция tselect, позволившая получить несколько логических таймеров из одного физического. Эта Функция и используемые с ней функции timeout и untimeout дают возможность задавать тайм-ауты сразу для нескольких событий, инкапсулируя внутри себя все сопутствующие этому детали.
Здесь была использована функция tselect, чтобы усовершенствовать пример совета 19. Применение tselect позволило задавать отдельные таймеры ретрансмиссии для каждого сообщения, посланного ненадежному удаленному хосту через сервер-шлюз xout3.
| | |


В этом разделе обсуждено состояние TIME-WAIT, которое часто понимают неправильно. Это состояние - важная часть механизма обеспечения надежности протокола TCP, и попытки обойти его неверны. Преждевременный выход из состояния TIME-WAIT может быть обусловлен «естественным» стечением обстоятельств в сети или программой, которая манипулирует опцией SO_LINGER.
| | |


В этом разделе рассмотрена опция сокета SO_REUSEADDR. Ее установка позволяет перезапустить сервер, от предыдущего «воплощения» которого еще осталось соединение в состоянии TIME-WAIT. Серверы должны всегда устанавливать эту опцию, которая не влечет угрозу безопасности.
| | |


В этом разделе разобран алгоритм Нейгла и его взаимодействие с алгоритмом отложенного подтверждения. Приложения, записывающие в сеть несколько маленьких блоков вместо одного большого, могут заметно снизить производительность.
Поскольку алгоритм Нейгла помогает предотвратить действительно серьезную проблему - переполнение сети крохотными пакетами, не следует отключать его для повышения производительности приложений, выполняющих запись мелкими блока­ми. Вместо этого следует переписать приложение так, чтобы все логические связан­ные данные выводились сразу. Здесь был рассмотрен удобный способ решения этой задачи с помощью системного вызова writev в UNIX или WSASend в Winsock.
| | |


Как видите, для переноса на разные платформы прерывать вызов connect с помощью тайм-аута более сложно, чем обычно. Поэтому при выполнении такого действия надо уделить особое внимание платформе.
Наконец, следует понимать, что сократить время ожидания connect можно, а увеличить - нет. Все вышерассмотренные методы направлены на то, чтобы прервать вызов connect раньше, чем это сделает TCP. He существует переносимого механизма для изменения значения тайм-аута TCP на уровне одного сокета.
| | |


В этом разделе описано, как избежать ненужного копирования данных. Во многих сетевых приложениях на копирование данных из одного буфера в другой тратится большая часть времени процессора.
Разработана схема взаимодействия между процессами, в которой использую система буферов в разделяемой памяти. Это позволило передавать единственнь экземпляр данных от одного процесса другому. Такая схема работает и в UNIX и в Windows.
| | |


В этом разделе рассказывалось, что в TCP/IP применяется стандартное представление в сетевом порядке байт для целых чисел, входящих в заголовки прото колов. Здесь также приведены функции htonl, htons, ntohl и ntohs, которь преобразуют целые из машинного порядка байт в сетевой и обратно. Кроме того. было отмечено, что в общем случае для преобразования форматов данных между машинами полезно средство XDR.
| | |


В этом разделе рекомендовано не «зашивать» адреса и номера портов в про­грамму. Также рассмотрено несколько стандартных схем получения этой информации и обсуждены их достоинства и недостатки.
| | |


В этом разделе рассмотрено использование вызова connect в протоколе UDP. Хотя на первый взгляд может показаться, что для протокола без установления со­единения это не имеет смысла, но, как вы видели, такое действие, во-первых, повышает производительность, а во-вторых, оно необходимо при желании получать некоторые сообщения об ошибках при отправке UDP-датаграмм. Здесь также описано, как использовать connect для приема датаграмм только от одного хоста.
| | |


В этом разделе говорилось об использовании языков сценариев в сетевом программировании. Нередко их применение имеет смысл при написании небольших утилит и тестовых программ.
| | |


Производительность TCP в значительной степени зависит от размеров буферов приема и передачи (совет 36). В этом разделе вы узнали, что оптимальный размер буфера для эффективной передачи больших объемов данных равен произведению полосы пропускания на задержку, но на практике это наблюдение не особенно полезно.
Хотя правило произведения применять трудно, есть другое, намного проще. Ему и рекомендуется всегда следовать: размер буфера передачи должен быть, по крайней мере, в три раза больше, чем MSS.
| | |


Утилита ping - это один из важнейших инструментов тестирования связи в сети. Поскольку для ее работы требуется лишь функционирование самых нижних уровней сетевых служб, она полезна для проверки связи в условиях, когда сервисы более высокого уровня, такие как TCP, или программы прикладного уровня типа telnet не работают.
С помощью ping часто удается сделать выводы об условиях в сети, наблюдая за значениями и дисперсией RTT и за числом потерянных ответов.
| | |


Программа tcpdump - это незаменимый инструмент для изучения того, что происходит в сети. Если знать, что в действительности посылается или принимается «по проводам», то трудные, на первый взгляд, ошибки удается легко найти и исправить. Эта программа представляет собой также важный инструмент для исследований динамики сети, а равно средство обучения. В последнем качестве она широко при­меняется в книгах серии «TCP/IP Illustrated», написанных Стивенсом.
| | |


Утилита traceroute - очень полезный инструмент для диагностики сетевых ошибок, изучения маршрутизации и исследования топологии сети. Топология Internet нередко достаточно запутанна, и это может быть причиной неожиданного поведения приложений. С помощью traceroute зачастую удается обнаружить аномалии в сети, из-за которых программа ведет себя странно.
Программы traceroute и tracert работают путем отправки хосту назначения датаграммы с последовательно увеличивающимся значением в поле TTL. Затем они отслеживают приходящие от промежуточных маршрутизаторов ICMP-сообщения «истекло время в пути». Разница в том, что traceroute посылает UDP-датаграммы, a tracert - эхо-запросы ICMP.
| | |


В этом разделе показано, как пользоваться программой ttcp для экспериментирования с различными параметрами TCP-соединения, ttcp можно применять также в целях тестирования собственных приложений, предоставляя для них источник или приемник данных, работающий по протоколу TCP либо UDP. И, наконец, вы видели, как использовать ttcp для организации сетевого конвейера между двумя или более машинами.
| | |


Здесь показано, как можно воспользоваться утилитой lsof для получения ответа на разнообразные вопросы об открытых файлах. К сожалению, нет версии lsof для Windows.
| | |


Здесь приведены утилита netstat и те сведения о системе, которые можно получить с ее помощью, netstat сообщает об активных сокетах, о сконфигурированных сетевых интерфейсах, о маршрутной таблице и о статистике протоколов. Иными словами, она выдает отчеты о самых разнообразных аспектах сетевой подсистемы, причем в различных форматах.
| | |


Здесь описано два способа применения утилиты трассировки системных вызовов. В первом примере ошибку удалось обнаружить путем анализа системных вызовов, выполненных приложением. Во втором примере надо было отслеживать не очередность системных вызовов, а время выполнения некоторых из них.
Ранее уже говорилось о том, что для выяснения причин аномального поведения программы часто бывает необходимо сопоставить результаты, полученные от различных утилит. Программы трассировки системных вызовов, такие как ktrace, truss и strace, - это еще одно средство анализа в арсенале сетевого программиста.
| | |


В этом разделе разработан инструмент для перехвата и печати ICMP-сообщений. Такая программа помогает при диагностике ошибок сети и маршрутизации.
В ходе разработки программы icmp использованы простые сокеты. Здесь вы познакомились с форматами IP- и UDP-датаграмм, а также со структурой ICMP-сообщений.
| | |


Один из лучших способов изучения сетевого программирования (да и любого другого) - это чтение программ, написанных людьми, уже достигшими верши] мастерства. До недавнего времени было нелегко получить доступ к исходным текстам операционных систем и их сетевых подсистем. Но в связи с распространением движения за открытость исходных текстов ситуация изменилась. Ко, нескольких реализаций стека TCP/IP и соответствующих утилит (telnet, FTI inetd и т.д.) доступен для проектов FreeBSD и Linux. Здесь приведены лишь некоторые источники, в Internet можно найти множество других.
Особенно полезны книги, в которых есть не только код, но и подробные комментарии к нему.
| | |

Сбой в сети


Сбои в сети происходят по разным причинам: от потери связи с маршрутизатором или отрезком опорной сети до выдергивания из разъема кабеля локальной Ethernet-сети. Сбои, происходящие вне оконечных точек, обычно временные, поскольку протоколы маршрутизации спроектированы так, чтобы обнаруживать по­врежденные участки и обходить их.

Примечание: Под оконечной точкой понимается локальная сеть или хост, на котором работает приложение.

Если же ошибка возникает в оконечной точке, то неисправность будет существовать, пока ее не устранят.

Если промежуточный маршрутизатор не посылает ICMP-сообщение о том, что хост или сеть назначения недоступны, то ни само приложение, ни стек TCP/IP на том же хосте не смогут немедленно узнать о сбое в сети (совет 10). В этом случае у отправителя через некоторое время возникнет тайм-аут, и он повторно отправит неподтвержденные сегменты. Это будет продолжаться, пока отправляющий TCP не признает доставку невозможной, после чего он обрывает соединение и сообщает об ошибке. В системе BSD это произойдет после 12 безуспешных попыток (примерно 9 мин). При наличии у TCP ожидающего запроса на чтение операция возвращает ошибку, и переменная errno устанавливается в ETIMEDOUT. Если ожидающего запроса на чтение нет, то следующая операция записи завершится ошибкой. При этом либо будет послан сигнал SIGPIPE, либо (если этот сигнал перехвачен или игнорируется) в переменную errno записано значение EPIPE.

Если промежуточный маршрутизатор не может переправить далее IР-датаграмму, содержащую некоторый сегмент, то он посылает хосту- отправителю ICMP-сообшение о том, что сеть или хост назначения недоступны. В этом случае некоторые реализации возвращают в качестве кода ошибки значение ENETUNREACH или EHOSTUNREACH



Система буферов в разделяемой памяти


Описанную систему буферов в разделяемой памяти легко реализовать. Основ­ная сложность в том, как получить область разделяемой памяти, отобразить ее на собственное адресное пространство и синхронизировать доступ к буферам. Конеч­но, это зависит от конкретной системы, поэтому далее будет приведена реализация как для UNIX, так и для Windows.

Но прежде чем перейти к системно-зависимым частям, обратимся к API и его реализации. На пользовательском уровне система состоит из пяти функций:

#include “etcp.h”

void  init_smb( int init_freelist);

void *smballoc( void );

Возвращаемое значение: указатель на буфер в разделяемой памяти.

void smbfrее( void *smbptr );

void smbsend( SOCKET s, void * smbptr );

void *smbrecv( SOCKET s );

Возвращаемое значение: указатель на буфер в разделяемой памяти.

Перед тем как пользоваться системой, каждый процесс должен вызвать функцию init_smb для получения и инициализации области разделяемой памяти и синхронизирующего мьютекса. При этом только один процесс должен вызвать init_smb с параметром init_f reelist, равным TRUE.

Для получения буфера в разделяемой памяти служит функция smballoc, воз­вращающая указатель на только что выделенный буфер. Когда буфер уже не ну­жен, процесс может вернуть его системе с помощью функции smb_frее.

Построив сообщение в буфере разделяемой памяти, процесс может передать буфер другому процессу, вызвав smbsend. Как уже говорилось, при этом передается только индекс буфера в массиве. Для получения буфера от отправителя процесс-получатель вызывает функцию smbrecv, которая возвращает указатель на буфер.

В данной системе для передачи индексов буферов используется TCP в качестве механизма IPC, но это не единственное и даже не оптимальное решение. Так Удобнее, поскольку этот механизм работает как под UNIX, так и под Windows, к тому же можно воспользоваться уже имеющимися средствами, а не изучать другие методы IPC. В системе UNIX можно было бы применить также сокеты в адрес­ом домене UNIX или именованные каналы. В Windows доступны SendMessage, QueueUserAPC и именованные каналы.


Начнем рассмотрение реализации с функций smballoc и smbfrее (листинг 3.28).

Листинг 3.28. Функции smballoc и smbfree

1    #include "etcp.h"

2    #define FREE_LIST  smbarray[ NSMB ].nexti

3    typedef  union

4    {

5    int nexti;

6    char buf[ SMBUFSZ ];

7    }smb_t;

8    smb_t *smbarray;

9    void *smballoc( void )

10   {

11   smb_t *bp;

12   lock_buf();

13   if ( FREE_LIST < 0 )

14   error( 1, 0, "больше нет буферов в разделяемой памяти\n" }

15   bр = smbarray + FREE_LIST;

16   FREE_LIST = bp->nexti;

17   unlock_buf ();

18   return bp;

19   }

20   void smbfree( void *b )

21   {

22   smb_t *bp;

23   bp = b;

24   lock_buf();

25   bp->nexti = FREE_LIST;

26   FREE_LIST  =  bp  -  smbarray;

27   unlock_buf();

28   }

Заголовок

2-8 Доступные буфера хранятся в списке свободных. При этом в первых sizeof( int ) байтах буфера хранится индекс следующего свободного буфера. Такая организация памяти отражена в объединении smb_t. В конце массива буферов есть одно целое число, которое содержит либо индекс первого буфера в списке свободных, либо -1, если этот список пуст. Доступ к этому числу вы получаете, адресуя его как smbarray [ NSMB ] . nexti. Для удобства это выражение инкапсулировано в мак­рос FREE_LIST. На сам массив буферов указывает переменная smbarray. Это, по сути, указатель на область разделяемой памяти, которую каж­дый процесс отображает на свое адресное пространство. В массиве ис­пользованы индексы, а не адреса элементов, так как в разных процес­сах эти адреса могут быть различны.

smballoc

12 Вызываем функцию lock_buf, чтобы другой процесс не мог обратиться к списку свободных. Реализация этой функции зависит от системы. В UNIX будут использованы семафоры, а в Windows - мыотексы.

13-16 Получаем буфер из списка свободных. Если больше буферов нет, то выводим диагностическое сообщение и завершаем сеанс. Вместо этого можно было бы вернуть NULL.

17-18 Открываем доступ к списку свободных и возвращаем указатель на буфер.



smbfree

23- 27 После блокировки списка свободных, возвращаем буфер, помещая его индекс в начало списка. Затем разблокируем список свободных и воз­вращаем управление.

Далее рассмотрим функции smbsend и smbrecv (листинг 3.29). Они посылают и принимают целочисленный индекс буфера, которым обмениваются процессы. Эти функции несложно адаптировать под иной механизм межпроцессного вза­имодействия.

Листинг 3.29. Функции smbsend и smbrecv

smb.c

1    void smbsend( SOCKET s, void *b )

2    {

3    int index;

4    index = ( smb_t * )b - smbarray;

5    if ( send( s, ( char * )&index, sizeoff (index ), 0 ) < 0 )

6      error( 1, errno, "smbsend: ошибка вызова send" );

7    }

8    void *smbrecv( SOCKET s )

9    {

10   int index;

11   int rc;

12   rc = readn( s, ( char * )&index, sizeoff index ) );

13   if ( rc == 0 )                 *,

14   error( 1, 0, "smbrecv: другой конец отсоединился\n" };

15   else if ( rc != sizeof( index ) )

16   error( 1, errno, "smbrecv: ошибка вызова readn" );

17   return smbarray + index;

18   }

smbsend

4-6 Вычисляем индекс буфера, на который указывает Ь, и посылаем его другому процессу с помощью send.

smbrecv

12-16 Вызываем readn для чтения переданного индекса буфера. В случае ошибки чтения или при получении неожиданного числа байт, выводим сообщение и завершаем работу.

17 В противном случае преобразуем индекс буфера в указатель на негеи возвращаем этот указатель вызывающей программе.


Скрытая ошибка


В качестве примера проблемы второго типа рассмотрим основанное на TCP приложение, занимающееся телеметрией. Здесь сервер каждую секунду принимает от удаленного датчика пакет с результатами измерений. Пакет может состоять из двух или трех целочисленных значений. В примитивной реализации подобного сервера мог бы присутствовать такой цикл:

int pkt[ 3 ] ;

for ( ; ; )

{

 rc = recv( s, ( char * ) pkt, sizeof( pkt ), 0 );

 if (rc != sizeof( int ) * 2 && rc != sizeof( int ) * 3 )

  /* Протоколировать ошибку и выйти. */

 else

  /* Обработать rc / sizeof( int ) значений. */

}

Из совета 6 вы знаете, что этот код некорректен, но попробуем провести простое моделирование. Напишем сервер (листинг 2.33), в котором реализован только что показанный цикл.

Листинг 2.33. Моделирование сервера телеметрии

telemetrys.c

1    #include "etcp.h"

2    #define TWOINTS ( sizeoff int ) * 2 )

3    #define THREEINTS ( sizeof( int ) * 3 )

4    int main( int argc, char **argv )

5    {

6    SOCKET s;

7    SOCKET s1;

8    int rc;

9    int i = 1;

10   int pkt [ 3 ] ;

11   INIT();

12   s = tcp_server( NULL, argv[ 1 ] );

13   s1 = accept( s, NULL, NULL );

14   if ( !isvalidsock( s1 ) )

15     error( 1, errno, "ошибка вызова accept" );

16   for ( ; ; )

17   {

18     rc =  recv( s1, ( char * }pkt, sizeoff pkt ), 0 );

19     if ( rc != TWOINTS && rc != THREEINTS )

20      error( 1, 0, "recv  вернула  %d\n", rc );

21     printf( "Пакет %d содержит %d значений в %d байтах\n" ,

22     i ++, ntohl pkt[ 0 ] ) , rc );

23   }

24   }

11-15 В этих строках реализована стандартная инициализация и прием соединения.

16-23 В данном цикле принимаются данные от клиента. Если получено при чтении не в точности sizeof ( int ) * 2 или sizeof ( int ) * 3 байт, то протоколируем ошибку и выходим. В противном случае байты первого числа преобразуются в машинный порядок (совет 28), а затем результат и число прочитанных байтов печатаются на stdout. В листинге 2.34 вы увидите, что клиент помещает число значений в первое число, посылаемое в пакете. Это поможет разобраться в том, что происходит. Здесь не используется это число как «заголовок сообщениям, содержащий его размер (совет 6).


Для тестирования этого сервера также необходим клиент, который каждую секунду посылает пакет целых чисел, имитируя работу удаленного датчика. Текст клиента приведен в листинге 2.34.

Листинг 2.34. Имитация клиента для сервера телеметрии

1    #include "etcp.h"

2    int main( int argc, char **argv )

3    {

4    SOCKET s;

5    int rc;

6    int i;

7    int pkt[ 3 ];

8    INIT();

9    s = tcp_client( argv[ 1 ], argv[ 2 ] );

Ю    for ( i = 2;; i = 5 - i )

И    {

12     pkt[ 0 ] = htonl( i ) ;

13     rc = send( s, ( char * )pkt, i * sizeof( int ), 0 );

14     if ( rc < 0 )

15      error( 1, errno, "ошибка вызова send" );

16     sleep( 1 );

17   }

18   }

8-9 Производим инициализацию и соединяемся с сервером.

10-17 Каждую секунду посылаем пакет из двух или трех целых чисел. Как говорилось выше, первое число в пакете - это количество последующих чисел (преобразованное в сетевой порядок байтов).

Для тестирования модели запустим сервер на машине bsd, а клиента – на машине spare. Сервер печатает следующее:

bsd: $ telemetrys 9000

Пакет 1 содержит 2 значения в 8 байтах

Пакет 2 содержит 3 значения в 12 байтах

Много строк опущено.

Пакет 22104 содержит 3 значения в 12 байтах

Пакет 22105 содержит 2 значения в 8 байтах

Клиент завершил сеанс через 6 ч 8 мин 15 с.

telemetrys: recv вернула 0

bsd: $

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

Примечание: Протокол сервера проверен с помощью сценария, написанного на awk - необходимо убедиться, что каждая операция чтения вернула правильное число байтов.

Однако при запуске того же сервера через Internet результаты получились совсем другие. Опять запустим клиента на машине spare, а сервер - на машине bsd, но на этот раз заставим клиента передавать данные через глобальную сеть, указав ему адрес сетевого интерфейса, подключенного к Internet. Как видно из последних строк, напечатанных сервером, фатальная ошибка произошла уже через 15 мин.



Пакет 893 содержит 2 значения в 8 байтах

Пакет 894 содержит 3 значения в 12 байтах

Пакет 895 содержит 2 значения в 12 байтах

Пакет 896 содержит -268436204 значения в 8 байтах

Пакет 897 содержит 2 значения в 12 байтах

Пакет 898 содержит -268436204 значения в 8 байтах

Пакет 899 содержит 2 значения в 12  байтах

Пакет 900 содержит -268436204 значения  в 12 байтах

telemetrys: recv вернула 4

bsd: $

Ошибка произошла при обработке пакета 895, когда нужно было прочесть8 байт, а прочли 12. На рис. 2.21 представлено, что произошло.

Числа слева показывают, сколько байтов было в приемном буфере TCP на стороне сервера. Числа справа - сколько байтов сервер реально прочитал. Вы видите, что пакеты 893 и 894 доставлены и обработаны, как и ожидалось. Но, когда telemetrys вызвал recv для чтения пакета 895, в буфере было 20 байт.

Примечание: Трассировка сетевого трафика, полученная с помощью программы tcpdump (совет 34), показывает, что в этот момент были потеряны TCP-сегменты, которыми обменивались два хоста. Вероятно, причиной послужила временная перегрузка сети, из-за которой промежуточный маршрутизатор отбросил пакет. Перед доставкой пакета 895 клиент telemetryc yжe подготовил пакет 896, и оба были доставлены вместе.

В пакете 895 было 8 байт, но, поскольку уже пришел пакет 896, сервер прочитал пакет 895 и первое число из пакета 896. Поэтому в распечатке видно, что было прочитано 12 байт, хотя пакет 895 содержит только два целых. При следующем чтении возвращено два целых из пакета 896, и telemetrys напечатал мусор вместо числа значений, так как telemetryc не инициализировал второе значение.



Рис. 2.21. Фатальная ошибка

Как видно из рис. 2.21, то же самое произошло с пакетами 897 и 898, так что при следующем чтении было доступно уже 28 байт. Теперь telemetrys читает пакет 899 и первое значение из пакета 900, остаток пакета 900 и первое значение из пакета 901 и наконец последнее значение из пакета 901. Последняя операция чтения возвращает только 4 байта, поэтому проверка в строке 19 завершается неудачно, а моделирование - с ошибкой.



К сожалению, на более раннем этапе моделирования произошло еще худшее:

Пакет 31 содержит 2 значения в 8 байтах

Пакет 32 содержит 3 значения в 12 байтах

Пакет 33 содержит 2 значения в 12 байтах

Пакет 34 содержит -268436204 значения в 8 байтах

Пакет 35 содержит 2 значения в 8 байтах

Пакет 36 содержит 3 значения в 12 байтах

Всего через 33 с после начала моделирования произошла ошибка, оставшаяся необнаруженной. Как показано на рис. 2.22, когда telemetrys читал пакет 33 в буфере было 20 байт, поэтому операция чтения вернула 12 байт вместо 8. Это означает, что пакет с двумя значениями ошибочно был принят за пакет с тремя значениями, а затем наоборот. Начиная с пакета 35, telemetrys восстановил синхронизацию, и ошибка прошла незамеченной.



Рис. 2.22. Незамеченная ошибка


Различайте протоколы, требующие и не требующие установления логического соединения


| | |

Один из фундаментальных вопросов сетевого программирования - это различие между протоколами, требующими установления логического соединения (connection-oriented protocols), и протоколами, не требующими этого (connectionless protocols). Хотя ничего сложного в таком делении нет, но начинающие их часто путают. Частично проблема кроется в выборе слов. Очевидно, что два компьютера должны быть как-то «соединены», если необходимо наладить обмен данными между ними. Тогда что означает «отсутствие логического соединения»?

О наличии и отсутствии логического соединения говорят применительно к протоколам. Иными словами, речь идет о способе передачи данных по физическому носителю, а не о самом физическом носителе. Протоколы, требующие и не требующие логического соединения, могут одновременно разделять общий физический носитель; на практике обычно так и бывает.

Но если это деление не имеет ничего общего с физическим носителем, по которому передаются данные, то что же лежит в его основе? Главное различие в том, что в протоколах, не требующих соединения, каждый пакет передается независимо от остальных. Тогда как протоколы, устанавливающие соединение, поддержи­вают информацию о состоянии, которая позволяет следить за последовательностью пакетов.

При работе с протоколом, не требующим соединения, каждый пакет, именуемый оатаграммой, адресуется и посылается приложением индивидуально (совет 30).С точки зрения протокола каждая датаграмма - это независимая единица, не имеющая ничего общего с другими датаграммами, которыми обмениваются приложения.

Примечание: Это не означает, что датаграммы независимы с точки зрения приложения. Если приложение реализует нечто более сложное, чем простой протокол запрос- ответ (клиент посылает серверу одиночный запрос и ожидает одиночного ответа на него), то, скорее всего, придется отслеживать состояние. Но суть в том, что приложение, а не протокол, отвечает за поддержание информации о состоянии. Пример сервера, который не требует установления соединения, но следит за последовательностью датаграмм, приведен в листинге 3.6.


Обычно это означает, что клиент и сервер не ведут сложного диалога, - клиент посылает запрос, а сервер отвечает на него. Если позже клиент посылает новый запрос, то с точки зрения протокола это новая транзакция, не связанна с предыдущей.
Кроме того, протокол не обязательно надежен, то есть сеть предпримет все возможное для доставки каждой датаграммы, но нет гарантий, что ни одна не будет потеряна, задержана или доставлена не в том порядке.
С другой стороны, протоколы, требующие установления соединения, самостоятельно отслеживают состояние пакетов, поэтому они используются в приложениях, ведущих развитый диалог. Сохраняемая информация о состоянии позволяет протоколу обеспечить надежную доставку. Например, отправитель запоминает, когда и какие данные послал, но они еще не подтверждены. Если подтверждение не приходит в течение определенного времени, отправитель повторяет передачу. Получатель запоминает, какие данные уже принял, и отбрасывает пакеты-дубликаты. Если пакет поступает не в порядке очередности, то получатель может «придержать» его, пока не придут логически предшествующие пакеты.
У типичного протокола, требующего наличия соединения, есть три фазы. Сначала устанавливается соединение между двумя приложениями. Затем происходит обмен данными. И, наконец, когда оба приложения завершили обмен данными, соединение разрывается.
Обычно такой протокол сравнивают с телефонным разговором, а протокол, не требующий соединения, - с отправкой письма. Каждое письмо запечатывается в отдельный конверт, на котором пишется адрес. При этом все письма оказываются самостоятельными сущностями. Каждое письмо обрабатывается на почте независимо от других посланий двух данных корреспондентов. Почта не отслеживает историю переписки, то есть состояние последовательности писем. Кроме того, не гарантируется, что письма не затеряются, не задержатся и будут доставлены в правильном порядке. Это соответствует отправке датаграммы протоколом, не требующим установления соединения.
Примечание: Хаверлок [Haverlock 2000] отмечает, что более правильная аналогия - не письмо, а почтовая открытка, так как письмо с неправильным адресом возвращается отправителю, а почтовая открытка - никогда (как и в типичном протоколе, не требующем наличия соединения).


А теперь посмотрим, что происходит, когда вы не посылаете письмо другу, а звоните по телефону. Для начала набираете его номер. Друг отвечает. Некоторое время вы разговариваете, потом прощаетесь и вешаете трубки. Так же обстоит дел и в протоколе, требующем соединения. В ходе процедуры установления соединения одна из сторон связывается с другой, стороны обмениваются «приветствиями» (на этом этапе они «договариваются» о тех параметрах и соглашениях, кот рым будут следовать далее), и соединение вступает в фазу обмена данными.
Во время телефонного разговора звонящий знает своего собеседника. И перед каждой фразой не нужно снова набирать номер телефона - соединение установлено. Алогично в фазе передачи данных протокола, требующего наличия соединения, надо передавать свой адрес или адрес другой стороны. Эти адреса - часть информации о состоянии, хранящейся вместе с логическим соединением. Остается только посылать данные, не заботясь ни об адресации, ни о других деталях, связанных с протоколом.
Как и в разговоре по телефону, каждая сторона, заканчивая передачу данных, формирует об этом собеседника. Когда обе стороны договорились о завершении, они выполняют строго определенную процедуру разрыва соединения.
Примечание: Хотя указанная аналогия полезна, но она все же не точна. В телефонной сети устанавливается физическое соединение. А приводимое «соединение» целиком умозрительно, оно состоит лишь из хранящейся на обоих концах информации о состоянии. Что - бы должным образом понять это, подумайте, что произойдет, если хост на одном конце соединения аварийно остановится и начнет перезагружаться. Соединение все еще есть? По отношению к перезагрузившемуся хосту — конечно, нет. Все соединения установлены в его «прошлой жизни». Но для его бывшего «собеседника» соединение по-прежнему существует, так коку него все еще хранится информация о состоянии, и не произошло ничего такого, что сделало бы ее недействительной.
В связи с многочисленными недостатками протоколов, не требующих соединения, возникает закономерный вопрос: зачем вообще нужен такой вид протоколов? Позже вы узнаете, что часто встречаются ситуации, когда для создания приложения использование именно такого протокола оправдано. Например, протокол без соединения может легко поддерживать связь одного хоста со многими и наоборот. Между тем протоколы, устанавливающие соединение, должны обычно организовать по одному соединению между каждой парой хостов. Важно, что протоколы, не требующие наличия соединения, - это фундамент, на котором строятся более сложные протоколы. Рассмотрим набор протоколов TCP/IP. В совете 14 говорится, что TCP/IP - это четырехуровневый стек протоколов (рис. 2.1).



Рис.2. 1 Упрощенное представление стека протоколов TCP/IP
Внизу стека находится интерфейсный уровень, который связан непосредственно с аппаратурой. Наверху располагаются такие приложения, как telnet, ftp и другие стандартные и пользовательские программы. Как видно из рис. 2.1, TCP и UDP построены поверх IP. Следовательно, IP - это фундамент, на котором возведено все здание TCP/IP. Но IP представляет лишь ненадежный сервис, не требующий установления соединения. Этот протокол принимает пакеты с выше расположенных приложенных уровней, обертывает их в IP-пакет и направляет подходящему аппаратному интерфейсу для отправки в сеть. Послав пакет, IP, как и все протоколы, не устанавливающие соединения, не сохраняет информацию о нем.
В этой простоте и заключается главное достоинство протокола IP. Поскольку IP не делает никаких предположений о физической среде передачи данных, он может работать с любым носителем, способным передавать пакеты. Так, IP работает на простых последовательных линиях связи, в локальных сетях на базе технологий Ethernet и Token Ring, в глобальных сетях на основе протоколов Х.25 и ATM (Asynchronous Transfer Mode - асинхронный режим передачи), в беспроводных сетях CDPD (Cellular Digital Packet Data - сотовая система передачи пакетов цифровых данных) и во многих других средах. Хотя эти технологии принципиально различны, с точки зрения IP они не отличаются друг от друга, поскольку способны передавать пакеты. Отсюда следует важнейший вывод: раз IP может работать в любой сети с коммутацией пакетов, то это относится и ко всему набору протоколов TCP/IP.
А теперь посмотрим, как протокол TCP пользуется этим простым сервисом, чтобы организовать надежный сервис с поддержкой логических соединений. Поскольку TCP-пакеты (они называются сегментами) посылаются в составе 1Р-датаграмм, у TCP нет информации, дойдут ли они до адреса, не говоря о возможности искажения данных или о доставке в правильном порядке. Чтобы обеспечить надежность, TCP добавляет к базовому IP-сервису три параметра. Во-первых, в ТСР-сегмент включена контрольная сумма содержащихся в нем данных. Это позволяет в пункте назначения убедиться, что переданные данные не повреждены сетью во время транспортировки. Во-вторых, TCP присваивает каждому байту порядковый номер, так что даже если данные прибывают в пункт назначения не в том порядке, в котором были отправлены, то получатель сможет собрать из них исходное сооб­щение.


Примечание: Разумеется, TCP не передает порядковый номер вместе с каждым байтом. Просто в заголовке каждого TCP-сегмента хранится порядковый номер первого байта. Тогда порядковые номера остальных байтов можно вычислить.
В-третьих, в TCP имеется механизм подтверждения и повторной передачи. который гарантирует, что каждый сегмент когда-то будет доставлен.
Из трех упомянутых выше добавлений механизм подтверждения/повторной передачи самый сложный, поэтому рассмотрим подробнее его работу.
Примечание: Здесь опускаются некоторые детали. Это обсуждение поверхностно затрагивает многие тонкости протокола TCP и их применение для обеспечения надежного и отказоустойчивого транспортного механизма. Более доступное и подробное изложение вы можете найти в RFC 793 [Pastel 1981b] и RFC 1122 [Braden1989], в книге [Stevens 1994]. В RFC 813 [Clark 1982] обсуждается механизм окон и подтверждений TCP.
На каждом конце TCP-соединения поддерживается окно приема, представляющее собой диапазон порядковых номеров байтов, который получатель готов принят отправителя. Наименьшее значение, соответствующее левому краю окна, - это порядковый номер следующего ожидаемого байта. Наибольшее значение, соответствующее правому краю окна, - это порядковый номер последнего байта, для косого у TCP есть место в буфере. Использование окна приема (вместо посылки только номера следующего ожидаемого байта) повышает надежность протокола счет предоставления средств управления потоком. Механизм управления потоком предотвращает переполнение буфера TCP.
Когда прибывает TCP-сегмент, все байты, порядковые номера которых оказываются вне окна приема, отбрасываются. Это касается как ранее принятых данных (с порядковым номерами левее окна приема), так и данных, для которых нет места в буфере (с порядковым номерами правее окна приема). Если первый допустимый байт в сегменте не является следующим ожидаемым, значит, сегмент прибыл не по порядку. В большинстве реализаций TCP такой сегмент помещается в очередь и находится в ней, пока не придут пропущенные данные. Если же номер первого допустимого байта совпадает со следующим ожидаемым, то данные становятся доступными для приложения, а порядковый номер следующего ожидаемого байта увеличивается на число байтов в сегменте. В этом случае считается, что окно сдви­гается вправо на число принятых байтов. Наконец, TCP посылает отправителю подтверждение (сегмент АСК), содержащее порядковый номер следующего ожидаемого байта.


Например, на рис. 2. 2а окно приема обведено пунктиром. Вы видите, что порядковый номер следующего ожидаемого байта равен 4, и TCP готов принять 9 байт (с 4 по 12). На рис. 2.26 показано окно приема после поступления байтов с номерами 4-7. Окно сдвинулось вправо на четыре номера, а в сегменте АСК, который пошлет TCP, номер следующего ожидаемого байта будет равен 8.

Рис. 2.2. Окно приема TCP
Теперь рассмотрим эту же ситуацию с точки зрения протокола TCP на посылающем конце. Помимо окна приема, TCP поддерживает окно передачи, разделенное на две части. В одной из них расположены байты, которые уже отосланы, но еще не подтверждены, а в другой – байты, которые еще не отправлены. Предполагается, что на байты 1-3 уже пришло подтверждение, поэтому на рис. 2.3а изображено окно передачи, соответствующее окну приема на рис. 2.2а. на рис. 2.3б вы видите окно передачи после пересылки байтов 4-7, но до прихода подтверждения. TCP еще может послать байты 8-12, не дожидаясь подтверждения от получателя. После отправки байтов 4-7 TCP начинает отсчет тайм – аута ретрансмиссии (retransmission timeout - RTO). Если до срабатывания таймера не пришло подтверждение на все четыре байта, TCP считает, что они потерялись, и посылает их повторно.
Примечание: Поскольку в многих реализациях не происходит отслеживания того, какие байты были посланы в конкретном сегменте, может случиться, что повторно переданный сегмент содержит больше байтов, чем первоначальный. Например, если байты 8 и 9 были посланы до срабатывания RTO-таймера, то такие реализации повторно передадут байты с 4 по 9.
Обратите внимание, что срабатывание RTO-таймера не означает, что исходные данные не дошли до получателя. Например, может потеряться АСК - сегмент с подтверждением или исходный сегмент задержаться в сети на время, большее чем тайм-аут ретрансмиссии. Но ничего страшного в этом нет, так как если первоначально отправленные данные все-таки прибудут, то повторно переданные окажутся вне окна приема TCP и будут отброшены.


После получения подтверждения на байты 4-7 передающий TCP «забывает» про них и сдвигает окно передачи вправо, как показано на рис. 2.3в

Рис. 2.3. Окно передачи TCP
TCP обеспечивает прикладного программиста надежным протоколом, требующим установления логических соединений. О таком протоколе рассказываете в совете 9.
С другой стороны, UDP предоставляет программисту ненадежный сервис, не требующий соединения. Фактически UDP добавляет лишь два параметра к протоколу IP, поверх которого он построен. Во-первых, необязательную контрольную сумму для обнаружения искаженных данных. Хотя у самого протокола IP тоже есть контрольная сумма, но вычисляется она только для заголовка IP-пакета, поэтому TCP и UDP также включают контрольные суммы для защиты собственных заголовков и данных. Во-вторых, UDP добавляет к IP понятие порта. Для отправки IP-датаграммы конкретному хосту используются IP-адреса, то есть адреса, которые обычно приводятся в стандартной десятичной нотации Internet (совет 2). Но по прибытии на хост назначения датаграмму еще необходимо доставить нужному приложению. Например, один UDP-пакет может быть предназначен для сервиса эхо - контроля, а другой - для сервиса «время дня». Порты как раз и дают способ направления данных нужному приложению (этот процесс называют демультиплексированием). С каждым TCP и UDP-сокетом ассоциирован номер порта. Приложение может явно указать этот номер путем обращения к системному вызову bind или поручить операционной системе выбор порта. Когда пакет прибывает, ядро «ищет» в списке сокетов тот, который ассоциирован с протоколом, парой адресов и парой номеров портов, указанных в пакете. Если сокет найден, то данные обрабатываются соответствующим протоколом (в примерах TCP или UDP) и передаются тем приложениям, которые этот сокет открыли.
Примечание: Если сокет открыт несколькими процессами или потоками (thread), то данные может считывать только один из них, и остальным они будут недоступны.
Возвращаясь к аналогии с телефонными переговорами и письмами, можно сказать, что сетевой адрес в TCP-соединении подобен номеру телефона офисной АТС, а номер порта - это добавочный номер конкретного телефона в офисе. Точно так же UDP-адрес можно представить как адрес многоквартирного дома, а номер порта - как отдельный почтовый ящик в его подъезде.

Выясните, что такое подсети и CIDR


| | |

Длина IP-адреса (в версии IPv4) составляет 32 бита. Адреса принято записывать в десятичной нотации - каждый из четырех байт представляется одним десятичным числом, которые отделяются друг от друга точками. Так, адрес 0x11345678 записывается в виде 17.52.86.120. При записи адресов нужно учитывать, что в некоторых реализациях TCP/IP принято стандартное для языка C соглашение о том, что числа, начинающиеся с нуля, записываются в восьмеричной системе. В таком        случае 17.52.86.120 - это не то же самое, что 017.52.86.120. В первом примере адреcе сети равен 17, а во втором - 15.



Разберитесь, что такое частные адреса и NAT


| | |

Раньше, когда доступ в Internet еще не был повсеместно распространен, организации выбирали произвольный блок IP-адресов для своих сетей. Считалось, что сеть не подключена и «никогда не будет подключена» к внешним сетям, hostomv выбор IP-адресов не имеет значения. Но жизнь не стоит на месте, и в настоящее время очень мало сетей, которые не имеют выхода в Internet.

Теперь необязательно выбирать для частной сети произвольный блок IP-адресов. В RFC 1918 [Rekhter, Moskowitz et al. 1996] специфицированы три блока адресов, которые не будут выделяться:

10.0.0.0-10.255.255.255 (префикс 10/8);

172.16.0.0-172.31.255.255 (префикс 172.16/12);

192.168.0.0-192.168.255.255 (префикс 192.168/16).

Если использовать для своей сети один из этих блоков, то любой хост сможет обратиться к другому хосту в этой же сети, не опасаясь конфликта с глобально выделенным IP-адресом. Разумеется, пока сеть не имеет выхода во внешние сети, выбор адресов не имеет значения. Но почему бы сразу не воспользоваться одним из блоков частных адресов и не застраховаться тем самым от неприятностей, которые могут произойти, когда внешний выход все-таки появится?

Что случится, когда сеть получит внешний выход? Как хост с частным IP-адресом сможет общаться с другим хостом в Internet или другой внешней сети? Самый распространенный ответ - нужно воспользоваться преобразованием сетевых адресов (Network Address Translation - NAT). Есть несколько типов устройств, поддерживающих NAT. Среди них маршрутизаторы, межсетевые экраны (firewalls) и автономные устройства с поддержкой NAT. Принцип работы NAT заключается в преобразовании между частными сетевыми адресами и одним или несколькими глобально выделенными IP-адресами. Большинство устройств с поддержкой NAT можно сконфигурировать в трех режимах:

статический. Адреса всех или некоторых хостов в частной сети отображаются на один и тот же фиксированный, глобально выделенный адрес;

выбор из пула. Устройство с поддержкой NAT имеет пул глобально выделенных IP-адресов и динамически назначает один из них хосту, которому нужно связаться с хостом во внешней сети;


РАТ, или преобразование адресов портов (port address translation). Этот метод применяется, когда есть единственный глобально выделенный адрес (рис. 2.11). При этом каждый частный адрес отображается на один и тот ж внешний адрес, но номер порта исходящего пакета заменяется уникальным значением, которое в дальнейшем используется для ассоциирования входящих пакетов с частным сетевым адресом.
На рис. 2.11 представлена небольшая сеть с тремя хостами, для которой и пользуется блок адресов 10/8. Имеется также маршрутизатор, помеченный NAT у которого есть адрес в частной сети и адрес в Internet.

Рис. 2.11. Частная сеть с маршрутизатором, который поддерживает NAT
Поскольку показан только один глобальный адрес, ассоциированный с NAT, предположим, что маршрутизатор сконфигурирован с возможностью использования метода РАТ. Статический режим и режим выбора из пула аналогичны методу РАТ, но проще его, поскольку не нужно преобразовывать еще и номера портов.
Допустим, что хосту Н2 надо отправить SYN-сегмент TCP по адресу 204.71.200.69 -на один из Web-серверов www.yahoo.com. - чтобы открыть соединение. На рис. 2.12а видно, что у сегмента, покидающего Н2, адрес получателя равен 204.71.200.69.80, а адрес отправителя - 10.0.0.2.9600.
Примечание: Здесь использована стандартная нотация, согласно которой адрес, записанный в форме A.B.C.D.P означает IP-адресA.B.C.D и порт Р.
В этом нет ничего особенного, за исключением того, что адрес отправителя принадлежит частной сети. Когда этот сегмент доходит до маршрутизатора, NAT Должен заменить адрес отправителя на 205.184.151.171, чтобы Web-сервер на сайте Yahoo знал, куда посылать сегмент SYN/ACK и последующие. Поскольку во всех пакетах, исходящих от других хостов в частной сети, адрес отправителя также будет заменен на 205.184.151.171, NAT необходимо изменить еще и номер пора некоторое уникальное значение, чтобы потом определять, какому хосту следует переправлять входящие пакеты. Исходящий порт 9600 преобразуется в 5555. Таким образом, у сегмента, доставленного на сайт Yahoo, адрес получателя будет 204.71.200.69.80, а адрес отправителя- 205.184.151.171.5555.



Рис. 2.12. Преобразование адресов портов
Из рис. 2.12б видно также, что в дошедшем до маршрутизатора ответе Yahoo адрес получателя равен 205.184.151.171.5555. NAT ищет этот номер порта в своей внутренней таблице и обнаруживает, что порт 5555 соответствует адресу 10.0.0.1.9600, так что после получения от маршрутизатора этого пакета в хосте Н2 появится информация, что адрес отправителя равен 204.71.200.69.80, а адрес получателя - 10.0.0.1.9600.
Описанный здесь метод PAT выглядит довольно примитивно, но есть много усложняющих его деталей. Например, при изменении адреса отправителя или но мера исходящего порта меняются как контрольная сумма заголовка IР - датаграммы так и контрольная сумма TCP-сегмента, поэтому их необходимо скорректировать.
В качестве другого примера возможных осложнений рассмотрим протокол передачи файлов FTP (File Transfer Protocol) [Reynolds and Postel 1985]. Когда FTP-клиенту нужно отправить файл или принять его от FTP-сервера, серверу посылается команда PORT с указанием адреса и номера порта, по которому будет ожидаться соединение (для передачи данных) от сервера. При этом NAT нужно распознать TCP-сегмент, содержащий команду PORT протокола FTP, и подменить в ней адрес и порт. В команде PORT адрес и номер порта представлены в виде ASCII-строк, поэтому при их подмене может измениться размер сегмента. А это, в свою очередь, повлечет изменение порядковых номеров байтов. Так что NAT должен за этим следить, чтобы вовремя скорректировать порядковые номера в сегменте подтверждения АСК, а также в последующих сегментах с того же хоста.
Несмотря на все эти сложности, NAT работает неплохо и широко распространен. В частности, PAT - это естественный способ подключения небольших сетей к Internet в ситуации, когда имеется только одна точка выхода.

Разрабатывайте и применяйте каркасы приложений


| | |

Большинство приложений TCP/IP попадают в одну из четырех категорий:

TCP-сервер;

TCP-клиент;

UDP-сервер;

UDP-клиент.

В приложениях одной категории обычно встречается почти одинаковый «стартовый» код, который инициализирует все, что связано с сетью. Например,TCP- сервер должен поместить в поля структуры sockaddr_in адрес и порт получателя, получить от системы сокет типа SOCK_STREAM, привязать к нему выбранный адрес и номер порта, установить опцию сокета SO_REUSEADDR (совет 23), вызвать listen, а затем быть готовым к приему соединения (или нескольких соединений) с помощью системного вызова accept.

На каждом из этих этапов следует проверять код возврата. А часть программы, занимающаяся преобразованием адресов, должна иметь дело как с числом так и с символическими адресами и номерами портов. Таким образом, в любом TCP-сервере есть порядка 100 почти одинаковых строк кода для выявления всех перечисленных выше задач. Один из способов решения этой проблемы - поместить стартовый код в одну или несколько библиотечных функций которые приложение может вызвать. Эта стратегия использована в книге. Но иногда приложению нужна слегка видоизмененная последовательность инициализации. В таком случае придется либо написать ее с нуля, либо извлечь нужный фрагмент кода из библиотеки и подправить его.

Чтобы справиться и с такими ситуациями, можно построить каркас приложения, в котором уже есть весь необходимый код. Затем скопировать этот каркас, внести необходимые изменения, после чего заняться логикой самого приложения. Не имея каркаса, легко поддаться искушению и срезать некоторые углы, например, жестко «зашить» в приложение адреса (совет 29) или сделать еще что-то сомнительное. Разработав каркас, вы сможете убрать все типичные функции в библиотеку, а каркас оставить только для необычных задач.

Чтобы сделать программы переносимыми, следует определить несколько макросов, в которых скрыть различия между API систем UNIX и Windows. Например, в UNIX системный вызов для закрытия сокета называется close, а в Windows - closesocket. Версии этих макросов для UNIX показаны в листинге 2.1. Версии для Windows аналогичны, приведены в приложении 2. Доступ к этим макросам из каркасов осуществляется путем включения файла skel.h.

Листинг 2.1. Заголовочный файл skel.h

1    #ifndef __SKEL_H__

2    #define __SKEL_H__

3    /*версия для UNIX */

4    #define INIT() ( program_name = \

5                     strrchr ( argv[ 0 ], '/' ) ) ? \

6                     program_name++ : \

7                    ( program_name = argv[ 0 ] )

8    #define EXIT(s) exit( s )

9    #define CLOSE(s) if ( close( s ) ) error( 1, errno, \

10                        "ошибка close " )

11   #define set_errno(e) errno = ( e )

12   #define isvalidsock(s) ( ( s ) >= 0 )

13   typedef int SOCKET;

14   #endif /* __SKEL_H__ */



Предпочитайте интерфейс сокетов интерфейсу XTI/TLI


| | |

В мире UNIX в качестве интерфейса к коммуникационным протоколам, в частности к TCP/IP, в основном используются следующие два API:

шкеты Беркли;

транспортный интерфейс XTI(X/Open Transport Interface).

Интерфейс сокетов разработан в Университете г. Беркли штата Калифорния и вошел в состав созданной там же версии операционной системы UNIX. Он получил широкое распространение вместе с версией 4.2BSD (1983), затем был усовершенствован в версии 4.3BSD Reno (1990) и теперь включается практически во все версии UNIX. API сокетов присутствует и в других операционных системах. Так, Winsock API популярной в мире Microsoft Windows основан на сокетах из BSD [Winsock Group 1997].

API интерфейса XTI - это расширение интерфейса к транспортному уровню (Transport Layer Interface - TLI), который впервые появился в системе UNIX System V Release 3.0 (SVR3) компании AT&T. TLI задумывался как интерфейс, не зависящий, от протокола, так как он сравнительно легко поддерживает новые протоколы, На его дизайн оказала значительное влияние модель протоколов OSI (совет 14). В то время многие полагали, что эти протоколы вскоре придут на смену TCP/IP И поэтому, с точки зрения программиста TCP/IP, дизайн этого интерфейса далек от оптимального. Кроме того, хотя имена функций TLI очень похожи на используемые в API сокетов (только они начинаются с t_), их семантика в ряде случаев кардинально отличается.

Тот факт, что интерфейс TLI все еще популярен, возможно, объясняется en использованием с протоколами Internetwork Packet Exchange/Sequenced Packe Exchange (IPX/SPX) в системах фирмы Novell. Поэтому при переносе программ написанных для IPX/SPX, под TCP/IP проще было воспользоваться тем же интерфейсом TLI [Kacker 1999].

В четвертой части первого тома книги «UNIX Network Programming» [Stevens 1998] имеется прекрасное введение в программирование XTI и подсистем STREAMS. Представить, насколько отличается семантика XTI и сокетов, можно хотя бы по тому, что обсуждению XTI посвящено более 100 страниц.


Надеясь, что протоколы OSI все-таки заменят TCP/IP, многие производители UNIX- систем рекомендовали писать новые приложения с использованием ТМ API. Одна фирма-производитель даже заявила, что интерфейс сокетов не будет поддерживаться в следующих версиях. Но такие прогнозы оказались несколько преждевременными.
Протоколы OSI можно считать устаревшими, но TLI и последовавший за ним XTI все еще поставляются в составе UNIX-систем, производных от System V. Поэтому при программировании для UNIX встает вопрос: что лучше использовать- сокеты или XTI?
Здесь необходимо напомнить, почему указанные протоколы называются интерфейсами. Для программиста TCP/IP это всего лишь разные способы доступа к стеку TCP/IP. Поскольку именно этот стек реализует коммуникационные протоколы, не имеет значения, какой API использует его клиент. Это означает, что приложение, написанное с помощью сокетов, может обмениваться данными с приложением на базе XTI. В системах типа SVR4 оба интерфейса обычно реализуются в виде библиотек, осуществляющих доступ к стеку TCP/IP с помощью подсистемы STREAMS.
Рассмотрим сначала интерфейс XTI. У него есть своя ниша в сетевом программировании. Поскольку он не зависит от протокола, с его помощью можно добавить в систему UNIX новый протокол, не имея доступа к коду ядра. Проектировщику протокола необходимо лишь реализовать транспортный провайдер в виде STREAMS-мультиплексора, связать его с ядром, а потом обращаться к нему через XTI.
Примечание: О том, как писать модули STREAMS, а также о программировании TLI и STREAMS вы можете прочесть в книге [Rago 199^
Обратите внимание, насколько специфична ситуация: нужно реализовать отсутствующий в системе протокол, когда нет доступа к исходным текстам ядра.
Примечание: Кроме того, этот протокол нужно разработать для сиcтемы SVR4 или любой другой, поддерживающей STREAMS и XTI/1 Начиная с версии Solaris 2.6, фирма Sun предоставляет такую же функциональность с помощью API сокетов.
Иногда утверждают, что проще писать не зависящий от протокола код с мощью XTI/TLI [Rago 1993]. Конечно, «простота» - понятие субъективное, но в разделе 11.9 книги «UNIX Network Programming» Стивенс с помощью сокетов реализовал простой, не зависящий от протокола сервер времени дня, который поддерживает IP версии 4, IP версии 6 и сокеты в адресном домене UNIX.


И, наконец, говорят, что при поддержке обоих интерфейсов сокеты обычно реализуются поверх TLI/XTI, так что TLI/XTI более эффективен. Это не так. Как отмечалось выше, в системах на базе SVR4 оба интерфейса обычно реализованы в виде библиотек, напрямую общающихся с подсистемой STREAMS. Фактически с версии Solaris 2.6 (Solaris - это версии SVR4, созданные фирмой Sun) сокеты ре­ализованы непосредственно в ядре; обращение к ним происходит через вызовы системы.
Большое преимущество сокетов - переносимость. Поскольку сокеты есть практически во всех системах с XTI/TLI, их использование гарантирует максимальную переносимость. Даже если ваше приложение будет работать только под UNIX, так как большинство операционных систем, поддерживающих TCP/IP, предоставляет интерфейс сокетов. И лишь немногие системы, не принадлежащие к UNIX, содержат интерфейс XTI/TLI (если вообще такие существуют). Например, создание приложения, переносимого между UNIX и Microsoft Windows, - сравнительно несложная задача, так как Windows поддерживает спецификацию Winsock, в которой реализован API сокетов.
Еще одно преимущество сокетов в том, что этот интерфейс проще использовать, чем XTI/TLI. Поскольку XTI/TLI проектировался в основном как общий интерфейс (имеются в виду протоколы OSI), программисту приходится при его использовании писать больше кода, чем при работе с сокетами. Даже сторонники XTI/TLI согласны с тем, что для создания приложений TCP/IP следует предпочесть интерфейс сокетов.
Руководство «Введение в библиотеку подпрограмм», поставляемое в составе Solaris 2.6, дает такой совет по выбору API: «При всех обстоятельствах рекомендуется использовать API сокетов, а не XTI и TLI. Если требуется переносимость на Другие системы, удовлетворяющие спецификации XPGV4v2, то следует использовать интерфейсы из библиотеки libxnet. Если же переносимость необязательна, то рекомендуется интерфейс сокетов из библиотек libsocket и libnsl, а не из libxnet. Если выбирать между XTI и TLI, то лучше пользоваться интерфейсом XTI (доступным через libxnet), а не TLI (доступным через libnsl)».

Помните, что TCP - потоковый протокол


| | |

TСР - потоковый протокол. Это означает, что данные доставляются получателю в виде потока байтов, в котором нет понятий «сообщения» или «границы сообщения». В этом отношении чтение данных по протоколу TCP похоже на чтение из последовательного порта - заранее не известно, сколько байтов будет возвращено после обращения к функции чтения.

Представим, например, что имеется TCP-соединение между приложения на хостах А и В. Приложение на хосте А посылает сообщения хосту В. Допустим, что у хоста А есть два сообщения, для отправки которых он дважды вызывает send - по разу для каждого сообщения. Естественно, эти сообщения передаются от хоста А к хосту В в виде раздельных блоков, каждое в своем пакете, как показано на рис. 2.13.

К сожалению, реальная передача данных вероятнее всего будет происходить, не так. Приложение на хосте А вызывает send, и вроде бы данные сразу же передаются на хост В. На самом деле send обычно просто копирует данные в буфер стека TCP/IP на хосте А и тут же возвращает управление. TCP самостоятельно определяет, сколько данных нужно передать немедленно. В частности, он может вообще отложить передачу до более благоприятного момента. Принятие такого решения зависит от многих факторов, например: окна передачи (объем данных, которые хост В готов принять), окна перегрузки (оценка загруженности сети), максимального размера передаваемого блока вдоль пути (максимально допустимый объем данных для передачи в одном блоке на пути от А к В) и количества данных в выходной очереди соединения. Подробнее это рассматривается в совете 15. На рис. 2.14 показано только четыре возможных способа разбиения двух сообщений по пакетам. Здесь М11 и М12 - первая и вторая части сообщения М1, а М21 и М22 - соответственно части М2. Как видно из рисунка, TCP не всегда посылает все сообщение в одном пакете.

Рис. 2.13. Неправильная модель отправки двух сообщений

Рис.2.14. Четыре возможных способа разбиения двух сообщений по пакетам

А теперь посмотрим на эту ситуацию с точки зрения приложения на хосте В. В общем случае оно не имеет информации относительно количества возвращаемых TCP данных при обращении к системному вызову recv. Например, когда приложение на хосте В в первый раз читает данные, возможны следующие варианты:


Так как количество возвращаемых в результате чтения данных непредсказуемо, вы должны быть готовы к обработке этой ситуации. Часто проблемы вообще не возникает. Допустим, вы пользуетесь для чтения данных стандартной библиотечной функцией fgets. При этом она сама будет разбивать поток байтов на строки (листинг 3.3). Иногда границы сообщений бывают важны, тогда приходится реализовывать их сохранение на прикладном уровне.
Самый простой случай - это сообщения фиксированной длины. Тогда вам нужно прочесть заранее известное число байтов из потока. В соответствии с вышесказанным, для этого недостаточно выполнить простое однократное чтение:
recv( s, msg, sizeof( msg ), 0 );
поскольку при этом можно получить меньше, чем sizеоf ( msg ) байт (рис. 2.14г). Стандартный способ решения этой проблемы показан в листинге 2.12
Листинг 2.12. Функция readn
readn.с
1    int readn( SOCKET fd, char *bp, size_t len)
2    {
3    int cnt;
4    int rc;
5    cnt = len;
6    while ( cnt > 0 )
7    {
8      rc = recv( fd, bp, cnt, 0 );
9      if ( rc < 0 ) /* Ошибка чтения? */
10     {
11      if ( errno == EINTR )  /* Вызов прерван? */
12       continue; /* Повторить чтение. */
13      return -1; /* Вернуть код ошибки. */
14     }
15     if ( rc == 0 ) /* Конец файла? */
16      return len - cnt; /* Вернуть неполный счетчик. */
17     bр += гс;
18     cnt -= rc;
19   }
20   return len;
21   }
Функция readn используется точно так же, как read, только она не возвращает управления, пока не будет прочитано len байт или не получен конец файла или не возникнет ошибка. Ее прототип выглядит следующим образом:
#include «etcp.h»
int readn ( SOCKET s, char *buf, size t len );
Возвращаемое значение: число прочитанных байтов или -1 в случае ошибки.
Неудивительно, что readn использует ту же технику для чтения заданного числа байтов из последовального порта или иного потокового устройства, когда количество данных, доступных в данный момент времени, неизвестно. Обычно readn (с заменой типа SOCKET на int и recv на read) применяется во всех этих ситуациях.


Оператор if
if ( errno == EINTR )
 continue;
в строках 11 и 12 возобновляет выполнение вызова recv, если он прерван сигналом. Некоторые системы возобновляют прерванные системные вызовы автоматически, в таком случае эти две строки не нужны. С другой стороны, они не мешают, так что для обеспечения максимальной переносимости лучше их оставить.
Если приложение должно работать с сообщениями переменной длины то в вашем распоряжении есть два метода. Во-первых, можно разделять записи специальными маркерами. Именно так надо поступить, используя стандартную функцию fgets для разбиения потока на строки. В этом случае естественным разделителем служит символ новой строки. Если маркер конца записи встретится в теле сообщения, то приложение-отправитель должно предварительно найти в сообщении все такие маркеры и экранировать их либо закодировать как-то еще чтобы принимающее приложение не приняло их по ошибке за конец записи. Например если в качестве признака конца записи используется символ-разделитель RS то отправитель сначала должен найти все вхождения этого символа в тело сообщения и экранировать их, например, добавив перед каждым символ \ Это означает, что данные необходимо сдвинуть вправо, чтобы освободить место для символа экранирования. Его, разумеется, тоже необходимо экранировать. Так, если для экранирования используется символ \, то любое его вхождение в тело сообщения следует заменить на \\.

Рис.2.15. Формат записи переменной длины
Принимающей стороне нужно просмотреть все сообщение, удалить символы экранирования и найти разделители записей. Поскольку при использовании маркеров конца записи все сообщение приходится просматривать дважды, этот метод лучше применять только при наличии «естественного» разделителя, например символа новой строки, разделяющего строки текста.
Другой метод работы с сообщениями переменной длины предусматривает снабжение каждого сообщения заголовком, содержащим (как минимум) длину следующего за ним тела. Этот метод показан на рис. 2.15.
Принимающее приложение читает сообщение в два приема: сначала заголовок фиксированной длины, и из него извлекается переменная длина тела сообщения, a затем- само тело. В листинге 2.13 приведен пример для простого случая, когда в заголовке хранится только длина записи.


Листинг 2.13. Функция для чтения записи переменной длины
1    int readvrec( SOCKET fd, char *bp, size_t len )
2    {
3    u_int32_t reclen;
4    int rc;
5    /* Прочитать длину записи. */
6    rc = readn( fd, ( char * )&reclen, sizeof( u_int32_t ) );
7    if ( rc != sizeof( u_int32_t ) )
8      return rc < 0 ? -1 : 0;
9    reclen = ntohl( reclen );
10   if ( reclen > len )
11   {
12     /*
13     * He хватает места в буфере для•размещения данных
14     * отбросить их и вернуть код ошибки.
15     */
16     while ( reclen > 0 )
17     {
18      rc = readn( fd, bp, len );
19      if ( rc != len )
20       return rc < 0 ? -1 : 0;
21      reclen -= len;
22      if ( reclen < len }
23       len = reclen;
24     }
25     set_errno( EMSGSIZE };
26     return -1;
27   }
28   /* Прочитать саму запись */
29   rc = readn( fd, bp, reclen );
30   if ( rc != reclen )
31     return rc < 0 ? -1 : 0;
32   return rc;
33   }
Чтение длины записи
6-8 Длина записи считывается в переменную reclen. Функция readvrec возвращает 0 (конец файла), если число байтов, прочитанных readn, не точно совпадает с размером целого, или -1 в случае ошибки. 1
9 Размер записи преобразуется из сетевого порядка в машинный. Подробнее об этом рассказывается в совете 28.
Проверка того, поместится ли запись в буфер
10-27 Проверяется, достаточна ли длина буфера, предоставленного вызывающей программой, для размещения в нем всей записи. Если места не хватит, то данные считываются в буфер частями по 1en байт, то есть, по сути, отбрасываются. Изъяв из потока отбрасываемые данные, функции присваивает переменной errno значение EMSGSIZE и возвращает -1.
Считывание записи
29-32 Наконец считывается сама запись, readvrec возвращает-1, 0 или reclen в зависимости от того, вернула ли readn код ошибки, неполный счетчик или нормальное значение.
Поскольку readvrec - функция полезная и ей найдется применение, необходимо записать ее прототип:
#include "etcp.h"


int readvrec( SOCKET s, char *buf, size_t len );
Возвращаемое значение: число прочитанных байтов или -1.
В листинге 2.14 дан пример простого сервера, который читает из ТСР-соединения записи переменной длины с помощью readvrec и записывает их на стандартный вывод.
Листинг 2.14. vrs - сервер, демонстрирующие применение функции readvrec
1    #include "etcp.h"
2    int main( int argc, char **argv )
3    {
4    struct sockaddr_in peer;
5    SOCKET s;
6    SOCKET s1;
7    int peerlen = sizeof( peer );
8    int n;
9    char buf[ 10 ] ;
10   INITO;
11   if ( argc == 2 )
12   s = tcp_server( NULL, argv[ 1 ] );
13   else
14   s = tcp_server( argv[ 1 ], argv[ 2 ] );
15   s1 = accept( s, ( struct sockaddr * )&peer, &peerlen );
16   if ( !isvalidsock( s1 ) )
17   error( 1, errno, "ошибка вызова accept" );
18   for ( ; ; )
19   {
20   n = readvrec( si, buf, sizeof ( buf ) );
21   if ( n < 0 )
22   error( 0, errno, "readvrec вернула код ошибки" );
23   else if ( n == 0 )
24   error( 1, 0, "клиент отключился\п" );
25   else
26   write( 1, buf, n );
27   }
28   EXIT( 0 ); /* Сюда не попадаем. */
29   }
10-17 Инициализируем сервер и принимаем только одно соединение.
20-24 Вызываем readvrec для чтения очередной записи переменной длины. Если произошла ошибка, то печатается диагностическое сообщение и читается следующая запись. Если readvrec возвращает EOF, то печатается сообщение и работа завершается.
26 Выводим записи на stdout.
В листинге 2.15 приведен соответствующий клиент, который читает сообщения из стандартного ввода, добавляет заголовок с длиной сообщения и посылает все это серверу.
Листинг 2.15. vrc - клиент, посылающий записи переменной длины
1    #include "etcp.h"
2    int main( int argc, char **argv )
3    {
4    SOCKET s;
5    int n;
6    struct
7    {
8      u_int32_t reclen;
9      char buf [ 128 ];
10   } packet;
11   INIT();
12   s = tcp_client( argvf 1 ], argv[ 2 ] );


13   while ( fgets( packet.buf, sizeof( packet.buf ), stdin )
14     != NULL )
15   {
16     n = strlen( packet.buf );
1'7    packet .reclen = htonl ( n ) ;
18     if ( send( s, ( char * }&packet,
19      n + sizeof( packet.reclen ), 0 ) < 0 )
20      error ( 1, errno, "ошибка вызова send" );
21   }
22   EXIT( 0 );
23   }
Определение структуры packet
6-10 Определяем структуру packet, в которую будем помещать сообщение и его длину перед вызовом send. Тип данных u_int32_t - это беззнаковое 32-разрядное целое. Поскольку в Windows такого типа нет, в версии заголовочного файла skel.h для Windows приведено соответству­ющее определение типа.
Примечание: В этом примере есть одна потенциальная проблема, о которой следует знать. Предположим, что компилятор упаковывав данные в структуру, не добавляя никаких символов заполнения. Поскольку второй элемент — это массив байтов, в большинстве систем это предположение выполняется, но всегда нужно помнить о возможной недостоверности допущений о способе упаковки данных в структуру компилятором. Об этом будет рассказано в совете 24 при обсуждении способов для отправки нескольких элементов информации одновременно.
Connect, read и send
6-10 Клиент соединяется с сервером, вызывая функцию tcp_client.
13-21 Вызывается f get s для чтения строки из стандартного ввода. Эта строка помещается в пакет сообщения. С помощью функции strlen определяется длина строки. Полученное значение преобразуется в сетевой порядок байтов и помещается в поле reclen пакета. В конце вызывается send для отправки пакета серверу.
Другой способ отправки сообщений, состоящих из нескольких частей, рассматривается в совете 24.
Протестируем эти программы, запустив сервер на машине sparc, а клиент - на машине bsd. Поскольку результаты показаны рядом, видно, что поступает на вход клиенту и что печатает сервер. Чтобы сообщение строки 4 уместилось на стра­нице, оно разбито на две строчки.

bsd: $ vrc spare 8050
123
123456789
1234567890
12
^C
spare: $ vrs 8050
123
123456789
vrs: readvrec  вернула код ошибки:
     Message too long (97)
12
vrs: клиент отключился

Поскольку длина буфера сервера равна 10 байт, функция readvrec возвращает код ошибки, когда отправляется 11байт 1,..., 0,<LF>.

Не надо недооценивать производительность TCP


| | |

TCP - это сложный протокол, обеспечивающий базовую службу доставки IP- датаграмм надежностью и управлением потоком. В то же время UDP добавляет лишь контрольную сумму. Поэтому может показаться, что UDP должен быть на порядок быстрее TCP. Исходя из этого предположения, многие программисты полагают, что с помощью UDP можно достичь максимальной производительности. Да, действительно, есть ситуации, когда UDP существенно быстрее TCP. Но иногда использование TCP оказывается эффективнее, чем применение UDP.

В сетевом программировании производительность любого протокола зависит от сети, приложения, нагрузки и других факторов, из которых не последнюю роль играет качество реализации. Единственный надежный способ узнать, какой протокол и алгоритм работают оптимально, - это протестировать их в предполагаешь условиях работы приложения. На практике это, конечно, не всегда осуществимо, но часто удается получить представление о производительности, воспользовавшись каркасами для построения простых программ, моделирующих ожидаемый сетевой трафик.

Перед созданием тестовых примеров необходимо разобраться, когда и почему производительность UDP больше, чем TCP. Прежде всего, поскольку TCP сложнее, он выполняет больше вычислений, чем UDP.

Примечание: В работе [Stevens, 1996] сообщается, что реализация TCP в системе 4.4 BSD содержит примерно 4500 строк кода на языке С в сравнении с 800 строками для UDP. Естественно, обычно выполняется намного меньше строк, но эти числа отражают сравнительную сложность кода.

Но в типичной ситуации большая часть времени процессора в обоих протоколах тратится на копирование данных и вычисление контрольных сумм (совет 26), поэтому здесь нет большой разницы. В своей работе [Partridge 1993] Джекобсон описывает экспериментальную версию TCP, в которой для выполнения всего кода обычно требуется всего 30 машинных инструкций RISC (исключая вычисление контрольных сумм и копирование данных в буфер пользовательской программы, которые производятся одновременно).


Нужно отметить, что для обеспечения надежности TCP должен посылать подтверждения (АСК-сегменты) на каждый принятый пакет. Это несколько увеличивает объем обработки на обоих концах. Во-первых, принимающая сторона может включить АСК в состав данных, которые она посылает отправителю. В действительности во многих реализациях TCP отправка АСК задерживается на несколько миллисекунд: предполагается, что приложение-получатель вскоре сгенерирует ответ на пришедший сегмент. Во-вторых, TCP не обязан подтверждать каждый сегмент. В большинстве реализаций при нормальных условиях АСК посылает только на каждый второй сегмент.
Примечание: RFC 1122 [Braden 1989] рекомендует откладывать посылку ACK до 0,5 с при подтверждении каждого второго сегмента.
Еще одно принципиальное отличие между TCP и UDP в том, что TCP требует логического соединения (совет 1) и, значит, необходимо заботиться об его установлении и разрыве. Для установления соединения обычно требуется обмен тремя сегментами. Для разрыва соединения нужно четыре сегмента, которые, кроме последнего, часто можно скомбинировать с сегментами, содержащими данные.
Предположим, что время, необходимое для разрыва соединения в большинстве случаев не расходуется зря, поскольку одновременно передаются данные. Следует выяснить, что же происходит во время установления соединения. Как показано на рис. 2.16, клиент начинает процедуру установления соединения, посылая серверу сегмент SYN (синхронизация). В этом сегменте указывается порядковый номер, который клиент присвоит первому посланному байту, а также другие параметры соединения. В частности, максимальный размер сегмента (MSS), который клиент готов принять, и начальный размер окна приема, Сервер в ответ посылает свой сегмент SYN, который также содержит подтверждение АСК на сегмент SYN клиента. И, наконец, клиент отсылает АСК на сегмент SYN сервера. На этом процедура установления соединения завершается. Теперь клиент может послать свой первый сегмент данных.

Рис. 2.16. Установление соединения


На рис. 2.16 RTT (round-trip time) - это период кругового обращения, то есть время, необходимое пакету для прохождения с одного хоста на другой и обратно. Для установления соединения нужно полтора таких периода.
При длительном соединении между клиентом и сервером (например, клиент и сервер обмениваются большим объемом данных) указанный период «размазывается» между всеми передачами данных, так что существенного влияния на производительность это не оказывает. Однако если речь идет о простой транзакции, в течение которой клиент посылает запрос, получает ответ и разрывает соединение, то время инициализации составляет заметную часть от времени всей транзакции. Таким образом, следует ожидать, что UDP намного превосходит TCP по производительности именно тогда, когда приложение организует короткие сеансы связи. И, наоборот, TСР работает быстрее, когда соединение поддерживается в течении длительного времени при передаче больших объемов данных.
Чтобы протестировать сравнительную производительность TCP и UDP, а заодно продемонстрировать, как пишутся небольшие тестовые программки, создадим несколько простых серверов и клиентов. Здесь имеется в виду не полнофункциональная контрольная задача, а лишь определение производительности двух протоколов при передаче большого объема данных. Примером такого рода приложения служит протокол FTP.

Не надо заново изобретать TCP


| | |

Как сказано в совете 7, UDP может быть намного производительнее TCP в простых приложениях, где есть один запрос и один ответ. Это наводит на мысль использовать в транзакционных задачах такого рода именно UDP. Однако протокол UDP не слишком надежен, поэтому эта обязанность лежит на приложении.

Как минимум, это означает, что приложение должно позаботиться о тех датаграммах, которые теряются или искажаются при передаче. Многие начинающие сетевые программисты полагают, что при работе в локальной сети такая возможность маловероятна, и потому полностью игнорируют ее. Но в совете 7 было показано, как легко можно потерять датаграммы даже тогда, когда клиент и север находятся на одной машине. Поэтому не следует забывать о защите от потери датаграмм.

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

В свете вышесказанного можно утверждать, что любое достаточно устойчивое UDP-приложение должно обеспечивать:

повторную посылку запроса, если ответ не поступил в течение разумного промежутка времени;

проверку соответствия между ответами и запросами.

Первое требование можно удовлетворить, если при посылке каждого запроса взводить таймер, называемый таймером ретрансмиссии (retransmission timer), и RTO-таймером. Если таймер срабатывает до получения ответа, то запрос посылается повторно. В совете 20 будет рассмотрено несколько способов эффективного решения этой задачи. Второе требование легко реализуется, если в каждый запрос включить его порядковый номер и обеспечить возврат этого номера сервером вместе с ответом.

Если приложение будет работать в Internet, то фиксированное время срабатывания RTO-таймера не годится, поскольку период кругового обращения (RTT) между двумя хостами может существенно меняться даже за короткий промежуток. Поэтому хотелось бы корректировать значение RTO-таймера в зависимости от условий в сети. Кроме того, если RTO-таймер срабатывает, следует увеличить eгo продолжительность перед повторной передачей, поскольку она, скорее всего была слишком мала. Это требует некоторой экспоненциальной корректировки. (exponential backoff) RTO при повторных передачах.


Далее, если приложение реализует что- либо большее, чем простой протокол запрос- ответ, когда клиент посылает запрос и ждет ответа, не посылая дополнительных датаграмм, или ответ сервера состоит из нескольких датаграмм, то необходим какой-то механизм управления потоком. Например, если сервер - это приложение, работающее с базой данных о кадрах, а клиент просит послать имена и адреса всех сотрудников конструкторского отдела, то ответ будет состоять из нескольких записей, каждая из которых посылается в виде отдельной датаграммы. Если управление потоком отсутствует, то при этом может быть исчерпан пул буферов клиента. Обычный способ решения этой проблемы - скользящее окно типа того, что используется в TCP (только подсчитываются не байты, а датаграммы).
И, наконец, если приложение передает подряд несколько датаграмм, необходимо позаботиться об управлении перегрузкой. В противном случае такое приложение может легко стать причиной деградации пропускной способности, которая затронет всех пользователей сети.
Все перечисленные действия, которые должно предпринять основанное на протоколе UDP приложение для обеспечения надежности, - это, по сути, вариант TCP. Иногда на это приходится идти. Ведь существуют транзакционные приложения, в которых накладные расходы, связанные с установлением и разрывом TCP - соединения, близки или даже превышают затраты на передачу данных.
Примечание: Обычный пример - это система доменных имен (Domain Name System - DNS), которая используется для отображения доменного имени хоста на его IP-адрес. Когда вводится имя хоста www. rfс-editor.org в Web-браузере, реализованный внутри браузера клиент DNS посылает DNS-cepвepy UDP-датаграмму с запросом IP-адреса, ассоциированного с этим именем. Сервер в ответ посылает датаграмму, содержащую IP-адрес 128.9.160.27. Подробнее система DNS обсуждается в совете 29.
Тем менее необходимо тщательно изучить природу приложения, чтобы понять стоит ли заново реализовывать TCP. Если приложению требуется надежность TCP, то, быть может, правильное решение - это использование TCP.


Маловероятно, что функциональность TCP, продублированная на прикладном уровне, будет реализована столь же эффективно, как в настоящем TCP. Отчасти это связано с тем, что реализации TCP - это плод многочисленных экспериментов и научных исследований. С годами TCP эволюционировал по мере того, как публиковались наблюдения за его работой в различных условиях и сетях в том числе и Internet.
Кроме того, TCP почти всегда исполняется в контексте ядра. Чтобы понять, почему это может повлиять на производительность, представьте себе, что происходит при срабатывании RTO-таймера в вашем приложении. Сначала ядру нужно «пробудить» приложение, для чего необходимо контекстное переключение из режима ядра в режим пользователя. Затем приложение должно послать данные. Для этого требуется еще одно контекстное переключение (на этот раз в режим ядра) в ходе которого данные из датаграммы копируются в буферы ядра. Ядро выбирает маршрут следования датаграммы, передает ее подходящему сетевому интерфейсу и возвращает управление приложению - снова контекстное переключена Приложение должно заново взвести RTO-таймер, для чего приходится вновь переключаться.
А теперь обсудим, что происходит при срабатывании RTO-таймера внутри TCP. У ядра уже есть сохраненная копия данных, нет необходимости повторно копировать их из пространства пользователя. Не нужны и контекстные переключения. TCP заново посылает данные. Основная работа связана с передачей данных из буферов ядра сетевому интерфейсу. Повторные вычисления не требуются, так как TCP сохранил всю информацию в кэше.
Еще одна причина, по которой следует избегать дублирования функциональности TCP на прикладном уровне, - потеря ответа сервера. Поскольку клиент не I получил ответа, у него срабатывает таймер, и он посылает запрос повторно. При этом сервер должен дважды обработать один и тот же запрос, что может быть нежелательно. Представьте клиент, который «просит» сервер перевести деньги с одного банковского счета на другой. При использовании TCP логика повторных попыток реализована вне приложения, так что сервер вообще не определит, повторный ли это запрос.


Примечание: Здесь не рассматривается возможность сетевого сбоя или отказа одного из хостов. Подробнее это рассматривается в совете 9
Транзакционные приложения и некоторые проблемы, связанные с применением в них протоколов TCP и UDP, обсуждаются в RFC 955 [Braden 1985]. В этой работе автор отстаивает необходимость промежуточного протокола между ненадежным, но не требующим соединений UDP, и надежным, но зависящим от соединений TCP. Соображения, изложенные в этом RFC, легли в основу предложенной Брейденом протокола TCP Extensions for Transactions (T/TCP), который рассмотрен ниже.
Один из способов обеспечить надежность TCP без установления соединения воспользоваться протоколом Т/ТСР. Это расширение TCP, позволяющее достичь для транзакций производительности, сравнимой с UDP, за счет отказа (как правило) от процедуры трехстороннего квитирования в ходе установления обычного ТСР - соединения и сокращения фазы TIME-WAIT (совет 22) при разрыве соединения.
Обоснование необходимости Т/ТСР и идеи, лежащие в основе его реализации, описаны в RFC 1379 [Braden 1992a]. RFC 1644 [Braden 1994] содержит функциональную спецификацию Т/ТСР, а также обсуждение некоторых вопросов реализации. В работе [Stevens 1996] рассматривается протокол Т/ТСР, приводятся сравнение его производительности с UDP, изменения в API сокетов, необходимые для поддержки нового протокола, и его реализация в системе 4.4BSD.
К сожалению, протокол Т/ТСР не так широко распространен, хотя и реализован в FreeBSD, и существуют дополнения к ядру Linux 2.0.32 и SunOS 4.1.3.
Ричард Стивенс ведет страницу, посвященную Т/ТСР, на которой есть ссылки на различные посвященные этому протоколу ресурсы. Адрес Web-страницы – .

При всей надежности у TCP есть и недостатки


| | |

Как уже неоднократно отмечалось, TCP - надежный протокол. Иногда эту мысль выражают так: «TCP гарантирует доставку отправленных данных». Хотя эта формулировка часто встречается, ее следует признать исключительно неудачной.

Предположим, что вы отсоединили хост от сети в середине передачи данных. В таком случае TCP не сможет доставить оставшиеся данные. А на практике происходят сбои в сети, аварии серверов, выключение машины пользователями без Разрыва TCP-соединения. Все это мешает TCP доставить по назначению данные, преданные приложением.

Но еще важнее психологическое воздействие фразы о «гарантируемой TCP ставке» на излишне доверчивых сетевых программистов. Разумеется, никто не считает, что TCP обладает магической способностью доставлять данные получателю, невзирая на все препятствия. Вера в гарантированную доставку проявляется в небрежном программировании и, в частности, в легкомысленном отношении к проверке ошибок.



Помните, что TCP не выполняет опрос соединения


| | |

Программисты, приступающие к изучению семейства протоколов TCP/IP, но имеющие опыт работы с другими сетевыми технологиями, часто удивляются, что TCP не посылает приложению немедленного уведомления о потере связи. Поэтому некоторые даже считают, что TCP не пригоден в качестве универсальной технологии обмена данными между приложениями. В этом разделе разъясняются причины отсутствия у TCP средств для уведомления, достоинства и недостатки такого подхода и способы обнаружения потери связи прикладным программистом.

Как вы узнали в совете 9, сетевой сбой или крах системы могут прервать сообщение между хостами, но приложения на обоих концах соединения «узнают» б этом не сразу. Приложение-отправитель остается в неведении до тех пор пока TCP не исчерпает все попытки. Это продолжается довольно долго, в системах на базе BSD- примерно 9 мин. Если приложение не посылает данные, то оно может вообще не получить информации о потере связи. Например, приложение-сервер ожидает, пока клиент не обратится со следующим запросом. Но, поскольку у клиента нет связи с сервером, следующий запрос не придет. Даже когда TCP на стороне клиента прекратит свои попытки и оборвет соединение, серверу об этом будет ничего не известно.

Другие коммуникационные протоколы, например SNA или Х.25, извещают приложение о потере связи. Если имеется нечто более сложное, чем простая двухточечная выделенная линия, то необходим протокол опроса, который постой проверяет наличие абонента на другом конце соединения. Это может быть сообщение типа «есть что-нибудь для отправки?» или скрытые фреймы, посылаемые в фоновом режиме для непрерывного наблюдения за состоянием виртуального канала. В любом случае, за эту возможность приходится расплачиваться пропускной способностью сети. Каждое такое опрашивающее сообщение потребляет сетевые ресурсы, которые могли бы использоваться для увеличения полезной нагрузки.

Очевидно, одна из причин, по которым TCP не уведомляет о потере связи немедленно, - это нежелание жертвовать полосой пропускания. Большинству приложений немедленное уведомление и не нужно. Приложение, которому действительно необходимо срочно узнавать о недоступности другого конца, может реализовать этой цели собственный механизм. Далее будет показано, как это сделать.


Есть и философское возражение против встраивания в TCP/IP механизма немедленного уведомления. Один из фундаментальных принципов, заложенных при проектировании TCP/IP, - это принцип «оконечного разума» [Saltzer et al. 1984]. В применении к сетям упрощенно подразумевается следующее. «Интеллекту» нужно находиться как можно ближе к оконечным точкам соединения, а сама сеть должна быть относительно «неинтеллектуальной». Именно поэтому TCP обрабатывает ошибки самостоятельно, не полагаясь на сеть. Как сказано в совете 1, протокол IP (значит, и построенный на его основе TCP) делает очень мало предположений о свойствах физической сети. Относительно мониторинга наличия связи между приложениями этот принцип означает, что такой механизм должен реализовываться теми приложениями, которым это необходимо, а не предоставляться всем приложениям без разбора. В работе [Huitema 1995] принцип «оконечного разума» интересно обсуждается в применении к Internet.
Однако веская причина отсутствия у TCP средств для немедленного уведом­ления о потере соединения связана с одной из главных целей его проектирования: способностью поддерживать связь при наличии сбоев в сети. Протокол TCP - это результат исследований, проведенных при финансовой поддержке Министерства обороны США, с целью создания надежной технологии связи между компьютера­ми. Такая технология могла бы функционировать даже в условиях обрывов сетей из-за военных действий или природных катастроф. Часто сетевые сбои быстро устраняются или маршрутизаторы находят другой маршрут для соединения. Допуская временную потерю связи, TCP часто может справиться со сбоями, не ставя об этом в известность приложения.
Недостаток такого подхода в том, что код, отслеживающий наличие связи, не­обходимо встраивать в каждое приложение (которому это нужно), а непродуманная реализация может привести к ненужному расходу ресурсов или как-то иначе повредить пользователям. Но и в этом случае при встраивании мониторинга в приложение можно осуществить тонкую настройку алгоритма, чтобы он удовлетворял нуждам приложения и по возможности естественно интегрировался с прикладным протоколом.

Будьте готовы к некорректному поведению партнера


| | |

Часто при написании сетевых приложений не учитывают возможность возникновения ошибки, считая ее маловероятной. В связи с этим ниже приведена выдержка из требований к хостам, содержащихся в RFC 1122 [Braden 1989, стр. 12]: «Программа должна обрабатывать любую возможную ошибку, как бы маловероятна она ни была; рано или поздно придет пакет именно с такой комбинацией ошибок и атрибутов, и если программа не готова к этому, то неминуем хаос. Правильнее всего предположить, что сеть насыщена злонамеренными агентами, которые посылают пакеты, специально подобранные так, чтобы вызвать максимально разрушительный эффект. Необходимо думать о том, как защититься, хотя надо признать, что наиболее серьезные проблемы в сети Internet были вызваны непредвиденными механизмами, сработавшими в результате сочетания крайне маловероятных событий. Никакой злоумышленник не додумался бы до такого!»

Сегодня этот совет более актуален, чем во время написания. Появилось множество реализаций TCP, и некоторые из них содержат грубые ошибки. К тому же все больше программистов разрабатывают сетевые приложения, но далеко не у всех есть опыт работы в этой области.

Однако самый серьезный фактор - это лавинообразный рост числа подключенных к Internet персональных компьютеров. Ранее можно было предполагать, что у пользователей есть хотя бы минимальная техническая подготовка, они понимали, к каким последствиям приведет, скажем, выключение компьютера без предварительного завершения сетевого приложения. Теперь это не так.

Поэтому особенно важно практиковать защитное программирование и предвидеть все действия, которые может предпринять хост на другом конце, какими бы маловероятными они ни казались. Эта тема уже затрагивалась в совете 9 при уяснении потенциальных ошибок при работе с TCP, а также в совете 10, где речь шла об обнаружении потери связи. В этом разделе будет рассмотрено, какие действия вашего партнера могут нанести ущерб. Главное- не думайте, что он будет следовать прикладному протоколу, даже если обе стороны протокола реализовывали вы сами.



Не думайте, что программа, работающая в локальной сети, будет работать и в глобальной


| | |

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

Несмотря на возможную потерю данных, показанную в совете 7, локальная сеть представляет собой среду, в которой датаграммы почти никогда не теряются, не задерживаются и практически всегда доставляются в правильном порядке. Однако из этого не следует делать вывод, что приложение, замечательно работающее в локальной сети, будет также хорошо функционировать и в глобальной сети или в Internet. Здесь можно столкнуться с проблемами двух типов:

производительность глобальной сети оказывается недостаточной из-за дополнительных сетевых задержек;

некорректный код, работавший в локальной сети, отказывает в глобальной.

Если вам встречается проблема первого типа, то, скорее всего, приложение следует перепроектировать.



Изучайте работу протоколов


| | |

В книге [Stevens 1998] автор отмечает, что основные проблемы в сетевом программировании не имеют отношения ни к программированию, ни к API. Они возникают из-за непонимания работы сетевых протоколов. Это подтверждают вопросы, которые задают в конференциях, посвященных сетям (совет 44). Например, некто, читая справочную документацию на своей UNIX- или Windows-машине обнаруживает, как отключить алгоритм Нейгла. Но если он не понимает принципов управления потоком, заложенных в TCP, и роли этого алгоритма, то вряд ли разберется, когда имеет смысл его отключать, а когда- нет.

Точно так же, отсутствие механизма немедленного уведомления о потере связи, обсуждавшееся в совете 10, может показаться серьезным недостатком, если вы не понимаете, почему было принято такое решение. Разобравшись с причинами, можно без труда организовать обмен сообщениями-пульсами именно с той частотой которая нужна конкретному приложению.

Есть несколько способов изучить протоколы, и многие из них будут рассмотрены в главе 4. Основной источник информации о протоколах TCP/IP - это RFC, который официально определяет, как они должны работать. В RFC обсуждается широкий спектр вопросов разной степени важности, в том числе все протоколы из семейства TCP/IP. Все RFC, а также сводный указатель находятся на следующем сайте: www.rfc-editor.org.

В совете 43 описаны также другие способы получения RFC.

Поскольку RFC - это плод труда многих авторов, они сильно различаются доступностью изложения. Кроме того, некоторые вопросы освещаются в нескольких RFC и не всегда просто составить целостную картину.

Существуют и другие источники информации о протоколах, более понятные для начинающих. Два из них будут рассмотрены здесь, а остальные - в главе 4.

В книге [Comer 1995] описываются основные протоколы TCP/IP и то, как они должны работать, с точки зрения RFC. Здесь содержатся многочисленные ссылки на RFC, которые облегчают дальнейшее изучение предмета и дают общее представление об организации RFC. Поэтому некоторые считают эту книгу теоретическим введением в противоположность книгам [Stevens 1994; Stevens 1995], где представлен подход, ориентированный в основном на практическое применение.


В книгах Стивенса семейство протоколов TCP/IP исследуется с точки зрения реализации. Иными словами, показывается, как основные реализации TCP/IP работают в действительности. В качестве инструмента исследования используются, главным образом, данные, выдаваемые программой tcpdump (совет 34), и временные диаграммы типа изображенной на рис. 2.16. В сочетании с детальным изложением форматов пакетов и небольшими тестовыми программами, призванными прояснить некоторые аспекты работы обсуждаемых протоколов, это дает возможность ясно представить себе их функционирование. С помощью формального описания добиться этого было бы трудно.
Хотя в этих книгах приняты разные подходы к освещению протоколов TCP/IP следует думать, будто один подход чем-то лучше другого, а стало быть, отдавать предпочтение только одной книге. Полезность каждой книги зависит от поставленной перед вами задачи в данный момент. По сути, издания взаимно дополняют друг друга. И серьезный программист, занимающийся разработкой сетевых приложений, должен включить эти две книги в свою библиотеку.

Не воспринимайте слишком серьезно семиуровневую эталонную модель OSI


| | |

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

Например, на рис. 2.1, где изображен упрощенный стек протоколов TCP/IP уровень IP предоставляет сервис, именуемый доставкой датаграмм, уровням TCP и UDP. Чтобы обеспечить такой сервис, IP пользуется сервисами для передачи датаграмм физическому носителю, которые предоставляет уровень сетевого интерфейса.



Разберитесь с операцией записи в TCP


| | |

Здесь и далее обсуждаются некоторые особенности операций чтения и записи при программировании TCP/IP. Представляет интерес не конкретный API и детали системных вызовов, а семантические вопросы, связанные с этими операциями.

Как сказано в совете 6, между операциями записи и посылаемыми TCP сегментами нет взаимно-однозначного соответствия. Как именно соотносятся обращения к операции записи с протоколом TCP, зависит от системы, но все же спецификации протокола достаточно определенны и можно сделать некоторые выводы при знакомстве с конкретной реализацией. Традиционная реализация подробно описана в системе BSD. Она часто рассматривается как эталонная, и ее исходные тексты доступны.

Примечание: Исходные  тексты  оригинальной реализации  для  системы 4.4BSD-lite2 можно получить на CD-ROM у компании WalnutCreek (http:// www.cdrom.com). Подробные пояснения к исходному тексту вы найдете в книге [Wright and Stevens 1995].



Разберитесь с аккуратным размыканием TCP-соединений


| | |

Как вы уже видели, в работе TCP-соединения есть три фазы:

1. Установления соединения.

2. Передачи данных.

3. Разрыва соединения.

В этом разделе будет рассмотрен переход от фазы передачи данных к фазе раз­рыва соединения. Точнее, как узнать, что хост на другом конце завершил фазу пе­редачи данных и готов к разрыву соединения, и как он может сообщить об этом партнеру.

Вы увидите, что один хост может прекратить отправку данных и сигнализиро­вать партнеру об этом, не отказываясь, однако, от приема данных. Это возможно, поскольку TCP-соединения полнодуплексные, потоки данных в разных направле­ниях не зависят друг от друга.

Например, клиент может соединиться с сервером, отправить серию запросов, а затем закрыть свою половину соединения, предоставив тем самым серверу информацию, что больше запросов не будет. Серверу для ответа клиенту, возможно, понадобится выполнить большой объем работы и даже связаться с другими серве­рами, так что он продолжает посылать данные уже после того, как клиент прекра­тил отправлять запросы. С другой стороны, сервер может послать в ответ сколько угодно данных, так что клиент не определяет заранее, когда ответ закончится. Поэтому сервер, вероятно, как и клиент, закроет свой конец соединения, сигнализи­руя о конце передачи.

После того как ответ на последний запрос клиента отправлен и сервер закрыл свой конец соединения, TCP завершает фазу разрыва. Обратите внимание, что за­крытие соединения рассматривается как естественный способ известить партнера о прекращении передачи данных. По сути, посылается признак конца файла EOF.



Подумайте о запуске своего приложения через inetd


| | |

В операционной системе UNIX и некоторых других имеется сетевой суперсервер inetd, который позволяет почти без усилий сделать приложение сетевым.

Рис. 3.1. Системные вызовы close и shutdown

Кроме того, если есть всего один процесс, который прослушивает входящие соеди­нения и входящие UDP-датаграммы, то можно сэкономить системные ресурсы. Обычно inetd поддерживает, по меньшей мере, протоколы TCP и UDP, а возмож­но, и некоторые другие. Здесь будут рассмотрены только два первых. Поведение inetd существенно зависит от того, с каким протоколом - TCP или UDP - он работает.



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


| | |

Проектировщик сетевого сервера сталкивается с проблемой выбора номера для хорошо известного порта. Агентство по выделению имен и уникальных параметров протоколов 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: $

Подумайте об использовании двух TCP-соединений


| | |

Во многих приложениях удобно разрешить нескольким процессам или потокам читать из TCP-соединений и писать в них. Особенно распространена эта практика в системе UNIX, где по традиции создается процесс-потомок, который, например, пишет в TTY-соединение, тогда как родитель занимается чтением.

Типичная ситуация изображена на рис. 3.3, где показан эмулятор терминала. Родительский процесс большую часть времени блокирован в ожидании ввода из TTY-соединения. Когда на вход поступают данные, родитель читает их и выводит на экран, возможно, изменяя на ходу формат. Процесс-потомок в основном блокирован, ожидая ввода с клавиатуры. Когда пользователь вводит данные, потомок выполняет необходимые преобразования и записывает данные в TTY-соединение.

Эта стратегия удобна, поскольку позволяет автоматически мультиплексировать ввод с клавиатуры и из TTY-соединения и разнести логику преобразования кодов клавиш и форматирования вывода на экран по разным модулям. За счет этого программа получается концептуально проще, чем в случае нахождения кода

Рис. 3.3. Два процесса, обслуживающие TTY-соединение

в одном месте. В действительности, до появления в операционной системе механизма select это был единственный способ обработки поступления данных из нескольких источников.



и следующем разделах будет рассказано


| | |
В этом и следующем разделах будет рассказано об использовании техники событийной управляемости в программировании TCP/IP. Будет разработан универсальный механизм тайм-аутов, позволяющий указать программе, что некоторое событие должно произойти до истечения определенного времени, и асинхронно приступить к обработке этого события в указанное время. Здесь рассмотрим реа­лизацию механизма таймеров, а в совете 21 вернемся к архитектуре с двумя соединениями и применим его на практике.
Разница между событийно-управляемым и обычным приложением хорошо иллюстрируется двумя написанными ранее программами: hb_client2 (листинги 2.26 и 2.27) и tcprw (листинг 2.21). В tcprw поток управления последовательный: сначала из стандартного ввода читается строка и передается удаленному хосту, а затем от него принимается ответ и записывается на стандартный вывод. Обратите внимание, что нет возможности ничего принять от удаленного хоста, пока ожидается ввод из stdin. Как вы видели, в результате можно не знать, что партнер за­вершил сеанс и послал ЕОЕ Ожидая также ответа от удаленного хоста, вы не можете читать новые данные из stdin. Это значит, что приложение, с точки зрения пользователя, слишком медленно реагирует. Кроме того, оно может «зависнуть», если удаленный хост «падает» до того, как приложение ответило.
Сравните это поведение с работой клиента hb_client2, который в любой момент способен принимать данные по любому соединению или завершиться по тайм-ауту. Ни одно из этих событий не зависит от другого, именно поэтому такая архитектура называется событийно-управляемой.
Заметим, что клиента hb_client2 можно легко обобщить на большее число соединений или источников входной информации. Для этого существует механизм select, который позволяет блокировать процесс в ожидании сразу нескольких событий и возвращать ему управление, как только произойдет любое из них. В системе UNIX этот механизм, а также родственный ему вызов poll, имеющийся в системах на базе SysV, - это единственный эффективный способ обработки асинхронных событий в немногопоточной среде.


Примечание: До недавнего времени считалось, что из соображений переносимости следует использовать select, а не poll, так как на платформе Windows, а равно в современных UNIX-системах поддерживается именно select, тогда как poll встречается обычно в реализациях на базе SysV. Однако некоторые большие серверные приложения (например, Web-серверы), поддерживающие очень много одновременных соединений, применяют механизм poll, так как он лучше масштабируется на большое число дескрипторов. Дело в том, что select ограничен фиксированным числом дескрипторов. Обычно их не больше 1024, но бывает и меньше. Так, в системе FreeBSD и производных от нее по умолчанию предел равен 256. Для изменения значения по умолчанию нужно пересобирать ядро, что неудобно, хотя и возможно. Но и пересборка ядра лишь увеличивает предел, а не снимает его. Механизм же poll не имеет встроенных ограничений на число дескрипторов. Следует также принимать во внимание эффективность. Типичная реализация select может быть очень неэффективной при большом числе дескрипторов. Подробнее это рассматривается в работе [Banga and Mogul 1998]. (В этой работе приводится еще один пример возникновения трудностей при экстраполяции результатов, полученных в локальной сети, на глобальную. Эта тема обсуждалась в совете 12.) Проблема большого числа де­скрипторов стоит особенно остро, когда ожидается немного событий на многих дескрипторах, то есть первый аргумент - maxfd - велик, но с помощью FD_SET было зарегистрировано всего несколько дескрипторов. Это связано с тем, что ядро долж­но проверить все возможные дескрипторы (0,..., maxfd), чтобы понять, ожидаются ли приложением события хотя бы на одном из них. В вызове poll используется массив дескрипторов, с помощью которого ядру сообщается о том, в каких событиях заинтересовано приложение, так что этой проблемы не возникает.
Итак, использование select или poll позволяет мультиплексировать несколько событий ввода/вывода. Сложнее обстоит дело с несколькими таймерами, поскольку в вызове можно указать лишь одно значение тайм-аута. Чтобы решить эту проблему и создать тем самым более гибкое окружение для событийно-управляемых программ, следует разработать вариант вызова select - tselect. Хотя функции timeout и untimeout, связанные с tselect, построены по той же схеме, что и одноименные подпрограммы ядра UNIX, они работают в адресном пространстве пользователя и используют select для мультиплексирования ввода/вывода и получения таймера..


Таким образом, существуют три функции, ассоциированные с tselect. Прежде всего это сама tselect, которая применяется аналогично select для мультиплексирования ввода/вывода. Единственное отличие в том, что у tselect нет параметра timeout (это пятый параметр select). События таймера задаются с помощью вызова функции timeout, которая позволяет указать длительность таймера и действие, которое следует предпринять при его срабатывании. Вызов untimeout отменяет таймер до срабатывания.
Порядок вызова этих функций описан следующим образом:
#nclude "etcp.h"
int tselect ( int maxfd, fd_set *rdmask, fd_set *wrmask, fd_set *exrnask );
Возвращаемое значение: число готовых событий, 0 - если событий нет, -1 -.ошибка.
unsigned int timeout( void (handler)(void * ), void *arg, int ms);
Возвращаемое значение: идентификатор таймера для передачи untimeout
void untimeout( unsigned int timerid);
Когда срабатывает таймер, ассоциированный с вызовом timeout, вызывается функция, заданная параметром handler, которой передается аргумент, заданный параметром arg. Таким образом, чтобы организовать вызов функции retransmit через полторы секунды с целым аргументом sock, нужно сначала написать
timeout( retransmit, ( void  * )  sock, 1500 );
а затем вызывать tselect. Величина тайм-аута ms задается в миллисекундах, но надо понимать, что разрешающая способность системных часов может быть ниже. Для UNIX-систем типичное значение составляет 10 мс, поэтому не следует ожи­дать от таймера более высокой точности.
Примеры использования tselect будут приведены далее, а пока рассмотрим ее реализацию. В листинге 3.14 приведено определение структуры tevent_t и объявления глобальных переменных.
Листинг 3.14. Глобальные данные для tselect
tselect.с
1    #include "etcp.h"
2    #define NTIMERS 25
3    typedef struct tevent_t tevent_t;
4    struct tevent_t
5    {
6    tevent_t   *next;
7    struct timeval tv;
8    void ( *func )( void * );
9    void *arg;
10   unsigned int id;


11   };
12   static tevent_t *active = NULL; /* Активные таймеры. */
13   static tevent_t *free_list = NULL; /* Неактивные таймеры. */
Объявления
2 Константа NTIMERS определяет, сколько таймеров выделять за один раз. Сначала таймеров нет вовсе, поэтому при первом обращении к timeout будет выделено NTIMERS таймеров. Если все они задействованы и происходит очередное обращение к timeout, то выделяется еще NTIMERS таймеров.
3-11 Каждый таймер представляет отдельную структуру типа tevent_t. Структуры связаны в список полем next. В поле tv хранится время срабатывания таймера. Поля func и arg предназначены для хранения указателя на функцию обработки события таймера (которая вызывается при срабатывании) и ее аргумента. Наконец, идентификатор активного таймера хранится в поле id.
12 Порядок расположения активных таймеров в списке определяется моментом срабатывания. Глобальная переменная active указывает на первый таймер в списке.
13 Неактивные таймеры находятся в списке свободных. Когда функции timeout нужно получить новый таймер, она берет его из этого списка. Глобальная переменная free_list указывает на начало списка свободных.
Далее изучим функцию timeout и подпрограммы выделения таймеров (листинг 3.15).
Листинг 3.15. Функции timeout и allocateJimer
tselect.с
1    static tevent_t *allocate_timer( void )
2    {
3    tevent_t *tp;
4    if ( free_list = NULL ) /* нужен новый блок таймеров? *./
5    {
6      free_list = malloc( NTIMERS * sizeof( tevent_t ));
7      if ( free_list = NULL )
8       error( 1, 0, "не удалось получить таймеры\n" };
9       for ( tp = free_list;
10       tp < free_list + NTIMERS - 1; tp+ + )
11      tp->next = tp + 1;
12      tp->next = NULL;
13     }
14     tp = free_list; /* Выделить первый. */
15     free_list = tp->next; /* Убрать его из списка. */
16     return tp;
17   }
18   unsigned int timeout ( void ( *func ) ( void * ), void *arg, int ms )
19   {
20     tevent_t *tp;
21     tevent_t *tcur;


22     tevent_t **tprev;
23     static unsigned int id = 1; /* Идентификатор таймера. */
24     tp = allocate_timer();
25     tp->func = func;
26     tp->arg = arg;
27     if ( gettimeofday( &tp->tv, NULL ) < 0 )
28      error( 1, errno, "timeout: ошибка вызова gettimeofday");
29     tp->tv.tv_usec + = ms * 1000;
30     if ( tp->tv.tv_usec > 1000000 )
31     {
32      tp->tv.tv_sec + = tp->tv.tv_usec / 1000000;
33      tp->tv.tv_usec %= 1000000;
34     }
35     for ( tprev = &active, tcur = active;
36      tcur && !timercmp( &tp->tv, &tcur->tv, < ); /* XXX */
37      tprev = &tcur->next, tcur = tcur->next )
38     { ; }
39     *tprev = tp;
40     tp->next   =   tcur;
41     tp->id =  id++; /* Присвоить значение идентификатору таймера. */
42     return  tp->id;
43   }
allocate_timer
4- 13 Функция allocate_timer вызывается из timeout для получения свободного таймера. Если список свободных пуст, то из кучи выделяется память для NTIMERS структур tevent_t, и эти структуры связываются в список.
14-16 Выбираем первый свободный таймер из списка и возвращаем его вызывающей программе.
timeout
24-26 Получаем таймер и помещаем в поля func и arg значения переданных нам параметров.
27-34 Вычисляем момент срабатывания таймера, прибавляя значение пара­метра ms к текущему времени. Сохраняем результат в поле tv.
35-38 Ищем в списке активных место для вставки нового таймера. Вставить таймер нужно так, чтобы моменты срабатывания всех предшествующих таймеров были меньше либо равны, а моменты срабатывания всех последующих - больше момента срабатывания нового. На рис. 3.6 показан

Рис. 3.6. Список активных таймеров до и после поиска точки вставьки
процесс поиска и значения переменных tcur и tprev. Вставляем новый таймер так, что его момент срабатывания tnew удовлетворяет условию t0 < t1, < tnew < t2. Обведенный курсивом прямоугольник tnew показывает позицию в списке, куда будет помещен новый таймер. Несколько странное использование макроса timercmp в строке 36 связано с тем, что вер­сия в файле winsock2.h некорректна и не поддерживает оператора >=.


27-34 Вставляем новый таймер в нужное место, присваиваем ему идентификатор и возвращаем этот идентификатор вызывающей программе. Возвращается идентификатор, а не адрес структуры tevent_t, чтобы избежать «гонки» (race condition). Когда таймер срабатывает, структура tevent_t возвращается в начало списка свободных. При выделении нового таймера будет использована именно эта структура. Если приложение теперь попытается отменить первый таймер, то при условии, что возвращается адрес структуры, а не индекс, будет отменен второй таймер. Эту проблему решает возврат идентификатора.
Идентификатор таймера, возвращенный в конце функции из листинга 3.15, используется функцией untimeout (листинг 3.16).
Листинг 3.16. Функция untimeout
tselect.с
1    void untimeout( unsigned int id )
2    {
3    tevent_t **tprev;
4    tevent_t *tcur;
5    for ( tprev = &active, tcur = active;
6      tcur && id != tcur->id;
7      tprev = &tcur->next, tcur = tcur->next);
8      { ; }
9    if ( tcur == NULL )
10   {
11     error( 0, 0,
12      "при вызове untimeout указан несуществующий таймер (%d) \n", id );
13      return;
14   }
15   *tprev = tcur->next;
16   tcur->next = free_list;
17   free_list = tcur;
18   }
Поиск таймера
5-8 Ищем в списке активных таймер с идентификатором id. Этот цикл похож на тот, что используется в timeout (листинг 3.15).
9-14 Если в списке нет таймера, который пытаемся отменить, то выводим диагностическое сообщение и выходим.
Отмена таймера
15-17 Для отмены таймера исключаем структуру tevent_t из списка активных и возвращаем в список свободных.
Последняя из функций, работающих с таймерами, - это tselect (листинг 3.17)
Листинг 3.17. Функция tselect
1    int tselect( int maxp1, fd_set *re, fd_set *we, fd_set *ee )
2    {
3    fd_set rmask;
4    fd_set wmask;
5    fd_set emask;
6    struct timeval now;
7    struct timeval tv;
8    struct timeval *tvp;
9    tevent_t *tp;
10   int n;
11   if ( re )


12     rmask = *re;
13   if ( we )
14     wmask = *we;
15   if ( ее )
16     emask = *ee;
17   for ( ; ; )
18   {
19     if ( gettimeofday( know, NULL ) < 0 )
20      error( 1, errno, "tselect: ошибка вызова gettimeofday" );
21     while ( active && !timercmp( know, &active->tv, < ) )
22     {
23      active->func( active->arg );
24      tp = active;
25      active = active->next;
26      tp->next = free_list;
27      free_list = tp;
28     }
29     if ( active )
30     {
31      tv.tv_sec = active->tv.tv_sec - now.tv_sec;
32      tv.tv_usec = active->tv.tv_usec - now.tv_usec;
33      if ( tv.tv_usec < 0 )
34      {
35       tv.tv_usec += 1000000;
36       tv.tv_sec--;
37      }
38      tvp = &tv;
39     }
40     else if ( re == NULL && we == NULL && ее == NULL )   •
41      return 0;
42     else
43      tvp = NULL;
44     n = select ( maxpl, re, we, ее, tvp );
45     if ( n < 0 )
46      return -1;
47     if ( n > 0 )
48      return n;
49     if ( re )
50      *re = rmask;
51     if ( we )
52      *we = wmask;
53     if ( ее )
54      *ee = emask;
55   }
56   }
Сохранение масок событий
11- 16 Поскольку при одном обращении к tselect может несколько раз вызываться select, сохраняем маски событий, передаваемых select.
Диспетчеризация событий таймера
19-28 Хотя в первой структуре tevent_t, находящейся в списке активных таймеров, время срабатывания меньше или равно текущему времени, вызываем обработчик этого таймера, исключаем структуру из списка активных и возвращаем в список свободных. Как и в листинге 3.15, странный вызов макроса timercmp обусловлен некорректной его реализацией в некоторых системах.
Вычисление времени следующего события
29-39 Если список активных таймеров не пуст, вычисляем разность между текущим моментом времени и временем срабатывания таймера в нача­ле списка. Это значение передаем системному вызову select.
40-41 Если больше таймеров нет и нет ожидаемых событий ввода/вывода, то tselect возвращает управление. Обратите внимание, что возвращается нуль, тем самым извещается об отсутствии ожидающих собы­тий. Семантика кода возврата отличается от семантики select.


42- 43 Если нет событий таймера, но есть события ввода/вывода, то устанавливаем tvp в NULL, чтобы select не вернулся из-за тайм-аута.
Вызов select
44-48 Вызываем select, чтобы он дождался события. Если select заверша­ется с ошибкой, то возвращаем код ошибки приложению. Если select возвращает положительное значение (произошло одно или более событий ввода/вывода), то возвращаем приложению число событий. По­скольку вызывали select, передавая указатели на маски событий, под­готовленные приложением, то биты событий в них уже установлены-
49-54 Если select вернул нуль, то сработал один или несколько таймеров. Поскольку в этом случае select обнулит все маски событий, установленные приложением, восстановим их перед тем, как возвращаться к началу цикла, где вызываются обработчики таймеров.
Для вставки и удаления таймеров из списка был использован линейный поиск. При небольшом числе таймеров это не страшно, но при увеличении их числа произ­водительность программы снижается, так как для поиска требуется О(n) операций, где n - число таймеров (для запуска обработчика события требуется время порядка O(1)). Вместо линейного поиска можно воспользоваться пирамидой [Sedgewick 1998] - для вставки, удаления и диспетчеризации требуется O(log n) операций - или хэширующим кольцом таймеров (hashing timing wheel) [Varghese and Lacuk 1997]; при этом эффективность может достигать О(1) для всех трех операций.
Заметим, что функция tselect не требует наличия ожидающих событий вво­да/вывода, поэтому ее вполне можно использовать только как механизм организа­ции тайм-аутов. В данном случае имеем следующие преимущества по сравнению с системным вызовом sleep:
в системе UNIX s leep позволяет задерживать исполнение на интервал, кратный секунде, то есть разрешающая способность очень мала. В Windows такого ограничения нет. Во многих реализациях UNIX есть иные механизмы с более высокой степенью разрешения, однако они не очень распространены. Хотелось бы иметь механизм таймеров с высокой степенью разрешения, работающий на возможно большем числе платформ. Поэтому в UNIX принято использовать для реализации высокоточных таймеров вызов select;
применение sleep или «чистого» select для организации нескольких таймеров затруднительно, поскольку требует введения дополнительных струк­тур данных. В функции tselect все это уже сделано.
К сожалению, в Windows функция tselect в качестве таймера работает не со­всем хорошо. В спецификации Winsock API [WinSock Group 1997] говорится, что использование selects качестве таймера «неудовлетворительно и не имеет оправданий». Хотя на это можно возразить, что «неудовлетворительность» -это когда системный вызов работает не так, как описано в опубликованной специ­фикации, все же придется придерживаться этой рекомендации. Тем не менее можно использовать функцию tselect и связанные с ней под Windows, только при этом следует указывать также и события ввода/вывода.