Текст книги "UNIX: разработка сетевых приложений"
Автор книги: Уильям Ричард Стивенс
Соавторы: Эндрю М. Рудофф,Билл Феннер
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 9 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]
Существует две группы функций преобразования адресов, которые мы рассматриваем в этом и следующем разделах. Они выполняют преобразование адресов Интернета из строк ASCII (удобных для человеческого восприятия) в двоичные значения с сетевым порядком байтов (эти значения хранятся в структурах адресов сокетов).
1. Функции inet_aton
, inet_ntoa
и inet_addr
преобразуют адрес IPv4 из точечно-десятичной записи (например, 206.168.112.96) в 32-разрядное двоичное значение в сетевом порядке байтов. Возможно, вы встретите эти функции в многочисленных существующих программах.
2. Более новые функции inet_pton
и inet_ntop
работают и с адресами IPv4, и с адресами IPv6. Эти функции, описываемые в следующем разделе, мы используем в книге.
#include
int inet_aton(const char * strptr, struct in_addr * addrptr);
Возвращает: 1, если строка преобразована успешно, 0 в случае ошибки
in_addr_t inet_addr(const char * strptr);
Возвращает: 32-разрядный адрес IPv4 в сетевом порядке байтов: INADDR_NONE в случае ошибки
char *inet_ntoa(struct in_addr inaddr);
Возвращает: указатель на строку с адресом в точечно-десятичной записи
Первая из названных функций, inet_aton
, преобразует строку, на которую указывает strptr
, в 32-разрядное двоичное число, записанное в сетевом порядке байтов, передаваемое через указатель addrptr
. При успешном выполнении возвращаемое значение равно 1, иначе возвращается нуль.
ПРИМЕЧАНИЕ
Функция inet_aton обладает одним недокументированным свойством: если addrptr – пустой указатель (null pointer), функция все равно выполняет проверку допустимости адреса, содержащегося во входной строке, но не сохраняет результата.
Функция inet_addr
выполняет то же преобразование, возвращая в качестве значения 32-разрядное двоичное число в сетевом порядке байтов. Проблема при использовании этой функции состоит в том, что все 2 32возможных двоичных значений являются действительными IP-адресами (от 0.0.0.0 до 255.255.255.255), но в случае возникновения ошибки функция возвращает константу INADDR_NONE
(обычно представленную двоичным числом, состоящим из 32 бит, установленных в единицу). Это означает, что точечно-десятичная запись 255.255.255.255 (ограниченный адрес для широковещательной передачи IPv4, см. раздел 18.2) не может быть обработана этой функцией, поскольку ее двоичное значение выглядит как указание на сбой при выполнении функции.
ПРИМЕЧАНИЕ
Характерной проблемой, сопровождающей выполнение функции inet_addr, может стать то, что, как утверждается в некоторых руководствах, в случае ошибки она возвращает значение -1 вместо INADDR_NONE. С некоторыми компиляторами это может вызвать проблемы при сравнении возвращаемого значения функции (значение без знака) с отрицательной константой.
На сегодняшний день функция inet_addr
является нерекомендуемой, или устаревшей, и в создаваемом коде вместо нее должна использоваться функция inet_aton
. Еще лучше использовать более новые функции, описанные в следующем разделе, работающие и с IPv4, и с IPv6.
Функция inet_ntoa
преобразует 32-разрядный двоичный адрес IPv4, хранящийся в сетевом порядке байтов, в точечно-десятичную строку. Строка, на которую указывает возвращаемый функцией указатель, находится в статической памяти. Это означает, что функция не допускает повторного вхождения, то есть не является повторно входимой (reentrant), что мы обсудим в разделе 11.14. Наконец, отметим, что эта функция принимает в качестве аргумента структуру, а не указатель на структуру.
3.7. Функции inet_pton и inet_ntopПРИМЕЧАНИЕ
Функции, принимающие структуры в качестве аргументов, встречаются редко. Более общим способом является передача указателя на структуру.
Эти функции появились с IPv6 и работают как с адресами IPv4, так и с адресами IPv6. Их мы и будем использовать в книге. Символы p
и n
обозначают соответственно формат представленияи численныйформат. Формат представления адреса часто является строкой ASCII, а численный формат – это двоичное значение, входящее в структуру адреса сокета. #include
int inet_pton(int family, const char * strptr, void * addrptr);
Возвращает: 1 в случае успешного выполнения функции: 0, если входная строка имела неверный формат представления; -1 в случае ошибки
const char *inet_ntop(int family, const void * addrptr,
char * strptr, size_t len);
Возвращает: указатель на результат, если выполнение функции прошло успешно. NULL в случае ошибки
Значением аргумента family
для обеих функций может быть либо AF_INET
, либо AF_INET6
. Если family
не поддерживается, обе функции возвращают ошибку со значением переменной errno
, равным EAFNOSUPPORT
.
Первая функция пытается преобразовать строку, на которую указывает strptr
, сохраняя двоичный результат с помощью указателя addrptr
. При успешном выполнении ее возвращаемое значение равно 1. Если входная строка находится в неверном формате представления для заданного семейства ( family
), возвращается нуль.
Функция inet_ntop
выполняет обратное преобразование: из численного формата ( addrptr
) в формат представления ( strptr
). Аргумент len
– это размер принимающей строки, который передается, чтобы функция не переполнила буфер вызывающего процесса. Чтобы облегчить задание этого размера, в заголовочный файл
включаются следующие определения:
#define INET_ADDRSTRLEN 16 /* для точечно-десятичной записи IPv4-адреса */
#define INET6_ADDRSTRLEN 46 /* для шестнадцатеричной записи IPv6-адреса */
Если аргумент len
слишком мал для хранения результирующего формата представления вместе с символом конца строки (terminating null), возвращается пустой указатель и переменной errno
присваивается значение ENOSPC
.
Аргумент strptr
функции inet_ntop
не может быть пустым указателем. Вызывающий процесс должен выделить память для хранения преобразованного значения и задать ее размер. При успешном выполнении функции возвращаемым значением является этот указатель.
На рис. 3.5 приведена схема действия пяти функций, описанных в этом и предыдущем разделах.
Рис. 3.5. Функции преобразования адресов
Даже если ваша система еще не поддерживает IPv6, вы можете использовать новые функции, заменив вызовы вида
foo.sin_addr.s_addr = inet_addr(cp);
на
inet_pton(AF_INET, cp, &foo.sin_addr);
а также заменив вызовы вида
ptr = inet_ntoa(foo.sin_addr);
на
char str[INET_ADDRSTRLEN];
ptr = inet_ntop(AF_INET, &foo.sin_addr, str, sizeof(str));
В листинге 3.6 представлено простое определение функции inet_pton
, поддерживающее только IPv4, а в листинге 3.7 – версия inet_ntop
, поддерживающая только IPv4.
Листинг 3.6. Простая версия функции inet_pton, поддерживающая только IPv4
//libfree/inet_pton_ipv4.c
10 int
11 inet_pton(int family, const char *strptr, void *addrptr)
12 {
13 if (family == AF_INET) {
14 struct in_addr in_val;
15 if (inet_aton(strptr, &in_val)) {
16 memcpy(addrptr, &in_val, sizeof(struct in_addr));
17 return (1);
18 }
19 return (0);
20 }
21 errno = EAFNOSUPPORT;
22 return (-1);
23 }
Листинг 3.7. Простая версия функции inet_ntop, поддерживающая только IPv4
//libfree/inet_ntop_ipv4.c
8 const char *
9 inet_ntop(int family, const void *addrptr, char *strptr, size_t len)
10 {
11 const u_char *p = (const u_char*)addrptr;
12 if (family == AF_INET) {
13 char temp[INET_ADDRSTRLEN];
14 snprintf(temp, sizeof(temp), "%d.%d.%d.%d",
15 p[0], p[1], p[2], p[3]);
16 if (strlen(temp) >= len) {
17 errno = ENOSPC;
18 return (NULL);
19 }
20 strcpy(strptr, temp);
21 return (strptr);
22 }
23 errno = EAFNOSUPPORT;
24 return (NULL);
25 }
Основная проблема, связанная с функцией inet_ntop
, состоит в том, что вызывающий процесс должен передать ей указатель на двоичный адрес. Этот адрес обычно содержится в структуре адреса сокета, поэтому вызывающему процессу необходимо знать формат структуры и семейство адресов. Следовательно, чтобы использовать эту функцию, для IPv4 нужно написать код следующего вида:
struct sockaddr_in addr;
inet_ntop(AF_INET, &addr.sin_addr, str, sizeof(str));
или для IPv6 такого вида:
struct sockaddr_in6 addr6:
inet_ntop(AF_INET6, &addr6.sin6_addr, str, sizeof(str));
Как видите, код становится зависящим от протокола.
Чтобы решить эту проблему, напишем собственную функцию и назовем ее sock_ntop
. Она получает указатель на структуру адреса сокета, исследует эту структуру и вызывает соответствующую функцию для того, чтобы возвратить формат представления адреса.
#include "unp.h"
char *sock_ntop(const struct sockaddr * sockaddr, socklen_t addrlen);
Возвращает: непустой указатель, если функция выполнена успешно, NULL в случае ошибки
sockaddr
указывает на структуру адреса сокета, длина которой равна значению addrlen
. Функция sock_ntop
использует свой собственный статический буфер для хранения результата и возвращает указатель на этот буфер.
Формат представления – либо точечно-десятичная форма записи адреса IPv4, либо шестнадцатеричная форма записи адреса IPv6, за которой следует завершающий символ (мы используем точку, как в программе netstat
), затем десятичный номер порта, а затем завершающий нуль. Следовательно, размер буфера должен быть равен как минимум INET_ADDRSTRLEN
плюс 6 байт для IPv4 (16 + 6 – 22) либо INET6_ADDRSTRLEN
плюс 6 байт для IPv6 (46 + 6 – 52).
ПРИМЕЧАНИЕ
Обратите внимание, что при статическом хранении результата функция не допускает повторного вхождения (не является повторно входимой) и не может быть использована несколькими программными потоками (не является безопасной в многопоточной среде – thread-safe). Более подробно мы поговорим об этом в разделе 11.18. Мы допустили такое решение для этой функции, чтобы ее было легче вызывать из простых программ, приведенных в книге.
В листинге 3.8 представлена часть исходного кода, обрабатывающая семейство AF_INET
.
Листинг 3.8. Наша функция sock_ntop
//lib/sock_ntop.c
5 char *
6 sock_ntop(const struct sockaddr *sa, socklen_t salen)
7 {
8 char portstr[7];
9 static char str[128]; /* макс. длина для доменного сокета Unix */
10 switch (sa->sa_family) {
11 case AF_INET: {
12 struct sockaddr_in *sin = (struct sockaddr_in*)sa;
13 if (inet_ntop(AF_INET, &sin->sin_addr. str, sizeof(str)) == NULL)
14 return (NULL);
15 if (ntohs(sin->sin_port) != 0) {
16 snprintf(portstr, sizeof(portstr), ntohs(sin->sin_port));
17 strcat(str, portstr);
18 }
19 return (str);
20 }
Для работы со структурами адресов сокетов мы определяем еще несколько функций, которые упростят переносимость нашего кода между IPv4 и IPv6.
#include "unp.h"
int sock_bind_wild(int sockfd, int family);
Возвращает: 0 в случае успешного выполнения функции, -1 в случае ошибки
int sock_cmp_addr(const struct sockaddr * sockaddr1,
const struct sockaddr * sockaddr2, socklen_t addrlen);
Возвращает: 0, если адреса относятся к одному семейству и совпадают, ненулевое значение в противном случае
int sock_cmp_port(const struct sockaddr * sockaddr1,
const struct sockaddr * sockaddr2, socklen_t addrlen);
Возвращает: 0, если адреса относятся к одному семейству и порты совпадают, ненулевое значение в противном случае
int sock_get_port(const struct sockaddr * sockaddr, socklen_t addrlen);
Возвращает: неотрицательный номер порта для адресов IPv4 или IPv6, иначе -1
char *sock_ntop_host(const struct sockaddr * sockaddr, socklen_t addrlen);
Возвращает: непустой указатель в случае успешного выполнения функции, NULL в случае ошибки
void sock_set_addr(const struct sockaddr * sockaddr,
socklen_t addrlen, void * ptr);
void sock_set_port(const struct sockaddr * sockaddr,
socklen_t addrlen, int port);
void sock_set_wild(struct sockaddr * sockaddr, socklen_t addrlen);
Функция sock_bind_wild
связывает универсальный адрес и динамически назначаемый порт с сокетом. Функция sock_cmp_addr
сравнивает адресные части двух структур адреса сокета, а функция sock_cmp_port
сравнивает номера их портов. Функция sock_get_port
возвращает только номер порта, а функция sock_ntop_host
преобразует к формату представления только ту часть структуры адреса сокета, которая относится к узлу (все, кроме порта, то есть IP-адрес узла). Функция sock_set_addr
присваивает адресной части структуры значение, указанное аргументом ptr
, а функция sock_set_port
задает в структуре адреса сокета только номер порта. Функция sock_set_wild
задает адресную часть структуры через символы подстановки. Как обычно, мы предоставляем для всех этих функций функции– обертки, которые возвращают значение, отличное от типа void, и в наших программах обычно вызываем именно обертки. Мы не приводим в данной книге исходный код для этих функций, так как он свободно доступен (см. предисловие).
Потоковые сокеты (например, сокеты TCP) демонстрируют с функциями read
и write
поведение, отличное от обычного ввода-вывода файлов. Функция read
или write
на потоковом сокете может ввести или вывести немного меньше байтов, чем запрашивалось, но это не будет ошибкой. Причиной может быть достижение границ буфера для сокета в ядре. Все, что требуется в этой ситуации – чтобы процесс повторил вызов функции read
или write
для ввода или вывода оставшихся байтов. (Некоторые версии Unix ведут себя аналогично при записи в канал (pipe) более 4096 байт.) Этот сценарий всегда возможен на потоковом сокете при выполнении функции read
, но с функцией write
он обычно наблюдается, только если сокет неблокируемый. Тем не менее вместо write
мы всегда вызываем функцию writen
на тот случай, если в данной реализации возможно возвращение меньшего количества данных, чем мы запрашиваем.
Введем три функции для чтения и записи в потоковый сокет.
#include "unp.h"
ssize_t readn(int filedes, void * buff, size_t nbytes);
ssize_t writen(int filedes, const void * buff, size_t nbytes);
ssize_t readline(int filedes, void * buff, size_t maxlen);
Все функции возвращают: количество считанных или записанных байтов, -1 в случае ошибки
В листинге 3.9 представлена функция readn
, в листинге 3.10 – функция writen
, а в листинге 3.11 – функция readline
.
Листинг 3.9. Функция readn: считывание n байт из дескриптора
//lib/readn.c
1 #include "unp.h"
2 ssize_t /* Считывает n байт из дескриптора */
3 readn(int fd, void *vptr, size_t n)
4 {
5 size_t nleft;
6 ssize_t nread;
7 char *ptr;
8 ptr = vptr;
9 nleft = n;
10 while (nleft > 0) {
11 if ((nread = read(fd, ptr, nleft)) < 0) {
12 if (errno == EINTR)
13 nread = 0; /* и вызывает снова функцию read() */
14 else
15 return (-1);
16 } else if (nread == 0)
17 break; /* EOF */
18 nleft -= nread;
19 ptr += nread;
20 }
21 return (n – nleft); /* возвращает значение >= 0 */
22 }
Листинг 3.10. Функция writen: запись n байт в дескриптор
//lib/writen.c
1 #include "unp.h"
2 ssize_t /* Записывает n байт в дескриптор */
3 writen(int fd, const void *vptr, size_t n)
4 {
5 size_t nleft;
6 ssize_t nwritten;
7 const char *ptr;
8 ptr = vptr;
9 nleft = n;
10 while (nleft > 0) {
11 if ((nwritten = write(fd, ptr, nleft)) <= 0) {
12 if (errno == EINTR)
13 nwritten = 0; /* и снова вызывает функцию write() */
14 else
15 return (-1); /* ошибка */
16 }
17 nleft -= nwritten;
18 ptr += nwritten;
19 }
20 return (n);
21 }
Листинг 3.11. Функция readline: считывание следующей строки из дескриптора, по одному байту за один раз
//test/readline1.с
1 #include "unp.h"
/* Ужасно медленная версия, приводится только для примера */
2 ssize_t
3 readline(int fd, void *vptr, size_t maxlen)
4 {
5 ssize_t n, rc;
6 char c, *ptr;
7 ptr = vptr;
8 for (n = 1; n < maxlen; n++) {
9 again:
10 if ((rc = read(fd, &c, 1)) == 1) {
11 *ptr++ = c;
12 if (c == 'n')
13 break; /* записан символ новой строки, как в fgets() */
14 } else if (rc == 0) {
15 if (n == 1)
16 return (0); /* EOF, данные не считаны */
17 else
18 break; /* EOF, некоторые данные были считаны */
19 } else {
20 if (errno == EINTR)
21 goto again;
22 return (-1); /* ошибка, errno задается функцией read() */
23 }
24 }
25 *ptr = 0; /* завершаем нулем, как в fgets() */
26 return (n);
27 }
Если функция чтения или записи ( read
или write
) возвращает ошибку, то наши функции проверяют, не совпадает ли код ошибки с EINTR (прерывание системного вызова сигналом, см. раздел 5.9). В этом случае прерванная функция вызывается повторно. Мы обрабатываем ошибку в этой функции, чтобы не заставлять процесс снова вызвать read
или write
, поскольку целью наших функций является предотвращение обработки нехватки данных вызывающим процессом.
В разделе 14.3 мы покажем, что вызов функции recv
с флагом MSG_WAITALL
позволяет обойтись без использования отдельной функции readn
.
Заметим, что наша функция readline
вызывает системную функцию read
один раз для каждого байта данных. Это очень неэффективно, поэтому мы и написали в примечании «Ужасно медленно!». Возникает соблазн обратиться к стандартной библиотеке ввода-вывода ( stdio
). Об этом мы поговорим через некоторое время в разделе 14.8, но учтите, что это может привести к определенным проблемам. Буферизация, предоставляемая stdio
, решает проблемы с производительностью, но при этом создает множество логистических сложностей, которые в свою очередь порождают скрытые ошибки в приложении. Дело в том, что состояние буферов stdio
недоступно процессу. Рассмотрим, например, строчный протокол взаимодействия клиента и сервера, причем такой, что могут существовать разные независимые реализации клиентов и серверов (достаточно типичное явление; например, множество веб-браузеров и веб-серверов были разработаны независимо в соответствии со спецификацией HTTP). Хороший стиль программирования заключается в том, что эти программы должны не только ожидать от своих собеседников соблюдения того же протокола, но и контролировать трафик на возможность получения непредвиденного трафика. Подобные нарушения протокола должны рассматриваться как ошибки, чтобы программисты имели возможность находить и устранять неполадки в коде, а также обнаруживать попытки взлома систем. Обработка некорректного трафика должна давать приложению возможность продолжать работу. Буферизация stdio
мешает достижению перечисленных целей, поскольку приложение не может проверить наличие непредвиденных (некорректных) данных в буферах stdio
в любой конкретный момент.
Существует множество сетевых протоколов, основанных на использовании строк текста: SMTP, HTTP, FTP, finger. Поэтому соблазн работать со строками будет терзать вас достаточно часто. Наш совет: мыслить в терминах буферов, а не строк. Пишите код таким образом, чтобы считывать содержимое буфера, а не отдельные строки. Если же ожидается получение строки, ее всегда можно поискать в считанном буфере.
В листинге 3.12 приведена более быстрая версия функции readline
, использующая свой собственный буфер (а не буферизацию stdio
). Основное достоинство этого буфера состоит в его открытости, благодаря чему вызывающий процесс всегда знает, какие именно данные уже приняты. Несмотря на это, использование readline
все равно может вызвать проблемы, как мы увидим в разделе 6.3. Системные функции типа select
ничего не знают о внутреннем буфере readline
, поэтому неаккуратно написанная программа с легкостью может очутиться в состоянии ожидания в вызове select
, при том, что данные уже будут находиться в буферах readline
. По этой причине сочетание вызовов readn
и readline
не будет работать так, как этого хотелось бы, пока функция readn
не будет модифицирована с учетом наличия внутреннего буфера.
Листинг 3.12. Улучшенная версия функции readline
//lib/readline.c
1 #include "unp.h"
2 static int read_cnt;
3 static char *read_ptr;
4 static char read_buf[MAXLINE];
5 static ssize_t
6 my_read(int fd, char *ptr)
7 {
8 if (read_cnt <= 0) {
9 again:
10 if ((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
11 if (errno == EINTR)
12 goto again;
13 return(-1);
14 } else if (read_cnt == 0)
15 return(0);
16 read_ptr = read_buf;
17 }
18 read_cnt–;
19 *ptr = *read_ptr++;
20 return(1);
21 }
22 ssize_t
23 readline(int fd, void *vptr, size_t maxlen)
24 {
25 ssize_t n, rc;
26 char c, *ptr;
27 ptr = vptr;
28 for (n = 1; n < maxlen; n++) {
29 if ((rc = my_read(fd, &c)) == 1) {
30 *ptr++ = c;
31 if (c== 'n')
32 break; /* Записан символ новой строки, как в fgets() */
33 } else if (rc == 0) {
34 *ptr = 0;
35 return(n – 1); /* EOF, считано n-1 байт данных */
36 } else
37 return(-1); /* ошибка, read() задает значение errno */
38 }
39 *ptr = 0; /* завершающий нуль, как в fgets() */
40 return(n);
41 }
42 ssize_t
43 readlinebuf(void **vptrptr)
44 {
45 if (read_cnt)
46 *vptrptr = read_ptr;
47 return(read_cnt);
48 }
2-21
Внутренняя функция my_read
считывает до MAXLINE
символов за один вызов и затем возвращает их по одному.
29
Единственное изменение самой функции readline
заключается в том, что теперь она вызывает функцию my_read
вместо read
.
42-48
Новая функция readlinebuf
выдает сведения о состоянии внутреннего буфера, что позволяет вызывающим функциям проверить, нет ли в нем других данных, помимо уже принятой строки.
ПРИМЕЧАНИЕ
К сожалению, использование переменных типа static в коде readline.c для поддержки информации о состоянии при последовательных вызовах приводит к тому, что функция больше не является безопасной в многопоточной системе (thread-safe) и повторно входимой (reentrant). Мы обсуждаем это в разделах 11.18 и 26.5. Мы предлагаем версию, безопасную в многопоточной системе, основанную на собственных данных программных потоков, в листинге 26.5.