Текст книги "UNIX: разработка сетевых приложений"
Автор книги: Уильям Ричард Стивенс
Соавторы: Эндрю М. Рудофф,Билл Феннер
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 31 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]
Можно получить информацию об имени и адресе без использования 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
.
Узлы компьютерных сетей мы обычно идентифицируем по их именам, удобным для человеческого восприятия. Но во всех примерах книги специально использовались 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]1
Все исходные коды программ, опубликованные в этой книге, вы можете найти по адресу http://www.piter.com.
[Закрыть]показана простая программа, вызывающая функцию 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: %sn", hptr->h_name);
16 for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)
17 printf("talias: %sn", *pptr);
18 switch (hptr->h_addrtype) {
19 case AF_INET:
20 pptr = hptr->h_addr_list;
21 for (; *pptr != NULL; pptr++)
22 printf("taddress: %sn",
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
Функция 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
.
Службы, как и узлы, также часто идентифицируются по именам. Используя в нашем коде имя службы вместо номера порта, при условии, что имена служб сопоставляются номерам портов в некотором файле (обычно /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.
Теперь мы можем изменить код нашего 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 %sn", 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
Функции 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-адреса
0 | 1 | 1 | 1 | 2 | 2 | 3 | Ошибка |
SOCK_STREAM | 1 | Ошибка | 1 | 1 | 2 | 2 | 2 |
SOCK_DGRAM | Ошибка | 1 | 1 | Ошибка | 1 | 1 | |
SOCK_SEQPACKET | Ошибка | Ошибка | 1 | Ошибка | 1 | 1 | 1 |
Более одной структуры 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 и серверов, которые будем создавать в оставшейся части книги.