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

Дополнительные сведения о сокетах udp

22.1. Введение

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

TCP — это потоковый протокол, использующий окно переменной величины (sliding window), поэтому в TCP отсутствует такое понятие, как граница записи, и невозможно переполнение буфера получателя отправителем в результате передачи слишком большого количества данных. Однако в случае UDP каждой операции ввода соответствует одна дейтаграмма UDP (запись), поэтому возникает вопрос: что произойдет, когда полученная дейтаграмма окажется больше приемного буфера приложения?

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

Если реализация не поддерживает параметр сокета

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

Большинство серверов UDP являются последовательными, но существуют приложения, обменивающиеся множеством дейтаграмм UDP между клиентом и сервером, что требует параллельной обработки. Примером может служить TFTP (Trivial File Transfer Protocol — упрощенный протокол передачи файлов). Мы рассмотрим два варианта подобного согласования — с использованием суперсервера

inetd
и без него.

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

22.2. Получение флагов, IP-адреса получателя и индекса интерфейса

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

sendmsg
и
recvmsg
использовались только для передачи дескрипторов через доменные сокеты Unix (см. раздел 15.7), но даже это происходило сравнительно редко. Однако в настоящее время популярность этих двух функций растет по двум причинам:

1. Элемент

msg_flags
, добавленный в структуру
msghdr
в реализации 4.3BSD Reno, возвращает приложению флаги сообщения. Эти флаги мы перечислили в табл. 14.2.

2. Вспомогательные данные используются для передачи все большего количества информации между приложением и ядром. В главе 27 мы увидим, что IPv6 продолжает эту тенденцию.

В качестве примера использования функции

recvmsg
мы напишем функцию
recvfrom_flags
, аналогичную функции recvfrom, но дополнительно позволяющую получить:

■ возвращаемое значение

msg_flags
;

■ адрес получателя полученной дейтаграммы (из параметра сокета

IP_RECVDSTADDR
);

■ индекс интерфейса, на котором была получена дейтаграмма (параметр сокета

IP_RECVIF
).

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

unp.h
следующую структуру:

struct in_pktinfo {

 struct in_addr ipi_addr;    /* IPv4-адрес получателя */

 int            ipi_ifindex; /* индекс интерфейса, на котором была

                                получена дейтаграмма */

};

Мы выбрали имена структуры и ее элементов так, чтобы получить определенное сходство со структурой IPv6

in6_pktinfo
, возвращающей те же два элемента для сокета IPv6 (см. раздел 22.8). Наша функция
recvfrom_flags
будет получать в качестве аргумента указатель на структуру
in_pktinfo
, и если этот указатель не нулевой, возвращать структуру через указатель.

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

IP_RECVDSTADDR
(то есть реализация не поддерживает данный параметр сокета). Обработать индекс интерфейса легко, поскольку нулевое значение может использоваться как указание на то, что индекс неизвестен. Но для IP-адреса все 32-разрядные значения являются действительными. Мы выбрали такое решение: адрес получателя 0.0.0.0 возвращается в том случае, когда действительное значение недоступно. Хотя это реальный IP-адрес, использовать его в качестве IP-адреса получателя не разрешается (RFC 1122 [10]). Он будет действителен только в качестве IP-адреса отправителя во время начальной загрузки узла, когда узел еще не знает своего IP-адреса.

ПРИМЕЧАНИЕ

К сожалению, Беркли-ядра принимают дейтаграммы, предназначенные для адреса 0.0.0.0 [128, с. 218-219]. Это устаревшие адреса широковещательной передачи, генерируемые ядрами 4.2BSD.

Первая часть нашей функции

recvfrom_flags
представлена в листинге 22.1[1]. Эта функция предназначена для использования с сокетом UDP.

Листинг 22.1. Функция recvfrom_flags: вызов функции recvmsg

//adviо/recvfromflags.c

 1 #include "unp.h"

 2 #include  /* макрос ALIGN для макроса CMSG_NXTHDR() */


 3 ssize_t

 4 recvfrom_flags(int fd, void *ptr, size_t nbytes, int *flagsp,

 5  SA *sa, socklen_t *salenptr, struct unp_in_pktinfo *pktp)

 6 {

 7  struct msghdr msg;

 8  struct iovec iov[1];

 9  ssize_t n;


10 #ifdef HAVE_MSGHDR_MSG_CONTROL

11  struct cmsghdr *cmptr;

12  union {

13   struct cmsghdr cm;

14   char control[CMSG_SPACE(sizeof(struct in_addr)) +

15    CMSG_SPACE(sizeof(struct unp_in_pktinfo))];

16  } control_un;


17  msg.msg_control = control_un.control;

18  msg.msg_controllen = sizeof(control_un.control);

19  msg.msg_flags = 0;

20 #else

21  bzero(&msg, sizeof(msg)); /* обнуление msg_accrightslen = 0 */

22 #endif


23  msg.msg_name = sa;

24  msg.msg_namelen = *salenptr;

25  iov[0].iov_base = ptr;

26  iov[0].iov_len = nbytes;

27  msg.msg_iov = iov;

28  msg.msg_iovlen = 1;


29  if ((n = recvmsg(fd, &msg, *flagsp)) < 0)

30   return(n);

31  *salenptr = msg.msg_namelen; /* возвращение результатов */

32  if (pktp)

33   bzero(pktp, sizeof(struct unp_in_pktinfo)); /* 0.0.0.0. интерфейс = 0 */

Подключаемые файлы

1-2
 Использование макроопределения
CMSG_NXTHDR
требует подключения заголовочного файла
.

Аргументы функции

3-5
 Аргументы функции аналогичны аргументам функции
recvfrom
за исключением того, что четвертый аргумент является указателем на целочисленный флаг (так что мы можем возвратить флаги, возвращаемые функцией
recvmsg
), а седьмой аргумент новый: это указатель на структуру
unp_in_pktinfo
, содержащую IPv4-адрес получателя пришедшей дейтаграммы и индекс интерфейса, на котором дейтаграмма была получена.

Различия реализаций

10-22
 При работе со структурой
msghdr
и различными константами
MSG_XXX
мы встречаемся со множеством различий в реализациях. Одним из вариантов обработки таких различий может быть использование имеющейся в языке С возможности условного подключения (директива
#ifdef
). Если реализация поддерживает элемент
msg_control
, то выделяется пространство для хранения значений, возвращаемых параметрами сокета
IP_RECVDSTADDR
и
IP_RECVIF
, и соответствующие элементы инициализируются.

Заполнение структуры msghdr и вызов функции recvmsg

23-33
 Заполняется структура
msghdr
и вызывается функция
recvmsg
. Значения элементов
msg_namelen
и
msg_flags
должны быть переданы обратно вызывающему процессу. Они являются аргументами типа «значение-результат». Мы также инициализируем структуру вызывающего процесса
unp_in_pktinfo
, устанавливая IP-адрес 0.0.0.0 и индекс интерфейса 0.

В листинге 22.2 показана вторая часть нашей функции.

Листинг 22.2. Функция recvfrom_flags: возвращаемые флаги и адрес получателя

//advio/recvfromflags.c

34 #ifndef HAVE_MSGHDR_MSG_CONTROL

35  *flagsp = 0; /* возвращение результатов */

36  return(n);

37 #else


38  *flagsp = msg.msg_flags; /* возвращение результатов */

39  if (msg.msg_controllen < sizeof(struct cmsghdr) ||

40   (msg.msg_flags & MSG_CTRUNC) || pktp == NULL)

41    return(n);


42   for (cmptr = CMSG_FIRSTHDR(&msg); cmptr != NULL;

43    cmptr = CMSG_NXTHDR(&msg, cmptr)) {


44 #ifdef IP_RECVDSTADDR

45    if (cmptr->cmsg_level == IPPROTO_IP &&

46     cmptr->cmsg_type == IP_RECVDSTADDR) {


47     memcpy(&pktp->ipi_addr, CMSG_DATA(cmptr),

48      sizeof(struct in_addr));

49     continue;

50    }

51 #endif


52 #ifdef IP_RECVIF

53    if (cmptr->cmsg_level == IPPROTO_IP && cmptr->cmsg_type == IP_RECVIF) {

54     struct sockaddr_dl *sdl;


55    sdl = (struct sockaddr_dl*)CMSG_DATA(cmptr);

56    pktp->ipi_ifindex = sdl->sdl_index;

57    continue;

58   }

59 #endif

60   err_quit("unknown ancillary data, len = %d, 
level = %d, type = %d",

61    cmptr->cmsg_len, cmptr->cmsg_level, cmptr->cmsg_type);

62  }

63  return(n);

64 #endif /* HAVE_MSGHDR_MSG_CONTROL */

65 }

34-37
 Если реализация не поддерживает элемента
msg_control
, мы просто обнуляем возвращаемые флаги и завершаем функцию. Оставшаяся часть функции обрабатывает информацию, содержащуюся в структуре
msg_control
.

Возвращение при отсутствии управляющей информации

38-41
 Мы возвращаем значение
msg_flags
и передаем управление вызывающей функции в том случае, если нет никакой управляющей информации, управляющая информация была обрезана или вызывающий процесс не требует возвращения структуры
unp_in_pktinfo
.

Обработка вспомогательных данных

42-43
 Мы обрабатываем произвольное количество объектов вспомогательных данных с помощью макросов
CMSG_FIRSTHDR
и
CMSG_NEXTHDR
.

Обработка параметра сокета IP_RECVDSTADDR

47-54
 Если в составе управляющей информации был возвращен IP-адрес получателя (см. рис. 14.2), он возвращается вызывающему процессу.

Обработка параметра сокета IP_RECVIF

55-63
 Если в составе управляющей информации был возвращен индекс интерфейса, он возвращается вызывающему процессу. На рис. 22.1 показано содержимое возвращенного объекта вспомогательных данных.

Рис. 22.1. Объект вспомогательных данных, возвращаемый для параметра IP_RECVIF

Вспомните структуру адреса сокета канального уровня (см. листинг 18.1). Данные, возвращаемые в объекте вспомогательных данных, представлены в одной из этих структур, но длины трех элементов являются нулевыми (длина имени, адреса и селектора). Следовательно, нет никакой необходимости указывать эти значения, и таким образом структура имеет размер 8 байт, а не 20, как было в листинге 18.1. Возвращаемая нами информация — это индекс интерфейса.

Пример: вывод IP-адреса получателя и флага обрезки дейтаграммы

Для проверки нашей функции мы изменим функцию

dg_echo
(см. листинг 8.2) так, чтобы она вызывала функцию
recvfrom_flags
вместо функции recvfrom. Новая версия функции
dg_echo
показана в листинге 22.3.

Листинг 22.3. Функция dg_echo, вызывающая нашу функцию recvfrom_flags

//advio/dgechoaddr.c

 1 #include "unpifi.h"


 2 #undef MAXLINE

 3 #define MAXLINE 20 /* устанавливаем новое значение, чтобы

                         пронаблюдать обрезку дейтаграмм */


 4 void

 5 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

 6 {

 7  int flags;

 8  const int on = 1;

 9  socklen_t len;

10  ssize_t n;

11  char
mesg[MAXLINE], str[INET6_ADDRSTRLEN], ifname[IFNAMSIZ];

12  struct in_addr in_zero;

13  struct in_pktinfo pktinfo;


14 #ifdef IP_RECVDSTADDR

15  if (setsockopt(sockfd, IPPROTO_IP, IP_RECVDSTADDR, &on, sizeof(on)) < 0)

16   err_ret("setsockopt of IP_RECVDSTADDR");

17 #endif

18 #ifdef IP_RECVIF

19  if (setsockopt(sockfd, IPPROTO_IP, IP_RECVIF, &on, sizeof(on)) < 0)

20   err_ret("setsockopt of IP_RECVIF");

21 #endif

22  bzero(&in_zero, sizeof(struct in_addr)); /* IPv4-адрес, состоящий

                                                из одних нулей */


23  for (;;) {

24   len = clilen;

25   flags = 0;

26   n = Recvfrom_flags(sockfd, mesg, MAXLINE, &flags,

27    pcliaddr, &len, &pktinfo);

28   printf("%d-byte datagram from %s", n, Sock_ntop(pcliaddr, len));

29   if (memcmp(&pktinfo.ipi_addr, &in_zero, sizeof(in_zero)) != 0)

30    printf(", to %s", Inet_ntop(AF_INET, &pktinfo.ipi_addr,

31     str, sizeof(str)));

32   if (pktinfo.ipi_ifindex > 0)

33    printf(", recv i/f = %s",

34    If_indextoname(pktinfо.ipi_ifindex, ifname));

35 #ifdef MSG_TRUNC

36   if (flags & MSG_TRUNC)

37    printf(" (datagram truncated)");

38 #endif

39 #ifdef MSG_CTRUNC

40   if (flags & MSG_CTRUNC)

41    printf(" (control info truncated)");

42 #endif

43 #ifdef MSG_BCAST

44   if (flags & MSG_BCAST)

45    printf(" (broadcast)");

46 #endif

47 #ifdef MSG_MCAST

48   if (flags & MSG_MCAST)

49    printf(" (multicast)");

50 #endif

51   printf("\n");


52   Sendto(sockfd, mesg, n, 0, pcliaddr, len);

53  }

54 }

Изменение MAXLINE

2-3
 Мы удаляем существующее определение
MAXLINE
, имеющееся в нашем заголовочном файле
unp.h
, и задаем новое значение — 20. Это позволит нам увидеть, что произойдет, когда мы получим дейтаграмму UDP, превосходящую размер буфера, переданного функции (в данном случае функции
recvmsg
).

Установка параметров сокета IP_RECVDSTADDR и IP_RECVIF

14-21
 Если параметр сокета
IP_RECVDSTADDR
определен, мы включаем его. Аналогично включается параметр сокета
IP_RECVIF
.

Чтение дейтаграммы, вывод IP-адреса отправителя и порта

24-28
 Дейтаграмма читается с помощью вызова функции
recvfrom_flags
. IP-адрес отправителя и порт ответа сервера преобразуются в формат представления функцией
sock_ntop
.

Вывод IP-адреса получателя

29-31
 Если возвращаемый IP-адрес ненулевой, он преобразуется в формат представления функцией
inet_ntop
и выводится.

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

32-34
 Если индекс интерфейса ненулевой, его имя будет возвращено функцией
if_indextoname
. Это имя наша функция печатает на экране.

Проверка различных флагов

35-51
 Мы проверяем четыре дополнительных флага и выводим сообщение, если какие-либо из них установлены.

22.3. Обрезанные дейтаграммы

В системах, происходящих от BSD, при получении UDP-дейтаграммы, размер которой больше буфера приложения, функция recvmsg устанавливает флаг

MSG_TRUNC
в элементе
msg_flags
структуры
msghdr
(см. табл. 14.2). Все Беркли-реализации, поддерживающие структуру
msghdr
с элементом
msg_flags
, обеспечивают это уведомление.

ПРИМЕЧАНИЕ

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

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

1. Лишние байты игнорируются, и приложение получает флаг

MSG_TRUNC
, что требует вызова функции
recvmsg
.

2. Игнорирование лишних байтов без уведомления приложения.

3. Сохранение лишних байтов и возвращение их в последующих операциях чтения на сокете.

ПРИМЕЧАНИЕ

POSIX задает первый тип поведения: игнорирование лишних байтов и установку флага MSG_TRUNC. Ранние реализации SVR4 действуют по третьему сценарию.

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

22.4. Когда UDP оказывается предпочтительнее TCP

В разделах 2.3 и 2.4 мы описали основные различия между UDP и TCP. Поскольку мы знаем, что TCP надежен, a UDP — нет, возникает вопрос: когда следует использовать UDP вместо TCP и почему? Сначала перечислим преимущества UDP:

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

■ UDP не требует установки и разрыва соединения. В соответствии с рис. 2.5 UDP позволяет осуществить обмен запросом и ответом в двух пакетах (если предположить, что размеры запроса и ответа меньше минимального размера MTU между двумя оконечными системами). В случае TCP требуется около 10 пакетов, если считать, что для каждого обмена «запрос-ответ» устанавливается новое соединение TCP.

Для анализа количества передаваемых пакетов важным фактором является также число циклов обращения пакетов, необходимых для получения ответа. Это становится важно, если время ожидания превышает пропускную способность, как показано в приложении А [112]. В этом тексте сказано, что минимальное время транзакции для запроса-ответа UDP равно RTT + SPT, где RTT — это время обращения между клиентом и сервером, a SPT — время обработки запроса сервером. Однако в случае TCP, если для осуществления каждой последовательности «запрос-ответ» используется новое соединение TCP, минимальное время транзакции будет равно 2×RTT+SPT, то есть на один период RTT больше, чем для UDP.

В отношении второго пункта очевидно, что если соединение TCP используется для множества обменов «запрос-ответ», то стоимость установления и разрыва соединения амортизируется во всех запросах и ответах. Обычно это решение предпочтительнее, чем использование нового соединения для каждого обмена «запрос- ответ». Тем не менее существуют приложения, использующие новое соединение для каждого цикла «запрос-ответ» (например, старые версии HTTP). Кроме того, существуют приложения, в которых клиент и сервер обмениваются в одном цикле «запрос-ответ» (например, DNS), а затем могут не обращаться друг к другу в течение часов или дней.

Теперь мы перечислим функции TCP, отсутствующие в UDP. Это означает, что приложение должно само реализовывать эти функции, если они ему необходимы. Мы говорим «необходимы», потому что не все свойства требуются всем приложениям. Например, может не возникнуть необходимости повторно передавать потерянные сегменты для аудиоприложений реального времени, если приемник способен интерполировать недостающие данные. Также для простых транзакций «запрос-ответ» может не потребоваться управление потоком, если два конца соединения заранее договорятся о размерах наибольшего запроса и ответа.

Положительные подтверждения, повторная передача потерянных пакетов, обнаружение дубликатов и упорядочивание пакетов, порядок следования которых был изменен сетью. TCP подтверждает получение всех данных, позволяя обнаруживать потерянные пакеты. Реализация этих двух свойств требует, чтобы каждый сегмент данных TCP содержал порядковый номер, по которому можно впоследствии проверить получение данного сегмента. Требуется также, чтобы TCP прогнозировал значение тайм-аута повторной передачи для соединения и чтобы это значение последовательно обновлялось по мере изменения сетевого трафика между конечными точками.

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

Медленный старт и предотвращение перегрузки. Это форма управления потоком, осуществляемого отправителем, служащая для определения текущей пропускной способности сети и позволяющая контролировать ситуацию во время переполнения сети. Все современные TCP-приложения должны поддерживать эти два свойства, и опыт (накопленный еще до того, как эти алгоритмы были реализованы в конце 80-х) показывает, что протоколы, не снижающие скорость передачи при перегрузке сети, лишь усугубляют эту перегрузку (см., например, [52]).

Суммируя вышесказанное, мы можем сформулировать следующие рекомендации:

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

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

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

Из этих правил есть исключения, в особенности для существующих приложений. Например, TFTP использует UDP для передачи большого количества данных. Для TFTP был выбран UDP, поскольку, во-первых, его реализация проще в отношении кода начальной загрузки (800 строк кода С для UDP в сравнении с 4500 строками для TCP, например в [128]), а во-вторых, TFTP используется только для начальной загрузки систем в локальной сети, а не для передачи большого количества данных через глобальные сети. Однако при этом требуется, чтобы в TFTP были предусмотрены такие свойства, как собственное поле порядкового номера (для подтверждений), тайм-аут и возможность повторной передачи.

NFS (Network File System — сетевая файловая система) является другим исключением из правила: она также использует UDP для передачи большого количества данных (хотя некоторые могут возразить, что в действительности это приложение типа «запрос-ответ», использующее запросы и ответы больших размеров). Отчасти это можно объяснить исторически сложившимися обстоятельствами: в середине 80-х, когда была разработана эта система, реализации UDP были быстрее, чем TCP, и система NFS использовалась только в локальных сетях, где потеря пакетов, как правило, происходит на несколько порядков реже, чем в глобальных сетях. Но как только в начале 90-х NFS начала использоваться в глобальных сетях, а реализации TCP стали обгонять UDP в отношении производительности при передаче большого количества данных, была разработана версия 3 системы NFS для поддержки TCP. Теперь большинство производителей предоставляют NFS как для и TCP, так и для UDP. Аналогичные причины (большая скорость по сравнению с TCP в начале 80-х плюс преобладание локальных сетей над глобальными) привели к тому, что в Apollo NCS (предшественник DCE RPC) сначала использовали UDP, а не TCP, хотя современные реализации поддерживают и UDP, и TCP.

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

22.5. Добавление надежности приложению UDP

Если мы хотим использовать UDP для приложения типа «запрос-ответ», как было отмечено в предыдущем разделе, мы должны добавить нашему клиенту две функции:

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

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

Эти два свойства предусмотрены в большинстве существующих приложений UDP, использующих простую модель «запрос-ответ»: например, распознаватели DNS, агенты SNMP, TFTP и RPC. Мы не пытаемся использовать UDP для передачи большого количества данных: наша цель — приложение, посылающее запрос и ожидающее ответа на этот запрос.

ПРИМЕЧАНИЕ

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

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

Более старый метод реализации тайм-аутов и повторной передачи заключался в отправке запроса и ожидании в течение N секунд. Если ответ не приходил, осуществлялась повторная передача и снова на ожидание ответа отводилось N секунд. Если это повторялось несколько раз, отправка запроса прекращалась. Это так называемый линейный таймер повторной передачи (на рис. 6.8 [111] показан пример клиента TFTP, использующего эту технологию. Многие клиенты TFTP до сих пор пользуются этим методом).

Проблема при использовании этой технологии состоит в том, что количество времени, в течение которого дейтаграмма совершает цикл в объединенной сети, может варьироваться от долей секунд в локальной сети до нескольких секунд в глобальной. Факторами, влияющими на время обращения (RTT), являются расстояние, скорость сети и переполнение. Кроме того, RTT между клиентом и сервером может быстро меняться со временем при изменении условий в сети. Нам придется использовать тайм-ауты и алгоритм повторной передачи, который учитывает действительное (измеряемое) значение периода RTT и изменения RTT с течением времени. В этой области ведется большая исследовательская работа, в основном направленная на TCP, но некоторые идеи применимы к любым сетевым приложениям.

Мы хотим вычислить тайм-аут повторной передачи (RTO), чтобы использовать его при отправке каждого пакета. Для того чтобы выполнить это вычисление, мы измеряем RTT — действительное время обращения для пакета. Каждый раз, измеряя RTT, мы обновляем два статистических показателя:

srtt
— сглаженную оценку RTT, и
rttvar
— сглаженную оценку среднего отклонения. Последняя является хорошей приближенной оценкой стандартного отклонения, но ее легче вычислять, поскольку для этого не требуется извлечения квадратного корня. Имея эти два показателя, мы вычисляем RTO как сумму
srtt
и
rttvar
, умноженного на четыре. В [52] даются все необходимые подробности этих вычислений, которые мы можем свести к четырем следующим уравнениям:

delta = measuredRTT - srtt

srtt ← srtt + g × delta

rttvar ← rttvar + h (|delta| - rttvar)

RTO = srtt + 4 × rttvar

delta
— это разность между измеренным RTT и текущим сглаженным показателем RTT (
srtt
).
g
— это приращение, применяемое к показателю RTT, равное 1/8.
h
 — это приращение, применяемое к сглаженному показателю среднего отклонения, равное ¼.

ПРИМЕЧАНИЕ

Два приращения и множитель 4 в вычислении RTO специально выражены степенями числа 2 и могут быть вычислены с использованием операций сдвига вместо деления и умножения. На самом деле реализация TCP в ядре (см. раздел 25.7 [128]) для ускорения вычислений обычно использует арифметику с фиксированной точкой, но мы для простоты используем в нашем коде вычисления с плавающей точкой.

Другой важный момент, отмеченный в [52], заключается в том, что по истечении времени таймера повторной передачи для следующего RTO должно использоваться экспоненциальное смещение (exponential backoff). Например, если наше первое значение RTO равно 2 с и за это время ответа не получено, следующее значение RTO будет равно 4 с. Если ответ все еще не последовал, следующее значение RTO будет 8 с, затем 16 и т.д.

Алгоритмы Джекобсона (Jacobson) реализуют вычисление RTO при измерении RTT и увеличение RTO при повторной передаче. Однако, когда клиент выполняет повторную передачу и получает ответ, возникает проблема неопределенности повторной передачи (retransmission ambiguity problem). На рис. 22.2 показаны три возможных сценария, при которых истекает время ожидания повторной передачи:

■ запрос потерян;

■ ответ потерян;

■ значение RTO слишком мало.

Рис. 22.2. Три сценария, возможные при истечении времени таймера повторной передачи

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

Алгоритм Карна (Karn) [58] обрабатывает этот сценарий в соответствии со следующими правилами, применяемыми в любом случае, когда ответ получен на запрос, отправленный более одного раза:

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

■ Поскольку ответ пришел до того, как истекло время нашего таймера повторной передачи, используйте для следующего пакета текущее значение RTO. Только когда мы получим ответ на запрос, который не был передан повторно, мы изменяем значение RTT и снова вычисляем RTO.

При написании наших функций RTT применить алгоритм Карна несложно, но оказывается, что существует и более изящное решение. Оно используется в расширениях TCP для сетей с высокой пропускной способностью, то есть сетей, обладающих либо широкой полосой пропускания, либо большим значением RTT, либо обоими этими свойствами (RFC 1323 [53]). Кроме добавления порядкового номера к началу каждого запроса, который сервер должен отразить, мы добавляем отметку времени, которую сервер также должен отразить. Каждый раз, отправляя запрос, мы сохраняем в этой отметке значение текущего времени. Когда приходит ответ, мы вычисляем величину RTT для этого пакета как текущее время минус значение отметки времени, отраженной сервером в своем ответе. Поскольку каждый запрос несет отметку времени, отражаемую сервером, мы можем вычислить RTT для каждого ответа, который мы получаем. Теперь нет никакой неопределенности. Более того, поскольку сервер только отражает отметку времени клиента, клиент может использовать для отметок времени любые удобные единицы, и при этом не требуется, чтобы клиент и сервер синхронизировали часы.

Пример

Свяжем теперь всю эту информацию воедино в примере. Мы начнем с функции

main
нашего клиента UDP, представленного в листинге 8.3, и изменим в ней только номер порта с
SERV_PORT
на 7 (стандартный эхо-сервер, см. табл. 2.1).

В листинге 22.4 показана функция

dg_cli
. Единственное изменение по сравнению с листингом 8.4 состоит в замене вызовов функций
sendto
и
recvfrom
вызовом нашей новой функции
dg_send_recv
.

Перед тем как представить функцию

dg_send_recv
и наши функции RTT, которые она вызывает, мы показываем в листинге 22.5 нашу схему реализации функциональных свойств, повышающих надежность клиента UDP. Все функции, имена которых начинаются с
rtt_
, описаны далее.

Листинг 22.4. Функция dg_cli, вызывающая нашу функцию dg_send_recv

//rtt/dg_cli.c

 1 #include "unp.h"


 2 ssize_t Dg_send_recv(int, const void*, size_t, void*, size_t,

 3  const SA*, socklen_t);


 4 void

 5 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

 6 {

 7  ssize_t n;

 8  char sendline[MAXLINE], recvline[MAXLINE + 1];


 9  while (Fgets(sendline, MAXLINE, fp) != NULL) {


10   n = Dg_send_recv(sockfd, sendline, strlen(sendline),

11    recvline, MAXLINE, pservaddr, servlen);


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

13   Fputs(recvline, stdout);

14  }

15 }

Листинг 22.5. Схема функций RTT и последовательность их вызова

static sigjmp_buf jmpbuf;


{


 формирование запроса


 signal(SIGALRM, sig_alrm); /* устанавливаем обработчик сигнала */

 rtt_newpack(); /* инициализируем значение счетчика rexmt нулем */

sendagain:

 sendto();


 alarm(rtt_start()); /* задаем аргумент функции alarm равным RTO */

 if (sigsetjmp(jmpbuf, 1) != 0) {

  if (rtt_timeout()) /* удваиваем RTO, обновляем оценочные значения */

   отказываемся от дальнейших попыток

  goto sendagain; /* повторная передача */

 }

 do {

  recvfrom();

 } while (неправильный порядковый номер);

 alarm(0); /* отключаем сигнал alarm */

 rtt_stop(); /* вычисляем RTT и обновляем оценочные значения */


 обрабатываем ответ


}


void sig_alrm(int signo) {

 siglongjmp(jmpbuf, 1);

}

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

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

Мы вызываем функции

sigsetjmp
и
siglongjmp
, чтобы предотвратить возникновение ситуации гонок с сигналом
SIGALRM
, который мы описали в разделе 20.5. В листинге 22.6 показана первая часть нашей функции
dg_send_recv
.

Листинг 22.6. Функция dg_send_recv: первая половина

//rtt/dg_send_recv.c

 1 #include "unprtt.h"

 2 #include 


 3 #define RTT_DEBUG


 4 static struct rtt_info rttinfo;

 5 static int rttinit = 0;

 6 static struct msghdr msgsend, msgrecv;

   /* предполагается, что обе структуры инициализированы нулем */

 7 static struct hdr {

 8  uint32_t seq; /* порядковый номер */

 9  uint32_t ts;  /* отметка времени при отправке */

10 } sendhdr, recvhdr;


11 static void signalrm(int signo);

12 static sigjmp_buf jmpbuf;


13 ssize_t

14 dg_send_recv(int fd, const void *outbuff, size_t outbytes,

15  void *inbuff, size_t inbytes,

16  const SA *destaddr, socklen_t destlen)

17 {

18  ssize_t n;

19  struct iovec iovsend[2], iovrecv[2];

20  if (rttinit == 0) {

21   rtt_init(&rttinfo); /* первый вызов */

22   rttinit = 1;

23   rtt_d_flag = 1;

24  }

25  sendhdr.seq++;

26  msgsend.msg_name = destaddr;

27  msgsend.msg_namelen = destlen;

28  msgsend.msg_iov = iovsend;

29  msgsend.msg_iovlen = 2;

30  iovsend[0].iov_base = &sendhdr;

31  iovsend[0].iov_len = sizeof(struct hdr);

32  iovsend[1].iov_base = outbuff;

33  iovsend[1].iov_len = outbytes;

34  msgrecv.msg_name = NULL;

35  msgrecv.msg_namelen = 0;

36  msgrecv.msg_iov = iovrecv;

37  msgrecv.msg_iovlen = 2;

38  iovrecv[0].iov_base = &recvhdr;

39  iovrecv[0].iov_len = sizeof(struct hdr);

40  iovrecv[l].iov_base = inbuff;

41  iovrecv[l].iov_len = inbytes;

1-5
 Мы включаем новый заголовочный файл
unprtt.h
, показанный в листинге 22.8, который определяет структуру
rtt_info
, содержащую информацию RTT для клиента. Мы определяем одну из этих структур и ряд других переменных.

Определение структур msghdr и структуры hdr

6-10
 Мы хотим скрыть от вызывающего процесса добавление порядкового номера и отметки времени в начало каждого пакета. Проще всего использовать для этого функцию
writev
, записав свой заголовок (структура
hdr
), за которым следуют данные вызывающего процесса, в виде одной дейтаграммы UDP. Вспомните, что результатом выполнения функции
writev
на дейтаграммном сокете является отправка одной дейтаграммы. Это проще, чем заставлять вызывающий процесс выделять для нас место в начале буфера, а также быстрее, чем копировать наш заголовок и данные вызывающего процесса в один буфер (под который мы должны выделить память) для каждой функции
sendto
. Но поскольку мы работаем с UDP и нам необходимо задать адрес получателя, следует использовать возможности, предоставляемые структурой
iovec
функций
sendmsg
и
recvmsg
и отсутствующие в функциях
sendto
и
recvfrom
. Вспомните из раздела 14.5, что в некоторых системах доступна более новая структура
msghdr
, включающая вспомогательные данные (
msg_control
), тогда как в более старых системах вместо них применяются элементы
msg_accright
(так называемые права доступа — access rights), расположенные в конце структуры. Чтобы избежать усложнения кода директивами
#ifdef
для обработки этих различий, мы объявляем две структуры
msghdr
как
static
. При этом они инициализируются только нулевыми битами, а затем неиспользованные элементы в конце структур просто игнорируются.

Инициализация при первом вызове

20-24
 При первом вызове нашей функции мы вызываем функцию
rtt_init
.

Заполнение структур msghdr

25-41
 Мы заполняем две структуры
msghdr
, используемые для ввода и вывода. Для данного пакета мы увеличиваем на единицу порядковый номер отправки, но не устанавливаем отметку времени отправки, пока пакет не будет отправлен (поскольку он может отправляться повторно, а для каждой повторной передачи требуется текущая отметка времени).

Вторая часть функции вместе с обработчиком сигнала

sig_alarm
показана в листинге 22.7.

Листинг 22.7. Функция dg_send_recv: вторая половина

//rtt/dg_send_rеcv.c

42  Signal(SIGALRM, sig_alrm);

43  rtt_newpack(&rttinfo); /* инициализируем для этого пакета */


44 sendagain:

45  sendhdr.ts = rtt_ts(&rttinfo);

46  Sendmsg(fd, &msgsend, 0);


47  alarm(rtt_start(&rttinfo)); /* вычисляем тайм-аут. запускаем таймер */

48  if (sigsetjmp(jmpbuf, 1) != 0) {

49   if (rtt_timeout(&rttinfо) < 0) {

50    err_msg("dg_send_recv: no response from server, giving up");

51    rttinit = 0; /* повторная инициализация для следующего вызова */

52    errno = ETIMEDOUT;

53    return (-1);

54   }

55   goto sendagain;

56  }

57  do {

58   n = Recvmsg(fd, &msgrecv, 0);

59  } while (n < sizeof(struct hdr) || recvhdr.seq != sendhdr.seq);


60  alarm(0); /* останавливаем таймер SIGALRM */

61  /* вычисляем и записываем новое значение оценки RTT */

62  rtt_stop(&rttinfo, rtt_ts(&rttinfo) — recvhdr.ts);


63  return (n - sizeof(struct hdr)); /* возвращаем размер полученной

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

64 }


65 static void

66 sig_alrm(int signo)

67 {

68  siglongjmp(jmpbuf, 1);

69 }

Установка обработчика сигналов

42-43
 Для сигнала
SIGALRM
устанавливается обработчик сигналов, а функция
rtt_newpack
устанавливает счетчик повторных передач в нуль.

Отправка дейтаграммы

45-47
 Функция
rtt_ts
получает текущую отметку времени. Отметка времени хранится в структуре
hdr
, которая добавляется к данным пользователя. Одиночная дейтаграмма UDP отправляется функцией
sendmsg
. Функция
rtt_start
возвращает количество секунд для этого тайм-аута, а сигнал
SIGALRM
контролируется функцией
alarm
.

Установка буфера перехода

48
 Мы устанавливаем буфер перехода для нашего обработчика сигналов с помощью функции sigsetjmp. Мы ждем прихода следующей дейтаграммы, вызывая функцию
recvmsg
. (Совместное использование функций
sigsetjmp
и
siglongjmp
вместе с сигналом
SIGALRM
мы обсуждали применительно к листингу 20.5.) Если время таймера истекает, функция
sigsetjmp
возвращает 1.

Обработка тайм-аута

49-55
 Когда возникает тайм-аут, функция
rtt_timeout
вычисляет следующее значение RTO (используя экспоненциальное смещение) и возвращает -1, если нужно прекратить попытки передачи дейтаграммы, или 0, если нужно выполнить очередную повторную передачу. Когда мы прекращаем попытки, мы присваиваем переменной
errno
значение
ETIMEDOUT
и возвращаемся в вызывающую функцию.

Вызов функции recvmsg, сравнение порядковых номеров

57-59
 Мы ждем прихода дейтаграммы, вызывая функцию
recvmsg
. Длина полученной дейтаграммы не должна быть меньше размера структуры
hdr
, а ее порядковый номер должен совпадать с порядковым номером запроса, ответом на который предположительно является эта дейтаграмма. Если при сравнении хотя бы одно из этих условий не выполняется, функция
recvmsg
вызывается снова.

Выключение таймера и обновление показателей RTT

60-62
 Когда приходит ожидаемый ответ, функция
alarm
отключается, а функция
rtt_stop
обновляет оценочное значение RTT. Функция
rtt_ts
возвращает текущую отметку времени, и отметка времени из полученной дейтаграммы вычитается из текущей отметки, что дает в результате RTT.

Обработчик сигнала SIGALRM

65-69
Вызывается функция
siglongjmp
, результатом выполнения которой является то, что функция
sigsetjmp
в
dg_send_recv
возвращает 1.

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

dg_send_recv
. В листинге 22.8 показан заголовочный файл
unprtt.h
.

Листинг 22.8. Заголовочный файл unprtt.h

//lib/unprtt.h

 1 #ifndef __unp_rtt_h

 2 #define __unp_rtt_h


 3 #include "unp.h"


 4 struct rtt_info {

 5  float    rtt_rtt;    /* последнее измеренное значение RTT в секундах */

 6  float    rtt_srtt;   /* сглаженная оценка RTT в секундах */

 7  float    rtt_rttvar; /* сглаженные средние значения отклонений

                            в секундах */

 8  float    rtt_rto;    /* текущее используемое значение RTO, в секундах */

 9  int      rtt_nrexmt; /* количество повторных передач: 0, 1, 2, ... */

10  uint32_t rtt_base;   /* число секунд, прошедшее после 1.1.1970 в начале */

11 };


12 #define RTT_RXTMIN    2 /* минимальное значение тайм-аута для

                              повторной передачи, в секундах */

13 #define RTT_RXTMAX   60 /* максимальное значение тайм-аута для

                              повторной передачи, в секундах */

14 #define RTT_MAXNREXMT 3 /* максимально допустимое количество

                              повторных передач одной дейтаграммы */


15 /* прототипы функций */

16 void     rtt_debug(struct rtt_info*);

17 void     rtt_init(struct rtt_info*);

18 void     rtt_newpack(struct rtt_info*);

19 int      rtt_start(struct rtt_info*);

20 void     rtt_stop(struct rtt_info*, uint32_t);

21 int      rtt_timeout(struct rtt_info*);

22 uint32_t rtt_ts(struct rtt_info*);

23 extern int rtt_d_flag; /* может быть ненулевым при наличии

                             дополнительной информации */

24 #endif /* _unp_rtt_h */

Структура rtt_info

4-11
 Эта структура содержит переменные, необходимые для того, чтобы определить время передачи пакетов между клиентом и сервером. Первые четыре переменных взяты из уравнений, приведенных в начале этого раздела.

12-14
 Эти константы определяют минимальный и максимальный тайм-ауты повторной передачи и максимальное число возможных повторных передач.

В листинге 22.9 показан макрос

RTT_RTOCALC
и первые две из четырех функций RTT.

Листинг 22.9. Макрос RTT_RTOCALC, функции rtt_minmax и rtt_init

//lib/rtt.c

 1 #include "unprtt.h"


 2 int rtt_d_flag = 0; /* отладочный флаг; может быть установлен в

                          ненулевое значение вызывающим процессом */

 3 /* Вычисление значения RTO на основе текущих значений:

 4  * сглаженное оценочное значение RTT + четырежды сглаженная

 5  * величина отклонения.

 6  */

 7 #define RTI_RTOCALC(ptr) ((ptr)->rtt_srtt + (4.0 * (ptr)->rtt_rttvar))


 8 static float

 9 rtt_minmax(float rto)

10 {

11  if (rto < RTT_RXTMIN)

12   rto = RTT_RXTMIN;

13  else if (rto > RTT_RXTMAX)

14   rto = RTT_RXTMAX;

15  return (rto);

16 }


17 void

18 rtt_init(struct rtt_info *ptr)

19 {

20  struct timeval tv;


21  Gettimeofday(&tv, NULL);

22  ptr->rtt_base = tv.tv_sec; /* количество секунд, прошедших с 1.1.1970 */


23  ptr->rtt_rtt = 0;

24  ptr->rtt_srtt = 0;

25  ptr->rtt_rttvar = 0.75;

26  ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));

27  /* первое RTO (srtt + (4 * rttvar)) = 3 с */

28 }

3-7
 Макрос вычисляет RTO как сумму оценочной величины RTT и оценочной величины среднего отклонения, умноженной на четыре.

8-16
 Функция
rtt_minmax
проверяет, что RTO находится между верхним и нижним пределами, заданными в заголовочном файле
unprtt.h
.

17-28
 Функция
rtt_init
вызывается функцией
dg_send_recv
при первой отправке пакета. Функция
gettimeofday
возвращает текущее время и дату в той же структуре
timeval
, которую мы видели в функции
select
(см. раздел 6.3). Мы сохраняем только текущее количество секунд с момента начала эпохи Unix, то есть с 00:00:00 1 января 1970 года (UTC). Измеряемое значение RTT обнуляется, а сглаженная оценка RTT и среднее отклонение принимают соответственно значение 0 и 0,75, в результате чего начальное RTO равно 3 с (4×0,75).

В листинге 22.10 показаны следующие три функции RTT.

Листинг 22.10. Функции rtt_ts, rtt_newpack и rtt_start

//lib/rtt.c

34 uint32_t

35 rtt_ts(struct rtt_info *ptr)

36 {

37  uint32_t ts;

38  struct timeval tv;


39  Gettimeofday(&tv, NULL);

40  ts = ((tv.tv_sec - ptr->rtt_base) * 1000) + (tv.tv_usec / 1000);

41  return (ts);

42 }


43 void

44 rtt_newpack(struct rtt_info *ptr)

45 {

46  ptr->rtt_nrexmt = 0;

47 }


48 int

49 rtt_start(struct rtt_info *ptr)

50 {

51  return ((int)(ptr->rtt_rto + 0.5)); /* округляем float до int */

52  /* возвращенное значение может быть использовано как аргумент

       alarm(rtt_start(&fоо)) */

53 }

34-42
 Функция
rtt_ts
возвращает текущую отметку времени для вызывающего процесса, которая должна содержаться в отправляемой дейтаграмме в виде 32-разрядного целого числа без знака. Мы получаем текущее время и дату из функции
gettimeofday
и затем вычитаем число секунд в момент вызова функции
rtt_init
(значение, хранящееся в элементе
rtt_base
структуры
rtt_info
). Мы преобразуем это значение в миллисекунды, а также преобразуем в миллисекунды значение, возвращаемое функцией
gettimeofday
в микросекундах. Тогда отметка времени является суммой этих двух значений в миллисекундах.

Разница во времени между двумя вызовами функции

rtt_ts
представляется количеством миллисекунд между этими двумя вызовами. Но мы храним отметки времени в 32-разрядном целом числе без знака, а не в структуре
timeval
.

43-47
 Функция
rtt_newpack
просто обнуляет счетчик повторных передач. Эта функция должна вызываться всегда, когда новый пакет отправляется в первый раз.

48-53
 Функция
rtt_start
возвращает текущее значение RTO в миллисекундах. Возвращаемое значение затем может использоваться в качестве аргумента функции
alarm
.

Функция

rtt_stop
, показанная в листинге 22.11, вызывается после получения ответа для обновления оценочного значения RTT и вычисления нового значения RTO.

Листинг 22.11. Функция rtt_stop: обновление показателей RTT и вычисление нового

//lib/rtt.c

62 void

63 rtt_stop(struct rtt_info *ptr, uint32_t ms)

64 {

65  double delta;


66  ptr->rtt_rtt = ms / 1000.0; /* измеренное значение RTT в секундах */


67  /*

68   * Обновляем оценочные значения RTT среднего отклонения RTT.

69   * (См. статью Джекобсона (Jacobson). SIGCOMM'88. Приложение А.)

70   * Здесь мы для простоты используем числа с плавающей точкой.

71   */


72  delta = ptr->rtt_rtt - ptr->rtt_srtt;

73  ptr->rtt_srtt += delta / 8; /* g - 1/8 */


74  if (delta < 0.0)

75   delta = -delta; /* |delta| */


76  ptr->rtt_rttvar += (delta - ptr->rtt_rttvar) / 4; /* h - 1/4 */


77  ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));

78 }

62-78
 Вторым аргументом является измеренное RTT, полученное вызывающим процессом при вычитании полученной в ответе отметки времени из текущей (функция
rtt_ts
). Затем применяются уравнения, приведенные в начале этого раздела, и записываются новые значения переменных
rtt_srtt
,
rtt_rttvar
и
rtt_rto
.

Последняя функция,

rtt_timeout
показана в листинге 22.12. Эта функция вызывается, когда истекает время таймера повторных передач.

Листинг 22.12. Функция rtt_timeout: применение экспоненциального смещения

//lib/rtt.c

83 int

84 rtt_timeout(struct rtt_info *ptr)

85 {

86  ptr->rtt_rto *= 2; /* следующее значение RTO */


87  if (++ptr->rtt_nrexmt > RTT_MAXNREXMT)

88   return (-1); /* закончилось время, отпущенное на попытки отправить

                     этот пакет */

89  return (0);

90 }

86
 Текущее значение RTO удваивается — в этом и заключается экспоненциальное смещение.

87-89
 Если мы достигли максимально возможного количества повторных передач, возвращается значение -1, указывающее вызывающему процессу, что дальнейшие попытки передачи должны прекратиться. В противном случае возвращается 0.

В нашем примере клиент соединялся дважды с двумя различными эхо-серверами в Интернете утром рабочего дня. Каждому серверу было отправлено по 500 строк. По пути к первому серверу было потеряно 8 пакетов, по пути ко второму — 16. Один из потерянных шестнадцати пакетов, предназначенных второму серверу, был потерян дважды, то есть пакет пришлось дважды передавать повторно, прежде чем был получен ответ. Все остальные потерянные пакеты пришлось передать повторно только один раз. Мы могли убедиться, что эти пакеты были действительно потеряны, посмотрев на выведенные порядковые номера каждого из полученных пакетов. Если пакет лишь опоздал, но не был потерян, после повторной передачи клиент получает два ответа: соответствующий запоздавшему первому пакету и повторно переданному. Обратите внимание, что у нас нет возможности определить, что именно было потеряно (и привело к необходимости повторной передачи клиентского запроса) — сам клиентский запрос или же ответ сервера, высланный после получения такого запроса.

ПРИМЕЧАНИЕ

Для первого издания этой книги автор написал для проверки этого клиента сервер UDP, который случайным образом игнорировал пакеты. Теперь он не используется. Нужно только соединить клиент с сервером через Интернет, и тогда нам почти гарантирована потеря некоторых пакетов!

22.6. Связывание с адресами интерфейсов

Одно из типичных применений функции

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

ПРИМЕЧАНИЕ

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

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

Листинг 22.13. Первая часть сервера UDP, который с помощью функции bind связывается со всеми адресами

//advio/udpserv03.c

 1 #include "unpifi.h"


 2 void mydg_echo(int, SA*, socklen_t, SA*);


 3 int

 4 main(int argc, char **argv)

 5 {

 6  int sockfd;

 7  const int on = 1;

 8  pid_t pid;

 9  struct ifi_info *ifi, *ifihead;

10  struct sockaddr_in *sa, cliaddr, wildaddr;


11  for (ifihead = ifi = Get_ifi_info(AF_INET, 1);

12   ifi != NULL; ifi = ifi->ifi_next) {


13   /* связываем направленный адрес */

14   sockfd = Socket(AF_INET, SOCK_DGRAM, 0);


15   Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));


16   sa = (struct sockaddr_in*)ifi->ifi_addr;

17   sa->sin_family = AF_INET;

18   sa->sin_port = htons(SERV_PORT);

19   Bind(sockfd, (SA*)sa, sizeof(*sa));

20   printf("bound %s\n", Sock_ntop((SA*)sa, sizeof(*sa)));


21   if ((pid = Fork()) == 0) { /* дочерний процесс */

22    mydg_echo(sockfd, (SA*)&cliaddr, sizeof(cliaddr), (SA*)sa);

23    exit(0); /* не выполняется */

24   }

Вызов функции get_ifi_info для получения информации об интерфейсе

11-12
 Функция
get_ifi_info
получает все адреса IPv4, включая дополнительные (псевдонимы), для всех интерфейсов. Затем программа перебирает все структуры
ifi_info
.

Создание сокета UDP и связывание адреса направленной передачи

13-20
 Создается сокет UDP, и с ним связывается адрес направленной передачи. Мы также устанавливаем параметр сокета
SO_REUSEADDR
, поскольку мы связываем один и тот же порт (параметр
SERV_PORT
) для всех IP-адресов.

ПРИМЕЧАНИЕ

Не все реализации требуют, чтобы был установлен этот параметр сокета. Например, Беркли-реализации не требуют этого параметра и позволяют с помощью функции bind связать уже связанный порт, если новый связываемый IP-адрес не является универсальным адресом и отличается от всех IP-адресов, уже связанных с портом. Однако Solaris 2.5 для успешного связывания с одним и тем же портом второго адреса направленной передачи требует установки этого параметра.

Порождение дочернего процесса для данного адреса

21-24
Вызывается функция
fork
, порождающая дочерний процесс. В этом дочернем процессе вызывается функция
mydg_echo
, которая ждет прибытия любой дейтаграммы на сокет и отсылает ее обратно отправителю.

В листинге 22.14 показана следующая часть функции

main
, которая обрабатывает широковещательные адреса.

Листинг 22.14. Вторая часть сервера UDP, который с помощью функции bind связывается со всеми адресами

//advio/udpserv03.c

25   if (ifi->ifi_flags & IFF_BROADCAST) {

26    /* пытаемся связать широковещательный адрес */

27    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

28    Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));


29    sa = (struct sockaddr_in*)ifi->ifi_brdaddr;

30    sa->sin_family = AF_INET;

31    sa->sin_port = htons(SERV_PORT);

32    if (bind(sockfd, (SA*)sa, sizeof(*sa)) < 0) {

33     if (errno == EADDRINUSE) {

34      printf("EADDRINUSE: %s\n",

35       Sock_ntop((SA*)sa, sizeof(*sa)));

36      Close(sockfd);

37      continue;

38     } else

39      err_sys("bind error for %s",

40       Sock_ntop((SA*)sa, sizeof(*sa)));

41    }

42    printf("bound %s\n", Sock_ntop((SA*)sa, sizeof(*sa)));


43    if ((pid = Fork()) == 0) { /* дочерний процесс */

44     mydg_echo(sockfd, (SA*)&cliaddr, sizeof(cliaddr),

45      (SA*)sa);

46     exit(0); /* не выполняется */

47    }

48   }

49  }

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

25-42
 Если интерфейс поддерживает широковещательную передачу, создается сокет UDP и с ним связывается широковещательный адрес. На этот раз мы позволим функции
bind
завершиться с ошибкой
EADDRINUSE
, поскольку если у интерфейса имеется несколько дополнительных адресов (псевдонимов) в одной подсети, то каждый из различных адресов направленной передачи будет иметь один и тот же широковещательный адрес. Подобный пример приведен после листинга 17.3. В этом сценарии мы предполагаем, что успешно выполнится только первая функция
bind
.

Порождение дочернего процесса

43-47
 Порождается дочерний процесс, и он вызывает функцию
mydg_echo
.

Заключительная часть функции

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

Листинг 22.15. Заключительная часть сервера UDP, связывающегося со всеми адресами

//advio/udpserv03.c

50  /* связываем универсальный адрес */

51  sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

52  Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));


53  bzero(&wildaddr, sizeof(wildaddr));

54  wildaddr.sin_family = AF_INET;

55  wildaddr.sin_addr.s_addr = htonl(INADDR_ANY);

56  wildaddr.sin_port = htons(SERV_PORT);

57  Bind(sockfd, (SA*)&wildaddr, sizeof(wildaddr));

58  printf("bound %s\n", Sock_ntop((SA*)&wildaddr, sizeof(wildaddr)));


59  if ((pid = Fork()) == 0) { /* дочерний процесс */

60   mydg_echo(sockfd, (SA*)&cliaddr, sizeof(cliaddr), (SA*)sa);

61   exit(0); /* не выполняется */

62  }

63  exit(0);

64 }

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

50-62
 Создается сокет UDP, устанавливается параметр сокета
SO_REUSEADDR
и происходит связывание с универсальным IP-адресом. Порождается дочерний процесс, вызывающий функцию
mydg_echo
.

Завершение работы функции main

63
 Функция
main
завершается, и сервер продолжает выполнять работу, как и все порожденные дочерние процессы.

Функция

mydg_echo
, которая выполняется всеми дочерними процессами, показана в листинге 22.16.

Листинг 22.16. Функция mydg_echo

//advio/udpserv03.c

65 void

66 mydg_echo(int sockfd, SA *pcliaddr, socklen_t clilen, SA *myaddr)

67 {

68  int n;

69  char mesg[MAXLINE];

70  socklen_t len;


71  for (;;) {

72   len = clilen;

73   n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

74   printf("child %d, datagram from %s", getpid(),

75   Sock_ntop(pcliaddr, len));

76   printf(", to %s\n", Sock_ntop(myaddr, clilen));


77   Sendto(sockfd, mesg, n, 0, pcliaddr, len);

78  }

79 }

Новый аргумент

65-66
 Четвертым аргументом этой функции является IP-адрес, связанный с сокетом. Этот сокет должен получать только дейтаграммы, предназначенные для данного IP-адреса. Если IP-адрес является универсальным, сокет должен получать только те дейтаграммы, которые не подходят ни для какого другого сокета, связанного с тем же портом.

Чтение дейтаграммы и отражение ответа

71-78
 Дейтаграмма читается с помощью функции
recvfrom
и отправляется клиенту обратно с помощью функции
sendto
. Эта функция также выводит IP-адрес клиента и IP-адрес, который был связан с сокетом.

Запустим эту программу на нашем узле

solaris
после установки псевдонима для интерфейса
hme0
Ethernet. Адрес псевдонима: узел 200 в сети 10.0.0/24.

solaris % udpserv03

bound 127.0.0.1:9877     интерфейс закольцовки

bound 10.0.0.200:9877    направленный адрес интерфейса hme0:1

bound 10.0.0.255:9877    широковещательный адрес интерфейса hme0:1

bound 192.168.1.20:9877  направленный адрес интерфейса hme0

bound 192.168.1.255:9877 широковещательный адрес интерфейса hme0

bound 0.0.0.0.9877       универсальный адрес

При помощи утилиты

netstat
мы можем проверить, что все сокеты связаны с указанными IP-адресами и портом:

solaris % netstat -na | grep 9877

127.0.0.1.9877       Idle

10.0.0.200.9877      Idle

    *.9877           Idle

192.129.100.100.9877 Idle

    *.9877           Idle

    *.9877           Idle

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

select
и не вызывая функцию
fork
. Проблема в данном случае будет заключаться в усложнении кода. Хотя использовать функцию
select
для всех дескрипторов несложно, нам придется осуществить некоторое сопоставление каждого дескриптора связанному с ним IP-адресу (вероятно, с помощью массива структур), чтобы иметь возможность вывести IP-адрес получателя после того, как на определенном сокете получена дейтаграмма. Часто бывает проще использовать отдельный процесс или поток для каждой операции или дескриптора вместо мультиплексирования множества различных операций или дескрипторов одним процессом.

22.7. Параллельные серверы UDP

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

Определение «длительное время» означает, что другой клиент вынужден ждать в течение некоторого заметного для него промежутка времени, пока обслуживается текущий клиент. Например, если два клиентских запроса приходят в течение 10 мс и предоставление сервиса каждому клиенту занимает в среднем 5 с, то второй клиент будет вынужден ждать ответа около 10 с вместо 5 с (если бы запрос был принят в обработку сразу же по прибытии).

В случае TCP проблема решается просто — требуется лишь породить дочерний процесс с помощью функции

fork
(или создать новый поток, что мы увидим в главе 23) и дать возможность дочернему процессу выполнять обработку нового клиента. При использовании TCP ситуация существенно упрощается за счет того, что каждое клиентское соединение уникально: пара сокетов TCP уникальна для каждого соединения. Но в случае с UDP мы вынуждены рассматривать два различных типа серверов.

1. Первый тип — простой сервер UDP, который читает клиентский запрос, посылает ответ и затем завершает работу с клиентом. В этом сценарии сервер, читающий запрос клиента, может с помощью функции

fork
породить дочерний процесс и дать ему возможность обработать запрос. «Запрос», то есть содержимое дейтаграммы и структура адреса сокета, содержащая адрес протокола клиента, передаются дочернему процессу в виде копии содержимого области памяти из функции
fork
. Затем дочерний процесс посылает свой ответ непосредственно клиенту.

2. Второй тип — сервер UDP, обменивающийся множеством дейтаграмм с клиентом. Проблема здесь в том, что единственный номер порта сервера, известный клиенту, — это номер заранее известного порта. Клиент посылает первую дейтаграмму своего запроса на этот порт, но как сервер сможет отличить последующие дейтаграммы этого клиента от запросов новых клиентов? Типичным решением этой проблемы для сервера будет создание нового сокета для каждого клиента, связывание при помощи функции

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

Примером второго типа сервера UDP является сервер TFTP (Trivial File Transfer Protocol — упрощенный протокол передачи файлов). Передача файла с помощью TFTP обычно требует большого числа дейтаграмм (сотен или тысяч, в зависимости от размера файла), поскольку этот протокол отправляет в одной дейтаграмме только 512 байт. Клиент отправляет дейтаграмму на известный порт сервера (69), указывая, какой файл нужно отправить или получить. Сервер читает запрос, но отправляет ответ с другого сокета, который он создает и связывает с динамически назначаемым портом. Все последующие дейтаграммы между клиентом и сервером используют для передачи этого файла новый сокет. Это позволяет главному серверу TFTP продолжать обработку других клиентских запросов, приходящих на порт 69, в то время как происходит передача файла (возможно, в течение нескольких секунд или даже минут).

Если мы рассмотрим автономный сервер TFTP (то есть случай, когда не используется демон

inetd
), то получим сценарий, показанный на рис. 22.3. Мы считаем, что динамически назначаемый порт, связанный дочерним процессом с его новым сокетом, — это порт 2134.

Рис. 22.3. Процессы, происходящие на автономном параллельном UDP-сервере

Если используется демон

inetd
, сценарий включает еще один шаг. Вспомните из табл. 13.4, что большинство серверов UDP задают аргумент
wait-flag
как
wait
. В описании, которое следовало за рис. 13.4, мы сказали, что при указанном значении этого флага демон
inetd
приостанавливает выполнение функции
select
на сокете до завершения дочернего процесса, давая возможность этому дочернему процессу считать дейтаграмму, доставленную на сокет. На рис. 22.4 показаны все шаги.

Рис. 22.4. Параллельный сервер UDP, запущенный демоном inetd

Сервер TFTP, являясь дочерним процессом функции

inetd
, вызывает функцию
recvfrom
и считывает клиентский запрос. Затем он с помощью функции
fork
порождает собственный дочерний процесс, и этот дочерний процесс будет обрабатывать клиентский запрос. Затем сервер TFTP вызывает функцию
exit
, отправляя демону
inetd
сигнал
SIGCHLD
, который, как мы сказали, указывает демону
inetd
снова вызвать функцию
select
на сокете, связанном с портом UDP 69.

22.8. Информация о пакетах IPv6

IPv6 позволяет приложению определять до пяти характеристик исходящей дейтаграммы:

■ IPv6-адрес отправителя;

■ индекс интерфейса для исходящих дейтаграмм;

■ предельное количество транзитных узлов для исходящих дейтаграмм;

■ адрес следующего транзитного узла;

■ класс исходящего трафика.

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

sendmsg
. Для сокета можно задать постоянные параметры, которые будут действовать на все отправляемые пакеты (раздел 27.7).

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

recvmsg
:

■ IPv6-адрес получателя;

■ индекс интерфейса для входящих дейтаграмм;

■ предельное количество транзитных узлов для входящих дейтаграмм.

■ класс входящего трафика.

На рис. 22.5 показано содержимое вспомогательных данных, о которых рассказывается далее.

Рис. 22.5. Вспомогательные данные для информации о пакете IPv6

Структура

in6_pktinfo
содержит либо IPv6-адрес отправителя и индекс интерфейса для исходящей дейтаграммы, либо IPv6-адрес получателя и индекс интерфейса для получаемой дейтаграммы:

struct in6_pktinfo {

 struct in6_addr ipi6_addr; /* IPv6-адрес отправителя/получателя */

 int ipi6_ifindex; /* индекс интерфейса для исходящей/получаемой дейтаграммы */

};

Эта структура определяется в заголовочном файле

, подключение которого позволяет ее использовать. В структуре
cmsghdr
, содержащей вспомогательные данные, элемент
cmsg_level
будет иметь значение
IPPROTO_IPV6
, элемент
cmsg_type
будет равен
IPV6_PKTINFO
и первый байт данных будет первым байтом структуры
in6_pktinfo
. В примере, приведенном на рис. 22.5, мы считаем, что между структурой
cmsghdr
и данными нет заполнения и целое число занимает 4 байта.

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

sendmsg
. Чтобы информация добавлялась ко всем отправляемым через сокет пакетам, необходимо установить параметр сокета
IPV6_PKTINFO
со значением
in6_pktinfo
. Возвращать эту информацию функция
recvmsg
будет, только если приложение включит параметр сокета
IPV6_RECVPKTINFO
.

Исходящий и входящий интерфейсы

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

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

Адрес отправителя и адрес получателя IPv6

IPv6-адрес отправителя обычно определяется при помощи функции

bind
. Но если адрес отправителя поставляется вместе с данными, это может снизить непроизводительные затраты. Этот параметр также позволяет серверу гарантировать, что адрес отправителя ответа совпадает с адресом получателя клиентского запроса — некоторым клиентам требуется такое условие, которое сложно выполнить в случае IPv4 (см. упражнение 22.4).

Когда IPv6-адрес отправителя задан в качестве вспомогательных данных и элемент

ipi6_addr
структуры
in6_pktinfo
имеет значение
IN6ADDR_ANY_INIT
, возможны следующие сценарии: если адрес в настоящий момент связан с сокетом, он используется в качестве адреса отправителя; если в настоящий момент никакой адрес не связан с сокетом, ядро выбирает адрес отправителя. Если же элемент
ipi6_addr
не является неопределенным адресом, но сокет уже связался с адресом отправителя, то значением элемента
ipi6_addr
перекрывается уже связанный адрес, но только для данной операции вывода. Затем ядро проверяет, действительно ли запрашиваемый адрес отправителя является адресом направленной передачи, присвоенным узлу.

Когда структура in6_

pktinfo
возвращается в качестве вспомогательных данных функцией
recvmsg
, элемент
ipi6_addr
содержит IPv6-адрес получателя из полученного пакета. По сути, это аналог параметра сокета
IP_RECVDSTADDR
для IPv4.

Задание и получение предельного количества транзитных узлов

Предельное количество транзитных узлов обычно задается параметром сокета

IPV6_UNICAST_HOPS
для дейтаграмм направленной передачи (см. раздел 7.8) или параметром сокета
IPV6_MULTICAST_HOPS
для дейтаграмм многоадресной передачи (см. раздел 21.6). Задавая предельное количество транзитных узлов в составе вспомогательных данных, мы можем заменить как значение этого предела, задаваемое ядром по умолчанию, так и ранее заданное значение — и для направленной, и для многоадресной передачи, но только для одной операции вывода. Предел количества транзитных узлов полученного пакета используется в таких программах, как
traceroute
, и в некоторых приложениях IPv6, которым нужно проверять, что полученное значение равно 255 (то есть что пакет не пересылался маршрутизаторами).

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

recvmsg
, только если приложение включает параметр сокета
IPV6_RECVHOPLIMIT
. В структуре
cmsghdr
, содержащей эти вспомогательные данные, элемент
cmsg_level
будет иметь значение
IPPROTO_IPV6
, элемент
cmsg_type
 — значение
IPV6_HOPLIMIT
, а первый байт данных будет первым байтом целочисленного предела повторных передач. Мы показали это на рис. 22.5. Нужно понимать, что значение, возвращаемое в качестве вспомогательных данных, — это действительное значение из полученной дейтаграммы, в то время как значение, возвращаемое функцией
getsockopt
с параметром
IPV6_UNICAST_HOPS
, является значением по умолчанию, которое ядро будет использовать для исходящих дейтаграмм на сокете.

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

sendmsg
. Обычные значения для предельного количества транзитных узлов лежат в диапазоне от 0 до 255 включительно, но если целочисленное значение равно -1, это указывает ядру, что следует использовать значение по умолчанию.

ПРИМЕЧАНИЕ

Предельное количество транзитных узлов не содержится в структуре in6_pktinfo — некоторые серверы UDP хотят отвечать на запросы клиентов, посылая ответы на том же интерфейсе, на котором был получен запрос, с совпадением IPv6-адреса отправителя ответа и IPv6-адреса получателя запроса. Для этого приложение может включить параметр сокета IPV6_RECVPKTINFO, а затем использовать полученную управляющую информацию из функции recvmsg в качестве управляющей информации для функции sendmsg при отправке ответа. Приложению вообще никак не нужно проверять или изменять структуру in6_pktinfo. Но если в этой структуре содержался бы предел количества транзитных узлов, приложение должно было бы проанализировать полученную управляющую информацию и изменить значение этого предела, поскольку полученный предел не является желательным значением для исходящего пакета.

Задание адреса следующего транзитного узла

Объект вспомогательных данных

IPV6_NEXTHOP
задает адрес следующего транзитного узла дейтаграммы в виде структуры адреса сокета. В структуре
cmsghdr
, содержащей эти вспомогательные данные, элемент
cmsg_level
будет иметь значение
IPPROTO_IPV6
, элемент
cmsg_type
— значение
IPV6_NEXTHOP
, а первый байт данных будет первым байтом структуры адреса сокета.

На рис. 22.5 мы показали пример такого объекта вспомогательных данных, считая, что структура адреса сокета — это 24-байтовая структура

sockaddr_in6
. В этом случае узел, идентифицируемый данным адресом, должен быть соседним для отправляющего узла. Если этот адрес совпадает с адресом получателя IPv6-дейтаграммы, мы получаем эквивалент параметра сокета
SO_DONTROUTE
. Установка этого параметра требует прав привилегированного пользователя. Адрес следующего транзитного узла можно устанавливать для всех пакетов на сокете, если включить параметр сокета
IPV6_NEXTHOP
со значением
sockaddr_in6
(раздел 27.7). Для этого необходимо обладать правами привилегированного пользователя.

Задание и получение класса трафика

Объект вспомогательных данных

IPV6_TCLASS
задает класс трафика для дейтаграммы. Элемент
cmsg_level
структуры
cmsghdr
, содержащей эти данные, будет равен
IPPROTO_IPV6
, элемент
cmsg_type
будет равен
IPV6_TCLASS
, а первый байт данных будет первым байтом целочисленного (4-байтового) значения класса трафика (см. рис. 22.5). Согласно разделу А.3, класс трафика состоит из полей
DSCP
и
ECN
. Эти поля должны устанавливаться одновременно. Ядро может маскировать или игнорировать указанное пользователем значение, если ему это нужно (например, если ядро реализует
ECN
, оно может установить биты
ECN
равными какому-либо значению, игнорируя два бита, указанных с параметром
IPV6_TCLASS
). Класс трафика обычно лежит в диапазоне 0–255. Значение -1 говорит ядру о необходимости использовать значение по умолчанию.

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

IPV6_TCLASS
(раздел 27.7). Класс трафика для принятого пакета возвращается функцией
recvmsg
во вспомогательных данных, только если приложение включило параметр сокета
IPV6_RECVTCLASS
.

22.9. Управление транспортной MTU IPv6

IPv6 предоставляет приложениям средства для управления механизмом обнаружения транспортной MTU (раздел 2.11). Значения по умолчанию пригодны для подавляющего большинства приложений, однако специальные программы могут настраивать процедуру обнаружения транспортной MTU так, как им нужно. Для этого имеется четыре параметра сокета.

Отправка с минимальной MTU

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

Минимальная MTU может использоваться приложениями двух типов. Во- первых, это приложения многоадресной передачи, которым нужно избегать порождения множества ICMP-сообщений «Message too big». Во-вторых, это приложения, выполняющие небольшие по объему транзакции с большим количеством адресатов (например, DNS). Обнаружение MTU для многоадресного сеанса может быть недостаточно выгодным, чтобы компенсировать затраты на получение и обработку миллионов ICMP-сообщений, а приложения типа DNS обычно связываются с серверами недостаточно часто, чтобы можно было рисковать утратой пакетов.

Использование минимальной MTU обеспечивается параметром сокета

IPV6_USE_MIN_MTU
. Для него определено три значения: -1 (по умолчанию) соответствует использованию минимальной MTU для многоадресных передач и обнаруженной транспортной MTU для направленных передач; 0 соответствует обнаружению транспортной MTU для всех передач; 1 означает использование минимальной MTU для всех адресатов.

Параметр

IPV6_USE_MIN_MTU
может быть передан и во вспомогательных данных. В этом случае элемент
cmsg_level
структуры
cmsghdr
должен иметь значение
IPPROTO_IPV6
, элемент
cmsg_type
должен иметь значение
IPV6_USE_MIN_MTU
, а первый байт данных должен быть первым байтом четырехбайтового целочисленного значения параметра.

Получение сообщений об изменении транспортной MTU

Для получения уведомлений об изменении транспортной MTU приложение может включить параметр сокета

IPV6_RECVPATHMTU
. Этот флаг разрешает доставку транспортной MTU во вспомогательных данных каждый раз, когда эта величина меняется. Функция recvmsg в этом случае возвратит дейтаграмму нулевой длины, но со вспомогательными данными, в которых будет помещена транспортная MTU. Элемент
cmsg_level
структуры
cmsghdr
будет иметь значение
IPPROTO_IPV6
, элемент
cmsg_type
будет
IPV6_PATHMTU
, а первый байт данных будет первым байтом структуры
iр6_mtuinfo
. Эта структура содержит адрес узла, для которого изменилась транспортная MTU, и новое значение этой величины в байтах.

struct ip6_mtuinfo {

 struct sockaddr_in6 ip6m_addr; /* адрес узла */

 uint32_t            ip6m_mtu;  /* транспортная MTU

                                   в порядке байтов узла */

};

Эта структура определяется включением заголовочного файла

.

Определение текущей транспортной MTU

Если приложение не отслеживало изменения MTU при помощи параметра

IPV6_RECVPATHMTU
, оно может определить текущее значение транспортной MTU присоединенного сокета при помощи параметра
IPV6_PATHMTU
. Этот параметр доступен только для чтения и возвращает он структуру
ip6_mtuinfo
(см. выше), в которой хранится текущее значение MTU. Если значение еще не было определено, возвращается значение MTU по умолчанию для исходящего интерфейса. Значение адреса из структуры
ip6_mtuinfo
в данном случае не определено.

Отключение фрагментации

По умолчанию стек IPv6 фрагментирует исходящие пакеты по транспортной MTU. Приложениям типа

traceroute
автоматическая фрагментация не нужна, потому что им нужно иметь возможность самостоятельно определять транспортную MTU. Параметр сокета
IPV6_DONTFRAG
используется для отключения автоматической фрагментации: значение 0 (по умолчанию) разрешает фрагментацию, тогда как значение 1 отключает ее.

Когда автоматическая фрагментация отключена, вызов send со слишком большим пакетом может возвратить ошибку

EMSGSIZE
, но это не является обязательным. Единственным способом определить необходимость фрагментации пакета является использование параметра сокета
IPV6_RECVPATHMTU
, который мы описали выше.

Параметр

IPV6_DONTFRAG
может передаваться и во вспомогательных данных. При этом элемент
cmsg_level
структуры
cmsghdr
должен иметь значение
IPPROTO_IPV6
, а элемент
cmsg_type
должен иметь значение
IPV6_DONTFRAG
. Первый байт данных должен быть первым байтом четырехбайтового целого.

22.10. Резюме

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

IP_RECVDSTADDR
и
IP_RFCVIF
. Аналогичная информация вместе с предельным значением количества транзитных узлов полученной дейтаграммы для сокетов IPv6 становится доступна при включении параметра сокета
IPV6_PKTINFO
.

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

В разделе 22.5 мы добавили нашему клиенту UDP определенные функциональные возможности, повышающие его надежность за счет обнаружения факта потери пакетов, для чего используются тайм-аут и повторная передача. Мы изменяли тайм-аут повторной передачи динамически, снабжая каждый пакет отметкой времени и отслеживая два параметра: период обращения RTT и его среднее отклонение. Мы также добавили порядковые номера, чтобы проверять, что данный ответ — это ожидаемый нами ответ на определенный запрос. Наш клиент продолжал использовать простой протокол остановки и ожидания (stop-and-wait), а приложения такого типа допускают применение UDP.

Упражнения

1. Почему в листинге 22.16 функция

printf
вызывается дважды?

2. Может ли когда-нибудь функция

dg_send_recv
(см. листинги 22.6 и 22.7) возвратить нуль?

3. Перепишите функцию

dg_send_recv
с использованием функции
select
и ее таймера вместо
alarm
,
SIGALRM
,
sigsetjmp
и
siglongjmp
.

4. Как может сервер IPv4 гарантировать, что адрес отправителя в его ответе совпадает с адресом получателя клиентского запроса? (Аналогичную функциональность предоставляет параметр сокета

IPV6_PKTINFO
.)

5. Функция

main
в разделе 22.6 является зависящей от протокола (IPv4). Перепишите ее, чтобы она стала не зависящей от протокола. Потребуйте, чтобы пользователь задал один или два аргумента командной строки, первый из которых — необязательный IP-адрес (например, 0.0.0.0 или 0::0), а второй — обязательный номер порта. Затем вызовите функцию
udp_client
, чтобы получить семейство адресов, номер порта и длину структуры адреса сокета.

Что произойдет, если вы вызовете функцию

udp_client
, как было предложено, не задавая аргумент
hostname
, поскольку функция
udp_client
не задает значение
AI_PASSIVE
функции
getaddrinfo
?

6. Соедините клиент, показанный в листинге 22.4, с эхо-сервером через Интернет, изменив функции

rtt_
так, чтобы выводилось каждое значение RTT. Также измените функцию
dg_send_recv
, чтобы она выводила каждый полученный порядковый номер. Изобразите на графике полученные в результате значения RTT вместе с оценочными значениями RTT и среднего отклонения.

Глава 23