Текст книги "UNIX: разработка сетевых приложений"
Автор книги: Уильям Ричард Стивенс
Соавторы: Эндрю М. Рудофф,Билл Феннер
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 33 (всего у книги 88 страниц) [доступный отрывок для чтения: 36 страниц]
В листинге 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 %sn", Sock_ntop((SA*)&cliaddr, len));
18 ticks = time(NULL);
19 snprintf(buff, sizeof(buff), "%.24srn", 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 %sn", Sock_ntop((SA*)&cliaddr, len));
21 ticks = time(NULL);
21 snprintf(buff, sizeof(buff), "%.24srn", 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. Создается дейтаграммный сокет. Выделяется память для одной структуры адреса сокета, и структура адреса сокета, соответствующая созданному сокету, копируется в память.
Теперь мы перепишем наш клиент времени и даты, показанный в листинге 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 %sn", 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
Наша функция 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 ничего не отправляется собеседнику. Если что-то не в порядке (собеседник недоступен или на заданном порте не запущен сервер), вызывающий процесс не обнаружит этого, пока не пошлет собеседнику дейтаграмму.
Наша последняя функция, предоставляющая более простой интерфейс для функции 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, нет необходимости устанавливать этот параметр при запуске сервера.
В листинге 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 %sn", Sock_ntop((SA*)&cliaddr, len));
22 ticks = time(NULL);
23 snprintf(buff, sizeof(buff), "% 24srn", ctime(&ticks));
24 Sendto(sockfd, buff, strlen(buff), 0, (SA*)&cliaddr, len);
25 }
26 }
Эта функция дополняет функцию 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.



























