34: fprintf(stderr, "сбой поиска имени хоста: %s\n",
35: gai_strerror(rc));
36: return 1;
37: }
38:
39: /* это позволяет получить доступ к sin_family и sin_port
40: (которые расположены там же, где и sin6_family и sin6_port) */
41: addrinfo = (struct sockaddr_in *) addr->ai_addr;
42:
43: if ((sock = socket(addrInfo->sin_family, addr->ai_socktype,
44: addr->ai_protocol)) < 0)
45: die("socket");
46:
47: addrInfo->sin_port = htons(4321);
48:
49: if (connect(sock, (struct sockaddr *) addrinfo,
50: addr->ai_addrlen))
51: die("connect");
52:
53: freeaddrinfo(addr);
54:
55: copyData(0, sock);
56:
57: close(sock);
58:
59: return 0;
60: }
17.6. Использование дейтаграмм UDP
Наряду с тем, что большинство приложений пользуются преимуществами потокового протокола TCP, некоторые предпочитают применять UDP. Давайте рассмотрим несколько причин, по которым дейтаграммная модель без установления соединений, предоставляемая UDP, может оказаться весьма полезной.
• Протоколы без соединений обрабатывают перезапуски машин более плавно, поскольку нет необходимости в переустановке соединений. Это очень заманчивое свойство для сетевых файловых систем (таких как NFS, действующей на основе UDP), поскольку оно позволяет перезапускать файловый сервер без уведомления клиента.
• Простейшие протоколы могут работать гораздо быстрее через дейтаграммный протокол. Служба имен доменов DNS использует UDP только по этой причине (несмотря на то что наряду с этим дополнительно поддерживается TCP). При установке соединения TCP клиентская машина отправляет сообщение на сервер, получает от сервера подтверждение, указывающее на активность соединения, затем сообщает серверу о том, что установлена клиентская сторона соединения[137]23. После этого клиент может отправить свой запрос имени хоста на взаимодействующий сервер. Все это в итоге составляет процесс из пяти сообщений, не считая проверки ошибок и ожидания фактического отправления запроса и ответа на него. Используя UDP, запросы имени хоста пересылаются как первый пакет на сервер, который отвечает одним или более UDP-пакетами, тем самым уменьшая общий счетчик пакетов до пяти. Если клиент не получает ответ, то он просто перепосылает запрос.
• При первичной установке компьютеров часто требуется установить для них IP-адрес, а затем загрузить первую часть операционной системы через сеть[138]. Применение UDP для подобных операций создает набор протоколов, который внедряется в такие машины гораздо проще, чем, если бы требовалась полная TCP-реализация.
17.6.1. Создание UDP-сокета
Как и любой другой сокет, UDP-сокет создается с помощью функции socket()
, однако второй аргумент должен быть SOCK_DGRAM
, а последний — либо IPPROTO_UDP
, либо просто ноль (так как UDP является стандартным IP-дейтаграммным протоколом).
После создания сокета ему необходимо присвоить номер локального порта. Это происходит тогда, когда программа удовлетворяет одному из следующих трех условий.
• Номер порта задается явно через вызов функции bind(). Этот шаг является обязательным для тех серверов, для которых необходимо получение дейтаграмм на номер официального порта. Системный вызов в точности совпадает с системным вызовом для TCP-серверов.
• Дейтаграмма посылается через сокет. Ядро присваивает данному сокету номер порта UDP при первой передаче данных через него. В большинстве клиентских программ применяется именно этот прием, поскольку номер используемого порта для них не имеет значения.
• Для сокета устанавливается удаленный адрес через функцию connect()
(которая является дополнительной для UDP-сокетов).
Также существует два различных способа присвоения номера удаленного порта. Вспомните о том, что TCP-сокеты имеют удаленный адрес, который присваивается через connect()
. Этот адрес может использоваться и для UDP-сокетов[139]. При этом функция connect()
для TCP вызывает обмен пакетами для инициализации соединения (что делает connect()
медленным системным вызовом), в то время как вызов connect()
для UDP-сокетов просто присваивает удаленный IP-адрес и номер порта для исходящих дейтаграмм (и является быстрым системным вызовом). Еще одно различие состоит в том, что приложения могут подключаться к TCP-сокету только один раз; UDP-сокеты могут повторно использовать свои адреса назначения[140].
Преимущество использования подключенных UDP-сокетов состоит в том, что только та машина и порт, которые указаны как удаленный адрес для сокета, могут передавать дейтаграммы в данный сокет. Произвольный IP-адрес и порт может посылать дейтаграммы в неподключенный UDP-сокет, который требуется в некоторых случаях (именно через него новые клиенты впервые связываются с серверами), однако при этом программы должны отслеживать место отправки дейтаграмм.
17.6.2. Отправка и получение дейтаграмм
Для отправки и получения UDP-пакетов обычно используются четыре системных вызова[141]: send()
, sendto()
, recv()
, recvfrom()
[142].
#include <sys/types.h>
#include <sys/sockets.h>
int send(int s, const void * data, size_t len, int flags);
int sendto(int s, const void * data, size_t len, int flags,
const struct sockaddr * to, socklen_t toLen);
int recv(int s, void * data, size_t maxlen, int flags);
int recvfrom(int s, void * data, size_t maxlen, int flags,
struct sockaddr * from, socklen_t * fromLen);
Здесь во всех случаях параметр flags
всегда равен нулю. В других ситуациях он может принимать множество значений, они подробно рассматриваются в [33].
Первый из названных вызовов send()
может применяться только для тех сокетов, для которых IP-адрес назначения и порт устанавливались через вызов connect()
. Он посылает первые len
байтов, на которые указывает data
, на другой конец сокета s
. Данные передаются как единая дейтаграмма. Если параметр len
задает слишком большое количество данных для передачи в одной дейтаграмме, то в переменной errno
возвращается значение EMSGSIZE
.
Следующий системный вызов sendto()
работает аналогично send()
, но позволяет указывать IP-адрес и номер порта назначения для неподключенных сокетов. Последние два параметра являются указателями на адрес сокета и длину адреса сокета. Применение этой функции не устанавливает адрес назначения для сокета; он остается неподключенным. Последующие вызовы sendto()
могут передавать дейтаграммы в другие пункты назначения. Если аргумент to равен NULL
, то функция sendto()
ведет себя точно также как и send()
.
137
Это называется трехсторонним квитированием TCP, которое на самом деле проходит несколько сложнее, чем описано выше.
139
UDP-сокеты, которые имеют постоянные пункты назначения, присвоенные через функцию connect()
, иногда называются присоединенными UDP-сокетами.
140
Есть также возможность превратить подключенный сокет в неподключенный с помощью функции connect()
, однако эта процедура не стандартизирована. Если вам все же необходимо ее применить, обратитесь к [33].
141
Данные функции могут применяться для передачи данных через любой сокет, и иногда возникают причины для использования их в TCP-соединениях.
142
Возможно также применение системных вызовов sendmsg()
и recvmsg()
, однако необходимость в этом встречается редко.