17:
18: int main(int argc, const char ** argv) {
19: struct addrinfo hints, *addr;
20: struct sockaddr_in * addrinfo;
21: int rc;
22: int sock;
23:
24: if (argc !=2) {
25: fprintf(stderr, "поддерживается только одиночный аргументn");
26: return 1;
27: }
28:
29: memset(&hints, 0, sizeof(hints));
30:
31: hints.ai_socktype = SOCK_STREAM;
32: hints.ai_flags = AI_ADDRCONFIG;
33: if ((rc = getaddrinfo(argv[1], NULL, &hints, &addr))) {
34: fprintf(stderr, "сбой поиска имени хоста: %sn",
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().
Системные вызовы recv() и recvfrom() подобны send() и sendto(), но они получают дейтаграммы, а не отправляют их. Оба вызова записывают одну дейтаграмму в data (не более чем *maxlen байт) и отбрасывают некоторую часть дейтаграммы, которая не помещается в буфер. Удаленный адрес, отправивший дейтаграмму, сохраняется в параметре from функции recvmsg(), если только его длина не превышает fromLen байт.
17.6.3. Простой tftp-сервер
Данный простой tftp-сервер иллюстрирует отправку и получение UDP-дейтаграмм как для подключенных, так и для неподключенных сокетов. Протокол tftp представляет собой несложный протокол передачи файлов, построенный на основе UDP[143]. Он часто используется встроенными компьютерными программами для пересылки первоначального загрузочного образа при сетевой загрузке. Сервер, который мы предлагаем рассмотреть, обладает рядом ограничений, поэтому он непригоден для какой-либо практической работы.
• С сервером одновременно может взаимодействовать только один клиент (этот недостаток легко устранить).
• Сервер может только отправлять файлы, но не может получать.
• Отсутствуют условия для ввода ограничений на передачу файлов анонимному удаленному пользователю.
• Выполняется очень поверхностная проверка ошибок, что, скорее всего, приведет к проблемам во время эксплуатации.
Клиент tftp начинает tftp-сеанс передачей "пакета запроса на чтение", содержащего имя файла, который нужно получить, и режим. Существует два исходных режима: netascii (выполняет некоторые простые преобразования файла) и octet (передает файл точно в таком же состоянии, в каком он находится на диске). Рассматриваемый сервер поддерживает только режим octet, поскольку он проще.