Отказ приложения
А теперь разберемся, что происходит, когда аварийно или как-либо иначе завершается приложение на другом конце соединения. Прежде всего следует понимать, что с точки зрения вашего приложения аварийное завершение другого конца отличается от ситуации, когда приложение на том конце вызывает функцию 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 );
с целью имитировать обработку на клиенте или загруженность системы, то получится тот же результат, что и при запуске клиента и сервера на одной машине.