UNIX: разработка сетевых приложений — страница 12 из 42

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

11.1. Введение

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

gethostbyname
и
gethostbyaddr
для преобразования имен узлов и IP-адресов, и
getservbyname
и
getservbyport
для преобразования имен служб и номеров портов. Здесь же мы рассмотрим две независимые от протоколов функции
getaddrinfo
и
getnameinfo
, осуществляющие преобразование между IP-адресами и именами узлов, а также между именами служб и номерами портов.

11.2. Система доменных имен

Система доменных имен (Domain Name System, DNS) используется прежде всего для сопоставления имен узлов и IP-адресов. Имя узла может быть либо простым (simple name), таким как

solaris
или
bsdi
, либо полным доменным именем (fully qualified domain name, FQDN), например
solaris.unpbook.com.
.

ПРИМЕЧАНИЕ

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

В этом разделе мы рассмотрим только основы DNS, необходимые нам для сетевого программирования. Читатели, интересующиеся более подробным изложением вопроса, могут обратиться к главе 14 [111] и к [1]. Дополнения, требуемые для IPv6, изложены в RFC 1886 [121].

Записи ресурсов

Записи в DNS называются записями ресурсов (resource records, RR). Нас интересуют только несколько типов RR.

■ А. Запись типа А преобразует имя узла в 32-разрядный адрес IPv4. Вот, например, четыре записи DNS для узла

freebsd
в домене
unpbook.com
, первая из которых — это запись типа А:

freebsd IN А    12.106.32.254

        IN AAAA 3ffe:b80:1f8d:1:a00:20ff:fea7:686b

        IN MX   5 freebsd.unpbook.com.

        IN MX   10 mailhost.unpbook.com.

■ AAAA. Запись типа AAAA, называемая «четыре А» (quad А), преобразует имя узла в 128-разрядный адрес IPv6. Название «четыре А» объясняется тем, что 128-разрядный адрес в четыре раза больше 32-разрядного адреса.

■ PTR. Запись PTR (pointer records — запись указателя) преобразует IP-адрес в имя узла. Четыре байта адреса IPv4 располагаются в обратном порядке. Каждый байт преобразуется в десятичное значение ASCII (0-255), а затем добавляется

in-addr.arpa
. Получившаяся строка используется в запросе PTR.

32 полубайта 128-разрядного адреса IPv6 также располагаются в обратном порядке. Каждый полубайт преобразуется в соответствующее шестнадцатеричное значение ASCII (

0-9
,
a-f)
и добавляется к
ip6.arpa
.

Например, две записи PTR для нашего узла

freebsd
будут выглядеть так:

254.32.106.12 in-addr.arpa

b.6.8.6.7.a.e.f.f.f.0.2.0.0.a.0.1.0.0.0.d.8.f.1.0.8.b.0.e.f.f.3.ip6.arpa

■ MX. Запись типа MX (Mail Exchange Record) определяет, что узел выступает в роли «маршрутизирующего почтового сервера» для заданного узла. В приведенном выше примере для узла

solaris
предоставлено две записи типа MX. Первая имеет предпочтительное значение 5, вторая — 10. Когда существует множество записей типа MX, они используются в порядке предпочтения начиная с наименьшего значения.

ПРИМЕЧАНИЕ

Мы не используем в примерах книги записей типа MX, но упоминаем о них, потому что они широко используются в реальной жизни.

■ CNAME. Аббревиатура CNAME означает «каноническое имя» (canonical name). Обычно такие записи используются для присвоения имен распространенным службам, таким как

ftp
и
www
. При использовании имен служб вместо действительного имени узла перемещение службы на другой узел становится прозрачным (то есть незаметным для пользователя). Например, для нашего узла
linux
каноническими именами могут быть следующие записи:

ftp IN CNAME linux.unpbook.com.

www IN CNAME linux.unpbook.com.

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

freebsd
и запись типа А, и запись типа AAAA. Автор помещает и запись типа А, и запись типа AAAA под каноническим именем узла (как показано ниже) и создает три записи RR. Первая запись RR, имя которой оканчивается на
-4
, содержит запись типа А; вторая, с именем, оканчивающимся на
-6
, содержит запись типа AAAA; а третья запись RR, имя которой оканчивается на
-611
, содержит запись типа AAAA с локальным в пределах физической подсети (link-local, см. главу 19) адресом узла (что иногда удобно использовать в целях отладки). Все записи для другого нашего узла будут выглядеть так:

aix-4   IN А    206.62.226.43

aix     IN А    206.62.226.43

        IN MX   5 aix.unpbook.com.

        IN MX   10 mailhost.unpbook.com.

Aix-4   IN A    192.168.42.2

aix-6   IN AAAA 3ffe:b80:1f8d:2:204:acff:fe17:bf38

aix-611 IN AAAA fe80::204:acff:fe17:bf38

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

Распознаватели и серверы имен

Организации обычно работают с одним или несколькими серверами имен (name servers). Часто в качестве сервера используется программа BIND (Berkeley Internet Name Domain). Приложения, такие как клиенты и серверы, которые мы создаем в этой книге, соединяются с сервером DNS при помощи вызова функций из библиотеки, называемой распознавателем (resolver). Обычные функции распознавателя —

gethostbyname
и
gethostbyaddr
, и обе они описаны в этой главе. Первая находит адрес узла по его имени, а вторая — наоборот.

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

gethostbyname
и
gethostbyaddr
.

Рис. 11.1. Типичное расположение приложений, распознавателей и серверов имен

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

/etc/resolv.conf
обычно содержит IP-адреса локальных серверов имен.

ПРИМЕЧАНИЕ

Было бы удобно указывать в файле /etc/resolv.conf имена, а не IP-адреса серверов имен, потому что имена удобнее запоминать и редактировать, однако это возвратило бы нас к вечной проблеме курицы и яйца: каким образом распознать имя сервера имен?

Распознаватель посылает запрос локальному серверу имен, используя UDP. Если локальный сервер имен не знает ответа, он обычно запрашивает другие серверы имен через Интернет, также используя UDP. Если ответ слишком велик, чтобы поместиться в один UDP-пакет, распознаватель автоматически переключается на TCP.


Альтернативы DNS

Можно получить информацию об имени и адресе без использования DNS. Типичной альтернативой служат статические файлы со списком узлов (обычно файл

/etc/hosts
, как мы указываем в табл. 11.2), информационная система сети (Network Information System, NIS) и упрощенный протокол службы каталогов (Lightweight Directory Access Protocol — LDAP). К сожалению, способ конфигурирования узла для использования различных типов служб имен зависит от реализации. Solaris 2.x, HP-UX 10 и более новых версий, а также FreeBSD 5.x используют файл
/etc/nswitch.conf
, тогда как AIX использует файл
/etc/netsvc.conf
. BIND 9.9 предоставляет свою собственную версию, которая называется IRS (Information Retrieval Service — служба получения информации), использующую файл
/etc/irs.conf
. Если сервер имен должен применяться для поиска имен узлов, все эти системы используют для задания IP-адресов серверов имен файл
/etc/resolv.conf
. К счастью, эти различия обычно скрыты от программиста приложений, поэтому мы просто вызываем функции распознавателя, такие как
gethostbyname
и
gethostbyaddr
.

11.3. Функция gethostbyname

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

connect
и
sendto
, и что возвращается функциями
accept
и
recvfrom
. Тем не менее большинство приложений имеют дело с именами, а не с адресами. Это особенно актуально при переходе на IPv6, поскольку адреса IPv6 (шестнадцатеричные строки) значительно длиннее адресов IPv4, записанных в точечно-десятичном представлении. (Например, запись типа AAAA и запись типа PTR для
ip6.arpa
в предыдущем разделе показывают это со всей очевидностью.)

Самая основная функция, выполняющая поиск имени узла, — это функция

gethostbyname
. При успешном выполнении она возвращает указатель на структуру
hostent
, содержащую все адреса IPv4 для узла. Однако она может возвращать только адреса IPv4. В разделе 11.6 рассматривается функция, возвращающая адреса IPv4 и IPv6. Стандарт POSIX предупреждает, что функция
gethostbyname
может быть исключена из будущей его версии.

ПРИМЕЧАНИЕ

Маловероятно, что реализации gethostbyname исчезнут раньше, чем весь Интернет перейдет на протокол IPv6, а произойдет это еще очень не скоро. Однако удаление функции из стандарта POSIX гарантирует, что она не будет использоваться в новых программах. Вместо нее мы рекомендуем использовать getaddrinfo (раздел 11.6).

#include 


struct hostent *gethostbyname(const char *hostname);

Возвращает: непустой указатель в случае успешного выполнения, -1 в случае ошибки

Непустой указатель, возвращаемый этой функцией, указывает на следующую структуру

hostent
:

struct hostent {

 char *h_name;        /* официальное (каноническое) имя узла */

 char **h_alihases;   /* указатель на массив указателей на псевдонимы */

 int   h_addrtype;    /* тип адреса узла: AF_INET */

 int   h_length;      /* длина адреса: 4 */

 char  **h_addr_list; /* указатель на массив указателей с адресами IPv4 или IPv6 */

};

В терминах DNS функция

gethostbyname
выполняет запрос на запись типа А. Функция возвращает только адреса IPv4.

На рис. 11.2 представлено устройство структуры

hostent
и содержащаяся в ней информация, в предположении, что искомое имя узла имеет два альтернативных имени и три адреса IPv4. Все имена узла представляют собой строки языка С.

Рис. 11.2. Структура hostent и ее одержимое

Возвращаемое имя

h_name
называется каноническим именем узла. Например, с показанными в предыдущем разделе записями CNAME каноническое имя узла
ftp://ftp.unpbook.com
будет иметь вид
linux.unpbook.com
. Также если мы вызываем функцию
gethostbyname
с узла
aix
с неполным именем, например
solaris
, то в качестве канонического имени возвращается полное доменное имя (FQDN)
solaris.unpbook.com.
.

ПРИМЕЧАНИЕ

Некоторые версии функции gethostbyname допускают, что аргумент hostname может быть записан в виде строки десятичных чисел, разделенных точками. То есть вызов в форме hptr = gethostbyname("206.62.226.33"); будет работать. Этот код был добавлен, поскольку клиент Rlogin принимает только имя узла, вызывая функцию gethostbyname, и не принимает точечно-десятичную запись [127]. Стандарт POSIX допускает это, но не устанавливает такое поведение в качестве обязательного, поэтому переносимое приложение не может использовать указанную особенность.


Функция

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

HOST_NOT_FOUND
;

TRY_AGAIN
;

NO_RECOVERY
;

NO_DATA
(идентично
NO_ADDRESS
).

Ошибка

NO_DATA
означает, что заданное имя действительно, но у него нет записи типа А. Примером может служить имя узла, имеющего только запись типа MX.

Самые современные распознаватели предоставляют функцию

hstrerror
, которая в качестве единственного аргумента получает значение
h_errno
и возвращает указатель типа
const char*
на описание ошибки. Некоторые примеры строк, возвращаемых этой функцией, мы увидим в следующем примере.

Пример

В листинге 11.1[1] показана простая программа, вызывающая функцию

gethostbyname
для любого числа аргументов командной строки и выводящая всю возвращаемую информацию.

Листинг 11.1. Вызов функции и вывод возвращаемой информации

//names/hostent.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  char *ptr, **pptr;

 6  char str[INET_ADDRSTRLEN];

 7  struct hostent *hptr;


 8  while (--argc > 0) {

 9   ptr = *++argv;

10   if ((hptr = gethostbyname(ptr)) == NULL) {

11    err_msg("gethostbyname error for host, %s: %s",

12     ptr, hstrerror(h_errno));

13    continue;

14   }

15   printf("official hostname: %s\n", hptr->h_name);


16   for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)

17    printf("\talias: %s\n", *pptr);


18   switch (hptr->h_addrtype) {

19   case AF_INET:

20    pptr = hptr->h_addr_list;

21    for (; *pptr != NULL; pptr++)

22     printf("\taddress: %s\n",

23    Inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));

24    break;


25   default:

26    err_ret("unknown address type");

27    break;

28   }

29  }

30  exit(0);

31 }

8-14
 Функция
gethostbyname
вызывается для каждого аргумента командной строки.

15-17
 Выводится каноническое имя узла, за которым идет список альтернативных имен.

18-24
 Переменная
pptr
указывает на массив указателей на индивидуальные адреса. Для каждого адреса мы вызываем функцию
inet_ntop
и выводим возвращаемую строку.

Сначала мы выполняем программу с именем нашего узла

aix
, у которого имеется только один адрес IPv4:

freebsd % hostent aix

official hostname: aix.unpbook.com

         address:  192.168 42.2

Обратите внимание, что официальное имя узла — это FQDN. Кроме того, хотя у узла имеется адрес IPv6, возвращается только адрес IPv4. Следующим будет веб-сервер с несколькими адресами IPv4:

solaris % hostent cnn.com

official hostname: cnn.com

         address: 64.236.16.20

         address: 64.236.16.52

         address: 64.236 16.84

         address: 64.236.16.116

         address: 64.236.24.4

         address: 64.236.24.12

         address: 64.236.24.20

         address: 64.236.24.28

Далее идет имя, представленное в разделе 11.2 как имя с записью типа CNAME:

solaris % hostent www

official hostname: linux.unpbook.com

         alias: www.unpbook.com

         address: 206.168.112.219

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

Чтобы увидеть строки ошибок, возвращаемые функцией

hstrerror
, мы сначала задаем несуществующее имя узла, а затем имя, имеющее только запись типа MX:

solaris % hostent nosuchname.invalid

gethostbyname error for host: nosuchname.invalid: Unknown host


solaris % hostent uunet.uu.net

gethostbyname error for host: uunet.uu.net: No address associated with name

11.4 Функция gethostbyaddr

Функция

gethostbyaddr
получает в качестве аргумента двоичный IP-адрес и пытается найти имя узла, соответствующее этому адресу. Ее действие обратно действию функции
gethostbyname
.

#include 


struct hostent *gethostbyaddr(const char *addr, size_t len, int family);

Возвращает: непустой указатель в случае успешного выполнения, -1 в случае ошибки

Эта функция возвращает указатель на ту же структуру

hostent
, которую мы описывали при рассмотрении функции
gethostbyname
. Обычно в этой структуре нас интересует поле
h_name
, каноническое имя узла.

Аргумент

addr
не относится к типу
char*
, но в действительности это указатель на структуру
in_addr
, содержащую адрес IPv4. Поле
len
— это длина структуры: 4 для адресов IPv4. Аргумент
family
будет иметь значение
AF_INET
.

В терминах DNS функция

gethostbyaddr
запрашивает у сервера имен запись типа PTR в домене
in-addr.arpa
.

11.5. Функции getservbyname и getservbyport

Службы, как и узлы, также часто идентифицируются по именам. Используя в нашем коде имя службы вместо номера порта, при условии, что имена служб сопоставляются номерам портов в некотором файле (обычно

/etc/services
), мы получаем следующее преимущество. Если этой службе будет назначен другой номер порта, то нам будет достаточно изменить одну строку в файле
/etc/services
, вместо того чтобы перекомпилировать все приложения. Следующая функция,
getservbyname
, ищет службу по ее заданному имени.

ПРИМЕЧАНИЕ

Канонический список номеров портов, назначенных определенным службам, поддерживается IANA и располагается по адресу http://www.iana.org/assignments/port-numbers (см. раздел 2.9). Файл /etc/services чаще всего содержит некоторое подмножество списка IANA.

#include 


struct servent *getservbyname(const char *servname, const char *protoname);

Возвращает: непустой указатель в случае успешного выполнения, NULL в случае ошибки

Функция возвращает указатель на следующую структуру:

struct servent {

 char *s_name;     /* официальное имя службы */

 char **s_aliases; /* список псевдонимов */

 int s_port;       /* номер порта, записанный в сетевом порядке байтов */

 char *s_proto;    /* протокол, который нужно использовать */

};

Имя службы

servname
должно быть указано обязательно. Если задан и протокол (то есть если
protoname
— непустой указатель), то в структуре должен быть указан совпадающий протокол. Некоторые службы Интернета позволяют использовать и TCP, и UDP (например, DNS и все службы, представленные в табл. 2.1), в то время как другие поддерживают только один протокол (протоколу FTP требуется TCP). Если аргумент
protoname
не задан и служба поддерживает несколько протоколов, то возвращаемый номер порта зависит от реализации. Обычно это не имеет значения, поскольку службы, поддерживающие множество протоколов, как правило, используют один и тот же номер порта для протоколов TCP и UDP, но вообще говоря это не гарантируется.

Более всего в структуре

servent
нас интересует поле номера порта. Поскольку номер порта возвращается в сетевом порядке байтов, мы не должны вызывать функцию
htons
при записи его в структуру адреса сокета.

Типичные вызовы этой функции могут быть такими:

struct servent *sptr;


sptr = getservbyname("domain", "udp"); /* DNS с использованием UDP */

sptr = getservbyname("ftp", "tcp");    /* FTP с использованием TCP */

sptr = getservbyname("ftp", NULL);     /* FTP с использованием TCP */

sptr = getservbyname("ftp", "udp");    /* этот вызов приведет к ошибке */

Поскольку протоколом FTP поддерживается только TCP, второй и третий вызовы эквивалентны, а четвертый вызов приводит к ошибке. Вот соответствующие строки из файла

/etc/services
:

freebsd % grep -e ^ftp -e ^domain /etc/services

ftp-data  20/tcp  #File Transfer [Default Data]

ftp       21/tcp  #File Transfer [Control]

domain    53/tcp  #Domain Name Server

domain    53/udp  #Domain Name Server

ftp-agent 574/tcp #FTP Software Agent System

ftp-agent 574/udp #FTP Software Agent System

ftps-data 989/tcp # ftp protocol, data, over TLS/SSL

ftps      990/tcp # ftp protocol, control, over TLS/SSL

Следующая функция,

getservbyport
, ищет службу по заданному номеру порта и (не обязательно) протоколу.

#include 


struct servent *getservbyport(int port, const char *protname);

Возвращает: непустой указатель в случае успешного выполнения, NULL в случае ошибки

Значение аргумента

port
должно быть записано в сетевом порядке байтов. Типичные примеры вызова этой функции приведены ниже:

struct servent *sptr;


sptr = getservbyport(htons(53), "udp"); /* DNS с использованием UDP */

sptr = getservbyport(htons(21), "tcp"); /* FTP с использованием TCP */

sptr = getservbyport(htons(21), NULL);  /* FTP с использованием TCP */

sptr = getservbyport(htons(21), "udp"); /* этот вызов приведет к ошибке */

Последний вызов оказывается неудачным, поскольку нет службы, использующей порт 21 с протоколом UDP.

Помните, что некоторые номера портов используются с TCP для одной службы, а с UDP — для совершенно другой, например:

freebsd % grep 514 /etc/services

shell  514/tcp cmd #like exec, but automatic

syslog 514/udp

Здесь показано, что порт 514 используется командой

rsh
с TCP и демоном
syslog
с UDP. Это характерно для портов 512-514.

Пример: использование функций gethostbyname и getservbyname

Теперь мы можем изменить код нашего TCP-клиента времени и даты, показанный в листинге 1.1, так, чтобы использовать функции

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

Листинг 11.2. Наш клиент времени и даты, использующий функции gethostbyname и getservbyname

//names/daytimetcpcli1.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd, n;

 6  char recvline[MAXLINE + 1];

 7  struct sockaddr_in servaddr;

 8  struct in_addr **pptr;

 9  struct in_addr *inetaddrp[2];

10  struct in_addr inetaddr;

11  struct hostent *hp;

12  struct servent *sp;


13  if (argc != 3)

14   err_quit("usage: daytimetcpcli1 ");


15  if ((hp = gethostbyname(argv[1])) == NULL) {

16   if (inet_aton(argv[1], &inetaddr) == 0) {

17    err_quit("hostname error for %s: %s", argv[1],

18    hstrerror(h_errno));

19   } else {

20    inetaddrp[0] = &inetaddr;

21    inetaddrp[1] = NULL;

22    pptr = inetaddrp;

23   }

24  } else {

25   pptr = (struct in_addr**)hp->h_addr_list;

26  }


27  if ((sp = getservbyname(argv[2], "tcp")) == NULL)

28   err_quit("getservbyname error for %s", argv[2]);


29  for (; *pptr != NULL; pptr++) {

30   sockfd = Socket(AF_INET, SOCK_STREAM, 0);


31   bzero(&servaddr, sizeof(servaddr));

32   servaddr.sin_family = AF_INET;

33   servaddr.sin_port = sp->s_port;

34   memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr));

35   printf("trying %s\n", Sock_ntop((SA*)&servaddr, sizeof(servaddr)));


36   if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) == 0)

37    break; /* успешное завершение */

38   err_ret("connect error");

39   close(sockfd);

40  }

41  if (*pptr == NULL)

42   err_quit("unable to connect");


43  while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {

44   recvline[n] = 0; /* null terminate */

45   Fputs(recvline, stdout);

46  }

47  exit(0);

48 }

Вызов функций gethostbyname и getservbyname

13-28
 Первый аргумент командной строки — это имя узла, передаваемое в качестве аргумента функции
gethostbyname
, а второй — имя службы, передаваемое в качестве аргумента функции
getservbyname
. Наш код подразумевает использование протокола TCP, что мы указываем во втором аргументе функции
getservbyname
. Если функции
gethostbyname
не удается найти нужное имя, мы вызываем функцию
inet_aton
(см. раздел 3.6), чтобы проверить, не является ли аргумент командной строки IP-адресом в формате ASCII. В этом случае формируется список из одного элемента — этого IP-адреса.

Перебор всех адресов

29-35
 Теперь мы пишем вызовы функций
socket
и
connect
в цикле, который выполняется для каждого адреса сервера, пока попытка вызова функции
connect
не окажется успешной или пока не закончится список серверов. После вызова функции
socket
мы заполняем структуру адреса сокета Интернета IP-адресом и номером порта сервера. Хотя в целях увеличения производительности мы могли бы вынести из цикла вызов функции
bzero
и последующие два присваивания, наш код легче читать в таком виде, как он представлен сейчас. Установление соединения с сервером редко является основным источником проблем с производительностью сетевого клиента.

Вызов функции connect

36-39
 Вызывается функция
connect
, и если вызов оказывается успешным, функция
break
завершает цикл. Если установить соединение не удается, мы выводим сообщение об ошибке и закрываем сокет. Вспомните, что дескриптор, для которого вызов функции
connect
оказался неудачным, не может больше использоваться и должен быть закрыт.

Завершение программы

41-42
 Если цикл завершается, потому что ни один вызов функции
connect
не закончился успехом, программа завершает работу.

Чтение ответа сервера

43-47
 Мы считываем ответ сервера и завершаем программу, когда сервер закрывает соединение.

Если мы запустим эту программу, указав один из наших узлов, на котором работает сервер времени и даты, мы получим ожидаемый результат:

freebsd % daytimetcpcli1 aix daytime

trying 192.168.42.2:13

Sun Jul 27 22:44:19 2003

Но еще интереснее запустить программу, обратившись к маршрутизатору с несколькими сетевыми интерфейсами, на котором не работает сервер времени и даты:

solaris % daytimetcpcli1 gateway.tuc.noao.edu daytime

trying 140.252.108.1:13

connect error: Operation timed out

trying 140.252.1.4:13

connect error: Operation timed out

trying 140.252.104.1:13

connect error: Connection refused

unable to connect

11.6. Функция getaddrinfo

Функции

gethostbyname
и
gethostbyaddr
поддерживают только IPv4. Интерфейс IPv6 разрабатывался в несколько этапов (история разработки описана в разделе 11.20), и в конечном итоге получилась функция
getaddrinfo
. Последняя осуществляет трансляцию имен в адреса и служб в порты, причем возвращает она список структур
sockaddr
, а не список адресов. Такие структуры могут непосредственно использоваться функциями сокетов. Благодаря этому функция
getaddrinfo
скрывает все различия между протоколами в библиотеке функций. Приложение работает только со структурами адресов сокетов, которые заполняются
getaddrinfo
. Эта функция определяется стандартом POSIX.

ПРИМЕЧАНИЕ

Определение этой функции в POSIX происходит от более раннего предложения Кейта Склоуэра (Keith Sklower) для функции, называемой getconninfo. Эта функция стала результатом обсуждений с Эриком Олменом (Eric Allman), Вилльямом Дастом (William Durst), Майклом Карелсом (Michael Karels) и Стивеном Вайсом (Steven Wise), а также более ранней реализации, написанной Эриком Олменом. Замечание о том, что указания имени узла и имени службы достаточно для соединения с этой службой независимо от деталей протокола, было сделано Маршалом Роузом (Marshall Rose) в проекте X/Open.

#include 


int getaddrinfo(const char *hostname, const char *service,

 const struct addrinfo *hints, struct addrinfo **result);

Возвращает: 0 в случае успешного выполнения, ненулевое значение в случае ошибки

(см. табл. 11.2).

Через указатель

result
функция возвращает указатель на связный список структур
addrinfo
, который задается в заголовочном файле
:

struct addrinfo {

 int    ai_flags;          /* AI_PASSIVE, AI_CANONNAME */

 int    ai_family;         /* AF_xxx */

 int    ai_socktype;       /* SOCK_xxx */

 int    ai_protocol;       /* 0 или IPPROTO_xxx для IPv4 и IPv6 */

 size_t ai_addrlen;        /* длина ai_addr */

 char*  ai_canonname;      /* указатель на каноническое имя узла */

 struct sockaddr *ai_addr; /* указатель на структуру адреса сокета */

 struct addrinfo *ai_next; /* указатель на следующую структуру в связном

                              списке */

};

Переменная

hostname
— это либо имя узла, либо строка адреса (точечно-десятичная запись для IPv4 или шестнадцатеричная строка для IPv6). Переменная
service
— это либо имя службы, либо строка, содержащая десятичный номер порта. (См. также упражнение 11.4.)

Аргумент

hints
— это либо пустой указатель, либо указатель на структуру
addrinfo
, заполненную рекомендациями вызывающего процесса о типах информации, которую он хочет получить. Например, если заданная служба предоставляется и для TCP, и для UDP (служба
domain
, которая ссылается на сервер DNS), вызывающий процесс может присвоить элементу
ai_socktype
структуры
hints
значение
SOCK_DGRAM
. Тогда возвращение информации будет иметь место только для дейтаграммных сокетов.

Вызывающим процессом могут быть установлены значения следующих элементов структуры

hints
:

ai_flags
(несколько констант
AI_XXX
, объединенных операцией ИЛИ);

ai_family
(значение
AF_xxx
);

ai_socktype
(значение
SOCK_xxx
);

ai_protocol
.

Поле

ai_flags
может содержать следующие константы:

AI_PASSIVE
указывает, что сокет будет использоваться для пассивного открытия;

AI_CANONNAME
указывает функции на необходимость возвратить каноническое имя узла;

AI_NUMERICHOST
запрещает преобразование между именами и адресами. Аргумент
hostname
должен представлять собой строку адреса;

AI_NUMERICSERV
запрещает преобразование между именами служб и номерами портов. Аргумент
service
должен представлять собой строку с десятичным номером порта;

AI_V4MAPPED
вместе с
ai_family = AF_INET6
указывает функции на необходимость вернуть адреса IPv4 из записей А, преобразованные к IPv6, если записи типа AAAA отсутствуют;

AI_ALL
при указании вместе с
AI_V4MAPPED
говорит о необходимости вернуть адреса IPv4, преобразованные к IPv6, вместе с истинными адресами IPv6;

AI_ADDRCONFIG
возвращает адреса, относящиеся к заданной версии IP, когда имеется несколько интерфейсов, имеющих IP-адреса другой версии.

Если аргументом структуры

hints
является пустой указатель, функция подразумевает нулевое значение для
ai_flags
,
ai_socktype
и
ai_protocol
и значение
AF_UNSPEC
для
ai_family
.

Если функция завершается успешно (0), то в переменную, на которую указывает аргумент

result
, записывается указатель на список структур
addrinfo
, связанных через указатель
ai_next
. Имеется два способа возвращения множественных структур.

1. Если существует множество адресов, связанных с узлом

hostname
, то одна структура возвращается для каждого адреса, который может использоваться с запрашиваемым семейством адресов (значение
ai_family
, если задано).

2. Если служба предоставляется для множества типов сокетов, то одна структура может быть возвращена для каждого типа сокета в зависимости от

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

Например, если структура

hints
пуста, а вы запрашиваете записи для службы
domain
на узле с двумя IP-адресами, возвращаются четыре структуры
addrinfo
:

■ одна для первого IP-адреса и типа сокета SOCK_STREAM;

■ одна для первого IP-адреса и типа сокета SOCK_DGRAM;

■ одна для второго IP-адреса и типа сокета SOCK_STREAM;

■ одна для второго IP-адреса и типа сокета SOCK_DGRAM.

Мы показываем схематическое изображение этого примера на рис. 11.3. Не существует никакого гарантированного порядка структур при возвращении множества элементов. Например, мы не можем считать, что службы TCP возвращаются перед службами UDP.

Рис. 11.3. Пример информации, возвращаемой функцией getaddrinfo


ПРИМЕЧАНИЕ

Хотя это и не гарантируется, реализация должна возвращать IP-адреса в том же порядке, в котором они возвращаются DNS. Некоторые распознаватели позволяют администратору указывать порядок сортировки адресов в файле /etc/resolv.conf. Протокол IPv6 определяет правила выбора адресов (RFC 3484 [28]), которые могут влиять на порядок адресов, возвращаемых getaddrinfo.

Информация, возвращаемая в структурах

addrinfo
, готова для передачи функциям
socket
и
connect
или
sendto
(для клиента) и
bind
(для сервера). Аргументы функции
socket
— это элементы
ai_family
,
ai_socktype
и
ai_protocol
. Второй и третий аргументы функций
connect
и
bind
— это элементы
ai_addr
(указатель на структуру адреса сокета соответствующего типа, заполняемую функцией
getaddrinfo
) и
ai_addrlen
(длина этой структуры адреса сокета).

Если в структуре

hints
установлен флаг
AI_CANONNAME
, элемент
ai_canonname
первой возвращаемой структуры указывает на каноническое имя узла. В терминах DNS это обычно полное доменное имя (FQDN). Программы типа
telnet
широко используют этот флаг для того, чтобы выводить канонические имена систем, к которым производится подключение. Пользователь может указать короткое имя узла или его альтернативное имя, но он должен знать, с какой системой он в результате соединился.

На рис. 11.3 представлена возвращаемая информация для следующего вызова:

struct addrinfo hints, *res;


bzero(&hints, sizeof(hints));

hints.ai_flags = AI_CANONNAME;

hints.ai_family = AF_INET;


getaddrinfo("bsdi", "domain", &hints, &res);

На этом рисунке все, кроме переменной

res
, относится к динамически выделяемой памяти (например, с помощью функции
malloc
). Предполагается, что каноническое имя узла
freebsd4
freebsd4.unpbook.com
, и что этот узел имеет два адреса IPv4 в DNS.

Порт 53 предназначен для службы

domain
, и нужно учитывать, что этот номер порта будет представлен в структурах адресов сокетов в сетевом порядке байтов. Мы приводим возвращаемые значения
ai_protocol
IPPROTO_TCP и IPPROTO_UDP. Функция
getaddrinfo
может возвращать значение
ai_protocol
равное 0 для структур SOCK_STREAM, если этого достаточно для однозначного определения протокола (типа сокета недостаточно, например, если в системе помимо TCP реализован и SCTP), и 0 для структур SOCK_DGRAM, если в системе не реализованы другие протоколы дейтаграмм для IP (на момент написания этой книги стандартизованных протоколов еще не было, но два уже разрабатывались IETF). Лучше всего, если
getaddrinfo
всегда будет возвращать конкретный тип протокола.

В табл. 11.1 показано число структур

addrinfo
для каждого возвращаемого адреса, определяемое на основе заданного имени службы (которое может быть представлено десятичным номером порта) и рекомендации
ai_socktype
.


Таблица 11.1. Число структур addrinfo, возвращаемых для каждого IP-адреса

Элемент ai_socktypeСлужба обозначена именем и предоставляется:Служба обозначена именем порта
Только TCPТолько UDPТолько SCTPTCP и UDPTCP и SCTPTCP, UDP и SCTP
0111223Ошибка
SOCK_STREAM1Ошибка11222
SOCK_DGRAMОшибка11Ошибка11
SOCK_SEQPACKETОшибкаОшибка1Ошибка111

Более одной структуры

addrinfo
возвращается для каждого IP-адреса только в том случае, когда поле
ai_socktype
структуры
hints
пусто и либо служба поддерживается TCP и UDP (как указано в файле
/etc/services
), либо задан номер порта для этой службы.

Если бы мы рассматривали все 64 возможных варианта сочетаний входных данных для функции

getaddrinfo
(имеется шесть входных переменных), многие сочетания оказались бы недопустимыми, а некоторые не имели бы смысла. Вместо этого рассмотрим наиболее типичные случаи.

■ Задание имени узла и службы. Это традиционный случай для клиента TCP и UDP. По завершении клиент TCP перебирает в цикле все возвращаемые IP-адреса, вызывая функции

socket
и
connect
для каждого из них, пока не установится соединение или пока не будут перебраны все адреса. Мы показываем такой пример с нашей функцией
tcp_connect
в листинге 11.2.

Для клиента UDP структура адреса сокета, заполняемая с помощью функции

getaddrinfo
, будет использоваться в вызове функции
sendto
или
connect
. Если клиент сообщит, что первый адрес не работает (ошибка на присоединенном сокете UDP или тайм-аут на неприсоединенном сокете), будет предпринята попытка обратиться к другому адресу.

Если клиент знает, что он обрабатывает только один тип сокета (например, клиентами Telnet и FTP обрабатываются только сокеты TCP, а клиентами TFTP — только сокеты UDP), то элементу

ai_socktype
структуры
hints
должно быть задано соответственно либо значение
SOCK_STREAM
, либо значение
SOCK_DGRAM
.

■ Типичный сервер задает службу (service), но не имя узла (hostname), и задает флаг

AI_PASSIVE
в структуре
hints
. Возвращаемая структура адреса сокета должна содержать IP-адрес, равный
INADDR_ANY
(для IPv4) или
IN6ADDR_ANY_INIT
(для IPv6). Сервер TCP затем вызывает функции
socket
,
bind
и
listen
. Если сервер хочет разместить в памяти с помощью функции
malloc
другую структуру адреса сокета, чтобы получить адрес клиента из функции
accept
, то возвращаемое значение
ai_addrlen
задает требуемый для этого размер.

Сервер UDP вызовет функции

socket
,
bind
и затем
recvfrom
. Если сервер хочет разместить в памяти с помощью функции
malloc
другую структуру адреса сокета, чтобы получить адрес клиента из функции
recvfrom
, возвращаемое значение
ai_addrlen
также задает нужный размер.

Как и в случае типичного клиентского кода, если сервер знает, что он обрабатывает только один тип сокета, то элемент

ai_socktype
структуры
hints
должен быть задан либо как
SOCK_STREAM
, либо как
SOCK_DGRAM
. Это позволяет избежать возвращения множества структур, с (возможно) неверным значением элемента
ai_socktype
.

■ До сих пор мы демонстрировали серверы TCP, создающие один прослушиваемый сокет, и серверы UDP, создающие один сокет дейтаграмм. Это тот вариант, который подразумевался в предыдущем абзаце. Альтернативным устройством является сервер, который обрабатывает множество сокетов с помощью функции

select
. В этом сценарии сервер должен последовательно перебрать все структуры из списка, возвращаемого функцией
getaddrinfo
, создать по одному сокету для каждой структуры и вызвать функцию
select
.

ПРИМЕЧАНИЕ

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

Невзирая на тот факт, что функция

getaddrinfo
«лучше», чем функции
gethostbyname
и
gethostbyaddr
(помимо того что эта функция упрощает написание кода, не зависящего от протокола, она обрабатывает и имя узла, и имя службы, и к тому же вся возвращаемая ею информация размещается в памяти динамически, а не статически), ее все же не так просто использовать, как это могло показаться. Проблема в том, что нам требуется разместить в памяти структуру
hints
, инициализировать ее нулем, заполнить необходимые поля, вызвать функцию
getaddrinfo
и затем пройти весь связный список, проверяя каждый его элемент. В последующих разделах мы предоставим более простые интерфейсы для типичных клиентов TCP и UDP и серверов, которые будем создавать в оставшейся части книги.

Функция

getaddrinfo
решает проблему преобразования имен узлов и имен служб в структуры адресов сокетов. В разделе 11.17 мы опишем обратную функцию
getnameinfo
, которая преобразует структуры адресов сокетов в имена узлов и имена служб.

11.7. Функция gai_strerror

Ненулевые значения ошибок, возвращаемых функцией

getaddrinfo
, имеют названия и значения, показанные в табл. 11.2. Функция
gai_strerror
получает одно из этих значений в качестве аргумента и возвращает указатель на соответствующую текстовую строку с описанием ошибки.

#include 


char *gai_strerror(int error);

Возвращает: указатель на строку с описанием ошибки


Таблица 11.2. Ненулевые возвращаемые значения (константы) ошибок функции getaddrinfo

КонстантаОписание
EAI_AGAINВременный сбой при попытке разрешения имен
EAI_BADFLAGSНедопустимое значение ai_flags
EAI_FAILНеисправимая ошибка при разрешении имен
EAI_FAMILYСемейство ai_family не поддерживается
EAI_MEMORYОшибка при выделении памяти
EAI_NONAMEИмя узла или имя службы неизвестны или равны NULL
EAI_OVERFLOWПереполнен буфер пользовательских аргументов (только для getnameinfo)
EAI_SERVICEЗапрошенная служба не поддерживается для данного типа сокета ai_socktype
EAI_SOCKTYPEТип сокета ai_socktype не поддерживается
EAI_SYSTEMДругая системная ошибка, возвращаемая в переменной errno

11.8. Функция freeaddrinfo

Вся память, занимаемая структурами

addrinfo
, структурами
ai_addr
и строкой
ai_canonname
, которые возвращаются функцией
getaddrinfo
, динамически выделяется функцией
malloc
. Эта память освобождается при вызове функции
freeaddrinfo
.

#include 


void freeaddrinfo(struct addrinfo *ai);

Переменная

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

Предположим, что мы вызываем функцию

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

ПРИМЕЧАНИЕ

Создание копии только самой структуры addrinfo, а не структур, на которые она, в свою очередь, указывает, называется поверхностным копированием (shallow сору). Копирование структуры addrinfo и всех структур, на которые она указывает, называется детальным копированием (deep сору).

11.9. Функция getaddrinfo: IPv6

Стандарт POSIX определяет как

getaddrinfo
, так и возвращаемые этой функцией данные для протоколов IPv4 и IPv6. Отметим следующие моменты, прежде чем свести возвращаемые значения воедино в табл. 11.3.

■ Входные данные функции

getaddrinfo
могут относиться к двум различным типам, которые выбираются в зависимости от того, какой тип структуры адреса сокета вызывающий процесс хочет получить обратно и какой тип записей нужно искать в DNS или иной базе данных.

■ Семейством адресов, указанным вызывающим процессом в структуре

hints
, задается тип структуры адреса сокета, который вызывающий процесс предполагает получить. Если вызывающий процесс задает
AF_INET
, функция не должна возвращать структуры
sockaddr_in6
, а для
AF_INET6
функция не должна возвращать структур
sockaddr_in
.

■ POSIX утверждает, что при задании семейства

AF_UNSPEC
должны возвращаться адреса, которые могут использоваться с любым семейством протоколов, допускающим применение имени узла и имени службы. Это подразумевает, что если у узла имеются как записи типа AAAA, так и записи типа А, то записи типа AAAA возвращаются как структуры
sockaddr_in6
, а записи типа A — как структуры
sockaddr_in
. Нет смысла возвращать еще и записи типа А как адреса IPv4, преобразованные к виду IPv6, в структурах
sockaddr_in6
, потому что при этом не возвращается никакой дополнительной информации — эти адреса уже возвращены в структурах
sockaddr_in
.

■ Это утверждение POSIX также подразумевает, что если флаг

AI_PASSIVE
задан без имени узла, то должен быть возвращен универсальный адрес IPv6 (
IN6ADDR_ANY_INIT
или 0::0) в структуре
sockaddr_in6
вместе с универсальным адресом IPv4 (
INADDR_ANY
или 0.0.0.0) в структуре
sockaddr_in
. Также нет смысла возвращать сначала универсальный адрес IPv4, поскольку мы увидим в разделе 12.2, что на узле с двойным стеком сокет сервера IPv6 может обрабатывать и клиенты IPv4, и клиенты IPv6.

■ Семейство адресов, указанное в поле

ai_family
структуры
hint
вместе с флагами
AI_V4MAPPED
и
AI_ALL
поля
ai_flags
, задают тип записей, поиск которых ведется в DNS (тип А или тип AAAA), и тип возвращаемых адресов (IPv4, IPv6 или IPv4, преобразованные к виду IPv6). Мы обобщили это в табл. 11.3.

■ Имя узла может также быть либо шестнадцатеричной строкой IPv6, либо строкой в точечно-десятичной записи IPv4. Допустимость этой строки зависит от семейства адресов, заданного вызывающим процессом. Шестнадцатеричная строка IPv6 неприемлема, если задано семейство

AF_INET
, а строка в точечно-десятичной записи IPv4 неприемлема, если задано семейство
AF_INET6
. Но если задано семейство
AF_UNSPEC
, то допустимы оба варианта, и при этом возвращается соответствующий тип структуры адреса сокета.

ПРИМЕЧАНИЕ

Можно возразить, что если в качестве семейства протоколов задано AF_INET6, строка в точечно-десятичной записи должна возвращаться как адрес IPv4, преобразованный к виду IPv6 в структуре sockaddr_in6. Но другим способом получения этого результата является установка префикса строки с десятичной точкой 0::ffff:.

В табл. 11.3 показано, как будут обрабатываться адреса IPv4 и IPv6 функцией

getaddrinfo
. Колонка «Результат» отражает то, что мы хотим возвратить вызывающему процессу, если входные переменные таковы, как показано в первых трех колонках. Колонка «Действия» — то, каким образом мы получаем этот результат.


Таблица 11.3. Функция getaddrinfo: ее действия и результаты

Имя узла, указанное вызывающим процессомСемейство адресов, указанное вызывающим процессомСтрока с именем узла содержитРезультатДействия
Ненулевая строка с именем узла; активное или пассивное открытиеAF_UNSPECИмя узлаВсе записи AAAA возвращаются как структуры sockaddr_in6{} и все записи А возвращаются как структуры sockaddr_in{}Поиск по записям AAAA и поиск по записям A
Шестнадцатеричная строкаОдна структура sockaddr_in6{}inet_pton(AF_INET6)
Строка в точечно- десятичной записиОдна структура sockaddr_in{}inet_pton(AF_INET)
AF_INET6Имя узлаВсе записи AAAA возвращаются как структуры sockaddr_in6{} либо все записи А возвращаются как структуры sockaddr_in6{} с адресами IPv4, преобразованными к виду IPv6Поиск по записям AAAA
Шестнадцатеричная строкаОдна структура sockaddr_in6{}inet_pton(AF_INET6)
Строка в точечно-десятичной записиИщется как имя узла
AF_INETИмя узлаВсе записи А возвращаются как структуры sockaddr_in{}Поиск по записям типа A
Шестнадцатеричная строкаОшибка: EAI_ADDRFAMILY
Строка в точечно-десятичной записиОдна структура sockaddr_in{}inet_pton(AF_INET)
Пустая строка с именем узла; пассивное открытиеAF_UNSPECНеявный адрес 0::0 Неявный адрес 0.0.0.0Одна структура sockaddr_in6{} и одна структура sockaddr_in{}inet_pton(AF_INET6) inet_pton(AF_INET)
AF_INET6Неявный адрес 0::0Одна структура sockaddr_in6{}inet_pton(AF_INET6)
AF_INETНеявный адрес 0.0.0.0Одна структура sockaddr_in{}inet_pton(AF_INET)
Пустая строка с именем узла; активное открытиеAF_UNSPECНеявный адрес 0::1 Неявный адрес 127.0.0.1Одна структура sockaddr_in6{} и одна структура sockaddr_in{}inet_pton(AF_INET6) inet_pton(AF_INET)
AF_INET6Неявный адрес 0::1Одна структура sockaddr_in6{}inet_pton(AF_INET6)
AF_INETНеявный адрес 127.0.0.1Одна структура sockaddr_in{}inet_pton(AF_INET)

Обратите внимание, что табл. 11.3 описывает только обработку адресов IPv4 и IPv6 функцией

getaddrinfo
, то есть количество и тип адресов, возвращаемых процессу в различных ситуациях. Реальное количество структур
addrinfo
зависит также от типа сокета и имени службы, о чем уже говорилось в связи с табл. 11.1.

11.10. Функция getaddrinfo: примеры

Теперь мы покажем некоторые примеры работы функции

getaddrinfo
, используя тестовую программу, которая позволяет нам вводить все параметры: имя узла, имя службы, семейство адресов, тип сокета и флаги
AI_CANONNAME
и
AI_PASSIVE
. (Мы не показываем эту тестовую программу, поскольку она содержит около 350 строк малоинтересного кода. Ее можно получить тем же способом, что и прочие исходные коды для этой книги.) Тестовая программа выдает информацию о переменном числе возвращаемых структур
addrinfo
, показывая аргументы вызова функции
socket
и адрес в каждой структуре адреса сокета. Сначала показываем тот же пример, что и на рис. 11.3:

freebsd % testga -f inet -c -h freebsd4 -s domain


socket(AF_INET, SOCK_DGRAM, 17) ai_canonname = freebsd4.unpbook.com

      address: 135.197.17.100:53

socket(AF_INET, SOCK_DGRAM, 17)

      address: 172:24.37.94:53

socket(AF_INET, SOCK_STREAM, 6) ai_canonname = freebsd4.unpbook.com

      address: 135.197.17.100:53

socket(AF_INET, SOCK_STREAM, 6)

      address: 172.24.37.94:53

Параметр

-f inet
задает семейство адресов, -с указывает, что нужно возвратить каноническое имя,
-h freebsd4
задает имя узла,
-s domain
задает имя службы.

Типичный сценарий клиента — задать семейство адресов, тип сокета (параметр

-t
), имя узла и имя службы. Следующий пример показывает это для узла с несколькими сетевыми интерфейсами с шестью адресами Ipv4:

freebsd % testga -f inet -t stream -h gateway.tuc.noao.edu -s daytime

socket(AF_INET, SOCK_STREAM, 6)

      address: 140.252.108.1:13


socket(AF_INET, SOCK_STREAM, 6)

      address: 140.252.1.4:13


socket(AF_INET, SOCK_STREAM, 6)

      address: 140.252.104.1:13


socket(AF_INET, SOCK_STREAM, 0)

      address: 140.252.3.6.13


socket(AF_INET, SOCK_STREAM, 0)

      address: 140.252.4.100.13


socket(AF_INET, SOCK_STREAM, 0)

      address: 140.252.1.4.13

Затем мы задаем наш узел

aix
, у которого имеется и запись типа AAAA, и запись типа А, не указывая семейства адресов. Имя службы —
ftp
, которая предоставляется только TCP.

freebsd % testga -h aix -s ftp -t stream


socket(AF_NET6, SOCK_STREAM, 6)

      address: [3ffe:b80:1f8d:2:204:acff:fe17:bf38]:21


socket(AF_INET, SOCK_STREAM, 6)

      address: 192.168.42.2:21

Поскольку мы не задали семейство адресов и запустили этот пример на узле, который поддерживает и IPv4, и IPv6, возвращаются две структуры: одна для IPv6 и одна для IPv4.

Затем мы задаем флаг

AI_PASSIVE
(параметр
), не указываем ни семейства адресов, ни имени узла (подразумевая универсальный адрес), задаем номер порта 8888 и не указываем тип сокета.

freebsd % testga -р -s 8888 -t stream


socket(AF_INET6, SOCK_STREAM, 6)

address: [::]:8888


socket(AF_INET, SOCK_STREAM, 6)

address: 0.0.0.0:8888

Возвращаются две структуры. Поскольку мы запустили эту программу на узле, поддерживающем и IPv4, и IPv6, не задав семейства адресов, функция

getaddrinfo
возвращает универсальный адрес IPv6 и универсальный адрес IPv4. Структура IPv6 возвращается перед структурой IPv4, поскольку, как мы увидим в главе 12, клиент или сервер IPv6 на узле с двойным стеком может взаимодействовать с собеседниками по IPv6 и по IPv4.

11.11. Функция host_serv

Наш первый интерфейс функции

getaddrinfo
не требует от вызывающего процесса размещать в памяти структуру рекомендаций и заполнять ее. Вместо этого аргументами нашей функции
host_serv
будут интересующие нас поля — семейство адресов и тип сокета.

#include "unp.h"


struct addrinfo *host_serv(const char *hostname, const char *service, int family, int socktype);

Возвращает: в случае успешного выполнения указатель на структуру addrinfo. NULL в случае ошибки

В листинге 11.3 показан исходный код этой функции.

Листинг 11.3. Функция host_serv

//lib/host_serv.c

 1 #include "unp.h"


 2 struct addrinfo*

 3 host_serv(const char *host, const char *serv, int family, int socktype)

 4 {

 5  int n;

 6  struct addrinfo hints, *res;


 7  bzero(&hints, sizeof(struct addrinfo));

 8  hints.ai_flags = AI_CANONNAME; /* всегда возвращает каноническое имя */

 9  hints.ai_family = family; /* AF_UNSPEC, AF_INET, AF_INET6, ... */

10  hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, ... */


11  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)

12   return (NULL);


13  return (res); /* возвращает указатель на первый элемент в связном

                     списке */

14 }

7-13
 Функция инициализирует структуру рекомендаций (
hints
), вызывает функцию
getaddrinfo
и возвращает пустой указатель, если происходит ошибка.

Мы вызываем эту функцию в листинге 16.11, когда нам нужно использовать

getaddrinfo
для получения информации об узле и о службе и при этом мы хотим установить соединение самостоятельно.

11.12. Функция tcp_connect

Теперь мы напишем две функции, использующие функцию

getaddrinfo
для обработки большинства сценариев клиентов и серверов TCP, которые мы создаем. Первая из этих функций,
tcp_connect
, выполняет обычные шаги клиента: создание сокета TCP и соединение с сервером.

#include "unp.h"


int tcp_connect(const char *hostname, const char *service);

Возвращает: в случае успешного соединения - дескриптор присоединенного сокета, в случае ошибки не возвращается ничего

В листинге 11.4 показан исходный код.

Листинг 11.4. Функция tcp_connect: выполнение обычных шагов клиента

/

/lib/tcp_connect.c

 1 #include "unp.h"


 2 int

 3 tcp_connect(const char *host, const char *serv)

 4 {

 5  int sockfd, n;

 6  struct addrinfo hints, *res, *ressave;


 7  bzero(&hints, sizeof(struct addrinfo));

 8  hints.ai_family = AF_UNSPEC;

 9  hints.ai_socktype = SOCK_STREAM;


10  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)

11   err_quit("tcp_connect error for %s, %s: %s",

12   host, serv, gai_strerror(n));

13  ressave = res;


14  do {

15   sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

16   if (sockfd < 0)

17    continue; /* игнорируем этот адрес */

18   if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)

19    break; /* успех */


20   Close(sockfd); /* игнорируем этот адрес */

21  } while ((res = res->ai_next) != NULL);


22  if (res == NULL) /* значение errno устанавливается при

                        последней попытке connect() */

23   err_sys("tcp_connect error for %s, %s", host, serv);


24  freeaddrinfo(ressave);


25  return (sockfd);

26 }

Вызов функции getaddrinfo

7-13
 функция
getaddrinfo
вызывается один раз, когда мы задаем семейство адресов
AF_UNSPEC
и тип сокета
SOCK_STREAM
.

Перебор всех структур addrinfo до успешного выполнения или до окончания списка

14-25
 Затем пробуется каждый IP-адрес: вызываются функции
socket
и
connect
. Если выполнение функции
socket
неудачно, это не фатальная ошибка, так как такое может случиться, если был возвращен адрес IPv6, а ядро узла не поддерживает IPv6. Если выполнение функции
connect
успешно, выполняется функция
break
для выхода из цикла. В противном случае, после того как перепробованы все адреса, цикл также завершается. Функция
freeaddrinfo
освобождает всю динамически выделенную память.

Эта функция (как и другие наши функции, предоставляющие более простой интерфейс для функции

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

Tcp_connect(const char *host, const char *serv) {

 return(tcp_connect(host, serv));

}

Тем не менее мы по-прежнему вызываем функцию-обертку вместо функции

tcp_connect
ради сохранения единообразия в оставшейся части книги.

ПРИМЕЧАНИЕ

Проблема с возвращаемым значением заключается в том, что дескрипторы неотрицательные, но мы не знаем, положительны или отрицательны значения EAI_xxx. Если бы эти значения были положительными, мы могли бы возвратить равные им по абсолютной величине отрицательные значения, когда вызов функции getaddrinfo окажется неудачным. Но мы также должны возвратить некое другое отрицательное значение, чтобы указать, что все структуры были перепробованы безуспешно.

Пример: клиент времени и даты

В листинге 11.5 показан наш клиент времени и даты из листинга 1.1, переписанный с использованием функции

tcp_connect
.

Листинг 11.5. Клиент времени и даты, переписанный с использованием функции tcp_connect

//names/daytimetcpcli.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd, n;

 6  char recvline[MAXLINE + 1];

 7  socklen_t len;

 8  struct sockaddr_storage *ss;


 9  if (argc != 3)

10   err_quit

11    ("usage, daytimetcpcli ");


12  sockfd = Tcp_connect(argv[1], argv[2]);


13  len = sizeof(ss);

14  Getpeername(sockfd, (SA*)&ss, &len);

15  printf("connected to %s\n", Sock_ntop_host((SA*)&ss, len));


16  while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {

17   recvline[n] = 0; /* завершающий нуль */

18   Fputs(recvline, stdout);

19  }

20  exit(0);

21 }

Аргументы командной строки

9-11
 Теперь нам требуется второй аргумент командной строки для задания либо имени службы, либо номера порта, что позволит нашей программе соединяться с другими портами.

Соединение с сервером

12
 Теперь весь код сокета для этого клиента выполняется функцией
tcp_connect
.

Вывод ответа сервера

13-15
 Мы вызываем функцию
getpeername
, чтобы получить адрес протокола сервера и вывести его. Мы делаем это для проверки протокола, используемого в примерах, которые скоро покажем.

Обратите внимание, что функция

tcp_connect
не возвращает размера структуры адреса сокета, который использовался для функции
connect
. Мы могли добавить еще один аргумент-указатель, чтобы получить это значение, но при создании этой функции мы стремились добиться меньшего числа аргументов, чем у функции
getaddrinfo
. Поэтому мы определяем константу
MAXSOCKADDR
в нашем заголовке
unp.h
так, чтобы ее размер равнялся размеру наибольшей структуры адреса сокета. Обычно это размер структуры адреса доменного сокета Unix (см. раздел 14.2), немного более 100 байт. Мы выделяем в памяти пространство для структуры указанного размера и заполняем ее с помощью функции
getpeername
.

Эта версия нашего клиента работает и с IPv4, и с IPv6, тогда как версия, представленная в листинге 1.1, работала только с IPv4, а версия из листинга 1.2 — только с IPv6. Сравните нашу новую версию с представленной в листинге Д.6, которую мы написали, чтобы использовать функции

gethostbyname
и
getservbyname
для поддержки и IPv4, и IPv6.

Сначала мы задаем имя узла, поддерживающего только IPv4:

freebsd % daytimetcpcli linux daytime

connected to 206 168.112.96

Sun Jul 27 23:06:24 2003

Затем мы задаем имя узла, поддерживающего и IPv4, и IPv6:

freebsd % daytimetcpcli aix daytime

connected to 3ffe:b80:1f8d:2:204:acff:fe17:bf38

Sun Jul 27 23:17:13 2003

Используется адрес IPv6, поскольку у узла имеется и запись типа AAAA, и запись типа А. Кроме того, функция

tcp_connect
устанавливает семейство адресов
AF_UNSPEC
, поэтому, как было отмечено в табл. 11.3, сначала идет поиск записей типа AAAA, и только если этот поиск неудачен, выполняется поиск записей типа А.

В следующем примере мы указываем на необходимость использования именно адреса IPv4, задавая имя узла с суффиксом

-4
, что, как мы отмечали в разделе 11.2, в соответствии с принятым нами соглашением означает имя узла, который поддерживает только записи типа А:

freebsd % daytimetcpcli aix-4 daytime

connected to 192.168.42.2

Sun Jul 27 23:17:48 2003

11.13. Функция tcp_listen

Наша следующая функция,

tcp_listen
, выполняет обычные шаги сервера TCP: создание сокета TCP, связывание его с заранее известным портом с помощью функции bind и разрешение приема входящих запросов через соединение. В листинге 11.6 представлен исходный код.

#include "unp.h"


int tcp_listen(const char *hostname, const char *service, socklen_t *lenptr);

В случае успешного выполнения возвращает дескриптор присоединенного сокета, в случае ошибки не возвращает ничего

Листинг 11.6. Функция tcp_listen: выполнение обычных шагов сервера TCP

//lib/tcp_listen.c

 1 #include "unp.h"


 2 int

 3 tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)

 4 {

 5  int listenfd, n;

 6  const int on = 1;

 7  struct addrinfo hints, *res, *ressave;


 8  bzero(&hints, sizeof(struct addrinfo));

 9  hints.ai_flags = AI_PASSIVE;

10  hints.ai_family = AF_UNSPEC;

11  hints.ai_socktype = SOCK_STREAM;


12  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)

13   err_quit("tcp_listen error for %s, %s: %s",

14    host, serv, gai_strerror(n));

15  ressave = res;


16  do {

17   listenfd =

18    socket(res->ai_family, res->ai_socktype, res->ai_protocol);

19   if (listenfd < 0)

20    continue; /* ошибка, пробуем следующий адрес */

21   Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

22   if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)

23    break; /* успех */


24   Close(listenfd); /* ошибка при вызове функции bind, закрываем

                         сокет и пробуем следующий адрес*/

25  } while ((res = res->ai_next) != NULL);


26  if (res == NULL) /* значение errno устанавливается при последнем

                        вызове функции socket() или bind() */

27   err_sys("tcp_listen error for %s, %s", host, serv);


28  Listen(listenfd, LISTENQ);


29  if (addrlenp)

30   *addrlenp = res->ai_addrlen; /* возвращает размер адреса протокола */


31  freeaddrinfo(ressave);


32  return (listenfd);

33 }

Вызов функции getaddrinfo

8-15
 Мы инициализируем структуру
addrinfo
с учетом следующих рекомендаций (элементов структуры
hints
):
AI_PASSIVE
, поскольку это функция для сервера,
AF_UNSPEC
для семейства адресов и
SOCK_STREAM
. Вспомните табл. 11.3: если имя узла не задано (что вполне нормально для сервера, который хочет связать с дескриптором универсальный адрес), то наличие значений
AI_PASSIVE
и
AF_UNSPEC
вызовет возвращение двух структур адреса сокета: первой для IPv6 и второй для IPv4 (в предположении, что это узел с двойным стеком).

Создание сокета и связывание с адресом

16-24
 Вызываются функции
socket
и
bind
. Если любой из вызовов окажется неудачным, мы просто игнорируем данную структуру
addrinfo
и переходим к следующей. Как было сказано в разделе 7.5, для сервера TCP мы всегда устанавливаем параметр сокета
SO_REUSEADDR
.

Проверка на наличие ошибки

25-26
 Если все вызовы функций
socket
и
bind
окажутся неудачными, мы сообщаем об ошибке и завершаем выполнение. Как и в случае с нашей функцией
tcp_connect
из предыдущего раздела, мы не пытаемся возвратить ошибку из этой функции.

27
 Сокет превращается в прослушиваемый сокет с помощью функции
listen
.

Возвращение размера структуры адреса

28-31
 Если аргумент
addrlenp
является непустым указателем, мы возвращаем размер адресов протокола через этот указатель. Это позволяет вызывающему процессу выделять память для структуры адреса сокета, чтобы получить адрес протокола клиента из функции accept (см. также упражнение 11.7).

Пример: сервер времени и даты

В листинге 11.7 показан наш сервер времени и даты из листинга 4.2, переписанный с использованием функции

tcp_listen
.

Листинг 11.7. Сервер времени и даты, переписанный с использованием функции tcp_listen

//names/daytimetcpsrv1.c

 1 #include "unp.h"

 2 #include 


 3 int

 4 main(int argc, char **argv)

 5 {

 6  int listenfd, connfd;

 7  socklen_t addrlen, len;

 8  char = buff[MAXLINE];

 9  time_t ticks;

10  struct sockaddr_storage cliaddr;


11  if (argc != 2)

12   err_quit("usage: daytimetcpsrv1 ");


13  listenfd = Tcp_listen(NULL, argv[1], &addrlen);


14  for (;;) {

15   len = sizeof(cliaddr);

16   connfd = Accept(listenfd, (SA*)&cliaddr, &len);

17   printf("connection from %s\n", Sock_ntop((SA*)&cliaddr, len));


18   ticks = time(NULL);

19   snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));

20   Write(connfd, buff, strlen(buff));


21   Close(connfd);

22  }

23 }

Ввод имени службы или номера порта в качестве аргумента командной строки

11-12
 Нам нужно использовать аргумент командной строки, чтобы задать либо имя службы, либо номер порта. Это упрощает проверку нашего сервера, поскольку связывание с портом 13 для сервера времени и даты требует прав привилегированного пользователя.

Создание прослушиваемого сокета

13
 Функция
tcp_listen
создает прослушиваемый сокет. В качестве третьего аргумента мы передаем нулевой указатель, потому что нам безразличен размер структуры адреса, используемого данным семейством: мы будем работать со структурой
sockaddr_storage
.

Цикл сервера

14-22
 Функция
accept
ждет соединения с клиентом. Мы выводим адрес клиента, вызывая функцию
sock_ntop
. В случае IPv4 или IPv6 эта функция выводит IP-адрес и номер порта. Мы могли бы использовать функцию
getnameinfo
(описанную в разделе 11.17), чтобы попытаться получить имя узла клиента, но это подразумевает запрос PTR в DNS, что может занять некоторое время, особенно если запрос PTR окажется неудачным. В разделе 14.8 [112] упоминается, что на занятом веб-сервере почти у 25% всех клиентов, соединяющихся с этим сервером, в DNS нет записей типа PTR. Поскольку мы не хотим, чтобы наш сервер (особенно последовательный сервер) в течение нескольких секунд ждал запрос PTR, мы просто выводим IP-адрес и порт.

Пример: сервер времени и даты с указанием протокола

В листинге 11.7 есть небольшая проблема: первый аргумент функции

tcp_listen
 — пустой указатель, объединенный с семейством адресов
AF_UNSPEC
, который задает функция
tcp_listen
, — может заставить функцию
getaddrinfo
возвратить структуру адреса сокета с семейством адресов, отличным от желаемого. Например, первой на узле с двойным стеком будет возвращена структура адреса сокета для IPv6 (см. табл. 11.3), но, возможно, нам требуется, чтобы наш сервер обрабатывал только IPv4.

У клиентов такой проблемы нет, поскольку клиент должен всегда задавать либо IP-адрес, либо имя узла. Клиентские приложения обычно позволяют пользователю вводить этот параметр как аргумент командной строки. Это дает нам возможность задавать имя узла, связанное с определенным типом IP-адреса (вспомните наши имена узлов -4 и -6 в разделе 11.2), или же задавать либо строку в точечно-десятичной записи (для IPv4), либо шестнадцатеричную строку (для IPv6).

И для серверов существует простая методика, позволяющая нам указать, какой именно протокол следует использовать — IPv4 или IPv6. Для этого нужно позволить пользователю ввести либо IP-адрес, либо имя узла в качестве аргумента командной строки и передать его функции

getaddrinfo
. В случае IP-адреса строка точечно-десятичной записи IPv4 отличается от шестнадцатеричной строки IPv6. Следующие вызовы функции
inet_pton
оказываются либо успешными либо нет, как это показано в данном случае:

inet_pton(AF_INET,  "0.0.0.0", &foo); /* успешно */

inet_pton(AF_INET,  "0::0",    &foo); /* неудачно*/

inet_pton(AF_INET6, "0.0.0.0", &foo); /* неудачно */

inet_pton(AF_INET6, "0::0",    &foo); /* успешно */

Следовательно, если мы изменим наши серверы таким образом, чтобы они получали дополнительный аргумент, то при вводе

% server

по умолчанию мы получим IPv6 на узле с двойным стеком, но при вводе

% server 0.0.0.0

явно задается IPv4, а при вводе

% server 0::0

явно задается IPv6.

В листинге 11.8 показана окончательная версия нашего сервера времени и даты.

Листинг 11.8. Не зависящий от протокола сервер времени и даты, использующий функцию tcp_listen

names/daytimetcpsrv2.c

 1 #include "unp.h"

 2 #include 


 3 int

 4 main(int argc, char **argv)

 5 {

 6  int listenfd, connfd;

 7  socklen_t addrlen, len;

 8  struct sockaddr_storage cliaddr;

 9  char buff[MAXLINE];

10  time_t ticks;


11  if (argc == 2)

12   listenfd = Tcp_listen(NULL, argv[1], &addrlen);

13  else if (argc == 3)

14   listenfd = Tcp_listen(argv[1], argv[2], &addrlen);

15  else

16   err_quit("usage; daytimetcpsrv2 [  ] ");


17  for (;;) {

18   len = sizeof(cliaddr);

19   connfd = Accept(listenfd, (SA*)&cliaddr, &len);

20   printf("connection from %s\n", Sock_ntop((SA*)&cliaddr, len));


21   ticks = time(NULL);

21   snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));

23   Write(connfd, buff, strlen(buff));


24   Close(connfd);

25  }

26 }

Обработка аргументов командной строки

11-16
 Единственное изменение по сравнению с листингом 11.6 — это обработка аргументов командной строки, позволяющая пользователю в дополнение к имени службы или порту задавать либо имя узла, либо IP-адрес для связывания с сервером.

Сначала мы запускаем этот сервер с сокетом IPv4 и затем соединяемся с сервером от клиентов на двух различных узлах, расположенных в локальной подсети:

freebsd % daytimetcpsrv2 0.0.0.0 9999

connection from 192.168.42.2:32961

connection from 192.168.42.2:1389

А теперь мы запустим сервер с сокетом IPv6:

solaris % daytimetcpsrv2 0::0 9999

connection from [3ffe:b80:1f8d:2:204:acff:fe17:bf38]:32964

connection from [3ffe:b80:1f8d:2:230:65ff:fe15:caa7]:49601

connection from [::ffff:192:168:42:3]:32967

connection from [::ffff:192:168:42:3]:49602

Первое соединение — от узла

aix
, использующего IPv6, а второе — от узла
macosx
, использующего IPv6. Два следующих соединения — от узлов
aix
и
macosx
, но они используют IPv4, а не IPv6. Мы можем определить это, потому что оба адреса клиента, возвращаемые функцией
accept
, являются адресами IPv4, преобразованными к виду IPv6.

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

11.14. Функция udp_client

Наши функции, предоставляющие более простой интерфейс для функции

getaddrinfo
, в случае UDP изменяются: в этом разделе мы представляем клиентскую функцию, создающую неприсоединенный сокет UDP, а в следующем — другую функцию, создающую присоединенный сокет UDP.

#include "unp.h"


int udp_client(const char *hostname, const char *service,

 void **saptr, socklen_t *lenp);

Возвращает: дескриптор неприсоединенного сокета в случае успешного выполнения, в случае ошибки не возвращает ничего

Эта функция создает неприсоединенный сокет UDP, возвращая три элемента. Во-первых, возвращаемое значение функции — это дескриптор сокета. Во-вторых,

saptr
 — это адрес указателя (объявляемого вызывающим процессом) на структуру адреса сокета (которая динамически размещается в памяти функцией
udp_client
), и в этой структуре функция хранит IP-адрес получателя и номер порта для будущих вызовов функции
sendto
. Размер этой структуры адреса сокета возвращается как значение переменной, на которую указывает
lenp
. Последний аргумент не может быть пустым указателем (как это допустимо для последнего аргумента функции
tcp_listen
), поскольку длина структуры адреса сокета требуется в любых вызовах функций
sendto
и
recvfrom
.

В листинге 11.9 показан исходный код для этой функции.

Листинг 11.9. Функция udp_client: создание неприсоединенного сокета UDP

//lib/udp_client.c

 1 #include "unp.h"


 2 int

 3 udp_client(const char *host, const char *serv, void **saptr, socklen_t *lenp)

 4 {

 5  int sockfd, n;

 6  struct addrinfo hints, *res, *ressave;


 7  bzero(&hints, sizeof(struct addrinfo));

 8  hints.ai_family = AF_UNSPEC;

 9  hints.ai_socktype = SOCK_DGRAM;


10  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)

11   err_quit("udp_client error for %s, %s: %s",

12   host, serv, gai_strerror(n));

13  ressave = res;


14  do {

15   sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

16   if (sockfd >= 0)

17    break; /* успех */

18  } while ((res = res->ai_next) != NULL);


19  if (res == NULL) /* значение errno устанавливается при последнем

                        вызове функции socket() */

20  err_sys("udp_client error for %s, %s", host, serv);


21  *saptr = Malloc(res->ai_addrlen);

22  memcpy(*saptr, res->ai_addr, res->ai_addrlen);

23  *lenp = res->ai_addrlen;


24  freeaddrinfo(ressave);


25  return (sockfd);

26 }

Функция

getaddrinfo
преобразует аргументы
hostname
и
service
. Создается дейтаграммный сокет. Выделяется память для одной структуры адреса сокета, и структура адреса сокета, соответствующая созданному сокету, копируется в память.

Пример: не зависящий от протокола UDP-клиент времени и даты

Теперь мы перепишем наш клиент времени и даты, показанный в листинге 11.3, так, чтобы в нем использовалась наша функция

udp_client
. В листинге 11.10 представлен не зависящий от протокола исходный код.

Листинг 11.10. UDP-клиент времени и даты, использующий нашу функцию udp_client

//names/daytimeudpcli1.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd, n;

 6  char recvline[MAXLINE + 1];

 7  socklen_t salen;

 8  struct sockaddr *sa;


 9  if (argc != 3)

10   err_quit

11    ("usage; daytimeudpcli1 ");


12  sockfd = Udp_client(argv[1], argv[2], (void**)&sa, &salen);


13  printf("sending to %s\n", Sock_ntop_host(sa, salen));


14  Sendto(sockfd, "", 1, 0, sa, salen); /* посылается 1-байтовая

                                            дейтаграмма */


15  n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

16  recvline[n] = 0; /* завершающий пустой байт */

17  Fputs(recvline, stdout);


18  exit(0);

19 }

12-17
 Мы вызываем нашу функцию
udp_client
и затем выводим IP-адрес и порт сервера, которому мы отправим нашу дейтаграмму UDP. Мы посылаем однобайтовую дейтаграмму и затем читаем и выводим ответ сервера.

ПРИМЕЧАНИЕ

Нам достаточно отправить дейтаграмму, содержащую 0 байт, поскольку ответ сервера времени и даты инициируется самим получением дейтаграммы от клиента, независимо от ее длины и содержания. Но многие реализации SVR4 не допускают нулевой длины дейтаграмм UDP.

Мы запускаем наш клиент, задавая имя узла с записью типа AAAA и типа А. Поскольку функция

getaddrinfo
в первую очередь возвращает структуру с записью типа AAAA, создается сокет IPv6:

freebsd % daytimeudpcli1 aix daytime

sending to 3ffe:b80:1f8d:2:204:acff:fe17:bf38

Sun Jul 23:21:12 2003

Затем мы задаем адрес того же узла в точечно-десятичной записи, в результате чего создается сокет IPv4:

freebsd % daytimeudpcli1 192.168.42.2 daytime

sending to 192.168.42.2

Sun Jul 23:21:40 2003

11.15. Функция udp_connect

Наша функция

udp_connect
создает присоединенный сокет UDP.

#include "unp.h"


int udp_connect(const char *hostname, const char *service);

Возвращает; дескриптор присоединенного сокета в случае успешного выполнения, в случае ошибки ничего не возвращает

В случае присоединенного сокета UDP два последних аргумента, которые требовались в функции

udp_client
, больше не нужны. Вызывающий процесс может вызвать функцию
write
вместо
sendto
, таким образом нашей функции не нужно возвращать структуру адреса сокета и ее длину. В листинге 11.11 представлен исходный код.

Листинг 11.11. Функция udp_connect: создание присоединенного сокета UDP

//lib/udp_connect.c

 1 #include "unp.h"


 2 int

 3 udp_connect(const char *host, const char *serv)

 4 {

 5  int sockfd, n;

 6  struct addrinfo hints, *res, *ressave;


 7  bzero(&hints, sizeof(struct addrinfo));

 8  hints.ai_family = AF_UNSPEC;

 9  hints.ai_socktype = SOCK_DGRAM;


10  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)

11   err_quit("udp_connect error for %s, %s: %s",

12    host, serv, gai_strerror(n));

13  ressave = res;


14  do {

15   sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

16   if (sockfd < 0)

17    continue; /* игнорируем этот адрес */


18   if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)

19    break; /* успех */


20   Close(sockfd); /* игнорируем этот адрес */

21  } while ((res = res->ai_next) != NULL);


22  if (res == NULL) /* значение errno устанавливается при

                        последнем вызове функции connect() */

23  err_sys("udp_connect error for %s, %s", host, serv);


24  freeaddrinfo(ressave);


25  return (sockfd);

26 }

Эта функция почти идентична функции

tcp_connect
. Однако отличие в том, что при вызове функции
connect
для сокета UDP ничего не отправляется собеседнику. Если что-то не в порядке (собеседник недоступен или на заданном порте не запущен сервер), вызывающий процесс не обнаружит этого, пока не пошлет собеседнику дейтаграмму.

11.16. Функция udp_server

Наша последняя функция, предоставляющая более простой интерфейс для функции

getaddrinfo
, — это функция
udp_server
.

#include "unp.h"


int udp_server(const char *hostname, const char *service, socklen_t *lenptr);

Возвращает; дескриптор неприсоединенного сокета в случае успешного выполнения, в случае ошибки не возвращает ничего

Аргументы функции те же, что и для функции

tcp_listen
: необязательный
hostname
, обязательный
service
(для связывания номер порта) и необязательный указатель на переменную, в которой возвращается размер структуры адреса сокета. В листинге 11.12 представлен исходный код.

Листинг 11.12. Функция udp_server: создание неприсоединенного сокета для сервера UDP

//lib/udp_server.c

 1 #include "unp.h"


 2 int

 3 udp_server(const char *host, const char *serv, socklen_t *addrlenp)

 4 {

 5  int sockfd, n;

 6  struct addrinfo hints, *res, *ressave;


 7  bzero(&hints, sizeof(struct addrinfo));

 8  hints.ai_flags = AI_PASSIVE;

 9  hints.ai_family = AF_UNSPEC;

10  hints.ai_socktype = SOCK_DGRAM;


11  if ((n = getaddrinfo(host, serv, &hints, &res)) != 0)

12   err_quit("udp_server error for %s, %s: %s",

13    host, serv, gai_strerror(n));

14  ressave = res;


15  do {

16   sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);

17   if (sockfd < 0)

18    continue; /* ошибка, пробуем следующий адрес */


19   if (bind(sockfd, res->ai_addr, res->ai_addrlen) == 0)

20    break; /* успех */


21   Close(sockfd); /* ошибка при вызове функции bind, закрываем

                       сокет и пробуем следующий адрес */

22  } while ((res = res->ai_next) != NULL);


23  if (res == NULL) /* значение errno устанавливается при

                        последнем вызове функции socket() or bind() */

24   err_sys("udp_server error for %s, %s", host, serv);


25  if (addrlenp)

26   *addrlenp = res->ai_addrlen; /* возвращается размер адреса

                                     протокола */

27  freeaddrinfo(ressave);


28  return (sockfd);

29 }

Эта функция практически идентична функции

tcp_listen
, в ней нет только вызова функции
listen
. Мы устанавливаем семейство адресов
AF_UNSPEC
, но вызывающий процесс может использовать ту же технологию, которую мы описали при рассмотрении листинга 11.6, чтобы потребовать использование определенного протокола (IPv4 или IPv6).

Мы не устанавливаем параметр сокета

SO_REUSEADDR
для сокета UDP, поскольку этот параметр сокета может допустить связывание множества сокетов с одним и тем же портом UDP на узлах, поддерживающих многоадресную передачу, как мы говорили в разделе 7.5. Поскольку у сокета UDP нет аналога состояния TIME_WAIT, свойственного сокетам TCP, нет необходимости устанавливать этот параметр при запуске сервера.

Пример: не зависящий от протокола UDP-сервер времени и даты

В листинге 11.13 представлен наш сервер времени и даты, полученный путем модификации листинга 11.8 и предназначенный для использования UDP.

Листинг 11.13. Не зависящий от протокола UDP-сервер времени и даты

//names/daytimeudpsrv2.c

 1 #include "unp.h"

 2 #include 


 3 int

 4 main(int argc, char **argv)

 5 {

 6  int sockfd;

 7  ssize_t n;

 8  char buff[MAXLINE];

 9  time_t ticks;

10  socklen_t addrlen, len;

11  struct sockaddr_storage cliaddr;


12  if (argc == 2)

13   sockfd = Udp_server(NULL, argv[1], &addrlen);

14  else if (argc == 3)

15   sockfd = Udp_server(argv[1], argv[2], &addrlen);

16  else

17   err_quit("usage: daytimeudpsrv [  ] ");


18  for (;;) {

19   len = sizeof(cliaddr);

20   n = Recvfrom(sockfd, buff, MAXLINE, 0, (SA*)&cliaddr, &len);

21   printf("datagram from %s\n", Sock_ntop((SA*)&cliaddr, len));


22   ticks = time(NULL);

23   snprintf(buff, sizeof(buff), "% 24s\r\n", ctime(&ticks));

24   Sendto(sockfd, buff, strlen(buff), 0, (SA*)&cliaddr, len);

25  }

26 }

11.17. Функция getnameinfo

Эта функция дополняет функцию

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

#include 


int getnameinfo(const struct sockaddr *sockaddr, socklen_t addrlen, char *host,

 size_t hostlen, char *serv, size_t servlen, int flags);

Возвращает 0 в случае успешного выполнения, -1 в случае ошибки

Аргумент

sockaddr
указывает на структуру адреса сокета, содержащую адрес протокола, преобразуемый в строку, удобную для человеческого восприятия, а аргумент
addrlen
содержит длину этой структуры. Эта структура и ее длина обычно возвращаются любой из следующих функций:
accept
,
recvfrom
,
getsockname
или
getpeername
.

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

host
и
hostlen
определяют строку, описывающую узел, а аргументы
serv
и
servlen
определяют строку, которая описывает службы. Если вызывающему процессу не нужна возвращаемая строка с описанием узла, задается нулевая длина этой строки (
hostlen
). Аналогично, нулевое значение аргумента
servlen
означает, что не нужно возвращать информацию о службе.

Разница между функциями

sock_ntop
и
getnameinfo
состоит в том, что первая не задействует DNS, а только возвращает IP-адрес и номер порта. Последняя же обычно пытается получить имя и для узла, и для службы.

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

getnameinfo
.


Таблица 11.4. Флаги функции getnameinfo

КонстантаОписание
NI_DGRAMДейтаграммный сокет
NI_NAMEREQDВозвращать ошибку, если невозможно получить имя узла по его адресу
NI_NOFQDNВозвращать только ту часть FQDN, которая содержит имя узла
NI_NUMERICHOSTВозвращать численное значение адреса вместо имени узла
NI_NUMERICSCOPEВозвращать численное значение идентификатора области
NI_NUMERICSERVВозвращать номер порта вместо имени службы

■ Флаг

NI_DGRAM
должен быть задан, когда вызывающий процесс знает, что работает с дейтаграммным сокетом. Причина в том, что если функции
getnameinfo
задать только IP-адрес и номер порта в структуре адреса сокета, она не сможет определить протокол (TCP или UDP). Существует несколько номеров портов, которые в случае TCP задействованы для одной службы, а в случае UDP для совершенно другой. Примером может служить порт 514, используемый службой
rsh
в TCP и службой
syslog
в UDP.

■ Флаг

NI_NAMEREQD
приводит к возвращению ошибки, если имя узла не может быть разрешено при использовании DNS. Этот флаг может использоваться серверами, которым требуется, чтобы IP-адресу клиента было сопоставлено имя узла. Затем эти серверы получают возвращаемое имя узла, вызывают функцию
gethostbyname
и проверяют, совпадают ли результаты вызова этих двух функций хотя бы частично.

■ Флаг

NI_NOFQDN
вызывает сокращение имени узла, отбрасывая все, что идет после первой точки. Например, если в структуре адреса сокета содержится IP-адрес 192.168.42.2, то функция
gethostbyaddr
возвратит имя
aix.unpbook.com
. Но если в функции
getnameinfo
задан флаг
NI_NOFQDN
, она возвратит в имени узла только
aix
.

■ Флаг

NI_NUMERICHOST
сообщает функции
getnameinfo
, что не нужно вызывать DNS (поскольку это занимает некоторое время). Вместо этого возвращается численное представление IP-адреса, вероятно, при помощи вызова функции
inet_ntop
. Аналогично, флаг
NI_NUMERICSERV
определяет, что вместо имени службы должен быть возвращен десятичный номер порта. Обычно серверы должны задавать этот флаг, поскольку номера портов клиента, как правило, не имеют соответствующего имени службы — это динамически назначаемые порты.
NI_NUMERICSCOPE
указывает на необходимость возвращения идентификатора области в численном, а не в текстовом виде.

Можно объединять несколько флагов путем логического сложения, если их сочетание имеет смысл, например

NI_DGRAM
и
NI_NUMERICHOST
.

11.18. Функции, допускающие повторное вхождение

Функция

gethostbyname
из раздела 11.3 имеет интересную особенность, которую мы еще не рассматривали: она не допускает повторное вхождение (nonreentrant). Мы еще столкнемся с этой проблемой в главе 23, когда будем обсуждать потоки, но не менее интересно найти решение этой проблемы сейчас, без необходимости обращаться к понятию потоков.

Сначала посмотрим, как эта функция работает. Если мы изучим ее исходный код (это несложно, поскольку исходный код для всей реализации BIND свободно доступен), то увидим, что обе функции — и

gethostbyname
, и
gethostbyaddr
— содержатся в одном файле, который имеет следующий вид:

static struct hostent host; /* здесь хранится результат */


struct hostent*

gethostbyname(const char *hostname) {

 return(gethostbyname2(hostname, family));

}


struct hostent*

gethostbyname2(const char *hostname, int family) {

 /* вызов функций DNS для запроса А или AAAA */


 /* заполнение структуры адреса узла */

 return(&host);

}


struct hostent*

gethostbyaddr(const char *addr, size_t len, int family) {

 /* вызов функций DNS для запроса PTR в домене in-addr.arpa */


 /* заполнение структуры адреса узла */

 return(&host);

}

Мы выделили полужирным шрифтом спецификатор класса памяти

static
итоговой структуры, потому что основная проблема в нем. Тот факт, что эти три функции используют общую переменную
host
, представляет другую проблему, которую мы обсудим в упражнении 11.1. (Вспомните табл. 11.4.) Функция
gethostbyname2
появилась в BIND 4.9.4 с добавлением поддержки IPv6. Мы будем игнорировать тот факт, что когда мы вызываем функцию
gethostbyname
, задействуется функция
gethostbyname2
, поскольку это не относится к предмету обсуждения.

Проблема повторного вхождения может возникнуть в нормальном процессе Unix, вызывающем функцию

gethostbyname
или
gethostbyaddr
и из управляющего элемента главного потока, и из обработчика сигнала. Когда вызывается обработчик сигнала (допустим, это сигнал
SIGALRM
, который генерируется раз в секунду), главный поток управляющего элемента процесса временно останавливается и вызывается функция обработки сигнала. Рассмотрим следующую ситуацию:

main() {

 struct hostent *hptr;

 ...

 signal(SIGALRM, sig_alrm);

 ...

 hptr = gethostbyname( ... );

 ...

}


void

sig_alrm(int signo) {

 struct hostent *hptr;

 ...

 hptr = gethostbyname( ... );

 ...

}

Если главный поток управления в момент остановки находится в середине выполнения функции

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

Если мы посмотрим на функции преобразования имен и адресов, представленные в этой главе и в главе 9, вместе с функциями

inet_XXX
из главы 4, мы заметим следующее:

■ Функции

gethostbyname
,
gethostbyname2
,
gethostbyaddr
,
getservbyname
и
getservbyport
традиционно не допускают повторного вхождения, поскольку все они возвращают указатель на статическую структуру.

Некоторые реализации, поддерживающие программные потоки (Solaris 2.x), предоставляют версии этих четырех функций, допускающие повторное вхождение, с именами, оканчивающимися суффиксом

_r
. О них рассказывается в следующем разделе.

В качестве альтернативы некоторые реализации с поддержкой программных потоков (Digital Unix 4.0 и HP_UX 10.30) предоставляют версии этих функций, допускающие повторное вхождение за счет использования собственных данных программных потоков.

■ Функции

inet_pton
и
inet_ntop
всегда допускают повторное вхождение.

■ Исторически функция

inet_ntoa
не допускает повторное вхождение, но некоторые реализации с поддержкой потоков предоставляют версию, допускающую повторное вхождение, которая строится на основе собственных данных потоков.

■ Функция

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

■ Функция

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

Похожая проблема возникает с переменной

errno
. Исторически существовало по одной копии этой целочисленной переменной для каждого процесса. Если процесс выполняет системный вызов, возвращающий ошибку, то в этой переменной хранится целочисленный код ошибки. Например, функция
close
из стандартной библиотеки языка С может выполнить примерно такую последовательность действий:

■ поместить аргумент системного вызова (целочисленный дескриптор) в регистр;

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

close
;

■ активизировать системный вызов (переключиться на ядро со специальной инструкцией);

■ проверить значение регистра, чтобы увидеть, что произошла ошибка;

■ если ошибки нет, возвратить (0);

■ сохранить значение какого-то другого регистра в переменной

errno
;

■ возвратить (-1).

Прежде всего заметим, что если ошибки не происходит, значение переменной

errno
не изменяется. Поэтому мы не можем посмотреть значение этой переменной, пока мы не узнаем, что произошла ошибка (обычно на это указывает возвращаемое функцией значение -1).

Будем считать, что программа проверяет возвращаемое значение функции

close
и затем выводит значение переменной
errno
, если произошла ошибка, как в следующем примере:

if (close(fd) < 0) {

 fprintf(stderr, "close error, errno = $d\n", errno);

 exit(1);

}

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

errno
. Если, например, при вызове обработчика сигналов главный поток управления находится между
close
и
fprintf
и обработчик сигналов делает какой-то другой системный вызов, возвращающий ошибку (допустим, вызывается функция
write
), то значение переменной
errno
, записанное при вызове функции
close
, заменяется на значение, записанное при вызове функции
write
.

При рассмотрении этих двух проблем в связи с обработчиками сигналов одним из решений проблемы с функцией

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

void sig_alrm(int signo) {

int errno_save;


errno_save = errno; /* сохраняем значение этой переменной

                       при вхождении */

if (write( ... ) != nbytes)

 fprintf(stderr, "write error, errno = %d\n", errno);

 errno = errno_save; /* восстанавливаем значение этой переменной

                        при завершении */

}

В этом коде мы также вызываем функцию

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

Мы вернемся к проблеме повторного вхождения в главе 26 и увидим, как проблема с переменной

errno
решается с помощью потоков. В следующем разделе описываются некоторые версии функций имен узлов, допускающие повторное вхождение.

11.19. Функции gethostbyname_r и gethostbyaddr_r

Чтобы превратить функцию, не допускающую повторное вхождение, такую как

gethostbyname
, в повторно входимую, можно воспользоваться двумя способами.

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

gethostbyname
(которая не допускает повторное вхождение) к функции
gethostbyname_r
(которая допускает повторное вхождение). Но это решение усложняется, поскольку помимо того, что вызывающий процесс должен предоставить структуру
hostent
для заполнения, эта структура также указывает на другую информацию: каноническое имя, массив указателей на псевдонимы, строки псевдонимов, массив указателей на адреса и сами адреса (см., например, рис. 11.2). Вызывающий процесс должен предоставить один большой буфер, используемый для дополнительной информации, и заполняемая структура
hostent
будет содержать различные указатели на этот буфер. При этом добавляется как минимум три аргумента функции: указатель на заполняемую структуру
hostent
, указатель на буфер, используемый для всей прочей информации, и размер этого буфера. Требуется также четвертый дополнительный аргумент — указатель на целое число, в котором будет храниться код ошибки, поскольку глобальная целочисленная переменная
h_errno
больше не может использоваться. (Глобальная целочисленная переменная
h_errno
создает ту же проблему повторного вхождения, которая описана нами для переменной
errno
.)

Эта технология также используется функциями

getnameinfo
и
inet_ntop
.

2. Входящая функция вызывает функцию

malloc
и динамически выделяет память. Это технология, используемая функцией
getaddrinfo
. Проблема при таком подходе заключается в том, что приложение, вызывающее эту функцию, должно вызвать также функцию
freeaddrinfo
, чтобы освободить динамическую память. Если эта функция не вызывается, происходит утечка памяти: каждый раз, когда процесс вызывает функцию, выделяющую память, объем памяти, задействованной процессом, возрастает. Если процесс выполняется в течение длительного времени (что свойственно сетевым серверам), то потребление памяти этим процессом с течением времени неуклонно растет.

Обсудим функции Solaris 2.x, допускающие повторное вхождение, не используемые для сопоставления имен с адресами, и наоборот (то есть для разрешения имен).

#include 


struct hostent *gethostbyname_r(const char *hostname,

struct hostent *result, char *buf, int buflen, int *h_errnop);

struct hostent *gethostbyaddr_r(const char *addr, int len,

 int type, struct hostent *result, char *buf, int buflen,

 int *h_errnop);

Обе функции возвращают: непустой указатель в случае успешного выполнения, NULL в случае ошибки

Для каждой функции требуется четыре дополнительных аргумента. Аргумент

result
 — это структура
hostent
, размещенная в памяти вызывающим процессом и заполняемая данной функцией. При успешном выполнении функции этот указатель также является возвращаемым значением.

Аргумент

buf
— это буфер, размещенный в памяти вызывающим процессом, a
buflen
 — его размер. Буфер будет содержать каноническое имя, массив указателей на псевдонимы, строки псевдонимов, массив указателей на адреса и сами адреса. Все указатели в структуре
hostent
, на которую указывает
result
, указывают на этот буфер. Насколько большим должен быть этот буфер? К сожалению, все, что сказано в большинстве руководств, это что-то неопределенное вроде «Буфер должен быть достаточно большим, чтобы содержать все данные, связанные с записью узла». Текущие реализации функции
gethostbyname
могут возвращать до 35 указателей на альтернативные имена (псевдонимы), до 35 указателей на адреса и использовать буфер размером 8192 байт для хранения альтернативных имен (псевдонимов) и адресов. Поэтому буфер размером 8192 байт можно считать подходящим.

Если происходит ошибка, код ошибки возвращается через указатель

h_errnop
, а не через глобальную переменную
h_errno
.

ПРИМЕЧАНИЕ

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

Во-вторых, не существует стандарта для функций _r. В этом разделе (в качестве примера) мы привели две функции _r, предоставляемые Solaris 2.x. В Linux присутствуют аналогичные функции, возвращающие hostent в качестве аргумента типа значение-результат. В Digital Unix и HP-UX имеются версии этих функций с другими аргументами. Первые два аргумента функции gethostbyname_r такие же, как и в версии Solaris, но оставшиеся три аргумента версии Solaris объединены в новую структуру hostent_data (которая должна быть размещена в памяти вызывающим процессом), а указатель на эту структуру — это третий и последний аргумент. Обычные функции gethostbyname и gethostbyaddr в Digital Unix 4.0 и в HP-UX 10.30 допускают повторное вхождение при использовании собственных данных потоков (см. раздел 23.5). Интересный рассказ о разработке функций _r Solaris 2.x содержится в [70].

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

11.20. Устаревшие функции поиска адресов IPv6

В процессе разработки IPv6 интерфейс поиска адресов IPv6 много раз претерпевал серьезные изменения. В какой-то момент интерфейс был сочтен усложненным и недостаточно гибким, так что от него полностью отказались в RFC 2553 [38]. Документ RFC 2553 предлагал собственные функции, которые в RFC 3493 [36] были попросту заменены

getaddrinfo
и
getnameinfo
. В этом разделе мы вкратце рассмотрим старые интерфейсы на тот случай, если вам придется переписывать программы, использующие их.

Константа RES_USE_INET6

Поскольку функция

gethostbyname
не имеет аргумента для указания нужного семейства адресов (подобного
hints.ai_family
для
getaddrinfo
), в первом варианте API использовалась константа
RES_USE_INET6
, которая должна была добавляться к флагам распознавателя посредством внутреннего интерфейса. Этот API был недостаточно переносимым, поскольку системам, использовавшим альтернативные внутренние интерфейсы распознавателя, приходилось имитировать интерфейс BIND.

Включение

RES_USE_INET6
приводило к тому, что функция
gethostbyname
начинала поиск с записей AAAA, а записи А возвращались только в случае отсутствия первых. Поскольку в структуре
hostent
есть только одно поле длины адреса, функция
gethostbyname
могла возвращать адреса только одного типа (либо IPv6, либо IPv4).

Кроме того, включение

RES_USE_INET6
приводило к тому, что функция
gethostbyname2
начинала возвращать адреса IPv4 в преобразованном к IPv6 виде.

Функция gethostbyname2

Функция

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

#include 


struct hostent *gethostbyname2(const char *hostname, int family);

Возвращает: непустой указатель в случае успешного выполнения, в случае ошибки возвращает NULL и задает значение переменной h_errno

Возвращаемое значение то же, что и у функции

gethostbyname
— указатель на структуру
hostent
, и сама эта структура устроена так же. Логика функции зависит от аргумента
family
и параметра распознавателя
RES_USE_INET6
(который мы упомянули в конце предыдущего раздела).

Функция getipnodebyname

Документ RFC 2553 [38] запретил использование

RES_USE_INET6
и
gethostbyname2
из-за глобальности флага
RES_USE_INET6
и желания предоставить больше возможностей по управлению возвращаемыми сведениями. Для решения перечисленных проблем была предложена функция
getipnodebyname
.

#include 

#include 


struct hostent *getipnodebyname(const char *name, int af,

 int flags, int *error_num);

Возвращает: ненулевой указатель в случае успешного завершения, нулевой в случае ошибки

Функция возвращает указатель на ту же структуру

hostent
, которая использовалась
gethostbyname
. Аргументы
af
и
flags
непосредственно соответствуют полям
hints.ai_family
и
hints.ai_flags
. Для обеспечения безопасности в многопоточной среде возвращаемое значение выделяется динамически, поэтому его приходится освобождать вызовом
freehostent
.

#include 


void freehostent(struct hostent *ptr);

Функции

getipnodebyname
и
getipnodebyaddr
были отменены в RFC 3493 [36], а вместо них было предложено использовать
getaddrinfo
и
getnameinfo
.

11.21. Другая информация о сетях

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

gethostbyname
и
gethostbyaddr
), реже — к службам (функции
getservbyname
и
getservbyaddr
) и еще реже — к сетям и протоколам.

Все четыре типа данных могут храниться в файле, и для каждого из четырех типов определены три функции:

1. Функция

getXXXent
, читающая следующую запись в файле, при необходимости открывающая файл.

2. Функция

setXXXent
, которая открывает файл (если он еще не открыт) и переходит к началу файла.

3. Функция

endXXXent
, закрывающая файл.

Для каждого из четырех типов данных определяется его собственная структура (соответственно, структуры

hostent
,
netent
,
protoent
и
servent
), что требует включения заголовка
.

В дополнение к трем функциям

get
,
set
и
end
, которые допускают последовательную обработку файла, для каждого из четырех типов данных предоставляются функции ключевого поиска, или поиска по ключу (keyed lookup). Эти функции последовательно проходят файл (вызывая функцию
getXXXent
для чтения каждой строки файла), но вместо того чтобы возвращать каждую строку вызывающему процессу, эти функции ищут элемент, совпадающий с аргументом. Имена функций поиска по ключу имеют вид
getXXXbyYYY
. Например, две функции ключевого поиска для информации об узле — это функции
gethostbyname
(ищет элемент, совпадающий с именем узла) и
gethostbyaddr
(ищет элемент, совпадающий с IP-адресом). Таблица 11.5 обобщает эту информацию.


Таблица 11.5. Четыре типа данных, относящихся к сетям

Тип данныхФайлСтруктураФункции поиска по ключу
Узлы/etc/hostsHostentgethostbyaddr, gethostbyname
Сети/etc/networksNetentgetnetbyaddr, getnetbyname
Протоколы/etc/protocolsProtoentgetprotobyname, getprotobynumber
Службы/etc/servicesServentgetservbyname, getservbyport

Как это применяется, если используется DNS? Прежде всего, с помощью DNS возможен доступ только к информации об узле и о сети. Информация о протоколе и службах всегда считывается из соответствующего файла. Ранее в этой главе мы отмечали (см. подраздел «Альтернативы DNS»), что в разных реализациях отличаются способы, с помощью которых администратор определяет, что именно использовать для получения информации об узле и сети — DNS или файл.

Далее, если DNS используется для получения информации об узле и о сети, имеют смысл только функции поиска по ключу. Используя, например, функцию

gethostent
, не стоит надеяться, что она выполнит последовательный перебор всех записей DNS! Если вызывается функция
gethostent
, она считывает только информацию об узлах и не использует DNS.

ПРИМЕЧАНИЕ

Хотя информацию о сети можно сделать доступной с помощью DNS, очень немногие пользуются этим. На с. 347-348 [1] рассказывается об этой возможности. Однако обычно администраторы создают и обслуживают файл /etc/networks, используемый вместо DNS. Программа netstat с параметром -i использует этот файл, если он есть, и выводит имя каждой сети. Однако бесклассовая адресация (см. раздел А.4) делает эти функции бесполезными, а поскольку они не поддерживают IPv6, новые приложения не должны использовать их.

11.22. Резюме

Набор функций, вызываемых приложением для преобразования имени узла в IP- адрес и обратно, называется распознавателем. Две функции,

gethostbyname
и
gethostbyaddr
, являются типичными точками входа. С переходом на IPv6 и многопоточное программирование полезными становятся
getaddrinfo
и
getnameinfo
, способные работать с адресами IPv6 и безопасные в многопоточной среде.

Для работы с именами служб и номерами портов широко используется функция

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

Альтернативой DNS, которую мы не упомянули, является непосредственный вызов функций распознавателя вместо использования функций

gethostbyname
и
gethostbyaddr
. Таким способом пользуется, например, программа
sendmail
, предназначенная для поиска записи типа MX, чего не может сделать функция
gethostbyXXX
. У функций распознавателя имена начинаются с
res_
. Примером такой функции является функция
res_init
, которую мы описали в разделе 11.4. Описание этих функций и пример вызывающей их программы находятся в главе 15 книги [1]. При вводе в командной строке man
resolver
должны отобразиться страницы руководства для этих функций.

Упражнения

1. Измените программу, представленную в листинге 11.1, так, чтобы для каждого возвращаемого адреса вызывалась функция

gethostbyaddr
, а затем выведите возвращаемое имя
h_name
. Сначала запустите программу, задав имя узла только с одним IP-адресом, а затем — с несколькими IP-адресами. Что происходит?

2. Устраните проблему, показанную в предыдущем упражнении.

3. Запустите программу, показанную в листинге 11.4, задав имя службы

chargen
.

4. Запустите программу, показанную в листинге 11.4, задав IP-адрес в точечно- десятичной записи в качестве имени узла. Допускает ли это ваш распознаватель? Измените листинг 11.4, чтобы разрешить IP-адрес в виде строки десятичных чисел с точками в качестве имени узла и строку с десятичным номером порта в качестве имени службы. В каком порядке должно выполняться тестирование IP-адреса для строки в точечно-десятичной записи и для имени?

5. Измените программу в листинге 11.4 так, чтобы можно было работать либо с IPv4, либо с IPv6.

6. Измените программу в листинге 8.5 так, чтобы сделать запрос DNS, и сравните возвращаемый IP-адрес со всеми IP-адресами узла получателя, то есть вызовите функцию

gethostbyaddr
, используя IP-адрес, возвращаемый функцией
recvfrom
, а затем вызовите
gethostbyname
для поиска всех IP-адресов для узла.

7. Измените листинг 11.6, чтобы вызвать функцию

getnameinfo
вместо функции
sock_ntop
. Какие флаги вы должны передать функции
getnameinfo
?

8. В разделе 7.5 мы обсуждали завладение портом с помощью параметра сокета

SO_REUSEADDR
. Чтобы увидеть, как это происходит, создайте не зависящий от протокола сервер времени и даты UDP, показанный в листинге 11.13. Запустите один экземпляр сервера в одном окне, свяжите его с универсальным адресом и некоторым портом, который вы выберете. Запустите в другом окне клиент и убедитесь, что этот сервер выполняет обработку клиента (отметьте вызов функции
printf
на узле сервера). Затем запустите другой экземпляр сервера в другом окне, и на этот раз свяжите его с одним из адресов направленной передачи узла и тем же портом, что и первый сервер. С какой проблемой вы сразу же столкнетесь? Устраните эту проблему и перезапустите второй сервер. Запустите клиент, отправьте дейтаграмму и проверьте, что второй сервер захватил порт первого сервера. Если возможно, запустите второй сервер снова с учетной записью, отличной от учетной записи первого сервера, чтобы проверить, происходит ли по-прежнему захват порта, поскольку некоторые производители не допускают второго связывания, если идентификатор пользователя отличен от идентификатора процесса, уже связанного с портом.

9. В конце раздела 2.12 мы показали два примера Telnet: сервер времени и даты и эхо-сервер. Зная, что клиент проходит через два этапа — функцию

gethostbyname
и функцию connect, определите, к каким этапам относятся строки вывода клиента.

10. Функции

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

Часть 3