Преждевременное завершение
Первый пример - это вариация на тему первой версии программы 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: $ |
Это удивительно. Ожидалось, что 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.