Rambler's Top100 Service Этот текст распечатан с домашней странички Андрея Калинина (www.kalinin.ru).
Оригинал статьи находится по этому адресу: http://www.kalinin.ru/programming/network/05_11_00.shtml


Определение ip-адреса по имени хоста, adns

05.11.00

Есть такой, характерный для организации "традиционного" UNIX'а, системный вызов под названием gethostbyname():

struct hostent *
gethostbyname(const char *name);

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

Собственно, обычно, для того, что бы получить IP-адрес по DNS-имени, используют именно этот вызов. Его недостатками являются уже упомянутая выше не "реентерабельность" (это английское слово, судя по всему, стало термином), отсутствие возможности определения адреса хоста, соответствующему иному протоколу, чем IPv4 и... низкая производительность, как это ни странно.

Надо понимать, что DNS, кроме самих серверов, включает в себя еще и протокол передачи данных, который принципиально ничем не отличается от других (того же HTTP). В предыдущей заметке в этом разделе я уже говорил о том, что программа, выкачивающая один файл, отличается от программы, которая выкачивает несколько файлов. Тоже самое и с обращениями к DNS-серверам: если "разрешение" одного хоста произойдет достаточно быстро (относительно), то в случае с большим количеством хостов все уже не так радужно.

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

Вообще говоря, конечно же, можно как-то попытаться ускорить процесс. Например, перед тем, как обратиться к DNS-серверу, gethostbyname() просматривает содержимое локального файла /etc/hosts... или, еще можно использовать тот факт, что DNS-сервер имеет свой кэш и, если подряд следуют два запроса на одинаковые хосты, ответ на второй запрос будет отдан клиенту из кэша. Таким образом, можно установить DNS-сервер (тот же named) на локальной машине, тогда все повторные запросы будут выполняться моментально.

В случае, когда все хосты известны заранее, можно разбить программу на две части: сначала происходит разрешение имен, а затем --- выкачивание информации. Но, если честно, это не выход.

Более логичное решение заключается в том, что, если программа управляется событиями ввода-вывода, то надо добавить сюда еще и события "определения IP-адреса". Это уже предполагает то, что gethostbyname() использовать больше нельзя: этот вызов блокирует процесс. Можно воспользоваться "реентерабельной" версией gethostbyname_r() и выделить несколько потоков в программе, которые будут постоянно ожидать завершения определения IP-адреса... только это... скорее всего опять несколько неправильно.
  Лирическое отступление:
 

Наверно, все-таки стоит об этом упомянуть здесь: потоки бывают трех типов: потоки ядра, реальные потоки и потоки пользовательские. Первые, понятно, работают внутри ядра и для его нужд; вторые --- то же самое, что и потоки ядра, но с доступом к данным процесса... А вот пользовательские потоки --- это библиотечная реализация потоков и может быть вообще никак не связана с реальными потоками в ОС. Те же pthreads --- на самом деле, пользовательские потоки; собственно, тем и хороши. Вполне вероятно, что по этой тематике я пройдусь когда-нибудь подробнее, но выделение DNS-ресолвера в отдельный пользовательский поток, это вполне здравая мысль. Если, конечно же, вы уверены в том, что сможете это корректно сделать.

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

Теперь, возвращаясь к, собственно, теме сегодняшней заметки. ADNS. Это такой пакет, в который входит библиотека с заголовочными файлами и несколько утилит, которые можно использовать в скриптах. Среди ее преимуществ, кроме того, что она исключает существенные недостатки gethostbyname(), можно выделить еще и такие, как простота использования. Поверьте, это достаточно существенно, потому что подобные библиотеки существуют давно, но их далеко не так удобно использовать в своих программах.

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

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

#include <sys/types.h>
#include <stdio.h>
#include <signal.h>

#include <adns.h>

int main(int argc, char* argv[])
{
  Лирическое отступление:
 

Любая программа, которая работает с сетями, должна каким-либо образом обрабатывать сигнал SIGPIPE, обычно этот сигнал просто игнорируется. Все дело в том, что ядро сгенерирует его для вашей программы в случае обрыва свзязи (что является обычной ситуацией), а обработчик по-умолчанию просто прекратит работу вашей программы.

Библиотека ADNS, в принципе, сама устанавливает обработчик SIGPIPE в SIG_IGN, но это можно отключить :-)

  struct sigaction sigpipe, sigpipeo;
  
  sigemptyset(&sigpipe.sa_mask);
  sigpipe.sa_flags = SA_RESTART;
  sigpipe.sa_handler = SIG_IGN;
  sigaction(SIGPIPE, &sigpipe, &sigpipeo);
  
  adns_state ads = NULL;
  
  /*
   * Инициализация ADNS. 
   */
  adns_init(&ads, adns_if_nosigpipe, 0);

  // ...
  
  /*
   * Осуществляем все запросы к ADNS. В данном случае ---
   * пачкой и сразу, но можно и в процессе обработки.
   */
  for( ; ; )
    {
       // ...

       adns_query aquery = 0;

       /*
        * А вот документацию на эту функцию, скорее
        * всего, посмотреть придется, потому что тут
        * надо указать флаги, характеризующие то, что
        * вы хотите от ADNS получить. Под hostname
        * понимается название хоста.
        */
       adns_submit(ads, hostname,
                   (adns_rrtype)adns_r_addr,
                   (adns_queryflags)adns_qf_owner,
                   0,
                   &aquery);

       // ...
    }
  
  /*
   * Теперь все запросы поданы, надо обработать
   * результаты. В данном случае, запрос _ожидается_.
   * Но можно и просто проверять наличие выполненных
   * запросов.
   */
  for( ; ; )
    {
      adns_answer* answer = NULL;
      adns_query query = NULL;
      
      adns_wait_poll(ads, &query, &answer, NULL);
      
      if(!answer) break;
      
      if(answer->status == adns_s_ok)
        {
          /*
           * Ответ получен. Теперь в answer находится то,
           * что вы "заказали" при submit'е. В данном
           * случае имеют интерес поля answer->owner,
           * answer->rrs и answer->nrrs.
           */
        }
    }
  
  /*
   * Завершение работы. Об этом надо сказать ADNS.
   */
  adns_finish(ads);
  
  return 0;
}

Ресолвер очень быстрый. Желающие могут написать маленькую итеративную программу с использованием gethostbyname(), а потом попробовать утилиту, идущую в комплекте с ADNS, под названием adnshost, и сравнить количество затраченного времени на большом списке хостов. Кстати сказать, еще один из недостатков libwww заключается как раз в том, что работа с DNS в ней, в конце-концов, сводится именно к вызову gethostbyname().

Резюме

Использование gethostbyname в реальных программах обычно приводит к потере производительности. Для решения этой проблемы можно использовать "реентерабельную" версию этого же системного вызова, но лучше взять библиотеки, использующие асинхронные операции ввода-вывода для работы с DNS, например, adns.

Ссылки по теме

http://www.chiark.greenend.org Официальная страница ADNS.
W. Richard Stevens Network programming, volume 1.
W. Richard Stevens TCP/IP illustrated, volume 1.

©2000-2001 by Andrey L. Kalinin,
andrey@kalinin.ru