355 500 произведений, 25 200 авторов.

Электронная библиотека книг » Уильям Ричард Стивенс » UNIX: разработка сетевых приложений » Текст книги (страница 9)
UNIX: разработка сетевых приложений
  • Текст добавлен: 17 сентября 2016, 20:42

Текст книги "UNIX: разработка сетевых приложений"


Автор книги: Уильям Ричард Стивенс


Соавторы: Эндрю М. Рудофф,Билл Феннер

Жанр:

   

ОС и Сети


сообщить о нарушении

Текущая страница: 9 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]

3.6. Функции inet_aton, inet_addr и inet_ntoa

Существует две группы функций преобразования адресов, которые мы рассматриваем в этом и следующем разделах. Они выполняют преобразование адресов Интернета из строк 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 }

3.8. Функция sock_ntop и связанные с ней функции

Основная проблема, связанная с функцией 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, и в наших программах обычно вызываем именно обертки. Мы не приводим в данной книге исходный код для этих функций, так как он свободно доступен (см. предисловие).

3.9. Функции readn, writen и readline

Потоковые сокеты (например, сокеты 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.


    Ваша оценка произведения:

Популярные книги за неделю