Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
40
Добавлен:
11.04.2015
Размер:
173.57 Кб
Скачать

Лекция 6

Вспомогательные процедуры преобразования

В стандартах TCP/IP определено представление для двоичных целых чисел, применяемых в заголовках протоколов. Оно предусматривает форму представления целых чисел со старшим байтом в начале и называется сетевой порядок байт. Сетевой порядок байт применяется в поле порта протокола структуры sockaddr_in.

Интерфейс сокетов включает функции, выполняющие прямое и обратное преобразование из сетевого порядка байтов в порядок байтов локального хоста (серверный порядок байт). Они подразделяются на два набора, с суффиксами s (short) и 1 (long), предназначенные для работы, соответственно, с 16-битовыми и 32-битовыми целыми числами. Функции htons (host to network short) и ntohs (network to host short) выполняют прямое и обратное преобразование целых чисел в коротком формате из серверного порядка байт в сетевой. Аналогичным образом работают функции htonl и ntohl над целыми числами в длинном формате.

Замечание. В программах следует всегда вызывать процедуры преобразования, да­же если порядок байтов хоста совпадает с сетевым порядком байт, поскольку соблюдение этого правила обеспечивает переносимость исходного кода на компьютеры с иной архитектурой.

Функции inet_aton() (address to network) и функция inet_ntoa() (network to address) осуществляют преобразование из точечной записи в двоичную форму с сетевым порядком следования байт и обратно.

Определение местонахождения сервера

В клиентском программном обеспечении может использоваться один из методов для определения IP-адреса и номера порта протокола сервера:

1) доменное имя или IP-адрес сервера могут быть заданы в виде константы во время трансляции;

2) требовать у пользователя указывать сервер при вызове программы на выполнение;

3) информация о местонахождении сервера предоставляется из из файла;

4) для поиска сервера используется отдельный протокол.

Поиск доменного имени

API-интерфейс сокетов включает библиотечные процедуры inet_addr и gethostbyname, которые выполняют преобразования IP-адреса из точечного десятичного обозначения в двоичную форму с сетевым порядком следования байт. Процедура inet_addr принимает в качестве параметра строку в коде ASCII, содержащую точечный десятичный адрес, и возвращает эквивалентный IP-адрес в виде двоичного кода. Процедура gethostbyname принимает строку в коде ASCII, содержащую доменное имя компьютера. Она возвращает адрес структуры hostent (host entry), которая, кроме всего прочего, содержит IP-адрес хоста в двоичном коде.

struct hostent

{

char *h name; /* Доменное имя хоста */

char **h_aliases; /* Псевдонимы */

int h_addrtype; /* Тип адреса */

int h_length; /* Длина адреса */

char **h_addr_list; /* Список адресов */

#define h_addr h_addr_list[0]

}

Поля, содержащие имена и адреса, должны представлять собой списки, поскольку хосты, которые имеют несколько сетевых интерфейсов, имеют также несколько имен и адресов. Кроме того, для совместимости с предыдущими версиями в файле определен идентификатор h_addr, который указывает на первый элемент в списке адресов хостов. Поэтому в программе идентификатор h_addr может использоваться так, как если бы он обозначал поле структуры.

Для преобразования доменного имени в IP-адрес используется функция gethostbyname.

#include <netdb.h>

struct hostent *gethostbyname(const char *name);

Эта функция получает имя хоста и возвращает указатель на структуру с его описанием. Рассмотрим эту структуру более подробно.

struct hostent {

char *h_name;

char **h_aliases;

int h_addrtype;

int h_length;

char **h_addr_list;

};

#define h_addr h_addr_list[0]

  • h_name. Имя хоста.

  • h_aliases. Массив строк, содержащих псевдонимы хоста. Завершается значением NULL.

  • h_addrtype. Тип адреса. Для Internet-домена - AF_INET.

  • h_length. Длина адреса в байтах.

  • h_addr_list. Массив, содержащий адреса всех сетевых интерфейсов хоста. Завершается нулём. Обратите внимание, что байты каждого адреса хранятся с сетевым порядке, поэтому htonl вызывать не нужно.

В случае успешного вызова процедура gethostbyname возвращает указатель на действительную структуру hostent. Если имя не удалось преобразовать в IP-адрес, вызов процедуры возвращает нуль.

Пример: Предположим, что клиентской программе передано доменное имя csc.neic.nsk.su в форме строки и она должна получить IP-адрес.

struct hostent *phe;

char *name = “csc.neic.nsk.su”;

if (phe = gethostbyname(name))

{

/* Теперь IP-адрес - в поле phe–>h_addr */

}

else

{

/* Ошибка в имени - обработать ошибку */

}

Как видим, gethostbyname возвращает достаточно полную информацию. Если нас интересует адрес хоста, мы можем выбрать его из массива h_addr_list. Часто берут самый первый адрес (как мы видели выше, для ссылки на него определён специальный макрос h_addr). Для определения имени хоста по адресу используется функция gethostbyaddr. Вместо строки она получает адрес (в виде sockaddr) и возвращает указатель на ту же самую структуру hostent. Используя эти две функции, нужно помнить, что они сообщают об ошибке не так, как остальные: вместо указателя возвращается NULL, а расширенный код ошибки записывается в глобальную переменную h_errno (а не errno). Соответственно, для вывода диагностического сообщения следует использовать herror вместо perror.

ПРЕДУПРЕЖДЕНИЕ Следует иметь в виду, что функции gethostbyname и gethostbyaddr возвращают указатель на статическую область памяти. Это означает, что каждое новое обращение к одной из этих функций приведёт к перезаписи данных, полученных при преыдущем обращении.

В заключение рассмотрим ещё одно семейство полезных функций - gethostname, getsockname и getpeername.

#include <unistd.h>

int gethostname(char *hostname, size_t size);

Функция gethostname используется для получения имени локального хоста. Далее его можно преобразовать в адрес при помощи gethostbyname. Это даёт нам способ в любой момент программно получить адрес машины, на которой выполняется наша программа, что может быть полезным во многих случаях.

#include <sys/socket.h>

int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

Функция getpeername позволяет в любой момент узнать адрес сокета на "другом конце" соединения. Она получает дескриптор сокета, соединённого с удалённым хостом, и записывает адрес этого хоста в структуру, на которую указывает addr. Фактическое количество записанных байт помещается по адресу addrlen (не забудьте записать туда размер структуры addr до вызова getpeername). Полученный адрес при необходимости можно преобразовать в строку, используя inet_ntoa или gethostbyaddr. Функция getsockname по назначению обратна getpeername и позволяет определить адрес сокета на "нашем конце" соединения.

Поиск порта по имени

В большинстве клиентских программ необходимо предусмотреть поиск порта протокола для конкретной службы, которая должна быть вызвана (файл /etc/services). Для этого используется функцию getservbyname, которая принимает два параметра: строку с указанием желаемой службы и строку с указанием используемого протокола транспортного уровня. Функция возвращает указатель на структуру типа servеnt (service entry) или нуль в случае ошибки:

struct servent

{

char *s_name; /* Стандартное имя службы */

char **s_aliases; /* Псевдонимы */

int s_port; /* Порт службы */

char *s_proto; /* Используемый протокол */

}

Пример: Пусть необходимо найти официально назначенный номер порта протокола для SMTP.

struct servent *pse;

if ( pse = getservbyname("smtp", "tcp"))

{

/* Теперь номер порта - в поле pse–>s_port */

}

else

{

/* Возникла ошибка - обработать ошибку */

}

Поиск протокола по имени

В интерфейсе сокетов предусмотрен механизм, позволяющий преобразовать в клиентской или серверной программе имя протокола в целочисленную константу, назначенную этому протоколу. Поиск соответствия между этими значениями выполняет библиотечная функция getprotobyname. Этой функции передается имя протокола в виде строкового параметра и она возвращает адрес структуры protoent (protocol entry). Если функция getprotobyname не может обратиться к базе данных (файл /etc/protocols) для преобразования или указанное имя не существует, она возвращает нуль.

struct protoent

{

char *p_name; /* Стандартное имя протокола */

char **p_aliases; /* Список допустимых псевдонимов */

int p_proto; /* Стандартный номер протокола */

}

Пример:

struct protoent *ppe;

if ( pptr = getprotobyname("udp"))

{

/* Теперь стандартный номер протокола - в попе ppe–>p_proto */

}

else

{

/* Возникла ошибка - обработать ошибку */

}

Клиент с установлением логического соединения (TCP)

Алгоритм:

Шаг1. Найти IP-адрес и номер порта протокола сервера, с которым необходимо установить связь.

Шаг2. Распределить сокет.

Шаг3. Указать, что для соединения нужен произвольный, неиспользуемый порт протокола на локальном компьютере, и позволить программному обеспечению TCP выбрать такой порт.

Шаг4. Подключить сокет к серверу.

Шаг5. Выполнять обмен данными с сервером по протоколу прикладного уровня.

Шаг6. Закрыть соединение.

Выбор локального номера порта протокола

Как правило, для клиента может быть выбран любой порт при соблюдении следующих условий: во-первых, порт не должен совпадать с портами, которые уже используются другими процессами на компьютере, и во-вторых, порт не должен быть предназначен для какой-либо службы в соответствии с общепринятым соглашением.

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

Проблема выбора локального IP-адреса

Если хост под­ключен только к одной сети, то задача выбора локального IP-адреса тривиальна. Но поскольку маршрутизаторы или многоадресные хосты имеют несколько IP-адресов, в них такая задача может оказаться сложнее. Как правило, сложность связана с тем, что правильный выбор зависит от маршрутизации, а приложения редко имеют доступ к информации о маршрутах.

При обмене данными по протоколу TCP с удаленным хостом каждый сегмент TCP помещается (инкапсулируется) в дейтаграмму протокола IP и передается ПО IP. ПО использует адрес назначения удаленного хоста и свою таблицу маршрутизации для выбора адреса следующего маршрутизатора и сетевого интерфейса, который может применяться для его достижения.

Возникает проблема: IP-адрес источника в исходящей дейтаграмме должен совпадать с IP-адресом сетевого интерфейса, через который протокол IP направляет дейтаграмму. Если приложение выбирает один из IP-адресов компьютера случайным образом, оно может выбрать адрес, не соответствующий интерфейсу, через который программное обеспечение IP направляет этот трафик.

Для решения этой проблемы предусмотрена возможность оставлять незаполненным поле локального IP-адреса и предоставлять ПО стека TCP/IP возможность выбрать локальный IP-адрес автоматически во время подключения клиента к серверу.

Замечание: На практике может создаваться впечатление, что клиентская программа работает правильно, даже если выбран неправильный адрес локальной оконечной точки, поскольку пакеты все равно передаются на клиентский компьютер, хотя и возможно по маршруту, отличному от того, по какому они передаются на сервер. Однако использование неправильного адреса является нарушением спецификации протоколов, затрудняет и вносит путаницу в управление сетью, а также приводит к снижению надежности программы.

Подключение сокета TCP к серверу

Системный вызов connect позволяет клиенту TCP инициировать соединение. Вызов функции connect инициирует процедуру тройного рукопожатия и не возвращает управление до тех пор, пока не будет установлено соединение TCP или не закончится время ожидания.

Функция connect выполняет четыре задачи.

  1. Проверяет, является ли действительным указанный сокет и не был ли он уже подключен.

  2. Заполняет поле адреса удаленной оконечной точки в дескрипторе сокета.

  3. Выбирает адрес локальной оконечной точки для соединения, если в дескрипторе сокета это значение не было задано.

Взаимодействие с сервером с использованием протокола TCP

При условии, что вызов функции connect привел к успешному созданию соединения, клиент может использовать это соединение для обмена данными с сервером. Обычно клиент вызывает функция send для передачи каждого запроса и функция recv для получения ответа.

Пример:

#define BLEN 120 /* Длина используемого буфера */

char *req = "request of some sort";

char buf[BLEN] ; /* Буфер для ответа */

char *bptr; /* Указатель на буфер */

int n; /* Количество считанных байтов */

int buflen; /* Место, оставшееся в буфере */

bptr = buf;

buflen = BLEN;

send(s, req, strlen(req), 0); /* Отправить запрос */

/* Получить ответ (может состоять из нескольких фрагментов) */

while((n = recv(s, bptr, buflen, 0)) > 0)

{

bptr+= n;

buflen–= n;

}

Код содержит единственный вызов функции send, но предусматривает повторное выполнение вызовов функции recv. До тех пор, пока вызовы функции recv возвращают данные, в коде уменьшается счетчик пространства, доступного в буфере, а указатель буфера продвигается вслед за полученными данными. Такая итерация необходима даже если приложение на другом конце соединения передает каждый раз только небольшой объем данных, поскольку протокол TCP не гарантирует доставку их в виде таких же фрагментов, какие были отправлены.

Закрытие соединения TCP

После того как приложение полностью заканчивает использование соединения, оно может вызвать функцию close для корректного завершения соединения и освобождения сокета. Однако закрытие соединения может оказаться в сложной задачей, поскольку протокол TCP допускает двухстороннюю связь. Поэтому для закрытия соединения требуется координация действий клиента и сервера. Пусть клиент выдает запросы, на которые отвечает сервер. С одной стороны, сервер не может закрыть соединение, поскольку не имеет информации о том, будет ли клиент присылать дополнительные запросы. С другой стороны, даже если клиентская программа имеет информацию о том, что она больше не должна передавать запросы, ей может быть неизвестно, получены ли все данные от сервера.

Для решения проблемы закрытия соединения существует функция shutdown, позволяющая закрывать соединение TCP в одном направлении. Функция принимает два параметра (дескриптор сокета s и спецификацию направления direction.

Параметр direction представляет собой целое число. Если он содержит SHUT_RD, то дальнейший ввод будет запрещен. Если он содержит SHUT_WR, будет запрещен дальнейший вывод. И наконец, если значение этого параметра равно SHUT_RDWR, соединение закрывается в обоих направлениях.

После того как клиент заканчивает отправку запросов, он может использовать функцию shutdown для указания того, что не имеет больше данных для передачи, не освобождая сокет. При этом сервер получает сигнал об обнаружении конца файла. После отправки последнего ответа сервер может закрыть соединение.

Соседние файлы в папке Lectures 1semestr