Использование select
Другой, более общий метод организации тайм-аута connect состоит в том, чтобы сделать сокет неблокирующим, а затем ожидать с помощью вызова select. При таком подходе удается избежать большинства трудностей, возникающих при попытке воспользоваться alarm, но остаются проблемы переносимости даже между разными UNIX-системами.
Сначала рассмотрим код установления соединения. В каркасе tcpclient.skel Модифицируйте функцию main, как показано в листинге 3.25.
Листинг 3.25. Прерывание connect по тайм-ауту с помощью select
connectto1.с
1 int main( int argc, char **argv )
2 {
3 fd_set rdevents;
4 fd_set wrevents;
5 fd_set exevents;
6 struct sockaddr_in peer;
7 struct timeval tv;
8 SOCKET s;
9 int flags;
10 int rc;
11 INIT();
12 set_address( argv[ 1 ], argv[ 2 ], &peer, "tcp" );
13 S = socket( AF_INET, SOCK_STREAM, 0 );
14 if ( !isvalidsock( s ) )
15 error( 1, errno, "ошибка вызова socket");
16 if( ( flags = fcntl( s, F_GETFL, 0 ) ) < 0 )
17 error( 1, errno, "ошибка вызова fcntl (F_GETFL)");
18 if ( fcntl( s, F_SETFL, flags | 0_NONBLOCK ) < 0 )
19 error( 1, errno, "ошибка вызова fcntl (F_SETFL)");
20 if ( ( rc = connect ( s, ( struct sockaddr * )&peer,
21 sizeoff peer ) ) ) && errno != EINPROGRESS )
22 error( 1, errno, "ошибка вызова connect" );
23 if ( rc == 0 ) /* Уже соединен? */
24 {
25 if ( fcntl( s, F_SETFL, flags ) < 0 )
26 error(1,errno,"ошибка вызова fcntl (восстановление флагов)”);
27 client( s, &peer );
28 EXIT( 0 );
29 }
30 FD_ZERO( &rdevents );
31 FD_SET( s, krdevents );
32 wrevents = rdevents;
33 exevents = rdevents;
34 tv.tv_sec = 5;
35 tv.tv_usec =0;
36 rc = select( s + 1, &rdevents, &wrevents, &exevents, &tv );
37 if ( rc < 0 )
38 error( 1, errno, "ошибка вызова select" );
39 else if ( rc == 0 )
40 error( 1, 0, "истек тайм-аут connect\n" );
41 else if ( isconnected( s, &rdevents, &wrevents, kexevents ))
42 {
43 if (fcntl (s, F_SETFL, flags) < 0)
44 error(1,errno,"ошибка вызова fcntl(восстановление флагов)");
45 client( s, &peer );
46 }
47 else
48 error( 1, errno, "ошибка вызова connect");
49 EXIT( 0 );
50 }
Инициализация
16- 19 Получаем текущие флаги, установленные для сокета, с помощью операции OR, добавляем к ним флаг O_NONBLOCK и устанавливаем новые флаги.
Инициирование connect
20-29 Начинаем установление соединения с помощью вызова connect. Поскольку сокет помечен как неблокирующий, connect немедленно возвращает управление. Если соединение уже установлено (это возможно, если, например, вы соединялись с той машиной, на которой запущена программа), то connect вернет нуль, поэтому возвращаем сокет в режим блокирования и вызываем функцию client. Обычно в момент Возврата из connect соединение еще не установлено, и приходит код EINPROGRESS. Если возвращается другой код, то печатаем диагностическое сообщение и завершаем программу.
Вызов select
30-36 Подготавливаем, как обычно, данные для select и, в частности, устанавливаем тайм-аут на пять секунд. Также следует объявить заинтересованность в событиях исключения. Зачем - станет ясно позже.
Обработка код возврата select
37-40 Если select возвращает код ошибки или признак завершения по тайм-ауту, то выводим сообщение и заканчиваем работу. В случае ответа можно было бы, конечно, сделать что-то другое.
41-46 Вызываем функцию isconnected, чтобы проверить, удалось ли установить соединение. Если да, возвращаем сокет в режим блокирования и вызываем функцию client. Текст функции isconnected приведен в листингах 3.26 и 3.27.
4 7-48 Если соединение не установлено, выводим сообщение и завершаем сеанс.
К сожалению, в UNIX и в Windows применяются разные методы уведомления об успешной попытке соединения. Поэтому проверка вынесена в отдельную функцию. Сначала приводится UNIX-версия функции isconnected.
В UNIX, если соединение установлено, сокет доступен для записи. Если же произошла ошибка, то сокет будет доступен одновременно для записи и для чтения. Однако на это нельзя полагаться при проверке успешности соединения, поскольку можно возвратиться из connect и получить первые данные еще до обращения к select. В таком случае сокет будет доступен и для чтения, и для записи -в точности, как при возникновении ошибки.
Листинг 3.26. UNIX-версия функции isconnected
1 int isconnected( SOCKET s, fd_set *rd, fd_set *wr, fd_set *ex )
2 {
3 int err;
4 int len = sizeoff err );
5 errno =0; /* Предполагаем, что ошибки нет. */
6 if ( !FD_ISSET( s, rd ) && !FD_ISSET( s, wr ) )
7 return 0;
8 if (getsockopt( s, SOL_SOCKET, SO_ERROR, &err, &len ) < 0)
9 return 0;
10 errno = err; /* Если мы не соединились. */
11 return err == 0;
12 }
5-7 Если сокет не доступен ни для чтения, ни для записи, значит, соединение не установлено, и возвращается нуль. Значение errno заранее установлено в нуль, чтобы вызывающая программа могла определить, что сокет действительно, не готов (разбираемый случай) или имеет Metro ошибка.
8-11 Вызываем getsockopt для получения статуса сокета. В некоторых версиях UNIX getsockopt возвращает в случае ошибки -1. В таком случае записываем в errno код ошибки. В других версиях система просто возвращает статус, оставляя его проверку пользователю. Идея кода, который корректно работает в обоих случаях, позаимствована из книги [Stevens 1998].
Согласно спецификации Winsock, ошибки, которые возвращает connect через неблокирующий сокет, индицируются путем возбуждения события исключения в select. Следует заметить, что в UNIX событие исключения всегда свидетельствует о поступлении срочных данных. Версия функции isconnected для;Windows показана в листинге 3.27.
Листинг 3.27. Windows-версия функции isconnected
1 int isconnected( SOCKET s, fd_set *rd, fd_set *wr, fd_set *ex)
2 {
3 WSASetLastError ( 0 );
4 if ( !FD_ISSET( s, rd ) && !FD_ISSET(s, wr ) )
5 return 0;
6 if ( FD_ISSET( s, ex ) )
7 return 0;
8 return 1;
9 }
3-5 Так же, как и в версии для UNIX, проверяем, соединен ли сокет. Если нет, устанавливаем последнюю ошибку в нуль и возвращаем нуль.
6-8 Если для сокета есть событие исключения, возвращается нуль, в противном случае - единица.