сообщение на сервер, получает от сервера подтверждение, указывающее на активность соединения, затем сообщает серверу о том, что установлена клиентская сторона соединения [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 ()
.
Системные вызовы recv()
и recvfrom()
подобны send()
и sendto()
, но они получают дейтаграммы, а не отправляют их. Оба вызова записывают одну дейтаграмму в data
(не более чем *maxlen
байт) и отбрасывают некоторую часть дейтаграммы, которая не помещается в буфер. Удаленный адрес, отправивший дейтаграмму, сохраняется в параметре from
функции recvmsg()
, если только его длина не превышает fromLen
байт.
17.6.3. Простой tftp-сервер
Данный простой tftp-сервер иллюстрирует отправку и получение UDP-дейтаграмм как для подключенных, так и для неподключенных сокетов. Протокол tftp представляет собой несложный протокол передачи файлов, построенный на основе UDP[143]. Он часто используется встроенными компьютерными программами для пересылки первоначального загрузочного образа при сетевой загрузке. Сервер, который мы предлагаем рассмотреть, обладает рядом ограничений, поэтому он непригоден для какой-либо практической работы.
• С сервером одновременно может взаимодействовать только один клиент (этот недостаток легко устранить).
• Сервер может только отправлять файлы, но не может получать.
• Отсутствуют условия для ввода ограничений на передачу файлов анонимному удаленному пользователю.
• Выполняется очень поверхностная проверка ошибок, что, скорее всего, приведет к проблемам во