Rambler's Top100 Service калинин.ru / программирование / сети /  << 01.12.00 >>

Неблокирующий connect()

В продолжение темы о замене блокирующего вызова gethostbyname(), хочется рассказать о другой функции интерфейса сокетов, connect(). Она имеет следующий прототип:

int connect(int s, const struct sockaddr *name, socklen_t namelen);

Эта функция предназначена для установки соединения с удаленной машиной после того, как на локальной машине уже открыт сокет под это соединение.

Функция забавна уже тем, как в нее передаются параметры. Все дело в том, что она может использоваться с любыми протоколами передачи данных (а не только TCP/IP), поэтому второй параметр имеет странный тип "указатель на struct sockaddr", которая на самом деле содержит в себе лишь общие определения для всех протоколов. Пусть вас не смущает название этого параметра: name обозначает "имя" не в смысле имени DNS, а в смысле уникального идентификатора сокета (например парой ip-адрес и номер порта).

Я не буду приводить примеры того, как эта функция используется, скорее всего вы и так все это можете найти в документации на вашу операционную систему. Дальше мне бы хотелось немного поговорить о том, как эта функция будет работать.

Проблема связанная с ней, опять заключается в том, что установка соединения вполне вероятно может быть очень длительной операцией за счет плохих каналов передачи данных. Ведь этот процесс заключается в том, что по сети передаются несколько пакетов "туда и обратно" с просьбой открыть сокет, послать подтверждение и прочее... то есть, фактически, connect() опять же ничем принципиально не отличается от функций чтения и записи данных в сокеты. Тем не менее, за счет использования select() мы можем читать из сокета и писать в сокет по готовности, а окончания connect() вынуждены ждать...

Вообще говоря, операции ввода-вывода могут быть блокирующими, неблокирующими, управляемыми сигналами, асинхронными... Нас интересуют первые два типа подобных операций, как видно из названия:

  • Блокирующие операции ввода-вывода приостанавливают процесс до окончания выполнения операции. Например, если что-то читается из сокета (вызовом read()), то управление вернется в программу только в том случае, если пришли реальные данные по этому соединению, либо произошла ошибка (например, разрыв связи).
  • Неблокирующие операции возвращают управление в процесс сразу же после вызова, завершая операцию параллельно с выполнением процесса.

Реальные сетевые программы практически никогда не используют блокирующие функции: это было бы слишком медленно. Или, точнее, могут использовать блокирующий read(), но только удостоверившись в том, что ему есть что прочитать посредством предварительного вызова select().

Но connect() все равно остается блокирующим! До тех пор, пока не будет установлена соответствующая опция сокета. То, о чем я хочу рассказать чуть ниже, не является фокусом, этот прием после Netscape очень давно применяется повсеместно, я сам вычитал это из Unix Network Programming, но почему-то очень часто этого не делают.

Все операции с сокетами можно сделать неблокирующими посредством предварительного вызова функции fcntl():

int fd = socket(AF_INET, SOCK_STREAM, 0);
int flags = fcntl(fd, F_GETFL, 0);
if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
{
  /*
   * Не получилось...
   */
}

"Неблокированность" сокета устанавливается вызовом fcntl() с параметром F_SETFL и соответствующим набором флагов, в который включен O_NONBLOCK. После этого можно не опасаясь вызывать connect(), вызывающий процесс продолжит свое выполнение в любом случае, даже если на установку соединения потребуется несколько десятков секунд:

if(connect(fd, (sockaddr*)&servaddr, sizeof(servaddr)) != 0)
  {
    Лирическое отступление:
   

Проверка кода ошибки после выполнения системного вызова с ошибкой обязательна! Вообще говоря, надо по крайней мере обрабатывать код ошибки EINTR, который является признаком того, что операция была прервана по сигналу, тогда вызов можно повторить.

В случае с EINTR, конечно же, можно установить для всех "безопасных" сигналов флаг SA_RESTART, после чего системный вызов будет автоматически повторен, но это пример тому, что если функция вернула ошибку, то это еще не значит, что что-то прошло неправильно.

    if(errno == EINPROGRESS)
      {
         /*
          * Это не ошибка, это признак
          * того, что соединение все еще
          * не установлено.
          */
      }
    else
      {
         /*
          * Произошла ошибка сразу же
          * при соединении.
          */
      }
  }
else
  {
      /*
       * Соединение было установлено за время
       * системного вызова, работа продолжается
       * традиционным способом.
       */ 
  }

Единственный интерес здесь представляет проверка errno на EINPROGRESS, потому что после этого, перед тем как начать работу с сокетом, надо будет сначала дождаться завершения открытия соединения (а что бы даром времени не терять, параллельно можно заняться другими делами).

Получить информацию о том, что сокет "готов", можно используя все тот же select(). Для этого надо установить файловый дескриптор сокета в обоих множествах (на чтение и на запись):

fd_set rfds, wfds;
struct timeval tv;
tv.tv_sec = 0; tv.tv_usec = 500;

FD_ZERO(&rfds); 
FD_ZERO(&wfds);

int max_fd = -1;

/*
 * Заполняем множества. Дескриптор 
 * сокета, с не открытым соединением
 * помещаем в оба множества.
 */
FD_SET(fd, &wfds);
FD_SET(fd, &rfds);
if(fd > max_fd) max_fd = fd;

select(max_fd + 1, &rfds, &wfds, NULL, &tv);

Если после выполнения select() интересующий нас дескриптор остался в каком-либо из множеств, то соединение завершено. Остается вопрос, как определить, не произошла ли ошибка?

if(FD_ISSET(fd, &wfds) || FD_ISSET(fd, &rfds))
{
  socklen_t err_len;
  int error;
		
  err_len = sizeof(error);
  if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &err_len) < 0 || error != 0)
    {
      /*
       * Произошла ошибка соединения.
       */
    }
  else
    {
      /*
       * Все нормально.
       */
    }
}

Теперь можно обработать ошибку или продолжить нормальное выполнение программы.

Понятно, что в случае одного сокета в программе, такой подход, скорее всего, будет нерационален. Но при обработке большого числа открытых сокетов с использованием select(), неблокирующий connect() будет очень прост в реализации и, одновременно, очень эффективен.

Обычно подпрограмму, которая работает с select() удобно вынести в отдельный поток управления и общаться с ней посредством очередей сокетов "на вход" и полученных данных "на выходе".

Резюме

Неблокирующий connect() является хорошим дополнением к правильно написанному клиенту. Скорость работы с сетью может вырасти значительно (в зависимости от того, насколько плох канал между клиентом и сервером, а, точнее, насколько много серверов с хорошим каналом и сколько серверов с плохим каналом). Очень прост в реализации и очень удобен: вообще, неблокирующие операции ввода-вывода, на мой взгляд, по соотношению скорости работы к удобству программирования самые приемлимые.


Версия для печати


  Ссылки по теме:
W. Richard Stevens
   Unix Network programming, volume 1.
W. Richard Stevens
   TCP/IP Illustrated, volume 1.
W. Richard Stevens,
   TCP/IP Illustrated, volume 2.
  Рядом в разделе:
Событийная модель в WinSock (12.12.00)
   Автором этого текста является Gregory Liokumovich ( ). Он любезно прислал мне этот текст, связанный с программированием сетей при помощи интерфейса...   >>>>
Определение ip-адреса по имени хоста, adns (05.11.00)
   Есть такой, характерный для организации "традиционного" UNIX'а, системный вызов под названием : struct hostent * gethostbyname(const char *name); Традиционен он тем,...   >>>>
  Рядом по дате:
Unix internals: the new frontiers (03.12.00)
   Хочу сразу же предупредить, что эта книга, насколько мне известно, в переводе на русский язык не существует, поэтому прошу прощения, если...   >>>>
cpp3.virtualave.net, C++ 3rd: комментарии (26.11.00)
   Ресурсы на русском языке, посвященные C++, отличаются своим количеством... существует множество сайтов, домашних страничек объединенных общей тематикой программирования на C++. Но...   >>>>
  Содержание:
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
   C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Просто так
Студенческое
Туризм
  Байки
Фотографии
Комментарии
   Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Благодарности
Форум
Хронология
 
  В этом разделе:
События ядра в FreeBSD. (16.07.01)
   Обработка большого количества сетевых соединений всегда затруднительна. Мало того, не существует стандартных решений, подходящих для проблем любого вида, в которых возникает...   >>>>
Обзор CORBA (14.01.01)
   CORBA (расшифровывается как Common Object Request Broker) это технология, которая позволяет рассматривать компоненты распределенной системы как объекты, отвечающие некоторым определенным интерфейсам....   >>>>
"Тонкий" клиент (19.12.00)
   В предыдущей заметке Gregory Liokumovich рассказывал о применении событийной модели WinSock для программирования сетевых приложений. На самом деле, как мне кажется,...   >>>>
Событийная модель в WinSock (12.12.00)
   Автором этого текста является Gregory Liokumovich ( ). Он любезно прислал мне этот текст, связанный с программированием сетей при помощи интерфейса...   >>>>
Неблокирующий connect() (01.12.00)
   В продолжение темы о замене блокирующего вызова , хочется рассказать о другой функции интерфейса сокетов, . Она имеет следующий прототип: int...   >>>>
Определение ip-адреса по имени хоста, adns (05.11.00)
   Есть такой, характерный для организации "традиционного" UNIX'а, системный вызов под названием : struct hostent * gethostbyname(const char *name); Традиционен он тем,...   >>>>
lingering close (29.10.00)
   Когда программа выкачивает один файл с удаленного сервера с использованием протокола TCP, а после этого сразу же "отваливается", то проблем, скорее...   >>>>
Содержание раздела полностью...
   Примерно в тоже время
Unix internals: the new frontiers (03.12.00)
   Хочу сразу же предупредить, что эта книга, насколько мне известно, в переводе на русский язык не существует, поэтому прошу прощения, если...   >>>>
cpp3.virtualave.net, C++ 3rd: комментарии (26.11.00)
   Ресурсы на русском языке, посвященные C++, отличаются своим количеством... существует множество сайтов, домашних страничек объединенных общей тематикой программирования на C++. Но...   >>>>
Хронология полностью...
   Содержание
Заглавная страница
Мой блог
Мое резюме
Дайджест
Программирование
  C&C++
Сети
Unix
Алгоритмы
Оптимизация
Соревнования
Отвлеченно
XML
TeX
Туризм
  Байки
Фотографии
Комментарии
  Книги
Web-ресурсы
Фильмы
Интернет
Программное обеспечение
Жизнь
Студенческое
Просто так
Благодарности
Форум
Хронология
© 2000-2008, Andrey L. Kalinin
mailto:andrey@kalinin.ru
Rambler's Top100