Наш клиент не запрашивал у ядра присваивания динамически назначаемого порта своему сокету (тогда как для клиента 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"
2 void
3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
Размещение другой структуры адреса сокета в памяти
9
Мы размещаем в памяти другую структуру адреса сокета при помощи функции
malloc
. Обратите внимание, что функция
dg_cli
все еще является не зависящей от протокола. Поскольку нам не важно, с каким типом структуры адреса сокета мы имеем дело, мы используем в вызове функции
malloc
только ее размер.
Сравнение возвращаемых адресов
12-13
В вызове функции
recvfrom
мы сообщаем ядру, что нужно возвратить адрес отправителя дейтаграммы. Сначала мы сравниваем длину, возвращаемую функцией
recvfrom
в аргументе типа «значение-результат», а затем сравниваем сами структуры адреса сокета при помощи функции
memcmp
.
Новая версия нашего клиента работает замечательно, если сервер находится на узле с одним единственным IP-адресом. Но эта программа может не сработать, если сервер имеет несколько сетевых интерфейсов (multihomed server). Запускаем эту программу, обращаясь к узлу
freebsd4
, у которого имеется два интерфейса и два IP-адреса: