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

       

Проверка завершения работы клиента


Предположим, что клиент извещает о желании завершить работу, посыла серверу запрос из одной строки, в которой есть только слово quit. Допустим далее, что сервер читает строки из входного потока с помощью функции геаdline (ее текст приведен в листинге 2.32), которая была описана в совете 9. Что произойдет, если клиент завершится (аварийно или нормально) раньше, чем пошлет команду quit? TCP на стороне клиента отправит сегмент FIN, после чего операция чтения на сервере вернет признак конца файла. Конечно, это просто обнаружить, только сервер должен обязательно это сделать. Легко представить себе такой код, предполагая правильное поведение клиента:

for ( ; ; )

{

 if ( readline( s, buf, sizeof( buf ) ) < 0 )

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

 if ( strcmp( buf, "quit\n" ) == 0)

  /* Выполнить функцию завершения клиента. */

 else

  /* Обработать  запрос. */

}

Хотя код выглядит правильным, он не работает, поскольку будет повторно обрабатывать последний запрос, если клиент завершился, не послав команду quit.

Предположим, что вы увидели ошибку в предыдущем фрагменте (или нашли ее после долгих часов отладки) и изменили код так, чтобы явно обнаруживался признак конца файла:

for ( ;; )

{

 rc = readline( s, buf, sizeof( buf ) );

 if ( rc < 0 )



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

 if ( rc == 0 strcmp( buf, "quit\n" ) == 0)

  /* Выполнить функцию завершения клиента. */

 else

  /* Обработать запрос. */

}

И этот код тоже неправилен, так как в нем не учитывается случай, когда хост клиента «падает» до того, как клиент послал команду quit или завершил работу. В этом месте легко принять неверное решение, даже осознавая проблему. Для проверки краха клиентского хоста надо ассоциировать таймер с вызовом readline. Потребуется примерно в два раза больше кода, если нужно организовать обработку «безвременной кончины» клиента. Представив себе, сколько придется писать, вы решаете, что шансов «грохнуться» хосту клиента мало.


Но проблема в том, что хосту клиента и необязательно завершаться. Если это ПК, то пользователю достаточно выключить его, не выйдя из программы. А это очень легко, поскольку клиент мог исполняться в свернутом окне или в окне, закрытом другими, так что пользователь про него, вероятно, забыл. Есть и другие возможности. Если соединение между хостами установлено с помощью модема на клиентском конце (так сегодня выполняется большинство подключений к Internet), то пользователь может просто выключить модем. Шум в линии также может привести к обрыву соединения. И все это с точки зрения сервера неотличимо от краха хоста клиента.

Примечание: При некоторых обстоятельствах ошибку, связанную с модемом, можно исправить, повторно набрав номер (помните, что TCP способен восстанавливаться после временных сбоев в сети), но зачастую IP-адреса обоих оконечных абонентов назначаются динамически сервис - провайдером при у становлении соединения. В таком случае маловероятно, что будет задан тот же адрес, и поэтому клиент не сможет оживить соединение.

Для обнаружения потери связи с клиентом необязательно реализовывать пульсацию, как это делалось в совете 10. Нужно всего лишь установить тайм-аут для операции чтения. Тогда, если от клиента в течение определенного времени не поступает запросов, то сервер может предположить, что клиента больше нет, и разорвать соединение. Так поступают многие FTP-серверы. Это легко сделать, либо явно установив таймер, либо воспользовавшись возможностями системного вызова select, как было сделано при реализации пульсации.

Если вы хотите, чтобы сервер не «зависал» навечно, то можете воспользоваться Механизмом контролеров для разрыва соединения по истечении контрольного тайм-аута. В листинге 2.30 приведен простой TCP-сервер, который принимает сообщение от клиента, читает из сокета и пишет результат на стандартный вывод. Чтобы сервер не «завис», следует задать для сокета опцию SO_KEEPALIVE с помо­щью вызова setsockopt. Четвертый аргумент setsockopt должен указывать на ненулевое целое число, если надо активировать посылку контролеров, или на нулевое целое, чтобы ее отменить.



Запустите этот сервер на машине bsd, а на другой машине - программу telnet в качестве клиента. Соединитесь с сервером, отправьте ему строку «hello», чтобы соединение точно установилось, а затем отключите клиентскую систему от сети. Сервер напечатает следующее:

bsd: $ keep 9000

hello

Клиент отключился от сети.



Спустя 2 ч 11 мин 15 с.

кеер: ошибка вызова recv: Operation timed out (60)

bsd: $

Как и следовало ожидать, TCP на машине bsd разорвал соединение и вернул серверу код ошибки ETIMEDOUT. В этот момент сервер завершает работу и освобождает все ресурсы.

Листинг 2.30. Сервер, использующий механизм контролеров

1    #include "etcp.h"

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

3    {

4    SOCKET s;

5    SOCKET s1;

6    int on = 1;

7    int rc;

8    char buf[ 128 ] ;

9    INIT();

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

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

12   if ( !isvalidsock( s1 ) )

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

14   if ( setsockopt( si, SOL_SOCKET, SO_KEEPALIVE,

15     ( char * )&on, sizeof ( on ) ) )

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

17   for ( ;; )

18   {

19     rc = readline( si, buf, sizeof( buf ) );

20     if ( rc == 0 )

21      error( 1, 0, "другой конец отключился\n" );

22     if ( rc < 0 )

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

24     write( 1, buf, rc );

25   }

26   }


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