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

       

Аккуратное размыкание соединений


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

Примечание: Термин «аккуратное размыкание» (orderly release) имеет некоторое отношение к команде t_sndrel из APIXTI (совет 5), которую также часто называют аккуратным размыканием в отличие от команды грубого размыкания (abortive release) t_snddis. Но путать их не стоит. Команда t_sndrel выполняет те же действия, что и shutdown. Обе команды используются для аккуратного размыкания соединения.

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

Чтобы поэкспериментировать с аккуратным размыканием, запрограммируйте клиент, который посылает серверу данные, а затем читает и печатает ответ сервера. Текст программы приведен в листинге 3.1. Клиент читает из стандартного входа данные для отправки серверу. Как только f gets вернет NULL, индицирующий конец файла, клиент начинает процедуру разрыва соединения. Параметр –с в командной строке управляет этим процессом. Если -с не задан, то программа shutdownc вызывает shutdown для закрытия передающего конца соединения. Если же параметр задан, то shutdownc вызывает CLOSE, затем пять секунд «спит» и завершает сеанс.

Листинг 3.1. Клиент для экспериментов с аккуратным размыканием

shutdownc.c

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 re;

8    int len;

9    int c;

10   int closeit = FALSE;



11   int err = FALSE;

12   char lin[ 1024 ];

13   char lout[ 1024 ];

14   INIT();

15   opterr = FALSE;

16   while ( ( с = getopt( argc, argv, "c" ) ) != EOF )

17   {

18     switch( с )

19     {

20      case 'c' :


21      closeit = TRUE;

22      break;

23      case '?' :

24      err = TRUE;

25     }

26   }

27   if ( err argc - optind != 2 )

28     error( 1, 0, "Порядок вызова: %s [-с] хост порт\n",

29    program_name );

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

31   FD_ZERO( &allreads );

32   FD_SET( 0, &allreads ) ;

33   FD_SET( s, &allreads ) ;

34   for ( ; ; )

35   {

36     readmask = allreads;

37     re = select) s + 1, &readmask, NULL, NULL, NULL );

38     if ( re <= 0 )

39      error( 1, errno, "ошибка: select вернул (%d)", re );

40     if ( FD_ISSET( s, &readmask ) )

41     {

42      re = recv( s, lin, sizeof( lin ) - 1, 0 );

43      if ( re < 0 )

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

45      if ( re == 0 )

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

47      lin[ re ] = '\0';

48      if ( fputs( lin, stdout ) == EOF )

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

50     }

51     if ( FD_ISSET( 0, &readmask ) )

52     {

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

54      {

55       FD_CLR( 0, &allreads ) ;

56       if ( closeit )

57       {

58        CLOSE( s );

59        sleep( 5 ) ;

60        EXIT( 0 ) ;

61       }

62       else if ( shutdown( s, 1 ) )

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

64      }

65      else

66      {

67       len = strlent lout );

68       re = send( s, lout, len, 0 );

69       if ( re < 0 )

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

71      }

72     }

73   }

74   }

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

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

Обработка данных.

40-50 Если в ТСР-сокете есть данные для чтения, программа пытается прочитать, сколько можно, но не более, чем помещается в буфер. При получении признака конца файла или ошибки завершаем сеанс, в противном случае выводим все прочитанное на stdout.



Примечание: Обратите внимание на конструкцию sizeof ( lin ) -1 в вызове recv на строке 42. Вопреки всем призывам избегать переполнения буфера, высказанным в совете 11, в первоначальной версии этой программы было написано sizeof ( lin ), что приведет к записи за границей буфера в операторе

lin[ re ] = '\0';

в строке 47.

53-64 Прочитав из стандартного входа EOF, вызываем либо shutdown, либо CLOSE в зависимости от наличия флага -с.

65- 71 В противном случае передаем прочитанные данные серверу.

Можно было бы вместе с этим клиентом использовать стандартный системным сервис эхо-контроля, но, чтобы увидеть возможные ошибки и ввести некоторую за­держку, напишите собственную версию эхо-сервера. Ничего особенного в программе tcpecho.с нет. Она только распознает дополнительный аргумент в командной строке, при наличии которого программа «спит» указанное число секунд между чтением и записью каждого блока данных (листинг 3.2).

Сначала запустим клиент shutdownc с флагом -с, чтобы он закрывал сокет после считывания EOF из стандартного ввода. Поставим в сервере tcpecho задержку на 4 с перед отправкой назад только прочитанных данных:

bsd: $ tcpecho 9000 4 &

[1] 3836

bsd: $ shutdownc –c localhost 9000

data1 Эти три строки были введены подряд максимально быстро

data2

^D

tcpecho: ошибка вызова send: Broken pipe (32) Спустя 4 с после отправки “data1”.

Листинг3.2. Эхо-сервер на базе TCP

tcpecho.c

1    #include "etcp.h"

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

3    {

4    SOCKET s;

5    SOCKET s1;

6    char buf[ 1024 ];

7    int re;

8    int nap = 0;

9    INIT();

10   if ( argc == 3 )

11     nap = atoi( argv[ 2 ] ) ;

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

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

14   if ( !isvalidsock( s1 ) )

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

16   signal( SIGPIPE, SIG_IGN ); /* Игнорировать сигнал SIGPIPE.*/

17   for ( ; ; )

18   {

19     re = recv( s1, buf, sizeof( buf ), 0 );

20     if ( re == 0 )

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



22     if ( re < 0 )

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

24     if ( nap )

25      sleep( nap ) ;

26     re = send( s1, buf, re, 0 );

27     if ( re < 0 )

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

29   }

30   }

Затем нужно напечатать две строки datal и data2 и сразу вслед за ними на­жать комбинацию клавиш Ctrl+D, чтобы послать программе shutdownc конец файла и вынудить ее закрыть сокет. Заметьте, что сервер не вернул ни одной стро­ки. В напечатанном сообщении tcpecho об ошибке говорится, что произошло. Когда сервер вернулся из вызова sleep и попытался отослать назад строку datal, он получил RST, поскольку клиент уже закрыл соединение.

Примечание: Как объяснялось в совете 9, ошибка возвращается при записи второй строки (data2). Заметьте, что это один из немногих случаев, когда ошибку возвращает операция записи, а не чтения. Подробнее об этом рассказано в совете 15.

В чем суть проблемы? Хотя клиент сообщил серверу о том, что больше не будет посылать данные, но соединение разорвал до того, как сервер успел завершить обработку, в результате информация была потеряна. В левой половине рис. 3.2 показано, как происходил обмен сегментами.

Теперь повторим эксперимент, но на этот раз запустим shutdownc без флага -с.

bsd: $ tcpecho 9000 4 &

[1] 3845

bsd: $ shutdownc localhost 9000

datal

data2

^D

datal Спустя 4 с после отправки "datal".

data2 Спустя 4 с после получения "datal".

tcpecho: клиент отсоединился

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

На этот раз все сработало правильно. Прочитав из стандартного входа признак кон­ца файла, shutdownc вызывает shutdown, сообщая серверу, что он больше не будет ничего посылать, но продолжает читать данные из соединения. Когда сервер tcpecho обнаруживает EOF, посланный клиентом, он закрывает соединение, в результате чего TCP посылает все оставшиеся в очереди данные, а вместе с ними FIN. Клиент, полу­чив EOF, определяет, что сервер отправил все, что у него было, и завершает сеанс.



Заметьте, что у сервера нет информации, какую операцию (shutdown или close) выполнит клиент, пока не попытается писать в сокет и не получит код ошибки или EOF. Как видно из рис. 3.1, оба конца обмениваются теми же сегментами, что и раньше, до того, как TCP клиента ответил на сегмент, содержащий строку datal.

Стоит отметить еще один момент. В примерах вы несколько раз видели, что, когда TCP получает от хоста на другом конце сегмент FIN, он сообщает об этом приложению, возвращая нуль из операции чтения. Примеры приводятся в строке 45 листинга 3.1 и в строке 20 листинга 3.2, где путем сравнения кода возврата recv с нулем проверяется, получен ли EOF. Часто возникает путаница, когда в ситуации, подобной той, что показана в листинге 3.1, используется системный вызов select. Когда приложение на другом конце закрывает отправляющую сторону соедине­ния, вызывая close или shutdown либо просто завершая работу, select возвра­щает управление, сообщая, что в сокете есть данные для чтения. Если приложение при этом не проверяет EOF, то оно может попытаться обработать сегмент нулевой длины или зациклиться, переключаясь между вызовами read и select.

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


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