Сетевые технологии лвс. Какие бывают технологии локальных сетей? Промышленные и специализированные сети

Приложения, использующие TCP и UDP, фундаментально отличаются друг от друга, потому что UDP является ненадежным протоколом дейтаграмм, не ориентированным на установление соединения, и этим принципиально непохож на ориентированный на установление соединения и надежную передачу потока байтов TCP. Тем не менее есть случаи, когда имеет смысл использовать UDP вместо TCP. Подобные случаи мы рассматриваем в разделе 22.4. Некоторые популярные приложения построены с использованием UDP, например DNS (Domain Name System - система доменных имен), NFS (сетевая файловая система - Network File System) и SNMP (Simple Network Management Protocol - простой протокол управления сетью).

На рис. 8.1 показаны вызовы функций для типичной схемы клиент-сервер UDP. Клиент не устанавливает соединения с сервером. Вместо этого клиент лишь отправляет серверу дейтаграмму, используя функцию sendto (она описывается в следующем разделе), которой нужно задать адрес получателя (сервера) в качестве аргумента. Аналогично, сервер не устанавливает соединения с клиентом. Вместо этого сервер лишь вызывает функцию recvfrom , которая ждет, когда придут данные от какого-либо клиента. Функция recvfrom возвращает адрес клиента (для данного протокола) вместе с дейтаграммой, и таким образом сервер может отправить ответ именно тому клиенту, который прислал дейтаграмму.

Рис. 8.1 . Функции сокета для модели клиент-сервер UDP

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

В этой главе мы опишем новые функции, применяемые с сокетами UDP, - recvfrom и sendto , и переделаем нашу модель клиент-сервер для применения UDP. Кроме того, мы рассмотрим использование функции connect с сокетом UDP и концепцию асинхронных ошибок.

8.2. Функции recvfrom и sendto

Эти две функции аналогичны стандартным функциям read и write , но требуют трех дополнительных аргументов.

ssize_t recvfrom(int sockfd , void * buff , size_t nbytes , int flags ,

struct sockaddr * from , socklen_t * addrlen);

ssize_t sendto(int sockfd , const void * buff , size_t nbytes , int flags ,

const struct sockaddr * to , socklen_t addrlen);

Обе функции возвращают количество записанных или прочитанных байтов в случае успешного выполнения, -1 в случае ошибки

Первые три аргумента, sockfd , buff и nbytes , идентичны первым трем аргументам функций read и write: дескриптор, указатель на буфер, из которого производится чтение или в который происходит запись, и число байтов для чтения или записи.

Мы расскажем об аргументе flags в главе 14, где мы рассматриваем функции recv , send , recvmsg и sendmsg , поскольку сейчас в нашем простом примере они не нужны. Пока мы всегда будем устанавливать аргумент flags в нуль.

Аргумент to для функции sendto - это структура адреса сокета, содержащая адрес протокола (например, IP-адрес и номер порта) адресата. Размер этой структуры адреса сокета задается аргументом addrlen . Функция recvform заполняет структуру адреса сокета, на которую указывает аргумент from, записывая в нее протокольный адрес отправителя дейтаграммы. Число байтов, хранящихся в структуре адреса сокета, также возвращается вызывающему процессу в целом числе, на которое указывает аргумент addrlen . Обратите внимание, что последний аргумент функции sendto является целочисленным значением, в то время как последний аргумент функции recvfrom - это указатель на целое значение (аргумент типа «значение-результат»).

Последние два аргумента функции recvfrom аналогичны двум последним аргументам функции accept: содержимое структуры адреса сокета по завершении сообщает нам, кто отправил дейтаграмму (в случае UDP) или кто инициировал соединение (в случае TCP). Последние два аргумента функции sendto аналогичны двум последним аргументам функции connect: мы заполняем структуру адреса сокета протокольным адресом получателя дейтаграммы (в случае UDP) или адресом узла, с которым будет устанавливаться соединение (в случае TCP).

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

Дейтаграмма может иметь нулевую длину. В случае UDP при этом возвращается дейтаграмма IP, содержащая заголовок IP (обычно 20 байт для IPv4 или 40 байт для IPv6), 8-байтовый заголовок UDP и никаких данных. Это также означает, что возвращаемое из функции recvfrom нулевое значение вполне приемлемо для протокола дейтаграмм: оно не является признаком того, что собеседник закрыл соединение, как это происходит при возвращении нулевого значения из функции read на сокете TCP. Поскольку протокол UDP не ориентирован на установление соединения, то в нем и не существует такого события, как закрытие соединения.

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

И функция recvfrom , и функция sendto могут использоваться с TCP, хотя обычно в этом нет необходимости.

8.3. Эхо-сервер UDP: функция main

Теперь мы переделаем нашу простую модель клиент-сервер из главы 5, используя UDP. Диаграмма вызовов функций в программах наших клиента и сервера UDP показана на рис. 8.1. На рис. 8.2 представлены используемые функции. В листинге 8.1 показана функция сервера main .

Рис. 8.2 . Простая модель клиент-сервер, использующая UDP

Листинг 8.1 . Эхо-сервер UDP

//udpcliserv/udpserv01.с

1 #include "unp.h"

3 intmain(int argc, char **argv)

6 struct sockaddr_in servaddr, cliaddr;

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

8 bzero(&servaddr, sizeof(servaddr));

9 servaddr.sin_family = AF_INET;

10 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

12 Bind(sockfd, (SA*)&servaddr, sizeof(servaddr));

13 dg_echo(sodkfd, (SA*)&cliaddr, sizeof(cliaddr));

Создание сокета UDP, связывание с заранее известным портом при помощи функции bind

7-12 Мы создаем сокет UDP, задавая в качестве второго аргумента функции socket значение SOCK_DGRAM (сокет дейтаграмм в протоколе IPv4). Как и в примере сервера TCP, адрес IPv4 для функции bind задается как INADDR_ANY , а заранее известный номер порта сервера - это константа SERV_PORT из заголовка unp.h .

13 Затем вызывается функция dg_echo для обработки клиентского запроса сервером.

8.4. Эхо-сервер UDP: функция dg_echo

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

Листинг 8.2 . Функция dg_echo: отражение строк на сокете дейтаграмм

1 #include "unp.h"

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

6 socklen_t len;

7 char mesg;

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

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

Чтение дейтаграммы, отражение отправителю

8-12 Эта функция является простым циклом, в котором очередная дейтаграмма, приходящая на порт сервера, читается функцией recvfrom и с помощью функции sendto отправляется обратно.

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

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

Для сокета на уровне UDP происходит неявная буферизация дейтаграмм в виде очереди. Действительно, у каждого сокета UDP имеется буфер приема, и каждая дейтаграмма, приходящая на этот сокет, помещается в его буфер приема. Когда процесс вызывает функцию recvfrom , очередная дейтаграмма из буфера возвращается процессу в порядке FIFO (First In, First Out - первым пришел, первым обслужен). Таким образом, если множество дейтаграмм приходит на сокет до того, как процесс может прочитать данные, уже установленные в очередь для сокета, то приходящие дейтаграммы просто добавляются в буфер приема сокета. Но этот буфер имеет ограниченный размер. Мы обсуждали этот размер и способы его увеличения с помощью параметра сокета SO_RCVBUF в разделе 7.5.

На рис. 8.3 показано обобщение нашей модели TCP клиент-сервер из главы 5, когда два клиента устанавливают соединения с сервером.

Рис. 8.3 . Обобщение модели TCP клиент-сервер с двумя клиентами

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

Рис. 8.4 . Обобщение модели UDP клиент-сервер с двумя клиентами

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

Функция main в листинге 8.1 является зависящей от протокола (она создает сокет семейства AF_INET , а затем выделяет и инициализирует структуру адреса сокета IPv4), но функция dg_echo от протокола не зависит. Причина, по которой функция dg_echo не зависит от протокола, заключается в том, что вызывающий процесс (в нашем случае функция main) должен разместить в памяти структуру адреса сокета корректного размера, и указатель на эту структуру вместе с ее размером передаются в качестве аргументов функции dg_echo . Функция dg_echo никогда не углубляется в эту структуру: она просто передает указатель на нее функциям recvfrom и sendto . Функция recvfrom заполняет эту структуру, вписывая в нее IP-адрес и номер порта клиента, и поскольку тот же указатель (pcliaddr) затем передается функции sendto в качестве адреса получателя, таким образом дейтаграмма отражается обратно клиенту, отправившему дейтаграмму.

8.5. Эхо-клиент UDP: функция main

Функция main клиента UDP показана в листинге 8.3.

Листинг 8.3 . Эхо-клиент UDP

//udpcliserv/udpcli01.c

1 #include "unp.h"

3 main(int argc, char **argv)

6 struct sockaddr_in servaddr;

7 if (argc != 2)

8 err_quit("usage: udpcli ");

9 bzero(&servaddr, sizeof(servaddr));

10 servaddr.sin_family = AF_INET;

11 servaddr.sin_port = htons(SERV_PORT);

12 Inet_pton(AF_INET, argv, &servaddr.sin_addr);

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

14 dg_cli(stdin, sockfd, (SA*)&servaddr, sizeof(servaddr));

Заполнение структуры адреса сокета адресом сервера

9-12 Структура адреса сокета IPv4 заполняется IP-адресом и номером порта сервера. Эта структура будет передана функции dg_cli . Она определяет, куда отправлять дейтаграммы.

13-14 Создается сокет UDP и вызывается функция dg_cli .

8.6. Эхо-клиент UDP: функция dg_cli

В листинге 8.4 показана функция dg_cli , которая выполняет большую часть работы на стороне клиента.

Листинг 8.4 . Функция dg_cli: цикл обработки клиента

1 #include "unp.h"

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

8 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

9 n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

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

11 Fputs(recvline, stdout);

7-12 В цикле обработки на стороне клиента имеется четыре шага: чтение строки из стандартного потока ввода при помощи функции fgets , отправка строки серверу с помощью функции sendto , чтение отраженного ответа сервера с помощью функции recvfrom и помещение отраженной строки в стандартный поток вывода с помощью функции fputs .

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

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

Как и в случае функции сервера dg_echo , функция клиента dg_cli является не зависящей от протокола, но функция main клиента зависит от протокола. Функция main размещает в памяти и инициализирует структуру адреса сокета, относящегося к определенному типу протокола, а затем передает функции dg_cli указатель на структуру вместе с ее размером.

8.7. Потерянные дейтаграммы

Клиент и сервер UDP в нашем примере являются ненадежными. Если дейтаграмма клиента потеряна (допустим, она проигнорирована неким маршрутизатором между клиентом и сервером), клиент навсегда заблокируется в своем вызове функции recvfrom внутри функции dg_cli , ожидая от сервера ответа, который никогда не придет. Аналогично, если дейтаграмма клиента приходит к серверу, но ответ сервера потерян, клиент навсегда заблокируется в своем вызове функции recvfrom . Единственный способ предотвратить эту ситуацию - поместить тайм-аут в клиентский вызов функции recvfrom . Мы рассмотрим это в разделе 14.2.

Простое помещение тайм-аута в вызов функции recvfrom - еще не полное решение. Например, если заданное время ожидания истекло, а ответ не получен, мы не можем сказать точно, в чем дело - или наша дейтаграмма не дошла до сервера, или же ответ сервера не пришел обратно. Если бы запрос клиента содержал требование типа «перевести определенное количество денег со счета А на счет Б» (в отличие от случая с нашим простым эхо-сервером), то тогда между потерей запроса и потерей ответа существовала бы большая разница. Более подробно о добавлении надежности в модель клиент-сервер UDP мы расскажем в разделе 22.5.

8.8. Проверка полученного ответа

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

Сначала мы изменяем функцию клиента main (см. листинг 8.3) для работы со стандартным эхо-сервером (см. табл. 2.1). Мы просто заменяем присваивание

servaddr.sin_port = htons(SERV_PORT);

присваиванием

servaddr.sin_port = htons(7);

Теперь мы можем использовать с нашим клиентом любой узел, на котором работает стандартный эхо-сервер.

Затем мы переписываем функцию dg_cli , с тем чтобы она размещала в памяти другую структуру адреса сокета для хранения структуры, возвращаемой функцией recvfrom . Мы показываем ее в листинге 8.5.

Листинг 8.5 . Версия функции dg_cli, проверяющая возвращаемый адрес сокета

//udpcliserv/dgcliaddr.c

1 #include "unp.h"

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

6 char sendline, recvline;

7 socklen_t len;

8 struct sockaddr *preply_addr;

9 preply_addr = Malloc(servlen);

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

11 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

12 len = servlen;

13 n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);

14 if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) {

15 printf("reply from %s (ignored)\n",

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

19 Fputs(recvline, stdout);

Размещение другой структуры адреса сокета в памяти

9 Мы размещаем в памяти другую структуру адреса сокета при помощи функции malloc . Обратите внимание, что функция dg_cli все еще является не зависящей от протокола. Поскольку нам не важно, с каким типом структуры адреса сокета мы имеем дело, мы используем в вызове функции malloc только ее размер.

Сравнение возвращаемых адресов

12-13 В вызове функции recvfrom мы сообщаем ядру, что нужно возвратить адрес отправителя дейтаграммы. Сначала мы сравниваем длину, возвращаемую функцией recvfrom в аргументе типа «значение-результат», а затем сравниваем сами структуры адреса сокета при помощи функции memcmp .

Новая версия нашего клиента работает замечательно, если сервер находится на узле с одним единственным IP-адресом. Но эта программа может не сработать, если сервер имеет несколько сетевых интерфейсов (multihomed server). Запускаем эту программу, обращаясь к узлу freebsd4 , у которого имеется два интерфейса и два IP-адреса:

macosx % host freebsd4

freebsd4.unpbook.com has address 172.24.37.94

freebsd4.unpbook.com has address 135.197.17.100

macosx % udpcli02 135.197.17.100

reply from 172.24.37.94:7 (ignored)

По рис. 1.7 видно, что мы задали IP-адрес из другой подсети. Обычно это допустимо. Большинство реализаций IP принимают приходящую IP-дейтаграмму, предназначенную для любого из IP-адресов узла, независимо от интерфейса, на который она приходит . Документ RFC 1122 называет это моделью системы с гибкой привязкой (weak end system model). Если система должна реализовать то, что в этом документе называется моделью системы с жесткой привязкой (strong end system model), она принимает приходящую дейтаграмму, только если дейтаграмма приходит на тот интерфейс, которому она адресована.

IP-адрес, возвращаемый функцией recvfrom (IP-адрес отправителя дейтаграммы UDP), не является IP-адресом, на который мы посылали дейтаграмму. Когда сервер отправляет свой ответ, IP-адрес получателя - это адрес 172.24.37.94. Функция маршрутизации внутри ядра на узле freebsd4 выбирает адрес 172.24.37.94 в качестве исходящего интерфейса. Поскольку сервер не связал IP-адрес со своим сокетом (сервер связал со своим сокетом универсальный адрес, что мы можем проверить, запустив программу netstat на узле freebsd4), ядро выбирает адрес отправителя дейтаграммы IP. Этим адресом становится первичный IP-адрес исходящего интерфейса . Если мы отправляем дейтаграмму не на первичный IP-адрес интерфейса (то есть на альтернативное имя, псевдоним), то наша проверка, показанная в листинге 8.5, также окажется неудачной.

Одним из решений будет проверка клиентом доменного имени отвечающего узла вместо его IP-адреса. Для этого имя сервера ищется в DNS (см. главу 11) на основе IP-адреса, возвращаемого функцией recvfrom . Другое решение - сделать так, чтобы сервер UDP создал по одному сокету для каждого IP-адреса, сконфигурированного на узле, связал с помощью функции bind этот IP-адрес с сокетом, вызвал функцию select для каждого из всех этих сокетов (ожидая, когда какой-либо из них станет готов для чтения), а затем ответил с сокета, готового для чтения. Поскольку сокет, используемый для ответа, связан с IP-адресом, который являлся адресом получателя клиентского запроса (иначе дейтаграмма не была бы доставлена на сокет), мы можем быть уверены, что адреса отправителя ответа и получателя запроса совпадают. Мы показываем эти примеры в разделе 22.6.

ПРИМЕЧАНИЕ

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

8.9. Запуск клиента без запуска сервера

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

Сначала мы запускаем программу tcpdump на узле macosx , а затем - клиент на том же узле, задав в качестве узла сервера freebsd4. Потом мы вводим одну строку, но эта строка не отражается сервером.

macosx % udpcli01 172.24.37.94

hello, world мы вводим эту строку,

но ничего не получаем в ответ

В листинге 8.6 показан вывод программы tcpdump .

Листинг 8.6 . Вывод программы tcpdump, когда процесс сервера не запускается на узле сервера

01 0.0 arp who-has freebsd4 tell macosx

02 0.003576 (0.0036) arp reply freebsd4 is-at 0:40:5:42:d6:de

03 0.003601 (0.0000) macosx.51139 > freebsd4.9877: udp 13

04 0.009781 (0.0062) freebsd4 >

В первую очередь мы замечаем, что запрос и ответ ARP получены до того, как узел клиента смог отправить дейтаграмму UDP узлу сервера. (Мы оставили этот обмен в выводе программы, чтобы еще раз подчеркнуть, что до отправки IP-дейтаграммы всегда следует отправка запроса и получение ответа по протоколу ARP.)

В строке 3 мы видим, что дейтаграмма клиента отправлена, но узел сервера отвечает в строке 4 сообщением ICMP о недоступности порта. (Длина 13 включает 12 символов плюс символ новой строки.) Однако эта ошибка ICMP не возвращается клиентскому процессу по причинам, которые мы кратко перечислим чуть ниже. Вместо этого клиент навсегда блокируется в вызове функции recvfrom в листинге 8.4. Мы также отмечаем, что в ICMPv6 имеется ошибка «Порт недоступен», аналогичная ошибке ICMPv4 (см. табл. А.5 и А.6), поэтому результаты, представленные здесь, аналогичны результатам для IPv6.

Эта ошибка ICMP является асинхронной ошибкой. Ошибка была вызвана функцией sendto , но функция sendto завершилась нормально. Вспомните из раздела 2.9, что нормальное возвращение из операции вывода UDP означает только то, что дейтаграмма была добавлена к очереди вывода канального уровня. Ошибка ICMP не возвращается, пока не пройдет определенное количество времени (4 мс для листинга 8.6), поэтому она и называется асинхронной.

Основное правило состоит в том, что асинхронные ошибки не возвращаются для сокета UDP, если сокет не был присоединен. Мы показываем, как вызвать функцию connect для сокета UDP, в разделе 8.11. Не все понимают, почему было принято это решение, когда сокеты были впервые реализованы. (Соображения о реализациях обсуждаются на с. 748-749 .) Рассмотрим клиент UDP, последовательно отправляющий три дейтаграммы трем различным серверам (то есть на три различных IP-адреса) через один сокет UDP. Клиент входит в цикл, вызывающий функцию recvfrom для чтения ответов. Две дейтаграммы доставляются корректно (то есть сервер был запущен на двух из трех узлов), но на третьем узле не был запущен сервер, и третий узел отвечает сообщением ICMP о недоступности порта. Это сообщение об ошибке ICMP содержит IP-заголовок и UDP-заголовок дейтаграммы, вызвавшей ошибку. (Сообщения об ошибках ICMPv4 и ICMPv6 всегда содержат заголовок IP и весь заголовок UDP или часть заголовка TCP, чтобы дать возможность получателю сообщения определить, какой сокет вызвал ошибку. Это показано на рис. 28.5 и 28.6.) Клиент, отправивший три дейтаграммы, должен знать получателя дейтаграммы, вызвавшей ошибку, чтобы точно определить, какая из трех дейтаграмм вызвала ошибку. Но как ядро может сообщить эту информацию процессу? Единственное, что может возвратить функция recvfrom , - это значение переменной errno . Но функция recvfrom не может вернуть в ошибке IP-адрес и номер порта получателя UDP-дейтаграммы. Следовательно, было принято решение, что эти асинхронные ошибки возвращаются процессу, только если процесс присоединил сокет UDP лишь к одному определенному собеседнику.

ПРИМЕЧАНИЕ

Linux возвращает большинство ошибок ICMP о недоступности порта даже для неприсоединенного сокета, если не включен параметр сокета SO_DSBCOMPAT. Возвращаются все ошибки о недоступности получателя, показанные в табл. А.5, за исключением ошибок с кодами 0, 1, 4, 5, 11 и 12.

Мы вернемся к проблеме асинхронных ошибок с сокетами UDP в разделе 28.7 и покажем простой способ получения этих ошибок на неприсоединенном сокете при помощи нашего собственного демона.

8.10. Итоговый пример клиент-сервера UDP

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

Рис. 8.5 . Обобщение модели клиент-сервер UDP с точки зрения клиента

Клиент должен задать IP-адрес сервера и номер порта для вызова функции sendto . Обычно клиентский IP-адрес и номер порта автоматически выбираются ядром, хотя мы отмечали, что клиент может вызвать функцию bind . Мы также отмечали, что если эти два значения выбираются для клиента ядром, то динамически назначаемый порт клиента выбирается один раз - при первом вызове функции sendto , и более никогда не изменяется. Однако IP-адрес клиента может меняться для каждой дейтаграммы UDP, которую отправляет клиент, если предположить, что клиент не связывает с сокетом определенный IP-адрес при помощи функции bind . Причину объясняет рис. 8.5: если узел клиента имеет несколько сетевых интерфейсов, клиент может переключаться между ними (на рис. 8.5 один адрес относится к канальному уровню, изображенному слева, другой - к изображенному справа). В худшем варианте этого сценария IP-адрес клиента, выбираемый ядром на основе исходящего канального уровня, будет меняться для каждой дейтаграммы.

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

На рис. 8.6 представлены те же четыре значения, но с точки зрения сервера.

Рис. 8.6 . Обобщение модели клиент-сервер UDP с точки зрения сервера

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

Таблица 8.1 . Информация, доступная серверу из приходящей дейтаграммы IP

У сервера TCP всегда есть простой доступ ко всем четырем фрагментам информации для присоединенного сокета, и эти четыре значения остаются постоянными в течение всего времени жизни соединения. Однако в случае соединения UDP IP-адрес получателя можно получить только с помощью установки параметра сокета IP_RECVDSTADDR для IPv4 или IPV6_PKTINFO для IPv6 и последующего вызова функции recvmsg вместо функции recvfrom . Поскольку протокол UDP не ориентирован на установление соединения, IP-адрес получателя может меняться для каждой дейтаграммы, отправляемой серверу. Сервер UDP может также получать дейтаграммы, предназначенные для одного из широковещательных адресов узла или для адреса многоадресной передачи, что мы обсуждаем в главах 20 и 21. Мы покажем, как определить адрес получателя дейтаграммы UDP, в разделе 20.2, после того как опишем функцию recvmsg .

8.11. Функция connect для UDP
ПРИМЕЧАНИЕ
ПРИМЕЧАНИЕ
ПРИМЕЧАНИЕ

Таблица 8.2

ПРИМЕЧАНИЕ

Рис. 8.7 . Присоединенный сокет UDP

Рис. 8.8

Многократный вызов функции connect для сокета UDP

Процесс с присоединенным сокетом UDP может снова вызвать функцию connect Для этого сокета, чтобы:

в– задать новый IP-адрес и порт;

в– отсоединить сокет.

Первый случай, задание нового собеседника для присоединенного сокета UDP, отличается от использования функции connect с сокетом TCP: для сокета TCP функция connect может быть вызвана только один раз.

Чтобы отсоединить сокет UDP, мы вызываем функцию connect , но присваиваем элементу семейства структуры адреса сокета (sin_family для IPv4 или sin6_family для IPv6) значение AF_UNSPEC . Это может привести к ошибке EAFNOSUPPORT , но это нормально. Именно процесс вызова функции connect на уже присоединенном сокете UDP позволяет отсоединить сокет .

ПРИМЕЧАНИЕ

В руководстве BSD по поводу функции connect традиционно говорилось: «Сокеты дейтаграмм могут разрывать связь, соединяясь с недействительными адресами, такими как пустые адреса». К сожалению, ни в одном руководстве не сказано, что представляет собой «пустой адрес», и не упоминается, что в результате возвращается ошибка (что нормально). Стандарт POSIX явно указывает, что семейство адресов должно быть установлено в AF_UNSPEC, но затем сообщает, что этот вызов функции connect может возвратить, а может и не возвратить ошибку EAFNOSUPPORT.

Производительность

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

в– присоединение сокета;

в– вывод первой дейтаграммы;

в– отсоединение сокета;

в– присоединение сокета;

в– вывод второй дейтаграммы;

в– отсоединение сокета.

ПРИМЕЧАНИЕ

Другой момент, который нужно учитывать, - количество поисков в таблице маршрутизации. Первое временное соединение производит поиск в таблице маршрутизации IP-адреса получателя и сохраняет (кэширует) эту информацию. Второе временное соединение отмечает, что адрес получателя совпадает с кэшированным адресом из таблицы маршрутизации (мы считаем, что обеим функциям sendto задан один и тот же получатель), и ему не нужно снова проводить поиск в таблице маршрутизации .

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

в– присоединение сокета;

в– вывод первой дейтаграммы;

в– вывод второй дейтаграммы.

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

8.12. Функция dg_cli (продолжение)

Вернемся к функции dg_cli , показанной в листинге 8.4, и перепишем ее, с тем чтобы она вызывала функцию connect . В листинге 8.7 показана новая функция.

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

//udpcliserv/dgcliconnect.c

1 #include "unp.h"

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

6 char sendline, recvline;

7 Connect(sockfd, (SA*)pservaddr, servlen);

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

9 Write(sockfd, sendline, strlen(sendline));

10 n = Read(sockfd, recvline, MAXLINE);

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

12 Fputs(recvline, stdout);

Изменения по сравнению с предыдущей версией - это добавление вызова функции connect и замена вызовов функций sendto и recvfrom вызовами функций write и read . Функция dg_cli остается не зависящей от протокола, поскольку она не вникает в структуру адреса сокета, передаваемую функции connect . Наша функция main клиента, показанная в листинге 8.3, остается той же.

Если мы запустим программу на узле macosx , задав IP-адрес узла freebsd4 (который не запускает наш сервер на порте 9877), мы получим следующий вывод:

macosx % udpcli04 172.24.37.94

hello, world

read error: Connection refused

Первое, что мы замечаем, - мы не получаем ошибку, когда запускаем процесс клиента. Ошибка происходит только после того, как мы отправляем серверу первую дейтаграмму. Именно отправка этой дейтаграммы вызывает ошибку ICMP от узла сервера. Но когда клиент TCP вызывает функцию connect , задавая узел сервера, на котором не запущен процесс сервера, функция connect возвращает ошибку, поскольку вызов функции connect вызывает отправку первого пакета трехэтапного рукопожатия TCP, и именно этот пакет вызывает получение сегмента RST от собеседника (см. раздел 4.3).

В листинге 8.8 показан вывод программы tcpdump .

Листинг 8.8 . Вывод программы tcpdump при запуске функции dg_cli

macosx % tcpdump

01 0.0 macosx.51139 > freebsd4 9877:udp 13

02 0.006180 (0.0062) freebsd4 > macosx: icmp: freebsd4 udp port 9877 unreachable

В табл. A.5 мы также видим, что возникшую ошибку ICMP ядро сопоставляет ошибке ECONNREFUSED , которая соответствует выводу строки сообщения Connection refused (В соединении отказано) функцией err_sys .

ПРИМЕЧАНИЕ

К сожалению, не все ядра возвращают сообщения ICMP присоединенному сокету UDP, как мы показали в этом разделе. Обычно ядра реализаций, происходящих от Беркли, возвращают эту ошибку, а ядра System V - не возвращают. Например, если мы запустим тот же клиент на узле Solaris 2.4 и с помощью функции connect соединимся с узлом, на котором не запущен наш сервер, то с помощью программы tcpdump мы сможем убедиться, что ошибка ICMP о недоступности порта возвращается узлом сервера, но вызванная клиентом функция read никогда не завершается. Эта ситуация была исправлена в Solaris 2.5. UnixWare не возвращает ошибку, в то время как AIX, Digital Unix, HP-UX и Linux возвращают.

8.13. Отсутствие управления потоком в UDP

Листинг 8.9

//udpcliserv/dgcliloop1.c

1 #include "unp.h"

8 char sendline;

Листинг 8.10

//udpcliserv/dgecholoop1.c

1 #include "unp.h"

3 static int count;

7 socklen_t len;

8 char mesg;

11 len = clilen;

17 recvfrom_int(int signo)

Листинг 8.11 . Вывод на узле сервера

freebsd % netstat -s -p udp

71208 datagrams received

0 with incomplete header

0 with bad data length field

0 with bad checksum

0 with no checksum

832 dropped due to no socket

0 not for hashed pcb

137685 datagrams output

freebsd % udpserv06 запускаем наш сервер

клиент посылает дейтаграммы

^C

freebsd % netstat -s -р udp

73208 datagrams received

0 with incomplete header

0 with bad data length field

0 with bad checksum

0 with no checksum

832 dropped due to no socket

16 broadcast/multicast datagrams dropped due to no socket

0 not for hashed pcb

137685 datagrams output

aix % udpserv06

^?

received 2000 datagrams

Приемный буфер сокета UDP

Число дейтаграмм UDP, установленных в очередь UDP, для данного сокета ограничено размером его приемного буфера. Мы можем изменить его с помощью параметра сокета SO_RCVBUF , как мы показали в разделе 7.5. В FreeBSD по умолчанию размер приемного буфера сокета UDP равен 42 080 байт, что допускает возможность хранения только 30 из наших 1400-байтовых дейтаграмм. Если мы увеличим размер приемного буфера сокета, то можем рассчитывать, что сервер получит дополнительные дейтаграммы. В листинге 8.12 представлена измененная функция dg_echo из листинга 8.10, которая увеличивает размер приемного буфера сокета до 240 Кбайт. Если мы запустим этот сервер в системе Sun, а клиент - в системе RS/6000, то счетчик полученных дейтаграмм будет иметь значение 103. Поскольку это лишь немногим лучше, чем в предыдущем примере с размером буфера, заданным по умолчанию, ясно, что мы пока не получили решения проблемы.

Листинг 8.12 . Функция dg_echo, увеличивающая размер приемного буфера сокета

//udpcliserv/dgecholоор2.c

1 #include "unp.h"

2 static void recvfrom_int(int);

3 static int count;

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

8 socklen_t len;

9 char mesg;

10 Signal(SIGINT, recvfrom_int);

11 n = 240 * 1024;

12 Setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

14 len = clilen;

15 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

20 recvfrom_int(int signo)

22 printf("\nreceived %d datagrams\n", count);

ПРИМЕЧАНИЕ

Почему мы устанавливаем размер буфера приема сокета равным 240Г-1024 байт в листинге 8.12? Максимальный размер приемного буфера сокета в BSD/OS 2.1 по умолчанию равен 262 144 байта (256Г-1024), но из-за способа размещения буфера в памяти (описанного в главе 2 ) он в действительности ограничен до 246 723 байт. Многие более ранние системы, основанные на 4.3BSD, ограничивали размер буфера приема сокета примерно до 52 000 байт.

8.14. Определение исходящего интерфейса для UDP

С помощью присоединенного сокета UDP можно также задавать исходящий интерфейс, который будет использован для отправки дейтаграмм к определенному получателю. Это объясняется побочным эффектом функции connect , примененной к сокету UDP: ядро выбирает локальный IP-адрес (предполагается, что процесс еще не вызвал функцию bind для явного его задания). Локальный адрес выбирается в процессе поиска адреса получателя в таблице маршрутизации, причем берется основной IP-адрес интерфейса, с которого, согласно таблице, будут отправляться дейтаграммы.

В листинге 8.13 показана простая программа UDP, которая с помощью функции connect соединяется с заданным IP-адресом и затем вызывает функцию getsockname , выводя локальный IP-адрес и порт.

Листинг 8.13 . Программа UDP, использующая функцию connect для определения исходящего интерфейса

//udpcliserv/udpcli09.c

1 #include "unp.h"

3 main(int argc, char **argv)

6 socklen_t len;

7 struct sockaddr_in cliaddr, servaddr;

8 if (argc != 2)

9 err_quit("usage: udpcli ");

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

11 bzero(&servaddr, sizeof(servaddr));

12 servaddr.sin_family = AF_INET;

13 servaddr.sin_port = htons(SERV_PORT);

14 Inet_pton(AF_INET, argv, &servaddr.sin_addr);

15 Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));

16 len = sizeof(cliaddr);

17 Getsockname(sockfd, (SA*)&cliaddr, &len);

18 printf("local address %s\n", Sock_ntop((SA*)&cliaddr, len));

Если мы запустим программу на узле freebsd с несколькими сетевыми интерфейсами, то получим следующий вывод:

freebsd % udpcli09 206.168.112.96

local address 12.106.32.254:52329

freebsd % udpcli09 192.168.42.2

local address 192.168.42.1:52330

freebsd % udpcli09 127.0.0.1

local address 127.0.0.1:52331

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

ПРИМЕЧАНИЕ

К сожалению, эта технология действует не во всех реализациях, что особенно касается ядер, происходящих от SVR4. Например, это не работает в Solaris 2.5, но работает в AIX, Digital Unix, Linux, MacOS X и Solaris 2.6.

8.15. Эхо-сервер TCP и UDP, использующий функцию select

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

Листинг 8.14 . Первая часть эхо-сервера, обрабатывающего сокеты TCP и UDP при помощи функции select

//udpcliserv/udpservselect01.c

1 #include "unp.h"

3 main(int argc, char **argv)

5 int listenfd, connfd, udpfd, nready, maxfdp1;

6 char mesg;

7 pid_t childpid;

10 socklen_t len;

11 const int on = 1;

12 struct sockaddr_in cliaddr, servaddr;

13 void sig_chld(int);

14 /* создание прослушиваемого сокета TCP */

15 listenfd = Socket(AF_INET, SOCK_STREAM, 0);

16 bzero(&servaddr, sizeof(servaddr));

17 servaddr.sin_family = AF_INET;

18 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

19 servaddr.sin_port = htons(SERV_PORT);

20 Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

21 Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));

22 Listen(listenfd, LISTENQ);

23 /* создание сокета UDP */

24 udpfd = Socket(AF_INET, SOCK_DGRAM, 0);

25 bzero(&servaddr, sizeof(servaddr));

26 servaddr.sin_family = AF_INET;

27 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

28 servaddr.sin_port = htons(SERV_PORT);

29 Bind(udpfd, (SA*)&servaddr, sizeof(servaddr));

Создание прослушиваемого сокета TCP

14-22 Создается прослушиваемый сокет TCP, который связывается с заранее известным портом сервера. Мы устанавливаем параметр сокета SO_REUSEADDR в случае, если на этом порте существуют соединения.

Создание сокета UDP

23-29 Также создается сокет UDP и связывается с тем же портом. Даже если один и тот же порт используется для сокетов TCP и UDP, нет необходимости устанавливать параметр сокета SO_REUSEADDR перед этим вызовом функции bind , поскольку порты TCP не зависят от портов UDP.

В листинге 8.15 показана вторая часть нашего сервера.

Листинг 8.15 . Вторая половина эхо-сервера, обрабатывающего TCP и UDP при помощи функции select

udpcliserv/udpservselect01.c

30 Signal(SIGCHLD, sig_chld); /* требуется вызвать waitpid() */

31 FD_ZERO(&rset);

32 maxfdp1 = max(listenfd, udpfd) + 1;

34 FD_SET(listenfd, &rset);

35 FD_SET(udpfd, &rset);

36 if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL))

37 if (errno == EINTR)

38 continue; /* назад в for() */

40 err_sys("select error");

42 if (FD_ISSET(listenfd, &rset)) {

43 len = sizeof(cliaddr);

44 connfd = Accept(listenfd, (SA*)&cliaddr, &len);

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

46 Close(listenfd); /* закрывается прослушиваемый сокет */

47 str_echo(connfd); /* обработка запроса */

50 Close(connfd); /* родитель закрывает присоединенный сокет */

52 if (FD_ISSET(udpfd, &rset)) {

53 len = sizeof(cliaddr);

54 n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA*)&cliaddr, &len);

55 Sendto(udpfd, mesg, n, 0, (SA*)&cliaddr, len);

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

30 Для сигнала SIGCHLD устанавливается обработчик, поскольку соединения TCP будут обрабатываться дочерним процессом. Этот обработчик сигнала мы показали в листинге 5.8.

Подготовка к вызову функции select

31-32 Мы инициализируем набор дескрипторов для функции select и вычисляем максимальный из двух дескрипторов, готовности которого будем ожидать.

Вызов функции select

34-41 Мы вызываем функцию select , ожидая только готовности к чтению прослушиваемого сокета TCP или сокета UDP. Поскольку наш обработчик сигнала sig_chld может прервать вызов функции select , обрабатываем ошибку EINTR .

Обработка нового клиентского соединения

42-51 С помощью функции accept мы принимаем новое клиентское соединение, а когда прослушиваемый сокет TCP готов для чтения, с помощью функции fork порождаем дочерний процесс и вызываем нашу функцию str_echo в дочернем процессе. Это та же последовательность действий, которую мы выполняли в главе 5.

Обработка приходящей дейтаграммы

52-57 Если сокет UDP готов для чтения, дейтаграмма пришла. Мы читаем ее с помощью функции recvfrom и отправляем обратно клиенту с помощью функции sendto .

8.16. Резюме

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

Сокеты UDP могут генерировать асинхронные ошибки, то есть ошибки, о которых сообщается спустя некоторое время после того, как пакет был отправлен. Сокеты TCP всегда сообщают приложению о них, но в случае UDP для получения этих ошибок сокет должен быть присоединенным.

В UDP отсутствует возможность управления потоком, что очень легко продемонстрировать. Обычно это не создает проблем, поскольку многие приложения UDP построены с использованием модели «запрос-ответ» и не предназначены для передачи большого количества данных.

Есть еще ряд моментов, которые нужно учитывать при написании приложений UDP, но мы рассмотрим их в главе 22 после описания функций интерфейсов, широковещательной и многоадресной передачи.

Упражнения

1. Допустим, у нас имеется два приложения, одно использует TCP, а другое - UDP. В приемном буфере сокета TCP находится 4096 байт данных, а в приемном буфере для сокета UDP - две дейтаграммы по 2048 байт. Приложение TCP вызывает функцию read с третьим аргументом 4096, а приложение UDP вызывает функцию recvfrom с третьим аргументом 4096. Есть ли между этими вызовами какая-нибудь разница?

2. Что произойдет в листинге 8.2, если мы заменим последний аргумент функции sendto (который мы обозначили len) аргументом clilen ?

3. Откомпилируйте и запустите сервер UDP из листингов 8.1 и 8.4, а затем - клиент из листингов 8.3 и 8.4. Убедитесь в том, что клиент и сервер работают вместе.

4. Запустите программу ping в одном окне, задав параметр -i 60 (отправка одного пакета каждые 60 секунд; некоторые системы используют ключ I вместо i), параметр -v (вывод всех полученных сообщений об ошибках ICMP) и задав адрес закольцовки на себя (обычно 127.0.0.1). Мы будем использовать эту программу, чтобы увидеть ошибку ICMP недоступности порта, возвращаемую узлом сервера. Затем запустите наш клиент из предыдущего упражнения в другом окне, задав IP-адрес некоторого узла, на котором не запущен сервер. Что происходит?

5. Рассматривая рис. 8.3, мы сказали, что каждый присоединенный сокет TCP имеет свой собственный буфер приема. Как вы думаете, есть ли у прослушиваемого сокета свой собственный буфер приема?

6. Используйте программу sock (см. раздел В.3) и такое средство, как, например, tcpdump (см. раздел В.5), чтобы проверить утверждение из раздела 8.10: если клиент с помощью функции bind связывает IP-адрес со своим сокетом, но отправляет дейтаграмму, исходящую от другого интерфейса, то результирующая дейтаграмма содержит IP-адрес, который был связан с сокетом, даже если он не соответствует исходящему интерфейсу.

7. Откомпилируйте программы из раздела 8.13 и запустите клиент и сервер на различных узлах. Помещайте printf в клиент каждый раз, когда дейтаграмма записывается в сокет. Изменяет ли это процент полученных пакетов? Почему? Вызывайте printf из сервера каждый раз, когда дейтаграмма читается из сокета. Изменяет ли это процент полученных пакетов? Почему?

8. Какова наибольшая длина, которую мы можем передать функции sendto для сокета UDP/IPv4, то есть каково наибольшее количество данных, которые могут поместиться в дейтаграмму UDP/IPv4? Что изменяется в случае UDP/IPv6?

Измените листинг 8.4, с тем чтобы отправить одну дейтаграмму UDP максимального размера, считать ее обратно и вывести число байтов, возвращаемых функцией recvfrom .

9. Измените листинг 8.15 таким образом, чтобы он соответствовал RFC 1122: для сокета UDP следует использовать параметр IP_RECVDSTADDR .

В конце разделе 8.9 мы упомянули, что асинхронные ошибки не возвращаются на сокете UDP, если сокет не был присоединен. На самом деле мы можем вызвать функцию connect для сокета UDP (см. раздел 4.3). Но это не приведет ни к чему похожему на соединение TCP: здесь не существует трехэтапного рукопожатия. Ядро просто проверяет, нет ли сведений о заведомой недоступности адресата, после чего записывает IP-адрес и номер порта собеседника, которые содержатся в структуре адреса сокета, передаваемой функции connect, и немедленно возвращает управление вызывающему процессу.

ПРИМЕЧАНИЕ

Перегрузка функции connect этой новой возможностью для сокетов UDP может внести путаницу. Если используется соглашение о том, что sockname - это адрес локального протокола, a peername - адрес удаленного протокола, то лучше бы эта функция называлась setpeername. Аналогично, функции bind больше подошло бы название setsockname.

С учетом этого необходимо понимать разницу между двумя видами сокетов UDP.

в– Неприсоединенный (unconnected) сокет UDP - это сокет UDP, создаваемый по умолчанию.

в– Присоединенный {connected) сокет UDP - результат вызова функции connect для сокета UDP.

Присоединенному сокету UDP свойственны три отличия от неприсоединенного сокета, который создается по умолчанию.

1. Мы больше не можем задавать IP-адрес получателя и порт для операции вывода. То есть мы используем вместо функции sendto функцию write или send . Все, что записывается в присоединенный сокет UDP, автоматически отправляется на адрес (например, IP-адрес и порт), заданный функцией connect .

ПРИМЕЧАНИЕ

Аналогично TCP, мы можем вызвать функцию sendto для присоединенного сокета UDP, но не можем задать адрес получателя. Пятый аргумент функции sendto (указатель на структуру адреса сокета) должен быть пустым указателем, а шестой аргумент (размер структуры адреса сокета) должен быть нулевым. В стандарте POSIX определено, что когда пятый аргумент является пустым указателем, шестой аргумент игнорируется.

2. Вместо функции recvfrom мы используем функцию read или recv . Единственные дейтаграммы, возвращаемые ядром для операции ввода через присоединенный сокет UDP, - это дейтаграммы, приходящие с адреса, заданного в функции connect . Дейтаграммы, предназначенные для адреса локального протокола присоединенного сокета UDP (например, IP-адрес и порт), но приходящие с адреса протокола, отличного от того, к которому сокет был присоединен с помощью функции connect , не передаются присоединенному сокету. Это ограничивает присоединенный сокет UDP, позволяя ему обмениваться дейтаграммами с одним и только одним собеседником.

ПРИМЕЧАНИЕ

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

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

В табл. 8.2 сводятся воедино свойства, перечисленные в первом пункте, применительно к 4.4BSD.

Таблица 8.2 . Сокеты TCP и UDP: может ли быть задан адрес протокола получателя

ПРИМЕЧАНИЕ

POSIX определяет, что операция вывода, не задающая адрес получателя на неприсоединенном сокете UDP, должна возвращать ошибку ENOTCONN, а не EDESTADDRREQ.

Solaris 2.5 допускает функцию sendto, которая задает адрес получателя для присоединенного сокета UDP. POSIX определяет, что в такой ситуации должна возвращаться ошибка EISCONN.

На рис. 8.7 обобщается информация о присоединенном сокете UDP.

Рис. 8.7 . Присоединенный сокет UDP

Приложение вызывает функцию connect , задавая IP-адрес и номер порта собеседника. Затем оно использует функции read и write для обмена данными с собеседником.

Дейтаграммы, приходящие с любого другого IP-адреса или порта (который мы обозначаем как «???» на рис. 8.7), не передаются на присоединенный сокет, поскольку либо IP-адрес, либо UDP-порт отправителя не совпадают с адресом протокола, с которым сокет соединяется с помощью функции connect . Эти дейтаграммы могут быть доставлены на какой-то другой сокет UDP на узле. Если нет другого совпадающего сокета для приходящей дейтаграммы, UDP проигнорирует ее и сгенерирует ICMP-сообщение о недоступности порта.

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

Еще один пример долгосрочного взаимодействия - это DNS (рис. 8.8).

Рис. 8.8 . Пример клиентов и серверов DNS и функции connect

Клиент DNS может быть сконфигурирован для использования одного или более серверов, обычно с помощью перечисления IP-адресов серверов в файле /etc/resolv.conf . Если в этом файле указан только один сервер (на рисунке этот клиент изображен в крайнем слева прямоугольнике), клиент может вызвать функцию connect, но если перечислено множество серверов (второй справа прямоугольник на рисунке), клиент не может вызвать функцию connect . Обычно сервер DNS обрабатывает также любые клиентские запросы, следовательно, серверы не могут вызывать функцию connect .

Теперь мы проверим, как влияет на работу приложения отсутствие какого-либо управления потоком в UDP. Сначала мы изменим нашу функцию dg_cli так, чтобы она отправляла фиксированное число дейтаграмм. Она больше не будет читать из стандартного потока ввода. В листинге 8.9 показана новая версия функции. Эта функция отправляет серверу 2000 дейтаграмм UDP по 1400 байт каждая.

Листинг 8.9 . Функция dg_cli, отсылающая фиксированное число дейтаграмм серверу

//udpcliserv/dgcliloop1.c

1 #include "unp.h"

2 #define NDG 2000 /* количество дейтаграмм для отправки */

3 #define DGLEN 1400 /* длина каждой дейтаграммы */

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

8 char sendline;

10 Sendto(sockfd, sendline, DGLEN, 0, pservaddr, servlen);

Затем мы изменяем сервер так, чтобы он получал дейтаграммы и считал число полученных дейтаграмм. Сервер больше не отражает дейтаграммы обратно клиенту. В листинге 8.10 показана новая функция dg_echo . Когда мы завершаем процесс сервера нажатием клавиши прерывания на терминале (что приводит к отправке сигнала SIGINT процессу), сервер выводит число полученных дейтаграмм и завершается.

Листинг 8.10 . Функция dg_echo, считающая полученные дейтаграммы

//udpcliserv/dgecholoop1.c

1 #include "unp.h"

2 static void recvfrom_int(int);

3 static int count;

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

7 socklen_t len;

8 char mesg;

9 Signal (SIGINT, recvfrom_int);

11 len = clilen;

12 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

17 recvfrom_int(int signo)

19 printf("\nreceived %d datagrams\n", count);

Теперь мы запускаем сервер на узле freebsd , который представляет собой медленный компьютер SPARCStation. Клиент мы запускаем в значительно более быстрой системе RS/6000 с операционной системой aix . Они соединены друг с другом напрямую каналом Ethernet на 100 Мбит/с. Кроме того, мы запускаем программу netstat -s на узле сервера и до, и после запуска клиента и сервера, поскольку выводимая статистика покажет, сколько дейтаграмм мы потеряли. В листинге 8.11 показан вывод сервера.

Листинг 8.11 . Вывод на узле сервера

freebsd % netstat -s -p udp

71208 datagrams received

0 with incomplete header

0 with bad data length field

0 with bad checksum

0 with no checksum

832 dropped due to no socket

16 broadcast/multicast datagrams dropped due to no socket

1971 dropped due to full socket buffers

0 not for hashed pcb

137685 datagrams output

freebsd % udpserv06 запускаем наш сервер

клиент посылает дейтаграммы

^C для окончания работы клиента вводим наш символ прерывания

freebsd % netstat -s -р udp

73208 datagrams received

0 with incomplete header

0 with bad data length field

0 with bad checksum

0 with no checksum

832 dropped due to no socket

16 broadcast/multicast datagrams dropped due to no socket

3941 dropped due to full socket buffers

0 not for hashed pcb

137685 datagrams output

Клиент отправил 2000 дейтаграмм, но приложение-сервер получило только 30 из них, что означает уровень потерь 98%. Ни сервер, ни клиент не получают сообщения о том, что эти дейтаграммы потеряны. Как мы и говорили, UDP не имеет возможности управления потоком - он ненадежен. Как мы показали, для отправителя UDP не составляет труда переполнить буфер получателя.

Если мы посмотрим на вывод программы netstat , то увидим, что общее число дейтаграмм, полученных узлом сервера (не приложением-сервером) равно 2000 (73 208 – 71 208). Счетчик dropped due to full socket buffers (отброшено из-за переполнения буферов сокета) показывает, сколько дейтаграмм было получено UDP и проигнорировано из-за того, что приемный буфер принимающего сокета был полон . Это значение равно 1970 (3941 – 1971), что при добавлении к выводу счетчика дейтаграмм, полученных приложением (30), дает 2000 дейтаграмм, полученных узлом. К сожалению, счетчик дейтаграмм, отброшенных из-за заполненного буфера, в программе netstat распространяется на всю систему. Не существует способа определить, на какие приложения (например, какие порты UDP) это влияет.

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

Если мы запустим тот же клиент и тот же сервер, но на этот раз клиент на медленной системе Sun, а сервер на быстрой системе RS/6000, никакие дейтаграммы не теряются.

aix % udpserv06

^? после окончания работы клиента вводим наш символ прерывания

received 2000 datagrams

В конце разделе 8.9 мы упомянули, что асинхронные ошибки не возвращаются на сокете UDP, если сокет не был присоединен. На самом деле мы можем вызвать функцию connect для сокета UDP (см. раздел 4.3). Но это не приведет ни к чему похожему на соединение TCP: здесь не существует трехэтапного рукопожатия. Ядро просто проверяет, нет ли сведений о заведомой недоступности адресата, после чего записывает IP-адрес и номер порта собеседника, которые содержатся в структуре адреса сокета, передаваемой функции connect, и немедленно возвращает управление вызывающему процессу.

ПРИМЕЧАНИЕ

Перегрузка функции connect этой новой возможностью для сокетов UDP может внести путаницу. Если используется соглашение о том, что sockname - это адрес локального протокола, a peername - адрес удаленного протокола, то лучше бы эта функция называлась setpeername. Аналогично, функции bind больше подошло бы название setsockname.

С учетом этого необходимо понимать разницу между двумя видами сокетов UDP.

в– Неприсоединенный (unconnected) сокет UDP - это сокет UDP, создаваемый по умолчанию.

в– Присоединенный {connected) сокет UDP - результат вызова функции connect для сокета UDP.

Присоединенному сокету UDP свойственны три отличия от неприсоединенного сокета, который создается по умолчанию.

1. Мы больше не можем задавать IP-адрес получателя и порт для операции вывода. То есть мы используем вместо функции sendto функцию write или send . Все, что записывается в присоединенный сокет UDP, автоматически отправляется на адрес (например, IP-адрес и порт), заданный функцией connect .

ПРИМЕЧАНИЕ

Аналогично TCP, мы можем вызвать функцию sendto для присоединенного сокета UDP, но не можем задать адрес получателя. Пятый аргумент функции sendto (указатель на структуру адреса сокета) должен быть пустым указателем, а шестой аргумент (размер структуры адреса сокета) должен быть нулевым. В стандарте POSIX определено, что когда пятый аргумент является пустым указателем, шестой аргумент игнорируется.

2. Вместо функции recvfrom мы используем функцию read или recv . Единственные дейтаграммы, возвращаемые ядром для операции ввода через присоединенный сокет UDP, - это дейтаграммы, приходящие с адреса, заданного в функции connect . Дейтаграммы, предназначенные для адреса локального протокола присоединенного сокета UDP (например, IP-адрес и порт), но приходящие с адреса протокола, отличного от того, к которому сокет был присоединен с помощью функции connect , не передаются присоединенному сокету. Это ограничивает присоединенный сокет UDP, позволяя ему обмениваться дейтаграммами с одним и только одним собеседником.

ПРИМЕЧАНИЕ

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

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

В табл. 8.2 сводятся воедино свойства, перечисленные в первом пункте, применительно к 4.4BSD.

Таблица 8.2 . Сокеты TCP и UDP: может ли быть задан адрес протокола получателя

ПРИМЕЧАНИЕ

POSIX определяет, что операция вывода, не задающая адрес получателя на неприсоединенном сокете UDP, должна возвращать ошибку ENOTCONN, а не EDESTADDRREQ.

Solaris 2.5 допускает функцию sendto, которая задает адрес получателя для присоединенного сокета UDP. POSIX определяет, что в такой ситуации должна возвращаться ошибка EISCONN.

На рис. 8.7 обобщается информация о присоединенном сокете UDP.

Рис. 8.7 . Присоединенный сокет UDP

Приложение вызывает функцию connect , задавая IP-адрес и номер порта собеседника. Затем оно использует функции read и write для обмена данными с собеседником.

Дейтаграммы, приходящие с любого другого IP-адреса или порта (который мы обозначаем как «???» на рис. 8.7), не передаются на присоединенный сокет, поскольку либо IP-адрес, либо UDP-порт отправителя не совпадают с адресом протокола, с которым сокет соединяется с помощью функции connect . Эти дейтаграммы могут быть доставлены на какой-то другой сокет UDP на узле. Если нет другого совпадающего сокета для приходящей дейтаграммы, UDP проигнорирует ее и сгенерирует ICMP-сообщение о недоступности порта.

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

Еще один пример долгосрочного взаимодействия - это DNS (рис. 8.8).

Рис. 8.8 . Пример клиентов и серверов DNS и функции connect

Клиент DNS может быть сконфигурирован для использования одного или более серверов, обычно с помощью перечисления IP-адресов серверов в файле /etc/resolv.conf . Если в этом файле указан только один сервер (на рисунке этот клиент изображен в крайнем слева прямоугольнике), клиент может вызвать функцию connect, но если перечислено множество серверов (второй справа прямоугольник на рисунке), клиент не может вызвать функцию connect . Обычно сервер DNS обрабатывает также любые клиентские запросы, следовательно, серверы не могут вызывать функцию connect .

Теперь мы проверим, как влияет на работу приложения отсутствие какого-либо управления потоком в UDP. Сначала мы изменим нашу функцию dg_cli так, чтобы она отправляла фиксированное число дейтаграмм. Она больше не будет читать из стандартного потока ввода. В листинге 8.9 показана новая версия функции. Эта функция отправляет серверу 2000 дейтаграмм UDP по 1400 байт каждая.

Листинг 8.9 . Функция dg_cli, отсылающая фиксированное число дейтаграмм серверу

//udpcliserv/dgcliloop1.c

1 #include "unp.h"

2 #define NDG 2000 /* количество дейтаграмм для отправки */

3 #define DGLEN 1400 /* длина каждой дейтаграммы */

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

8 char sendline;

10 Sendto(sockfd, sendline, DGLEN, 0, pservaddr, servlen);

Затем мы изменяем сервер так, чтобы он получал дейтаграммы и считал число полученных дейтаграмм. Сервер больше не отражает дейтаграммы обратно клиенту. В листинге 8.10 показана новая функция dg_echo . Когда мы завершаем процесс сервера нажатием клавиши прерывания на терминале (что приводит к отправке сигнала SIGINT процессу), сервер выводит число полученных дейтаграмм и завершается.

Листинг 8.10 . Функция dg_echo, считающая полученные дейтаграммы

//udpcliserv/dgecholoop1.c

1 #include "unp.h"

2 static void recvfrom_int(int);

3 static int count;

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

7 socklen_t len;

8 char mesg;

9 Signal (SIGINT, recvfrom_int);

11 len = clilen;

12 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

17 recvfrom_int(int signo)

19 printf("\nreceived %d datagrams\n", count);

Теперь мы запускаем сервер на узле freebsd , который представляет собой медленный компьютер SPARCStation. Клиент мы запускаем в значительно более быстрой системе RS/6000 с операционной системой aix . Они соединены друг с другом напрямую каналом Ethernet на 100 Мбит/с. Кроме того, мы запускаем программу netstat -s на узле сервера и до, и после запуска клиента и сервера, поскольку выводимая статистика покажет, сколько дейтаграмм мы потеряли. В листинге 8.11 показан вывод сервера.

Листинг 8.11 . Вывод на узле сервера

freebsd % netstat -s -p udp

71208 datagrams received

0 with incomplete header

0 with bad data length field

0 with bad checksum

0 with no checksum

832 dropped due to no socket

16 broadcast/multicast datagrams dropped due to no socket

1971 dropped due to full socket buffers

0 not for hashed pcb

137685 datagrams output

freebsd % udpserv06 запускаем наш сервер

клиент посылает дейтаграммы

^C для окончания работы клиента вводим наш символ прерывания

freebsd % netstat -s -р udp

73208 datagrams received

0 with incomplete header

0 with bad data length field

0 with bad checksum

0 with no checksum

832 dropped due to no socket

16 broadcast/multicast datagrams dropped due to no socket

3941 dropped due to full socket buffers

0 not for hashed pcb

137685 datagrams output

Клиент отправил 2000 дейтаграмм, но приложение-сервер получило только 30 из них, что означает уровень потерь 98%. Ни сервер, ни клиент не получают сообщения о том, что эти дейтаграммы потеряны. Как мы и говорили, UDP не имеет возможности управления потоком - он ненадежен. Как мы показали, для отправителя UDP не составляет труда переполнить буфер получателя.

Если мы посмотрим на вывод программы netstat , то увидим, что общее число дейтаграмм, полученных узлом сервера (не приложением-сервером) равно 2000 (73 208 – 71 208). Счетчик dropped due to full socket buffers (отброшено из-за переполнения буферов сокета) показывает, сколько дейтаграмм было получено UDP и проигнорировано из-за того, что приемный буфер принимающего сокета был полон . Это значение равно 1970 (3941 – 1971), что при добавлении к выводу счетчика дейтаграмм, полученных приложением (30), дает 2000 дейтаграмм, полученных узлом. К сожалению, счетчик дейтаграмм, отброшенных из-за заполненного буфера, в программе netstat распространяется на всю систему. Не существует способа определить, на какие приложения (например, какие порты UDP) это влияет.

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

Если мы запустим тот же клиент и тот же сервер, но на этот раз клиент на медленной системе Sun, а сервер на быстрой системе RS/6000, никакие дейтаграммы не теряются.

aix % udpserv06

^? после окончания работы клиента вводим наш символ прерывания

received 2000 datagrams