Книга: Разработка приложений в среде Linux. Второе издание

17.5.5. Преобразование имен в адреса

17.5.5. Преобразование имен в адреса

Длинные последовательности чисел являются отлично подходящим методом идентификации для компьютеров, позволяющим им однозначно узнавать друг друга. Однако большинство людей охватывает ужас при мысли о том, что придется иметь дело с большим количеством цифр. Для того чтобы разрешить людям применять текстовые названия для компьютеров вместо числовых, в состав протоколов TCP/IP входит распределенная база данных для взаимных преобразований имен хостов и IP-адресов. Эта база данных называется DNS (Domain Name System — служба имен доменов), она подробно рассматривается в [34] и [1].

Служба DNS предлагает много функций, но сейчас нас интересует одна — возможность преобразования IP-адресов в имена хостов и наоборот. Несмотря на то что это преобразование должно выполняться как однозначное соответствие, на самом деле оно представляет собой отношение типа "многие ко многим". Другими словами, каждый IP-адрес может соответствовать нулю или более именам хостов, а каждое имя хоста соответствует нулю или более IP-адресам.

Использование неоднозначного соответствия между именами хостов и IP-адресами может показаться странным. Однако многие Internet-сайты применяют одну и ту же машину для ftp-сайта и Web-сайта. При этом адреса www.some.org и ftp.some.org должны ссылаться на одну и ту же машину, а для одной машины не нужны два IP-адреса. Таким образом, два имени хостов сводятся к одному IP-адресу. Каждый IP-адрес имеет одно первичное, или каноническое имя хоста, которое используется, если IP-адрес требуется преобразовать в единственное имя хоста во время обратного поиска имен.

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

Появление IPv6 повлекло за собой еще одну причину, по которой одно имя хоста должно иметь несколько адресов. Многие машины сейчас имеют одновременно и IPv4-, и IPv6-адреса.

Библиотечная функция getaddrinfo()[134] предлагает программам простой доступ к преобразованиям имен хостов DNS.

#include <sys/types.h>
#include <socket.h>
#include <netdb.h>
int getaddrinfo(const char * hostname, const char * servicename,
 const struct addrinfo * hints, struct addrinfo ** res);

Концепция этой функции достаточно простая, однако весьма мощная, в связи с этим ее описание может показаться несколько запутанным. Идея заключается в том, что функция принимает имя хоста, имя службы (или оба из них) и превращает их в список IP-адресов. Затем с использованием hints список фильтруется и те адреса, которые не нужны приложению, отбрасываются. Окончательный список возвращается в виде связного списка в переменной res.

Искомое имя хоста содержится в первом параметре и может равняться NULL, если производится поиск только службы. Параметр hostname может быть именем (например, www.ladweb.net) или IP-адресом (с точками или двоеточиями в качестве разделителей), который функция getaddrinfo() преобразует в двоичный адрес.

Второй параметр servicename указывает имя той службы, для которой нужно извлечь официальный порт. Если он равен NULL, то поиск службы не выполняется.

Структура struct addrinfo используется как для hints (при фильтрации полного списка адресов), так и для передачи окончательного списка в приложение.

#include <netdb.h>
struct addrinfo {
 int ai_flags;
 int ai_family;
 int ai_socktype;
 int ai_protocol;
 socklen_t ai_addrlen;
 struct sockaddr_t * ai_addr;
 char * ai_canonname;
 struct addrinfo * next;
}

Если struct addrinfo используется для параметра hints, то участвуют только первые четыре члена, остальные должны равняться нулю или NULL. Если задано значение ai_family, то getaddrinfo() возвращает адреса только для указанного семейства протоколов (например, PF_INET). Аналогично, если устанавливается ai_socktype, то возвращаются только адреса данного типа сокета.

Член ai_protocol позволяет ограничивать результаты определенным протоколом. Этот параметр нельзя применять, если не установлен параметр ai_family, а также, если числовое значение протокола (такое как IPPROTO_TCP) не является уникальным среди всех протоколов; он хорошо подходит только для PF_INET и PF_INET6.

Последний член, используемый для hints — это aflags, который принимает одно или несколько (объединенных логическим "ИЛИ") из перечисленных ниже значений.

AI_ADDRCONFIG

По умолчанию функция getaddrinfo() возвращает все адреса, соответствующие запросу. Данный флаг указывает на возврат адресов только тех протоколов, чьи адреса сконфигурированы в локальной системе. Другими словами, она возвращает только IPv4-адреса в системах с IPv4-интерфейсами и только IPv6-адреса в системах с интерфейсами IPv6.

AI_CANONNAME

При возврате поле ai_canonname содержит каноническое имя хоста для адреса, указанного в struct addrinfo. Поиск этого адреса сопровождается дополнительными поисками в службе DNS и, как правило, не является необходимым.

AI_NUMERICHOST

Параметр hostname должен представлять собой адрес в форме с разделительными запятыми или двоеточиями. Никакие преобразования имени хоста не выполняются. Это предохраняет getaddrinfo() от каких-либо поисков имени хоста, которые могут оказаться весьма длительным процессом.

AI_PASSIVE

Если hostname равен NULL и присутствует этот флаг, то возвращается неустановленный адрес, который позволяет ожидать соединений на всех интерфейсах. Если данный флаг не указан (а значение hostname равно NULL), возвращается адрес обратной связи[135].

Последний параметр res в getaddrinfo() должен быть адресом указателя на struct addrinfo. Для успешного завершения переменная, на которую указывает res, устанавливается на первую запись в односвязном списке адресов, который соответствует запросу. Член ai_next структуры struct addrinfo указывает на следующий член связного списка, и для последнего узла в списке параметр ai_next равен NULL.

Когда приложение завершает работу с возвращенным связным списком, функция freeaddrinfo() освобождает память, занимаемую списком.

#include <sys/types.h>
#include <socket.h>
#include <netdb.h>
void freeaddrinfo(struct addrinfo * res);

Единственным параметром для freeaddrinfo является указатель на первый узел в списке.

Каждый узел в возвращаемом списке имеет тип struct addrinfo и специфицирует один адрес, соответствующий запросу. Каждый адрес содержит не только IPv4- или IPv6-адрес, он также определяет тип соединения (например, дейтаграмма) и протокол (такой как UDP). Если для одного IP-адреса в запросе подходит несколько типов соединений, то данный адрес включается в несколько узлов.

Каждый узел содержит описанную ниже информацию.

• ai_family — семейство протоколов (PF_INET или PF_INET6), к которому принадлежит адрес.

• ai_socktype — тип соединения для адреса (как правило, принимает одно из значений SOCK_STREAM, SOCK_DGRAM или SOCK_RAW).

• ai_protocol — протокол для адреса (обычно IPPROTO_TCP или IPPROTO_UDP).

• Если в параметре hints был указан флаг AI_CANONNAME, то ai_canonname содержит каноническое имя для адреса.

• ai_addr указывает на struct sockaddr для соответствующего протокола. Например, если ai_family принимает значение PF_INET, то ai_addr указывает на struct sockaddr_in. Член ai_addrlen определяет длину структуры, на которую указывает ai_addr.

• Если предусмотрен параметр servicename, то в качестве номера порта в каждом адресе устанавливается официальный порт данной службы. В противном случае номер порта для каждого адреса равен нулю.

• Если не был передан параметр hostname, то номера портов устанавливаются для каждого адреса, однако в качестве IP-адреса определяется или адрес обратной связи, или неустановленный адрес (как указывалось ранее в описании флага AI_PASSIVE).

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

 1: /* clientlookup.c */
 2:
 3: #include <netdb.h>
 4: #include <stdio.h>
 5: #include <string.h>
 6:
 7: int main(int argc, const char ** argv) {
 8:  struct addrinfo hints, * addr;
 9:  const char * host = argv[1], * service = argv[2];
10:  int rc;
11:
12:  if (argc != 3) {
13:   fprintf(stderr, "требуется в точности два аргументаn");
14:   return 1;
15:  }
16:
17:  memset(&hints, 0, sizeof(hints));
18:
19:  hints.ai_socktype = SOCK_STREAM;
20:  hints.ai_flags = AI_ADDRCONFIG;
21:  if ((rc = getaddrinfo(host, service, &hints, &addr)))
22:   fprintf(stderr, "сбой поискаn");
23:  else
24:   freeaddrinfo(addr);
25:
26:  return 0;
27: }

Давайте обратим внимание на строки 17–24 этой программы. После очистки структуры hints приложение запрашивает адреса SOCK_STREAM, которые используют протокол, сконфигурированный на локальной системе (путем установки флага AI_ADDRCONFIG). Затем активизируется функция getaddrinfo() с именем хоста, именем службы, подсказками и в случае невозможности найти соответствие отображается сообщение об ошибке. Если все проходит нормально, то первый элемент в связном списке, на который указывает addr, представляет собой соответствующий адрес, который программа может использовать для установки соединения с указанной службой и хостом. Программа не решает, через какой протокол (IPv4 или IPv6) соединение будет лучшим.

Серверные приложения немного проще. В них, как правило, требуется согласиться на соединение с определенным портом, при этом на всех адресах. Если установлены флаги AI_PASSIVE, функция getaddrinfo() возвращает адрес, вынуждающий ядро разрешать все соединения (со всеми адресами, которые оно знает) при условии, что в качестве первого параметра передается NULL. Как и в клиентском примере, используется AI_ADDRCONFIG, дабы убедиться, что возвращаемый адрес соответствует протоколу, который поддерживает данная машина.

 1: /* serverlookup.с */
 2:
 3: #include <netdb.h>
 4: #include <stdio.h>
 5: #include <string.h>
 6:
 7: int main(int argc, const char ** argv) {
 8:  struct addrinfo hints, * addr;
 9:  const char * service = argv[1];
10:  int rc;
11:
12:  if (argc != 3) {
13:   fprintf(stderr, "требуется в точности один аргументn");
14:   return 1;
15:  }
16:
17:  memset(&hints, 0, sizeof(hints));
18:
19:  hints.ai_socktype = SOCK_STREAM;
20:  hints.ai_flags = AI_ADDRCONFIG | AI_PASSIVE;
21:  if ((rc = getaddrinfo(NULL, service, &hints, &addr)))
22:   fprintf(stderr, "сбой поискаn");
23:  else
24:   freeaddrinfo(addr);
25:
26:  return 0;
27: }

После успешного завершения работы getaddrinfo() первый узел в связном списке может использоваться сервером для установки сокета.

Следующий пример демонстрирует куда более полезную программу. Она предоставляет интерфейс командной строки для большинства возможностей getaddrinfo(). Она дает возможность пользователю указывать имя хоста или имя службы (или оба имени), тип сокета (потоковый или дейтаграммный), семейство адресов, протокол (TCP или UDP). Пользователь может также запрашивать программу отображать каноническое имя или только те адреса для протоколов, для которых сконфигурирована машина (через флаг AI_ADDRCONFIG). Ниже показано, как можно применить программу для извлечения адреса для telnet-соединения с локальной машиной (данная машина сконфигурирована и под IPv4, и под IPv6).

$ ./netlookup --hdst localhost --service telnet
IPv6 stream tcp port 23 host ::1
IPv6 dgram  udp port 23 host ::l
IPv4 stream tcp port 23 host 127.0.0.1
IPv4 dgram  udp port 23 host 127.0.0.1

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

[ewt@patton code]$ ./netlookup --host localhost -service telnet --stream
IPv6 stream tcp port 23 host ::1
IPv4 stream tcp port 23 host 127.0.0.1

После возврата локальной машины в исходное состояние для IPv6, та же самая команда выглядит следующим образом.

[ewt@patton code]$ ./netlookup --host localhost --service telnet —stream
IPv4 stream tcp port 23 host 127.0.0.1

Вот так выглядит поиск соответствия для хоста Internet, который имеет и IPv4, и IPv6 конфигурации.

$ ./netlookup --host www.6bone.net —stream
IPv6 stream tcp host 3ffe:b00:c18:1::10
IPv4 stream tcp host 206.123.31.124

Для того чтобы увидеть полный перечень опций командной строки, которые предлагает netlookup.с, запустите данную программу без параметров.

  1: /* netlookup.с */
  2:
  3: #include <netdb.h>
  4: #include <arpa/inet.h>
  5: #include <netinet/in.h>
  6: #include <stdio.h>
  7: #include <string.h>
  8: #include <stdlib.h>
  9:
 10: /* Вызывается, если во время обработки командной строки происходит ошибка;
 11:    отображает короткое сообщение для пользователя и завершается. */
 12: void usage(void) {
 13:  fprintf(stderr, "использование: netlookup [--stream] [--dgram] "
 14:   "[--ipv4] [--ipv6] [--name] [--udp]n");
 15:  fprintf (stderr, " [--tcp] [--cfg] "
 16:   "[--service <служба>] [--host <имя_хоста>]n");
 17:  exit(1);
 18: }
 19:
 20: int main(int argc, const char ** argv) {
 21:  struct addrinfo * addr, * result;
 22:  const char ** ptr;
 23:  int rc;
 24:  struct addrinfo hints;
 25:  const char * serviceName = NULL;
 26:  const char * hostName = NULL;
 27:
 28:  /* очищает структуру подсказок */
 29:  memset(&hints, 0, sizeof(hints));
 30:
 31:  /* анализирует аргументы командной строки, игнорируя argv[0]
 32:
 33:     Структура hints, параметры serviceName и hostName будут
 34:     заполнены на основе переданных аргументов. */
 35:  ptr = argv + 1;
 36:  while (*ptr && *ptr[0] == '-') {
 37:   if (!strcmp(*ptr, "--ipv4"))
 38:    hints.ai_family = PF_INET;
 39:   else if (!strcmp(*ptr, "--ipv6"))
 40:    hints.ai_family = PF_INET6;
 41:   else if (!strcmp(*ptr, "--stream"))
 42:    hints.ai_socktype = SOCK_STREAM;
 43:   else if (!strcmp(*ptr, "--dgram"))
 44:    hints.ai_socktype = SOCK_DGRAM;
 45:   else if (!strcmp(*ptr, "--name"))
 46:    hints.ai_flags |= AI_CANONNAME;
 47:   else if (!strcmp(*ptr, "--cfg"))
 48:    hints.ai_flags |= AI_ADDRCONFIG;
 49:   else if (!strcmp(*ptr, "--tcp")) {
 50:    hints.ai_protocol = IPPROTO_TCP;
 51:   } else if (!strcmp(*ptr, "--udp")) {
 52:    hints.ai_protocol = IPPROTO_UDP;
 53:   } else if (!strcmp(*ptr, "--host")) {
 54:    ptr++;
 55:    if (!*ptr) usage();
 56:    hostName = *ptr;
 57:   } else if (!strcmp(*ptr, "--service")) {
 58:    ptr++;
 59:    if (!*ptr) usage();
 60:    serviceName = *ptr;
 61:   } else
 62:    usage();
 63:
 64:   ptr++;
 65:  }
 66:
 67:  /* необходимы имена hostName, serviceName или оба */
 68:  if (!hostName && !serviceName)
 69:   usage();
 70:
 71:  if ((rc = getaddrinfo(hostName, serviceName, &hints,
 72:   &cresult))) {
 73:   fprintf(stderr, "сбой поиска службы: %sn",
 74:    gai_strerror(rc));
 75:   return 1;
 76:  }
 77:
 78:  /* проходит по связному списку, отображая все результаты */
 79:  addr = result;
 80:  while (addr) {
 81:   switch (addr->ai_family) {
 82:   case PF_INETs: printf("IPv4");
 83:    break;
 84:   case PF_INET6: printf("IPv6");
 85:    break;
 86:   default: printf("(%d) addr->ai_family);
 87:    break;
 88:   }
 89:
 90:   switch (addr->ai_socktype) {
 91:   case SOCK_STREAM: printf("tstream");
 92:    break;
 93:   case SOCK_DGRAM: printf("tdgram");
 94:    break;
 95:   case SOCK_RAW: printf("traw");
 96:    break;
 97:   default: printf("t(%d)
 98:    addr->ai_socktype);
 99:    break;
100:   }
101:
102:   if (addr->ai_family == PF_INET ||
103:    addr->ai_family == PF_INET6)
104:    switch (addr->ai_protocol) {
105:    case IPPROTO_TCP: printf("ttcp");
106:     break;
107:    case IPPROTO_UDP: printf("tudp");
108:     break;
109:    case IPPROTO_RAW: printf("traw");
110:     break;
111:    default: printf("t(%d)
112:     addr->ai_protocol);
113:     break;
114:    }
115:   else
116:    printf("t");
117:
118:   /* отобразить информацию и для IPv4-, и для IPv6-адресов */
119:
120:   if (addr->ai_family == PF_INET) {
121:    struct sockaddr_in * inetaddr = (void*)addr->ai_addr;
122:    char nameBuf[INET_ADDRSTRLEN];
123:
124:    if (serviceName)
125:     printf("tпорт%d", ntohs(inetaddr->sin_port));
126:
127:    if (hostName)
128:     printf("tхост%s",
129:      inet_ntop(AF_INET, &inetaddr->sin_addr,
130:       nameBuf, sizeof(nameBuf)));
131:   } else if (addr->ai_family == PF_INET6) {
132:    struct sockaddr_in6 * inetaddr =
133:     (void*)addr->ai_addr;
134:    char nameBuf[INET6_ADDRSTRLEN];
135:
136:    if (serviceName)
137:     printf("tпорт%d", ntohs(inetaddr->sin6_port));
138:
139:    if (hostName)
140:     printf("tхост%s",
141:      inet_ntop(AF_INET6, &inetaddr->sin6_addr,
142:       nameBuf, sizeof(nameBuf)));
143:   }
144:
145:   if (addr->ai_canonname)
146:    printf("tname%s", addr->ai_canonname);
147:
148:   printf("n");
149:
150:   addr = addr->ai_next;
151:  }
152:
153:  /* очистить результаты getaddrinfo() */
154:  freeaddrinfo(result);
155:
156:  return 0;
157: }

В отличие от большинства библиотечных функций, getaddrinfo() возвращает целое число, которое равно нулю в случае успеха, и описывает ошибку в случае неудачи. Такие функции, как правило, не используют errno. В табл. 17.3 описаны различные коды ошибок, которые могут возвращать подобные функции.

Таблица 17.3. Ошибки поиска соответствия адреса и имени

Ошибка Описание
EAI_AGAIN Имя не может быть найдено. Повторный поиск может оказаться успешным.
EAI_BADFLAGS В функцию переданы недействительные флаги.
EAI_FAIL В процессе поиска соответствия возникла постоянная ошибка.
EAI_FAMILY Семейство адресов не распознано.
EAI_MEMORY Запрос на выделение памяти не выполнен.
EAI_NONAME Имя или адрес невозможно преобразовать.
EAI_OVERFLOW Переданный буфер слишком мал.
EAI_SERVICE Для данного типа сокета служба не существует.
EAI_SOCKTYPE Был передан недействительный тип сокета.
EAI_SYSTEM Произошла системная ошибка; сама ошибка содержится в переменной errno.

Коды ошибок можно преобразовать в строки, описывающие проблему, с помощью функции gai_strerror().

#include <netdb.h>
const char * gai_strerror(int error);

Здесь параметр error должен быть ненулевым значением, возвращенным функцией getaddrinfo(). Если произошла ошибка EAI_SYSTEM, то для получения более точного описания программа должна использовать strerror(errno).

Оглавление книги


Генерация: 1.307. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз