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

       

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


Первый пример - это вариация на тему первой версии программы 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.


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