Аккуратное размыкание соединений
Теперь, когда вы познакомились с вызовом 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, -это всего лишь признак конца файла.