Выбрать главу

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, которое на самом деле проходит несколько сложнее, чем описано выше.

вернуться

138

Этот процесс называется сетевой загрузкой.

вернуться

139

UDP-сокеты, которые имеют постоянные пункты назначения, присвоенные через функцию connect(), иногда называются присоединенными UDP-сокетами.

вернуться

140

Есть также возможность превратить подключенный сокет в неподключенный с помощью функции connect(), однако эта процедура не стандартизирована. Если вам все же необходимо ее применить, обратитесь к [33].

вернуться

141

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

вернуться

142

Возможно также применение системных вызовов sendmsg() и recvmsg(), однако необходимость в этом встречается редко.