Обработка тайм-аутов
25-28
Если функция
select
возвращает нуль, это означает, что время таймера истекло, и мы возвращаем вызывающему процессу ошибку
ETIMEDOUT
. Мы также закрываем сокет, чтобы трехэтапное рукопожатие не продолжалось.
Проверка возможности чтения или записи
29-34
Если дескриптор готов для чтения или для записи, мы вызываем функцию
getsockopt
, чтобы получить ошибку сокета (
SO_ERROR
), ожидающую обработки. Если соединение завершилось успешно, это значение будет нулевым. Если при установлении соединения произошла ошибка, это значение является значением переменной
errno
, соответствующей ошибке соединения (например,
ECONNREFUSED
,
ETIMEDOUT
и т.д.). Мы также сталкиваемся с нашей первой проблемой переносимости. Если происходит ошибка, Беркли-реализации функции
getsockopt
возвращают нуль, а ошибка, ожидающая обработки, возвращается в нашей переменной
error
. Но в системе Solaris сама функция
getsockopt
возвращает -1, а переменная
errno
при этом принимает значение, соответствующее ошибке, ожидающей обработки. В нашем коде обрабатываются оба сценария.
Восстановление возможности блокировки сокета и завершение
36-42
Мы восстанавливаем флаги, задающие статус файла, и возвращаемся. Если наша переменная errno имеет ненулевое значение в результате выполнения функции
getsockopt
, это значение хранится в переменной
errno
, и функция возвращает -1.
Как мы сказали ранее, проблемы переносимости для функции
connect
связаны с различными реализациями сокетов и отключения блокировки. Во-первых, возможно, что установление соединения завершится и придут данные для собеседника до того, как будет вызвана функция
select
. В этом случае сокет будет готов для чтения и для записи при успешном выполнении функции, как и при неудачном установленном соединении. В нашем коде, показанном в листинге 16.7, этот сценарий обрабатывается при помощи вызова функции
getsockopt
и проверки на наличие ошибки, ожидающей обработки, для сокета.
Во-вторых, проблема в том, как определить, успешно завершилось установление соединения или нет, если мы не можем считать возможность записи единственным указанием на успешное установление соединения. В Usenet предлагалось множество решений этой проблемы, которые заменяют наш вызов функции
getsockopt
в листинге 16.7:
1. Вызвать функцию
getpeername
вместо функции
getsockopt
. Если этот вызов окажется неудачным и возвратится ошибка
ENOTCONN
, значит, соединение не было установлено, и чтобы получить ошибку, ожидающую обработки, следует вызвать для сокета функцию
getsockopt
с
SO_ERROR
.
2. Вызвать функцию
read
с нулевым значением аргумента
length
. Если выполнение функции read окажется неудачным, функция connect выполнилась неудачно, и переменная errno из функции
read
при этом указывает на причину неудачной попытки установления соединения. Если соединение успешно установлено, функция
read
возвращает нуль.
3. Снова вызвать функцию
connect
. Этот вызов окажется неудачным, и если ошибка —
EISCONN
, сокет уже присоединен, а значит, первое соединение завершилось успешно.
К сожалению, неблокируемая функция
connect
— это одна из самых сложных областей сетевого программирования с точки зрения переносимости. Будьте готовы к проблемам совместимости, особенно с более ранними реализациями. Более простой технологией является создание потока (см. главу 26) для обработки соединения.
Прерванная функция connect
Что происходит, если наш вызов функции
connect
на обычном блокируемом сокете прерывается, скажем, перехваченным сигналом, прежде чем завершится трехэтапное рукопожатие TCP? Если предположить, что функция
connect
не перезапускается автоматически, то она возвращает ошибку
EINTR
. Но мы не можем снова вызвать функцию connect, чтобы добиться завершения установления соединения. Это приведет к ошибке
EADDRINUSE
.
Все, что требуется сделать в этом сценарии, — вызвать функцию
select
, так, как мы делали в этом разделе для неблокируемой функции
connect
. Тогда функция
select
завершится, если соединение успешно устанавливается (делая сокет доступным для записи) или если попытка соединения неудачна (сокет становится доступен для чтения и для записи).
16.5. Неблокируемая функция connect: веб-клиент
Первое практическое использование неблокируемой функции
connect
относится к веб-клиенту Netscape (см. раздел 13.4 [112]). Клиент устанавливает соединение HTTP с веб-сервером и попадает на домашнюю страницу. На этой странице часто присутствуют ссылки на другие веб-страницы. Вместо того чтобы получать последовательно по одной странице за один раз, клиент может получить сразу несколько страниц, используя неблокируемые функции
connect
. На рис. 16.5 показан пример установления множества параллельных соединений. Сценарий, изображенный слева, показывает все три соединения, устанавливаемые одно за другим. Мы считаем, что первое соединение занимает 10 единиц времени, второе — 15, а третье — 4, что в сумме дает 29 единиц времени.
Рис. 16.5. Установление множества параллельных соединений
В центре рисунка показан сценарий, при котором мы выполняем два параллельных соединения. В момент времени 0 запускаются первые два соединения, а когда первое из них устанавливается, мы запускаем третье. Общее время сократилось почти вдвое и равно 15, а не 29 единицам времени, но учтите, что это идеальный случай. Если параллельные соединения совместно используют общий канал связи (допустим, клиент использует модем для соединения с Интернетом), то каждое из этих соединений конкурирует с другими за обладание ограниченными ресурсами этого канала связи, и время установления каждого соединения может возрасти. Например, время 10 может дойти до 15, 15 — до 20, а время 4 может превратиться в 6. Тем не менее общее время будет равно 21 единице, то есть все равно меньше, чем в последовательном сценарии.
В третьем сценарии мы выполняем три параллельных соединения и снова считаем, что эти три соединения не мешают друг другу (идеальный случай). Но общее время при этом такое же (15 единиц), как и во втором сценарии.
При работе с веб-клиентами первое соединение устанавливается само по себе, за ним следуют соединения по ссылкам, обнаруженным в данных от первого соединения. Мы показываем это на рис. 16.6.