UNIX: разработка сетевых приложений - Уильям Стивенс
Шрифт:
Интервал:
Закладка:
Сравнение времени выполнения различных версий функции str_cli
Итак, мы продемонстрировали четыре различных версии функции str_cli. Для каждой версии мы покажем время, которое потребовалось для ее выполнения, в том числе и для версии, использующей программные потоки (см. листинг 26.1). В каждом случае было скопировано 2000 строк от клиента Solaris к серверу с периодом RTT, равным 175 мс:
■ 354,0 с, режим остановки и ожидания (см. листинг 5.4);
■ 12,3 с, функция select и блокируемый ввод-вывод (см. листинг 6.2);
■ 6,9 с, неблокируемый ввод-вывод (см. листинг 16.1);
■ 8,7 с, функция fork (см. листинг 16.6);
■ 8,5 с, версия с потоками (см. листинг 26.1).
Наша версия с неблокируемым вводом-выводом почти вдвое быстрее версии, использующей блокируемый ввод-вывод с функцией select. Наша простая версия с применением функции fork медленнее версии с неблокируемым вводом- выводом. Тем не менее, учитывая сложность кода неблокируемого ввода-вывода по сравнению с кодом функции fork, мы рекомендуем более простой подход.
16.3. Неблокируемая функция connect
Когда сокет TCP устанавливается как неблокируемый, а затем вызывается функция connect, она немедленно возвращает ошибку EINPROGRESS, однако трехэтапное рукопожатие TCP продолжается. Далее мы с помощью функции select проверяем, успешно или нет завершилось установление соединения. Неблокируемая функция connect находит применение в трех случаях:
1. Трехэтапное рукопожатие может наложиться на какой-либо другой процесс. Для выполнения функции connect требуется один период обращения RTT (см. раздел 2.5), и это может занять от нескольких миллисекунд в локальной сети до сотен миллисекунд или нескольких секунд в глобальной сети. Это время мы можем провести с пользой, выполняя какой-либо другой процесс.
2. Мы можем установить множество соединений одновременно, используя эту технологию. Этот способ уже стал популярен в применении к веб-браузерам, и такой пример мы приводим в разделе 16.5.
3. Поскольку мы ждем завершения установления соединения с помощью функции select, мы можем задать предел времени для функции select, что позволит нам сократить тайм-аут для функции connect. Во многих реализациях тайм-аут функции connect лежит в пределах от 75 с до нескольких минут. Бывают случаи, когда приложению нужен более короткий тайм-аут, и одним из решений может стать использование неблокируемой функции connect. В разделе 14.2 рассматриваются другие способы помещения тайм-аута в операции с сокетами.
Как бы просто ни выглядела неблокируемая функция connect, есть ряд моментов, которые следует учитывать.
■ Даже если сокет является неблокируемым, то когда сервер, с которым мы соединяемся, находится на том же узле, обычно установление соединения происходит немедленно при вызове функции connect.
■ В Беркли-реализациях (а также POSIX) имеются два следующих правила, относящихся к функции select и неблокируемой функции connect: во-первых, когда соединение устанавливается успешно, дескриптор становится готовым для записи [128, с. 531], и во-вторых, когда при установлении соединения встречается ошибка, дескриптор становится готовым как для чтения, так и для записи [128, с. 530].
ПРИМЕЧАНИЕЭти два правила в отношении функции select выпадают из общего ряда наших правил из раздела 6.3 относительно условий, при которых дескриптор становится готовым для чтения или записи. В сокет TCP можно записывать, если достаточно места в буфере отправки (что всегда будет выполнено в случае присоединенного сокета, поскольку мы еще ничего не записали в сокет) и сокет является присоединенным (что выполняется, только когда завершено трехэтапное рукопожатие). При наличии ошибки, ожидающей обработки, появляется возможность читать из сокета и записывать в сокет.
С неблокируемыми функциями connect связано множество проблем переносимости, которые мы отметим в последующих примерах.
16.4. Неблокируемая функция connect: клиент времени и даты
В листинге 16.7 показана наша функция connect_nonb, вызывающая неблокируемую функцию connect. Мы заменяем вызов функции connect, имеющийся в листинге 1.1, следующим фрагментом кода:
if (connect_nonb(sockfd, (SA*)&servaddr, sizeof(servaddr), 0) < 0)
err_sys("connect error");
Первые три аргумента являются обычными аргументами функции connect, а четвертый аргумент — это число секунд, в течение которых мы ждем завершения установления соединения. Нулевое значение подразумевает отсутствие тайм- аута для функции select; следовательно, для установления соединения TCP ядро будет использовать свой обычный тайм-аут.
Листинг 16.7. Неблокируемая функция connect
//lib/connect_nonb.c
1 #include "unp.h"
2 int
3 connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec)
4 {
5 int flags, n, error;
6 socklen_t len;
7 fd_set rset, wset;
8 struct timeval tval;
9 flags = Fcntl(sockfd, F_GETFL, 0);
10 Fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
11 error = 0;
12 if ((n = connect(sockfd, saptr, salen)) < 0)
13 if (errno != EINPROGRESS)
14 return (-1);
15 /* Пока соединение устанавливается, мы можем заняться чем-то другим */
16 if (n == 0)
17 goto done; /* функция connect завершилась немедленно */
18 FD_ZERO(&rset);
19 FDSET(sockfd, &rset);
20 wset = rset;
21 tval.tv_sec = nsec;
22 tval.tv_usec = 0;
23 if ((n = Select(sockfd + 1, &rset, &wset, NULL,
24 nsec ? &tval : NULL)) == 0) {
25 close(sockfd); /* тайм-аут */
26 errno = ETIMEDOUT;
27 return (-1);
28 }
29 if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
30 len = sizeof(error);
31 if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
32 return (-1); /*в Solaris ошибка, ожидающая обработки */
33 } else
34 err_quit("select error: sockfd not set");
35 done:
36 Fcntl(sockfd, F_SETFL, flags); /* восстанавливаем флаги, задающие статус файла */
37 if (error) {
38 close(sockfd); /* на всякий случай */
39 errno = error;
40 return (-1);
41 }
42 return (0);
43 }
Задание неблокируемого сокета9-10 Мы вызываем функцию fcntl, которая делает сокет неблокируемым.
11-14 Мы вызываем неблокируемую функцию connect. Ошибка, которую мы ожидаем (EINPROGRESS), указывает на то, что установление соединения началось, но еще не завершилось [128, с. 466]. Любая другая ошибка возвращается вызывающему процессу.
Выполнение других процессов во время установления соединения15 На этом этапе мы можем делать все, что захотим, ожидая завершения установления соединения.
Проверка немедленного завершения16-17 Если неблокируемая функция connect возвратила нуль, установление соединения завершилось. Как мы сказали, это может произойти, когда сервер находится на том же узле, что и клиент.
Вызов функции select18-24 Мы вызываем функцию select и ждем, когда сокет будет готов либо для чтения, либо для записи. Мы обнуляем rset, включаем бит, соответствующий sockfd в этом наборе дескрипторов и затем копируем rset в wset. Это присваивание, возможно, является структурным присваиванием, поскольку обычно наборы дескрипторов представляются как структуры. Далее мы инициализируем структуру timeval и затем вызываем функцию select. Если вызывающий процесс задает четвертый аргумент нулевым (что соответствует использованию тайм-аута по умолчанию), следует задать в качестве последнего аргумента функции select пустой указатель, а не структуру timeval с нулевым значением (означающим, что мы не ждем вообще).
Обработка тайм-аутов25-28 Если функция select возвращает нуль, это означает, что время таймера истекло, и мы возвращаем вызывающему процессу ошибку ETIMEDOUT. Мы также закрываем сокет, чтобы трехэтапное рукопожатие не продолжалось.
Проверка возможности чтения или записи29-34 Если дескриптор готов для чтения или для записи, мы вызываем функцию getsockopt, чтобы получить ошибку сокета (SO_ERROR), ожидающую обработки. Если соединение завершилось успешно, это значение будет нулевым. Если при установлении соединения произошла ошибка, это значение является значением переменной errno, соответствующей ошибке соединения (например, ECONNREFUSED, ETIMEDOUT и т.д.). Мы также сталкиваемся с нашей первой проблемой переносимости. Если происходит ошибка, Беркли-реализации функции getsockopt возвращают нуль, а ошибка, ожидающая обработки, возвращается в нашей переменной error. Но в системе Solaris сама функция getsockopt возвращает -1, а переменная errno при этом принимает значение, соответствующее ошибке, ожидающей обработки. В нашем коде обрабатываются оба сценария.