1. Соотношение между OSI/ISO и TCP/IP
2. Архитектура протоколов TCP/IP
4. Протокол управления передачей TCP
5. Протокол дэйтаграмм пользователя UDP
6. Межсетевой протокол управляющих сообщений ICMP
Протоколы сетевого взаимодействия TCP/IP являются результатом эволюционного развития протоколов глобальной вычислительной сети ARPANET.
Работы по созданию сети ARPANET были начаты рядом университетов США и фирмой BBN в 1968 г. В 1971 г. сеть была введена в регулярную эксплуатацию и обеспечивала для всех своих узлов три основные услуги:
Все эти средства базировались на транспортных услугах предоставляемых программой управления сети NCP (Network Control Program), реализующей свой внутренний набор протоколов.
Накопленный к 1974 г. опыт эксплуатации сети ARPANET выявил многие недостатки протоколов NCP и позволил определить основные требования к новому набору протоколов, получившему название TCP/IP:
Широко используемая ныне версия 4 протоколов TCP/IP была стандартизирована в 1981 г. в виде документов, называемых RFC (Request For Comment). Полный переход сети ARPANET на новые протоколы был завершен в 1982 г. Эта сеть сыграла роль "зародыша" всемирной сети Internet, построенной на базе протоколов TCP/IP.
Реализация протоколов TCP/IP оказалась наиболее удачной в версиях BSD4.2 и BSD4.3 операционной системы UNIX. Эта реализация является эталоном (станартом "de facto") для всех последующих.
Примечание. Первичным сервером хранения всех RFC является узел nisc.sri.com (доступ через анонимный FTP).
В 1984 г. международная стандартизирующая организация ISO предложила модель взаимодействия открытых систем OSI (Open System Interconnection), являющуюся удобным средством описания стеков протоколов.
На рис. 1.1 представлено соотношение четырехуровневой архитектуры протоколов TCP/IP и семиуровневой архитектуры OSI.
Модель OSI/ISO TCP/IP +-------------------+ - - - +-----------------------------+ | Прикладной | | | +-------------------+ - - - | Прикладной | | Представительский | | (Application) | +-------------------+ - - - | | | Сеансовый | | | +-------------------+ - - - +-----------------------------+ | Транспортный | | Транспортный (Transmission) | +-------------------+ - - - +-----------------------------+ | Сетевой | | Межсетевой (Internetwork) | +-------------------+ - - - +-----------------------------+ | Канальный | | Сетевой | +-------------------+ - - - | (Network) | | Физический | | | +-------------------+ - - - +-----------------------------+ Рис. 1.1
Объединение канального и физического уровней модели OSI в единый сетевой уровень TCP/IP было обусловлено требованием независимости от используемой среды передачи данных. Дело в том, что функции протоколов канального и физического уровней реализуются в настоящее время , как правило, едиными техническими средствами (сетевыми контроллерами).
Согласно терминологии TCP/IP элементы сетевого уровня называются подсетями (subnetworks). Идеология TCP/IP допускает, чтобы в качестве "подсетей" выступали реальные сети с их собственными стеками протоколов, узлами, шлюзами и т.п.
Внимание. Далее в данном учебном пособии для обозначения уровней стека протоколов используется терминология TCP/IP, а не OSI/ISO (если это не оговорено особо).
Внимание. В данном учебном пособии термин "шлюз" используется как обобщающий для понятий "маршрутизатор" (router), "мост" (bridge) и, собственно, "шлюз" (gateway).
На рис. 2.1 представлена архитектура основных протоколов TCP/IP, используемых на трех нижних уровнях стека.
Транспортный +-------+ +-------+ уровень | TCP | | UDP | +-------+ +-------+ Межсетевой +------+ +--------+ уровень | IP | | ICMP | +------+ +--------+ Сетевой +----------+ +------+ +------------+ уровень | Ethernet | | X.25 | | Token Ring | . . . +----------+ +------+ +------------+ Рис. 2.1
Краеугольным камнем всей архитектуры является межсетевой протокол IP (Internet Protocol). С его помощью реализуется адресация узлов сети и доставка данных. Межсетевой протокол управляющих сообщений ICMP (Internet Control Message Protocol) предназначен для передачи диагностической информации и сообщений об ошибках в работе сети.
Примечание. Протокол ICMP отнесен к межсетевому уровню условно, т.к., с одной стороны, он пользуется возможностями протокола IP для транспортировки собственных данных, но, с другой стороны, сам для транспортировки данных пользователя не применяется.
Двумя основными протоколами транспортного уровня являются надежный протокол управления передачей данных TCP (Transmission Control Protocol) и быстрый протокол дэйтаграмм пользователя UDP (User Datagram Protocol). TCP реализует сетевое взаимодействие в режиме с установлением логического (виртуального) соединения, а UDP - без оного.
Функции каждого протокола реализуются компонентой программного обеспечения (обычно входящей в состав операционной системы), которую будем называть модулем. Взаимодействие модулей соседних уровней осуществляется через стандартизированный интерфейс, имеющий, как правило, процедурный характер.
Внимание. На каждом уровне стека протоколов TCP/IP обмен данными ведется блоками данных конечной длины. К сожалению, отсутствует устоявшаяся терминология в обозначении этих блоков. В данном учебном пособии названия блоков данных зависят от уровня стека протоколов, как это показано ниже.
Уровень | Название -----------------+----------- Транспортный | Пакет Межсетевой | Сегмент Сетевой | Кадр
Содержание
3.3. Фрагментация IP-сегментов
3.4. Дополнительные данные IP-заголовка
Межсетевой протокол IP специфицирован в RFC 791. Его основные характеристики перечислены ниже:
На рис. 3.1 приведен формат заголовка IP-сегмента.
0 3 7 15 18 23 31 +------+-------+---------------+-----+---------+---------------+ |Версия| Длина | Тип | Длина | | |заг-ка | обслуживания | сегмента | +------+-------+---------------+-+-+-+---------+---------------+ | Идентификатор | |D|M| Смещение | | | |F|F| фрагмента | +--------------+---------------+-+-+-+-------------------------+ | Время | Транспорт | Контрольная сумма | | жизни | | заголовка | +--------------+---------------+-------------------------------+ | Адрес | | источника | +--------------------------------------------------------------+ | Адрес | | приемника | +----------------------------------------------+---------------+ | Дополнительные | Данные | | данные заголовка | выравнивания | +----------------------------------------------+---------------+ Рис. 3.1
Версия
4-хбитовое поле, содержащее номер версии протокола IP (номер текущей версии равен 4);
Длина заголовка
4-хбитовое поле, содержащее длину заголовка IP-сегмента в 32-битных словах. Минимальная (и типичная) длина заголовка - пять слов.
Тип обслуживания
байт, содержащий набор критериев, определяющих тип обслуживания IP-сегментов. Детальное описание отдельных битов дано ниже:
На практике в большинстве реализаций протокола IP данное поле почти всегда равно 0, в UNIX-реализациях это поле не используется вовсе.
Длина сегмента
двухбайтовое поле, содержащее длину (в байтах) всего IP-сегмента, включая длину заголовка. Максимальная длина IP-сегмента (включая заголовок) - 65535 байт. Спецификация IP протокола устанавливает, что что любой узел сети должен быть способен обрабатывать IP-сегменты длиной, по крайней мере, не менее 576 байт (что соответствует 512 байтам данных при возможной длине заголовка до 64 байт). На практике же узлы сети могут обрабатывать IP-сегменты много длинее, чем 576 байт (как правило, допустимая длина IP-сегмента связана с максимальной длиной кадра нижележащего сетевого уровня).
Идентификатор
двухбайтовое поле, содержащее уникальный идентификатор IP-сегмента, присваиваемый ему посылающим узлом. Это поле используется для распознавания фрагментов одного IP-сегмента (в ситуациях, когда в ходе перемещения по глобальной сети единый IP-сегмент был разбит на несколько фрагментов по причине его недопустимо большой длины).
DF, MF
биты, используемые при обработке фрагментированных IP-сегментов.
Если бит DF (Don't Fragment) установлен в 1, то это означает, что IP-сегмент не может быть разбит на фрагменты ни при каких условиях (даже, если он не может быть передан без этого далее к адресату и должен быть уничтожен).
Бит MF (More Fragments) указывает, является (MF=0) или нет (MF=1) данный IP-"подсегмент" последним в цепочке IP-"подсегментов", в которую был преобразован (фрагментирован) исходный IP-сегмент.
Алгоритм фрагментации описан ниже в разделе "Фрагментация IP-сегментов".
Смещение фрагмента
13-битное поле, используемое только в IP-сегменте, являющемся фрагментом (IP-фрагментом) другого (исходного) IP-сегмента. Это поле содержит смещение данных, содержащихся в IP-фрагменте, по отношению к началу данных исходного IP-сегмента. Смещение измеряется в восьмибайтных единицах, поэтому 13 битов достаточно для представления смещения в IP-сегменте максимальной возможной длины (8 * 2^13 - 1 = 65535).
Время жизни
однобайтовое поле, заполняемое создающим IP-сегмент узлом сети количеством единиц времени жизни IP-сегмента в сети. RFC 791 специфицирует в качестве этих единиц секунды и требует, чтобы каждый транзитный узел сети, через который проходит IP-сегмент, уменьшал содержимое этого поля по крайней мере на 1 (даже при условии, что обработка сегмента на самом деле заняла меньше одной секунды). Таким образом, на практике, время жизни (TTL - Time To Live) - это максимальное количество узлов, которое может пройти до своего уничтожения IP-сегмент.
Каждый IP-модуль на любом узле сети обязан уничтожать IP-сегменты, для которых поле "время жизни" стало равным нулю. Этим предотвращается появление в сети IP-сегментов, "блуждающих" по ней бесконечное время. При этом узлу-источнику уничтоженного IP-сегмента посылается ICMP-сегмент, извещающий об этом событии.
В UNIX-реализациях, как правило, это поле заполняется источником IP-сегмента числом из диапазона 15...30.
Транспорт
поле размером в байт, содержащее идентификатор протокола более высокого (обычно, транспортного) уровня, для которого предназначены данные IP-сегмента. Ниже приведены идентификаторы для ряда протоколов.
--------+-------------+------------------------------------------ Иденти- : Сокращенное : Имя протокола фикатор : название : --------+-------------+------------------------------------------ 1 ICMP Межсетевой протокол управляющих сообщений 2 IGMP Межсетевой протокол группового управления 3 GGP Протокол "шлюз-шлюз" 6 TCP Протокол управления передачей 8 EGP Протокол "внешних" шлюзов 17 UDP Протокол дейтаграмм пользователя 27 RDP Протокол надежных данных 28 IRTP Протокол межсетевой надежной передачи 29 ISO TP4 Транспортный протокол ISO 4 класса 80 ISO IP Межсетевой протокол ISO 89 OSPF Протокол "кратчайший путь первым" -------+--------------+------------------------------------------
Контрольная сумма заголовка
двухбайтовое поле, содержащее контрольную сумму заголовка IP-сегмента (обращаем внимание, что для данных IP-сегмента контрольная сумма не подсчитывается; контролировать данные - задача протоколов транспортного уровня).
Поскольку заголовок IP-сегмента содержит поле "время жизни", изменяющее свое значение в каждом узле, через который следует IP-сегмент, то для вычисления контрольной суммы должен использоваться эффективный (а, следовательно, простой алгоритм). Во всех протоколах, входящих в архитектуру TCP/IP, используется так называемая Internet-контрольная сумма, которая представляет собой дополнение 16-битной суммы всех 16-битных слов контролируемой информации.
Адрес источника и адрес приемника
четырехбайтовые IP-адреса узлов сети. Подробно структура IP-адреса описана ниже в "IP-адрес".
Дополнительные данные заголовка
последовательность полей произвольной длины, описывающих необязательные данные заголовка. Такие данные используются для специальных целей (управление сетью, секретность и т.п.) и кратко описаны ниже в "Дополнительные данные IP-заголовка".
Данные выравнивания
не имеющие смысла данные, включаемые в заголовок только для выравнивания его длины до границы четырехбайтового слова.
IP-адрес представляет собой четырехбайтовое число, старшие (крайние левые) биты которого определяют класс IP-адреса. Для классов A, B и C четыре байта адреса делятся между идентификатором (номером) сети и идентификатором (номером) узла в сети как это показано на рис. 3.2.
| 0 7| 15 23 31 +-+---------+-------------+-------------+-------------+ |0| | : : | Класс A +-+---------+-------------+-------------+-------------+ | +-------------+ 15| +-+-+-------+-------------+-------------+-------------+ |1|0| : | : | Класс B +-+-+-------+-------------+-------------+-------------+ | +-------------+ 23| +-+-+-+-----+-------------+-------------+-------------+ |1|1|0| : : | | Класс C +-+-+-+-----+-------------+-------------+-------------+ | Идентификатор сети | Идентификатор узла Рис. 3.2
Сети классов A, B и C абсолютно равноправны и отличаются лишь допустимым количеством узлов в них. Идентификаторы узлов, состоящие из одних нулевых или единичных битов имеют специальный смысл:
IP-адреса принято записывать в так называемой "точечной нотации" - в виде последовательности разделенных точками четырех десятичных (или шестнадцатиричных с префиксом 0x) чисел, представляющих значения отдельных байтов.
Каждый узел в сети имеет, по крайней мере, один уникальный IP-адрес.
Кроме классов A, B и C существуют еще два класса IP-адресов - D и E (см. рис. 3.3).
+-+-+-+-+------+------------+------------+------------+ |1|1|1|0| Идентификатор группы узлов | Класс D +-+-+-+-+------+------------+------------+------------+ +-+-+-+-+------+------------+------------+------------+ |1|1|1|1| : : : | Класс E +-+-+-+-+------+------------+------------+------------+ Рис. 3.3
Класс D используется для организации многопунктового (multicast) режима посылки сообщений: IP-сегмент, посылаемый по по IP-адресу класса D, доставляется всем узлам сети, имеющим указанный идентификатор группы узлов. Описание данного режима дано в RFC 1112.
Примечание. Не все современные реализации протоколов TCP/IP поддерживают многопунктовое вещание.
Для обеспечения гибкости при создании и администрировании сетей различного размера в 1985 г. было введено понятие "подсеть" (RFC 950), позволяющее использовать один и тот же IP-адрес классов A,B или C для разных подсетей.
Такая возможность обеспечивается специальной битовой маской (netmask), ассоциированной с IP-адресом и определяющей распределение битов IP-адреса между идентификатором подсети и идентификатором узла.
Пусть, например, IP-адрес класса C 194.85.36.0 планируется использовать для организации четырех подсетей. Это потребует выделения двух битов из части IP-адреса, относящейся к идентификатору узла. Такое "перепланирование" структуры IP-адреса реализуется сетевой маской 255.255.255.192, где десятичное 192 - это двоичное 11000000.
Эта сетевая маска формирует IP-адрес не из двух, а из трех комронент:
Каждая из четырех образованных подсетей может иметь до 62 узлов с идентификаторами от 1 до 62, идентификатор узла с номером 63 является широковещательным идентификатором для подсети.
Примечание. Для идентификатора подсети можно выделять только старшие (самые левые) биты из части IP-адреса, отводимой под идентификатор узла.
Примечание. Возможность разбиения сетей на подсети обусловливается, в первую очередь, средствами маршрутизации IP-сегментов, а не средствами IP-модулей, формирующих и обрабатывающих IP-сегменты.
Примечание. Некоторые современные реализации протоколов маршрутизации для TCP/IP позволяют выделять "подподсети" в подсетях.
Для того, чтобы существовала возможность передачи IP-сегментов через сети различного типа, межсетевой протокол обеспечивает адаптацию их размера к требованиям каждой сети. Это дает возможность, например, IP-сегментам, порожденным в сети на базе Ethernet (максимальный размер кадра - 1526 байт), беспрепятственно перемещаться до адресата по сети на базе X.25 (максимальный размер кадра - 128 байт). Изменение размера IP-сегмента в процессе перемещения по сети может быть связано и с соображениями эффективности передачи.
Изменение размера IP-сегмента реализуется механизмом, называемым фрагментацией. IP-модуль на любом узле сети должен иметь возможность:
Каждый IP-фрагмент представляет собой полноценный IP-сегмент со своим собственным IP-заголовком. Однако заголовки всех IP-фрагментов содержат одинаковый идентификатор, совпадающий с идентификатором исходного IP-сегмента. Это позволяет распознавать все IP-фрагменты, относящиеся к одному исходному IP-сегменту.
IP-фрагменты в своих заголовках содержат поле "Смещение фрагмента", описывающее смещение данных IP-фрагмента в данных исходного IP-сегмента. Это поле позволяет корректно восстановить данные исходного IP-сегмента в принимающем IP-фрагменты узле даже в ситуации, когда IP-фрагменты приходят в порядке, от порядка их посылки (такое вполне возможно, т.к. IP-фрагменты могут следовать от источника к адресату по разным маршрутам).
Рассмотрим процесс фрагментации более подробно на следующем примере. IP-модуль на некотором узле получил IP-сегмент с идентификатором 9876 и данными длиной 300 байт (при этом бит запрета фрагментации DF установлен в 0). Этот IP-сегмент должен быть передан дальше к адресату через сеть, максимальный размер кадра которой равен 128 байтам.
Рис. 3.4 схематично представляет разбиение исходного IP-сегмента на 3 IP-фрагмента.
Исходный IP-сегмент +-----------+--------------------------------------+ | Заголовок | Данные (300 байт) | +-----------+--------------------------------------+ : : : : : : : : IP-фрагмент 1 : : : : +-----------+-------------+ : : | Заголовок | 104 байта | : : +-----------+-------------+ : : IP-фрагмент 2 : : : +-----------+-------------+ : | Заголовок | 104 байта | : +-----------+-------------+ : IP-фрагмент 3 : : +-----------+----------+ | Заголовок | 92 байта | +-----------+----------+ Рис. 3.4
IP-фрагмент 1 содержит в своем заголовке следующую информацию:
IP-фрагмент 2 содержит в своем заголовке следующую информацию:
IP-фрагмент 3 содержит в своем заголовке следующую информацию:
Заметим, что т.к. смещение фрагмента измеряется в восьмибайтных единицах, то длина данных в каждом IP-фрагменте (кроме последнего в цепочке) обязательно должна быть кратна 8. Вот почему в нашем примере это 104 байта (13 восьмибайтных единиц), а не 108, как допускает максимальная длина кадра в 128 байт (128 - 20 = 108, где 20 - длина заголовка).
IP-модуль на принимающем IP-фрагменты узле в ситуации, когда он должен транслировать IP-сегмент далее по сети, имеет три варианта действий с фрагментами:
В работе с IP-фрагментами на принимающей стороне используется специальный таймер, который с приходом первого фрагмента IP-сегмента устанавливается в исходное состояние (для UNIX-реализаций это, обычно, 30 сек) и начинает обратный счет. До момента обнуления таймера должны прийти все IP-фрагменты, относящиеся к этому сегменту. Если этого не произойдет, то все частично полученные данные IP-сегмента сбрасываются, а сам IP-сегмент считается утерянным.
Ниже кратко описываются дополнительные данные, которые могут включаться в IP-заголовок в случае необходимости.
Предписываемый маршрут
список IP-адресов узлов сети, через которые должен следовать до адресата IP-сегмент. Предписываемый маршрут может быть "строгим" или "мягким". В первом случае IP-сегмент должен следовать строго только по указанным в списке узлам сети, во втором - допустимо прохождение через любые промежуточные узлы, не указанные в списке.
Пройденный маршрут
список IP-адресов узлов сети, которые посетил IP-сегмент по пути к адресату. Каждый транзитный узел, через который следует IP-сегмент, помещает в этот список свой IP-адрес.
Временные метки (time stamp)
список моментов времени прохождения IP-сегмента через узлы сети, составляющие маршрут.
Секретность
указание на обработку IP-сегмента в соответствии с требованиями безопасности (RFC 1038). Эта возможность имеется только в нескольких (военных) реализациях TCP/IP.
Флаг окончания
указание на завершение дополнительных данных IP-заголовка.
Каждый элемент дополнительных данных представляет собой
Для дополнительных данных, пополняемых в ходе продвижения IP-сегмента по сети (например, "пройденный маршрут"), источник IP-сегмента должен зарезервировать место необходимого объема в IP-заголовке. Такой подход обеспечивает упрощение (а, следовательно, и ускорение) обработки IP-сегмента в узлах маршрута.
RFC 1063 (1988 г.) предлагает механизм определения оптимального размера IP-сегмента, при посылке его к определенному адресату. Этот механизм использует дополнительные данные IP-заголовка, называемые probe MTU (probe Maximum Transfer Unit - тестовый максимальный блок передачи). Каждый узел в маршруте IP-сегмента, содержащего такие дополнительные данные, сравнивает MTU следующей по маршруту сети с MTU, содержащемся в заголовке, и заменяет в нем старое значение на новое, если новое оказывается меньше. Конечный адресат IP-сегмента возвращает определенное таким образом значение источнику IP-сегмента. Использование в дальнейших посылках найденного размера IP-сегментов, позволяет избежать их фрагментации. Этот механизм широкого распространения еще не получил.
Примечание. Понятие MTU подробно рассматривается в "Протоколы сетевого уровня" .
Содержание
4.3. Принцип "скользящего окна"
4.7. Алгоритмы повышения эффективности
Протокол управления передачей TCP (Transmission Control Protocol) является протоколом транспортного уровня и базируется на возможностях, предоставляемых межсетевым протоколом IP. Основная задача TCP - обеспечение надежной передачи данных в сети. Его транспортный адрес в заголовке IP-сегмента равен 6. Описание протокола TCP дано в RFC 793.
Его основные характеристики перечислены ниже:
Несмотря на то, что для пользователя передача данных с использованием протокола TCP выглядит как потоковая, на самом же деле обмен между партнерами осуществляется посредством пакетов данных, которые мы будем называть "TCP-пакетами".
На рис. 4.1 приведен формат заголовка TCP-пакета.
0 3 9 15 23 31 +------+-----------+-----------+---------------+---------------+ | Порт | Порт | | источника | приемника | +------------------------------+-------------------------------+ | Номер | | в последовательности | +--------------------------------------------------------------+ | Номер | | подтверждения | +------+-----------+-+-+-+-+-+-+-------------------------------+ |Смеще-| Зарезер- |U|A|P|R|S|F| Размер | | ние | вировано |R|C|S|S|Y|I| окна | |данных| |G|K|H|T|N|N| | +------+-----------+-+-+-+-+-+-+-------------------------------+ | Контрольная | Указатель | | сумма | | +------------------------------+---------------+---------------+ | Дополнительные | Данные | | данные заголовка | выравнивания | +----------------------------------------------+---------------+ Рис. 4.1
Порт источника и порт приемника
16-битовые поля, содержащие номера портов, соответственно, источника и адресата TCP-пакета. Подробное описание понятия "номер порта" дано в "Номер порта".
Номер в последовательности (sequence number)
32-битовое поле, содержимое которого определяет (косвенно) положение данных TCP-пакета внутри исходящего потока данных, существующего в рамках текущего логического соединения.
В момент установления логического соединения каждый из двух партнеров генерирует свой начальный "номер в последовательности", основное требование к которому - не повторяться в промежутке времени, в течение которого TCP-пакет может находиться в сети (по сути, это время жизни IP-сегмента). Партнеры обмениваются этими начальными номерами и подтверждают их получение. Во время отправления TCP-пакетов с данными поле "номер в последовательности" содержит сумму начального номера и количества байт ранее переданных данных.
Номер подтверждения (acknowledgement number)
32-битовое поле, содержимое которого определяет (косвенно) количество принятых данных из входящего потока к TCP-модулю, формирующему TCP-пакет.
Смещение данных
четырехбитовое поле, содержащее длину заголовка TCP-пакета в 32-битовых словах и используемое для определения начала расположения данных в TCP-пакете.
Флаг URG
бит, установленное в 1 значение которого означает, что TCP-пакет содержит важные (urgent) данные. Подробно о данных этого типа сказано в "Важные данные".
Флаг ACK
бит, установленное в 1 значение которого означает, что TCP-пакет содержит в поле "номер подтверждения" верные данные.
Флаг PSH
бит, установленное в 1 значение которого означает, что данные содержащиеся в TCP-пакете должны быть немедленно переданы прикладной программе, для которой они адресованы. Подтверждение для TCP-пакета, содержащего единичное значение во флаге PSH, означает, что и все предыдущие TCP-пакеты достигли адресата.
Флаг RST
бит, установливаемый в 1 в TCP-пакете, отправляемом в ответ на получение неверного TCP-пакета. Также может означать запрос на переустановление логического соединения.
Флаг SYN
бит, установленное в 1 значение которого означает, что TCP-пакет представляет собой запрос на установление логического соединения. Получение пакета с установленым флагом SYN должно быть подтверждено принимающей стороной.
Флаг FIN
бит, установленное в 1 значение которого означает, что TCP-пакет представляет собой запрос на закрытие логического соединения и является признаком конца потока данных, передаваемых в этом направлении. Получение пакета с установленым флагом FIN должно быть подтверждено принимающей стороной.
Размер окна
16-битовое поле, содержащее количество байт информации, которое может принять в свои внутренние буфера TCP-модуль, отправляющий партнеру данный TCP-пакет. Данное поле используется принимающим поток данных TCP-модулем для управления интенсивностью этого потока: так, установив значение поля в 0, можно полностью остановить передачу данных, которая будет возобновлена только, когда размер окна примет достаточно большое значение. Максимальный размер окна зависит от реализации, в некоторых реализациях максимальный размер может устанавливаться системным администратором (типичное значение максимального размера окна - 4096 байт). Определение оптимального размера окна - одна из наиболее сложных задач реализации протокола TCP (см. "Исключение малых окон").
Контрольная сумма
16-битовое поле, содержащее Internet-контрольную сумму, подсчитанную для TCP-заголовка, данных пакета и псевдозаголовка. Псевдозаголовок включает в себя ряд полей IP-заголовка и имеет показанную на рис. 4.2 структуру.
0 7 15 31 +-----------+-----------+-----------------------+ | IP-адрес источника | +-----------+-----------+-----------------------+ | IP-адрес приемника | +-----------+-----------+-----------------------+ | Нули | Транспорт | Длина IP-сегмента | +-----------+-----------+-----------------------+ Рис. 4.2
Указатель
16-битовое поле, содержащее указатель (в виде смещения) на первый байт в теле TCP-пакета, начинающий последовательность важных (urgent) данных. Данные этого типа и механизм их обработки описаны в "Важные данные".
Дополнительные данные заголовка
последовательность полей произвольной длины, описывающих необязательные данные заголовка. Протокол TCP определяет только три типа дополнительных данных заголовка:
Дополнительные данные последнего типа посылаются в TCP-заголовке в момент установления логического соединения для выражения готовности TCP-модулем принимать пакеты длиннее 536 байтов. В UNIX-реализациях длина пакета обычно определяется максимальной длиной IP-сегмента для сети.
Номера портов играют роль адресов транспортного уровня, идентифицируя на конкретных узлах сети, по сути дела, потребителей транспортных услуг, предоставляемых как протоколом TCP, так и протоколом UDP. При этом протоколы TCP и UDP имеют свои собственные адресные пространства: например, порт номер 513 для TCP не идентичен порту номер 513 для UDP.
Примечание. Своя собственная адресация на транспортном уровне стека протоколов сетевого взаимодействия необходима для обеспечения возможности функционирования на узле сети одновременно многих сетевых приложений. Наличие в TCP-заголовке номера порта позволяет TCP-модулю, получающему последовательности TCP-пакетов, формировать раздельные потоки данных к прикладным программам.
Взаимодействие прикладных программ, использующих транспортные услуги протокола TCP (или UDP), строится согласно модели "клиент-сервер", которая подразумевает, что одна программа (сервер) всегда пассивно ожидает обращения к ней другой программы (клиента). Связь программы-клиента и сервера идентифицируется пятеркой:
Для того, чтобы клиент мог обращаться к необходимому ему серверу, он должен знать номер порта, по которому сервер ожидает обращения к нему ("слушает сеть"). Для прикладных программ, получивших наибольшее распространение в сетях на основе TCP/IP, номера портов фиксированы и носят название "хорошо известных номеров портов" (well-known port numbers). В UNIX-системах такие номера портов содержатся в файле /etc/services. Ниже приводятся примеры хорошо известных номеров портов для некоторых серверов (служб).
Служба Номер порта Протокол --------------------------------- ftp-data 20 TCP ftp 21 TCP telnet 23 TCP smtp 25 TCP time 37 TCP time 37 UDP finger 79 TCP portmap 111 TCP portmap 111 UDP exec 512 TCP login 513 TCP shell 514 TCP who 513 UDP talk 517 UDP route 520 UDP Xserver 6000 TCP
Примечание. Обратите внимание, что некоторые серверы (такие, например, как для службы portmap с номером порта 111) могут работать как по протоколу TCP, так и по протоколу UDP.
Программы-клиенты, являющиеся активной стороной во взаимодействии "клиент-сервер", могут использовать, как правило, произвольные номера портов, назначаемые динамически непосредственно перед обращением к серверу (как любые свободные на данном узле).
Примечание. Любая прикладная программа (будь то клиент или сервер) может открывать для взаимодействия любое количество портов для использования любых транспортных протоколов.
Средства разработки сетевых приложений на базе транспортных протоколов TCP и UDP описаны в "Сетевое программирование".
Протоколы транспортного уровня, обеспечивающие надежную передачу данных, предполагают обязательное подтверждение принимающей стороной правильности полученных данных.
В "простых" протоколах сторона, отправляющая данные, отсылает пакет с данными принимающей стороне и переходит в состояние ожидания подтверждения получения правильных данных. Только после приема подтверждения становится возможной следующая посылка. Очевидно, что такой подход использует пропускную способность сети неэффективно.
В протоколе TCP используется более совершенный принцип "скользящего окна" (sliding window), который заключается в том, что каждая сторона может отправлять партнеру максимум столько байт, сколько партнер указал в поле "размер окна" заголовка TCP-пакета, подтверждающего получение предыдущих данных.
Принцип "скользящего окна" обеспечивает "опережающую" посылку данных с "отложенным" их подтверждением. Следует отметить недостаток этого механизма: если в течение некоторого времени не будет получено "отсроченное" подтверждение ранее отправленного пакета, то отправляющий TCP-модуль будет вынужден повторить посылку всех TCP-пакетов, начиная с неподтвержденного.
Размер окна, как правило, определяется объемом свободного места в буферах принимающего TCP-модуля.
Протокол TCP предусматривает возможность информирования принимающей стороны взаимодействия отправляющей стороной о наличии в TCP-пакете важных данных (urgent data), требующих особого внимания согласно логике прикладной задачи.
Примечание. Отличие важных данных от данных основного потока заключается в том, что принимающая сторона должна, как правило, обработать их прежде ранее полученных, но еще не обработанных данных потока.
Для индикации наличия в TCP-пакете важных данных используется флаг URG TCP-заголовка, местоположение важных данных в теле TCP-пакета определяется полем "Указатель" TCP-заголовка - оно задает смещение (в стиле языка программирования C) первого байта важных данных в теле TCP-пакета. Рис. 4.3 иллюстрирует расположение важных данных в теле TCP-пакета.
+---------------------------+ | | | v +-------+-----|-----++-------------------+-------------------+ | URG=1 | Указатель || Данные потока | Важные данные | +-------+-----------++-------------------+-------------------+ TCP-заголовок Тело TCP-пакета Рис. 4.3.
Примечание. Протокол TCP предусматривает передачу важных (urgent) данных в рамках общего потока данных ("in-band"). Существуют протоколы (например, ISO), поддерживающие режим передачи важных (expedited) данных вне общего потока данных ("out-band"), что в общем случае быстрее.
Взаимодействие партнеров с использованием протокола TCP строится в три этапа:
Ниже с помощью трех рисунков дается описание каждого из этапов. Рисунки иллюстрируют последовательность обмена TCP-пакетами двумя TCP-модулями: A и B. TCP-пакеты представлены тремя полями TCP-заголовка ("Номер в последовательности", "Номер подтверждения", "Флаги") и числом, характеризующим длину данных, составляющих тело TCP-пакета (заметим, что реально поля длины данных в TCP-заголовке нет). Стрелками показаны направления пересылки пакетов.
Номер в Номер Флаги Длина TCP A последовательности подтверждения данных TCP B +------------------+-------------+---------+------+ ---> | 1000 | | SYN | 0 | +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ | 5000 | 1001 | SYN,ACK | 0 | <--- +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ ---> | 1001 | 5001 | ACK | 0 | +------------------+-------------+---------+------+ Рис. 4.4
Рис. 4.4 иллюстрирует этап установления соединения, реализуемый как "трехшаговое рукопожатие" (three-way handshake). На первом шаге TCP-модуль A, играя роль клиента, посылает TCP-модулю B пакет с установленным флагом SYN и начальным значением номера в последовательности равным 1000. TCP-модуль B, будучи готов со своей стороны установить соединение, отвечает TCP-пакетом, подтверждающим правильный прием запроса (поле "Номер подтверждения" на 1 больше начального номера в последовательности для TCP-модуля A и среди флагов есть установленный в 1 флаг ACK) и информирующим о готовности установить соединение (взведен флаг SYN и установлен в 5000 начальный номер в последовательности). На третьем шаге TCP-модуль A подтверждает правильность приема TCP-пакета от B.
Примечание. Некоторые протоколы транспортного уровня (но не TCP) допускают обмен данными уже на этапе установления логического соединения.
Номер в Номер Флаги Длина TCP A последовательности подтверждения данных TCP B +------------------+-------------+---------+------+ ---> | 1001 | 5001 | ACK | 50 | +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ | 5001 | 1051 | ACK | 80 | <--- +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ ---> | 1051 | 5081 | ACK | 0 | +------------------+-------------+---------+------+ Рис. 4.5
Рис. 4.5 иллюстрирует этап двустороннего обмена данными между TCP-модулями A и B. TCP-модуль, принимающий адресованные ему данные, всегда подтверждает их прием, вычисляя значение поля "Номер подтверждения" в заголовке ответного TCP-пакета как сумму пришедшего "Номера в последовательности" и длины правильно принятых данных. Отметим, что посылка данных к партнеру и подтверждение принятых от него данных реализуются в рамках одного TCP-пакета.
Номер в Номер Флаги Длина TCP A последовательности подтверждения данных TCP B +------------------+-------------+---------+------+ ---> | 1051 | 5081 | ACK,FIN | 0 | +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ | 5081 | 1052 | ACK | 0 | <--- +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ | 5081 | 1052 | ACK | 40 | <--- +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ ---> | 1052 | 5121 | ACK | 0 | +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ | 5121 | 1052 | ACK,FIN | 0 | <--- +------------------+-------------+---------+------+ +------------------+-------------+---------+------+ ---> | 1052 | 5122 | ACK | 0 | +------------------+-------------+---------+------+ Рис. 4.6
Рис. 4.6 иллюстрирует закрытие соединения по инициативе TCP-модуля A, посылающего партнеру TCP-пакет с установленным флагом FIN. Прием запроса на закрытие соединения TCP-модуль B подтверждает пакетом, содержащем в своем заголовке поле "Номер подтверждения", значение которого (1052) на 1 больше значения принятого "Номера в последовательности" (1051). После этого посылка каких-либо данных TCP-модулем A становится невозможной, однако модуль B имеет данные для передачи, которые он отправляет TCP-модулю A и получает подтверждение на их прием. Затем TCP-модуль B формирует пакет с флагом FIN, после подтверждения его приема соединение считается закрытым.
Примечание. Обратите внимание на то обстоятельство, что при подтверждении TCP-пакетов, содержащих единичные флаги SYN или FIN, значение поля "Номер подтверждения" на 1 больше значения соответствующего поля "Номер в последовательности", несмотря на то, что никакие данные в подтверждаемых TCP-пакетах не передаются.
Примечание. Рассмотренный пример не включает в себя ситуации, связанные с "потерей" TCP-пакетов в сети, и их обработку, связанную с повторной передачей данных.
Содержание
4.6.1. Таймер повторной передачи
4.6.2. Таймер возобновления передачи
4.6.4. Таймеры поддержки соединения
Данный таймер взводится значением RTO (Retransmission TimeOut - интервал до повторной передачи) в момент посылки TCP-пакета адресату. Если таймер окажется сброшенным в ноль до момента получения подтверждения пакета, то этот пакет должен быть послан вновь.
Ясно, что величина RTO не может быть фиксированной, т.к. TCP-пакеты до разных адресатов следуют по различным маршрутам через сети, скорость передачи данных в которых может различаться более чем в тысячи раз. Для вычисления "оптимального" значения RTO в каждом логическом соединении используется специальная процедура, специфицированная в RFC 793.
Согласно этой процедуре, для каждого TCP-пакета измеряется величина RTT (Round Trip Time - интервал времени от момента посылки TCP-пакета до момента получения подтверждения на него). На основе измеренных RTT вычисляется величина SRTT (Smoothed RTT - сглаженный RTT) по следующей формуле:
SRTT = k*SRTT + (1 - k)*RTT,
где k - сглаживающий коэффициент (например, 0.9).
Примечание. Приведенная формула обеспечивает фильтрацию нетипичных (пиковых) значений измеренной величины RTT.
"Оптимальное" значение RTO вычисляется по формуле:
RTO = min(U, max(L, p*SRTT)),
где:
U - ограничение сверху на значение RTO (например, 30 секунд);
L - ограничение снизу на значение RTO (например, 1 секунда);
p - коэффициент "запаса" (например, 2).
Если после повторной посылки TCP-пакета, опять не будет получено его подтверждение за интервал времени RTO, то попытки послать TCP-пакеты будут повторены (до 12 раз), но каждый раз с экспоненциально возрастающим значением RTO. Только после неудачи всей серии повторных посылок связь между партнерами будет считаться аварийно закрытой.
В ходе взаимодействия двух TCP-модулей (A и B) вполне возможна следующая ситуация:
Средством выхода из такого тупикового состояния и служит таймер возобновления передачи (persistence timer - "настойчивый" таймер). Он взводится в момент получения TCP-пакета с нулевым значением поля "Размер окна" в его заголовке (типичное начальное значение для этого таймера - 5 секунд). Если до момента обнуления таймера не будет получено разрешение на возобновление передачи данных, то ожидающий разрешения TCP-модуль отправляет партнеру пакет, содержащий всего лишь 1 байт данных. По реакции партнера, возвращающего пакет с нулевым/ненулевым значением размера окна, TCP-модуль продолжает ожидание или возобновляет посылку данных.
Протокол TCP предусматривает следующий простой прием предотвращения появления в сети TCP-пакетов, не имеющих адресатов: после закрытия логического соединения между партнерами номера портов, использовавшихся в этом соединении, остаются еще некоторый интервал времени действительными, что дает возможность долго блуждавшим по сети TCP-пакетам добраться до места назначения (где они будут просто проигнорированы). Величина этого интервала равна удвоенному времени жизни IP-сегмента (обычно, 2*15=30 секунд).
Примечание. Пользователи ОС UNIX могут почувствовать эффект от использования этого приема, попытавшись перезапустить некоторую прикладную программу, использующую TCP, сразу же после ее завершения.
Ниже описывается механизм, используемый для проверки ненарушенности логического соединения между TCP-модулями.
Каждый TCP-модуль, участвующий в логическом соединении, через фиксированный промежуток времени (keep-alive timer), равный обычно 45 секундам, периодически отправляет партнеру пустые (не содержащие данных) TCP-пакеты и ждет их подтверждения. Каждое полученное подтверждение говорит о ненарушенности соединения. Если же в течении определенного интервала времени (idle timer), равного обычно 360 секудам, не будет получено ни одного подтверждения, то логическое соединение считается оборванным.
Примечание. Очевидно, что данный механизм имеет смысл включать в работу только тогда, когда партнеры по TCP-взаимодействию приостановили по какой-либо причине обмен данных на достаточно длительный срок (более 45 секунд).
Примечание. Стандартная спецификация протокола TCP не включает в себя описанный механизм, однако он реализован во всех UNIX-системах.
Содержание
4.7.3. Исключение коротких TCP-пакетов
4.7.4. Алгоритм медленного старта
Ниже описываются некоторые алгоритмы, используемые для повышения эффективности взаимодействия по протоколу TCP в UNIX-реализациях и не являющиеся частью спецификации TCP.
Задержка отсылки подтверждения принятого пакета используется для сокращения числа TCP-пакетов, которыми обмениваются партнеры по взаимодействию. Поясним эффект от такой задержки следующим примером.
Пусть клиентская часть некоторого приложения (например, службы telnet) направляет серверной части некоторые данные (в случае telnet - строку символов, представляющих команду OC UNIX, которая должна быть выполнена на удаленном узле сети). Серверная часть, получив данные и обработав их, должна вернуть клиенту результат (в случае с telnet - это стандартный вывод исполненной команды).
В ситуации без задержки TCP-модуль на стороне сервера, приняв пакет с данными и разместив их в своем буфере, сразу же отвечает подтверждающим пакетом, содержащим в своем заголовке и некоторый (уменьшенный) размер окна для приема последующих данных. Спустя некоторое (обычно, очень короткое) время данные из буфера передаются серверной части прикладной программы. Освобождение места в буфере заставляет TCP-модуль отправлять партнеру на стороне клиента TCP-пакет с новым (увеличившимся) размером окна. Тем временем прикладная программа, обработав полученные данные (часто за небольшое время), передает результат TCP-модулю для отсылки его клиенту, для чего модуль формирует еще один пакет. Итого: одна транзакция потребовала от TCP-модуля на стороне сервера посылки трех TCP-пакетов.
Введение же задержки при отсылке подтверждающего TCP-пакета позволяет в ряде случаев уменьшить количество пакетов с трех до одного, содержащего сразу подтверждение, новый размер окна и результирующие данные. Экспериментальные исследования показали, что во многих случаях "оптимальным" значением задержки является 0.2 секунды.
Для того, чтобы введение задержки сказывалось минимальным образом на приложения, предъявляющие жесткие требования к пропускной способности сети, задержка устанавливается нулевой при условии, что размер окна изменяется более чем на 35% или (в абсолютном исчислении) на удвоенный максимальный размер TCP-пакета.
Возможны ситуации, когда прикладная программа, использующая TCP-сервис, "выбирает" из буфера обслуживающего ее TCP-модуля пришедшие для нее данные малыми порциями. Это приводит к генерации TCP-модулем большого количества TCP-пакетов, содержащих в своих заголовках малую величину размера окна, что в свою очередь приводит к генерации на передающей данные стороне многих TCP-пакетов с "короткими" данными. Как результат - "засорение" сети короткими пакетами и снижение ее пропускной способности.
Во избежание деградации сети вследствие описанного явления используется следующий прием: TCP-пакет, информирующий посылающую данные сторону об увеличении размера окна, формируется только при выполнении одного из двух условий:
Кроме того TCP-модуль, отправляющий данные, должен делать это большими порциями.
"Засорение" сети короткими TCP-пакетами возможно и в ситуации, когда прикладная программа, отправляющая данные партнеру по взаимодействию, делает это короткими порциями (типичный пример - любая программа, использующая графическую систему X Window System).
Для борьбы с этим используется следующий прием:
Однако этот подход может сказаться на быстродействии некоторых приложений, чтобы избежать этого прикладной программе предоставляются средства для принудительного "выталкивания" буферизованных данных в необходимых случаях. Кроме того, существует возможность отключения описанного механизма.
Опыт эксплуатации сетей на основе TCP/IP показал, что с повышением загрузки сети (особенно, сети со шлюзом) ее пропускная способность падает (хотя, казалось бы, она должна оставаться постоянной). Исследования показали, что падение обусловлено появлением в сети большого числа TCP-пакетов, повторно посылаемых к активно используемому узлу сети (обычно это шлюз в другие сети). Дело в том, что приемный буфер TCP-модуля на шлюзе очень быстро заполняется, и TCP-модуль вынужден сбрасывать поступающие к нему пакеты.
Для предупреждения подобной ситуации необходимо согласование темпа передачи TCP-пакетов с возможностями их приема на узле-адресате. Задачу согласования решает алгоритм медленного старта, постепенно повышающий темп передачи данных от медленного до "оптимального", при котором нет повторных передач TCP-пакетов. Алгоритм использует так называемое "окно перегруженности" (congestion window), используемое на передающей стороне для определения максимального объема передаваемых данных вместо размера, получаемого от принимающей стороны в поле окна подтверждающего пакета.
Размер "окна перегруженности" определяется на передающей стороне путем постепенного его увеличения до момента появления повторных передач (ясно, что размер этого окна никогда не превышает размера окна на принимающей стороне). Однажды определенный размер "окна перегруженности" остается неизменным, пока вновь не появятся повторные передачи, однако периодически делаются осторожные попытки и увеличить этот размер.
Эксперименты показали, что данный алгоритм позволяет уменьшить количество повторно передаваемых TCP-пакетов на 50% и повысить пропускную способность сети на 30%.
Протокол дэйтаграмм пользователя UDP (User Datagram Protocol) является протоколом транспортного уровня и базируется на возможностях, предоставляемых межсетевым протоколом IP. Основная задача TCP - обеспечение "быстрой" передачи данных в сети. Его транспортный адрес в заголовке IP-сегмента равен 17. Описание протокола UDP дано в RFC 768.
Его основные характеристики перечислены ниже:
Следует отметить, что, по сути дела, протокол транспортного уровня UDP играет роль интерфейса для прикладных программ к средствам протокола межсетевого уровня IP.
На рис. 5.1 приведен формат заголовка UDP-пакета.
0 15 31 +------------------------------+-------------------------------+ | Порт источника | Порт приемника | +------------------------------+-------------------------------+ | Длина | Контрольная сумма | +------------------------------+-------------------------------+ Рис. 5.1
Порт источника и порт приемника
16-битовые поля, содержащие номера портов, соответственно, источника и адресата UDP-пакета. Понятие "номер порта" обсуждается в "Протокол управления передачей TCP".
Длина
16-битовое поле, содержащее длину (в байтах) всего UDP-пакета, включая заголовок и данные.
Контрольная сумма
16-битовое поле, содержащее Internet-контрольную сумму, подсчитанную для UDP-заголовка, данных пакета и псевдозаголовка. Псевдозаголовок (такой же, как для подсчета контрольной суммы в TCP-заголовке) включает в себя ряд полей IP-заголовка и имеет показанную на рис. 5.2 структуру.
0 7 15 31 +-----------+-----------+-----------------------+ | IP-адрес источника | +-----------+-----------+-----------------------+ | IP-адрес приемника | +-----------+-----------+-----------------------+ | Нули | Транспорт | Длина IP-сегмента | +-----------+-----------+-----------------------+ Рис. 5.2
Если поле "Контрольная сумма" UDP-заголовка содержит нулевое значение, это означает, что источник UDP-пакета контрольную сумму не подсчитывал, и приемник выполнять ее проверку не должен. Некоторые реализации протокола UDP (например, в SunOS - клоне ОС UNIX от Sun Microsystems) контрольную сумму не подсчитывают в принципе, полагаясь на возможности контроля целостности данных, реализованные в протоколах сетевого уровня (например, в Ethernet).
Межсетевой протокол управляющих сообщений ICMP (Internet Control Message Protocol), специфицированный в RFC 792, играет роль транспортного протокола для управляющей и диагностической информации, которой обмениваются между собой IP-, TCP- или UDP-модули скрытно от приложений. Протокол ICMP поддерживается в обязательном порядке ка ждым IP-модулем. Его транспортный адрес в IP-заголовке равен 1.
Поскольку протокол ICMP используется для транспортировки весьма различной информации, то фиксируется лишь общая структура заголовка ICMP-пакета, имеющего формат, показанный на рис. 6.1.
0 7 15 31 +-----------+-----------+-----------------------+ | Тип | Код | Контрольная сумма | +-----------+-----------+-----------------------+ | Разное | +-----------------------------------------------+ : Тело пакета: : : IP-заголовок и следующие за ним 8 байт данных : : или : : тестовые данные : +-----------------------------------------------+ Рис. 6.1
Тип
однобайтовое поле, содержащее идентификатор типа ICMP-пакета. Возможные значения этого поля приведены в таблице.
-----------+-------------------------------- Поле "Тип" | Назначение -----------+-------------------------------- 0 Ответ на запрос эха 3 Адресат недоступен 4 Подавление источника 5 Перенаправление 8 Запрос эха 11 Исчерпано время жизни 12 Ошибка в параметре 13 Запрос временной метки 14 Ответ на запрос временной метки -----------+--------------------------------
Код
однобайтовое поле, значение которого конкретизирует назначение ICMP-пакета определенного типа.
Контрольная сумма
16-битовое поле, содержащее Internet-контрольную сумму, подсчитанную для всего ICMP-пакета целиком.
Разное
четырезбайтовое поле, предназначенное для хранения разнообразной информации, специфичной для ICMP-пакетов определенного типа (например, номера в TCP-последовательности, IP-адреса и т.п.).
Тело пакета
Здесь содержится заголовок IP-сегмента, явившегося порождения данного ICMP-пакета, и первые 8 байт данных тела этого IP-сегмента. Если ICMP-пакет есть результат проявления аномалии в TCP- или UDP-взаимодействии, то эти 8 байт будут представлять собой первые восемь байтов, соответственно, TCP- или UDP-заголовка, что дает возможность определить, в частности, номера портов (а, следовательно, и использующие их прикладные программы).
Для ICMP-пакетов некоторых типов это может содержать не начало IP-сегмента, а тестовые данные.
Источниками и обработчиками ICMP-пакетов могуть быть как IP-модули, так и TCP- и UDP-модули (но никогда прикладные программы).
Проблемы в доставке и обработке ICMP-пакетов никогда не приводят к порождению новых ICMP-пакетов, уведомляющих об этих проблемах. Сделано это с целью избежать возможных бесконечных циклов генерации ICMP-пакетов в сети.
Здесь рассматриваются 6 типов ICMP-пакетов, реализованных во всех клонах и версиях ОС UNIX.
ICMP-пакет этого типа генерируется в следующих случаях:
В ситуациях, когда некоторый узел (как правило, шлюз) не имеет достаточно места в своих буферах для размещения интенсивно поступающих к нему данных, он может послать узлам-источникам ICMP-пакет данного типа (source quench). Узел-источник в ответ на такое уведомление обязан уменьшить темп передачи данных.
В ранних UNIX-реализациях протоколов TCP/IP ICMP-пакеты этого типа игнорировались. В TCP-реализациях, поддерживающих алгоритм медленного старта, в ответ на это сообщение уменьшается размер "окна перегруженности". UDP-модули игнорируют это сообщение, информируя при этом обслуживаемую прикладную программу о требовании приемника уменьшить интенсивность и/или размер дэйтаграмм.
ICMP-пакет этого типа посылается источнику данных, когда узел-шлюз обнаруживает, что источник может направлять свои данные непосредственно к следующему шлюзу маршрута. Такой ICMP-пакет содержит в себе IP-адрес этого шлюза. Этот IP-адрес должен быть включен в таблицу маршрутизации на узле-источнике данных.
Для реализации эха IP-модуль на узле A отправляет узлу B ICMP-пакет типа "запрос эха", содержащий в своем теле вместо IP-заголовка тестовые данные произвольной длины. Узел B, получив такой запрос, возвращает узлу A ICMP-пакет типа "ответ на запрос эха", содержащий те же данные, что и в запросе. Эхо-посылки используются для проверки достижимости удаленных узлов сети и измерения времени прохождения данных.
ICMP-пакет данного типа посылается источнику IP-сегмента, который должен быть сброшен по одной из двух причин:
1) исчерпано время жизни IP-сегмента;
2) исчерпано допустимое время на сборку фрагментированного IP-сегмента.
С помощью ICMP-пакета данного типа источник IP-сегмента информируется о том, что данный сегмент сброшен вследствие наличия ошибки в каком-либо из полей его заголовка.
Содержание
Ниже кратко описывается реализвция стека протоколов TCP/IP на базе ряда протоколов сетевого уровня.
Протокол Ethernet был разработан в начале 1970-х годов совместно фирмами Xerox, DEC и Intel. На его базе в 1982 г. был принят международный стандарт IEEE 802.3 .
Использование протокола сетевого уровня Ethernet совместно с протоколами TCP/IP регламентируется RFC 894.
Основными характеристиками протокола Ethernet являются следующие:
В качестве физической среды передачи данных Ethernet использует:
В первых трех случаях физическая топология сети реально является шинной, в последнем - физическая топология сети представляет собой "звезду".
Примечание. Существуют современные версии Ethernet, обеспечивающие скорость передачи в 100 мегабит в секунду.
Примечание. Ethernet позволяет объединить в локальную сеть узлы, расположенные друг от друга на расстоянии от нескольких десятков метров (10baseT) до нескольких километров (сегменты 10base5, связанные повторителями).
Механизм CSMA/CD (Carrier Sense Multiple Acces with Collision Detection - Множественный Доступ с Контролем Носителя и Обнаружением Столкновений) подразумевает следующий алгоритм получения узлом сети доступа к шине:
Обмен данными по протоколу Ethernet всегда реализуется программно-аппаратно с помощью двух компонентов:
Примечание. В ОС UNIX сетевой контроллер и его драйвер принято называть "сетевым интерфейсом".
На рис. 7.1 представлен формат кадра данных протокола Ethernet. Для иллюстративных целей показана вложенность в кадр IP-сегмента (содержащего, в свою очередь, TCP-пакет).
+-----------+ | Преамбула | | (64 бита) | +-----------+ | Адрес | | приемника | | (48 бит) | +-----------+ | Адрес | | источника | | (48 бит) | +-----------+ | Тип кадра | | (16 бит) | IP-сегмент +-----------+ - - +--------------+ | | | IP-заголовок | | Тело | | (>= 20 байт) | TCP-пакет | кадра | +--------------+ - - +---------------+ | (область | | | | TCP-заголовок | | данных) | | Тело | | (>= 20 байт) | | | | IP-сегмента | +---------------+ | | | (TCP-, UDP- | | Тело | | (максимум | | или ICMP- | | TCP-пакета | | 1500 байт)| | пакет) | | (данные | | | | | | пользователя) | +-----------+ - - +--------------+ - - +---------------+ | Контроль- | | ная сумма | | (32 бита) | Рис. 7.1 +-----------+
Преамбула
64-битовое поле, содержащее фиксированную последовательность битов, используемую для синхронизации схем приема сигналов на узле-адресате.
Адрес приемника и адрес источника
48-битовые поля, содержащие Ethernet-адреса принимающего и передающего кадр узлов сети.
Каждый Ethernet-контроллер в мире имеет уникальный 6-байтовый адрес. Ethernet-адрес принято записывать в виде последовательности шести разделенных символом "двоеточие" двузначных шестнадцатиричных чисел, где каждое число представляет собой значение одного байта адреса, напрмер, f1:e2:d3:c4:b5:a6.
Примечание. Как правило, Ethernet-адрес жестко "зашит" в контроллере, однако существуют контроллеры, допускающие его изменение программным путем.
Тип кадра
16-битовое поле, содержащее идентификатор протокола вышележащего уровня, использующего данный Ethernet-кадр. Т.е. поле определяет принадлежность содержимого тела кадра. Наличие данного поля в кадре обеспечивает возможность функционирования в одной сети на базе Ethernet одновременного нескольких различных стеков протоколов, а не только одного TCP/IP.
Примерами значений данного поля являются следующие:
Тело кадра
содержит данные, передаваемые в кадре протоколом вышележащего уровня (на рисунке это IP-сегмент, тело которого используется для пересылки TCP-пакета).
Максимальная длина тела кадра протокола сетевого уровня обозначается как MTU (Maximum Transmission Unit) и для Ethernet составляет 1500 байтов.
Контрольная сумма
32-битовое поле, содержащее CRC-контрольную сумму, подсчитанную для всего кадра.
Минимальная длина Ethernet-кадра составляет 64 байта (512 бит). Такое ограничение связано с тем, что контроль столкновений различных кадров в Ethernet-шине согласно алгоритму CSMA/CD выполняется на интервале времени в 47 микросекунд. За это время осуществляется передача 470 бит (при скорости 10 мегабит в секунду), так что 512 - это округление 470 до числа, являющегося степенью 2.
В ситуациях, когда длина данных, передаваемых в теле кадра, недостаточна для формирования кадра длиной не менее 64 байтов, драйвер Ethernet-контроллера искусственно дополняет тело пакета до необходимой длины.
Примечание. Интересно, что согласно стандарту IEEE 802.3 рассмотренное выше 16-битовое поле типа кадра на самом деле является полем длины (в байтах) тела Ethernet-кадра. Для идентификации типа содержимого тела кадра предлагается использовать специальный протокол LLC (Logic Link Control - протокол управления логической связью), занимающий промежуточное положение между Ethernet и вышележащими протоколами. Однако протокол LLC в среде UNIX (а, значит, и в большинстве других ОС) реализован не был: стандартом "de facto" остаются спецификации RFC 894. Хотя надо отметить, что выбор значений идентификаторов типа кадра (0x0800 и больше) не исключает возможности использования этого поля одновременно и для идентификации типа, и для хранения длины тела кадра (максимум 1500).
Ethernet подобно другим протоколам сетевого уровня обладает собственной системой адресации узлов сети, отличной от системы адресации, принятой в TCP/IP. Это приводит к необходимости взаимной трансляции адресов "IP-адрес в Ethernet-адрес" и обратно.
В UNIX-системах такая трансляция выполняется с помощью специальной таблицы соответствий пар адресов различного типа, которая динамически создается и обновляется сетевым интерфейсом. В момент активизации сетевого интерфейса содержимое таблицы трансляции может загружаться из созданного вручную специального административного файла, однако это не является обязательным.
Для поддержания таблицы трансляции в актуальном состоянии, отражающем текущий состав узлов Ethernet-сети, используется протокол ARP (Address Resolution Protocol), описанный в RFC 826.
Структура ARP-сегмента приведена на рис. 7.2.
0 7 15 31 +--------------+---------------+-------------------------------+ | Hardware | Идентификатор протокола | +--------------+---------------+-------------------------------+ | Длина | Длина | Операция | | HW-адреса | адреса | | +==============+===============+===============================+ | Ethernet-адрес источника (байты 0...3) | +------------------------------+-------------------------------+ | Ethernet-адрес источника | IP-адрес источника | | (байты 4...5) | (байты 0...1) | +------------------------------+-------------------------------+ | IP-адрес источника | Ethernet-адрес приемника | | (байты 2...3) | (байты 0...1) | +------------------------------+-------------------------------+ | Ethernet-адрес приемника (байты 2...5) | +--------------------------------------------------------------+ | IP-адрес приемника (байты 0...3) | +--------------------------------------------------------------+ Рис. 7.2
Поле "Hardware" содержит идентификатор типа адреса на сетевом уровне (в нашем случае - Ethernet).
Поле "Идентификатор протокола" определяет протокол межсетевого уровня (в нашем случае - IP).
Поля длин задают длину адресов ( в нашем случае: "Длина HW-адреса" равна 6, а "Длина адреса" равна 4).
Поле "Операция" содержит идентификатор типа ARP-сегмента (запрос или ответ).
Примечание. Как видно из структуры ARP-сегмента протокол ARP может быть использован для совместной работы TCP/IP не только с протоколом Ethernet, но и с другими протоколами сетевого уровня, когда в этом есть необходимость.
Алгоритм использования протокола ARP для построения таблицы трансляции на некотором узле сети (назовем его А) выглядит следующим образом.
Для того, чтобы таблица трансляции адресов с малым временем реакции отслеживала изменения в сети, ее строки периодически (через 1...20 минут) принудительно очищаются.
Примечание. Очевидно, что использование протокола ARP возможно только для сетей, обеспечивающих широковещательную рассылку данных.
Задачу построения строк таблицы трансляции по известному Ethernet-адресу решает протокол RARP (Reverse ARP), описанный в RFC 903 и использующий сегмент той же структуры, что протокол ARP. Определение IP-адреса по известному Ethernet-адресу требуется в момент начальной загрузки бездисковых ЭВМ, подключенных к сети.
Примечание. Использование протоколов ARP и RARP может быть отключено системным администратором.
Протокол SLIP (Serial Line Internet Protocol) обеспечивает соединение двух ЭВМ через последовательный интерфейс (например, V.24). Протокол SLIP описан в RFC 1055.
Протокол очень прост. Все SLIP-кадры начинаются со служебного символа 0xEB, называемого ESC, а заканчиваются служебным символом 0xC0, называемым END. Между этими символами располагаются передаваемые данные.
Если служебные символы встречаются в передаваемых данных, то они отсылаются приемнику в виде двухбайтовых последовательностей: {ESC, 0xEC} и {ESC, 0xED}. На принимающей стороне двухбайтовые последовательности преобразуются в ESC и END.
RFC 1055 не специфицирует максимальной длины кадра (MTU), но существующие реализации протокола ориентированы на значение MTU равное 1006 байт.
Примечание. Очевидно, что скорость передачи данных по последовательному интерфейсу невелика. Для повышения эффективности протокола SLIP в RFC 1144 была предложена его модификация, учитывающая то обстоятельство, что при TCP-взаимодействии по последовательной линии большинство полей IP- и TCP-заголовков остаются неизменными на все время логического соединения. Данная модификация SLIP реально пересылает в своих кадрах только те поля IP- и TCP-заголовков, которые меняют свое значение от кадра к кадру.
Протокол PPP (Point-to-Point Protocol) также может быть использован для соединения двух ЭВМ по последовательному интерфейсу. Протокол PPP (RFC 1331) разработан позднее протокола SLIP, поэтому в нем ликвидированы некоторые недостатки протокола SLIP, в частности:
Для идентификации границ PPP-кадра используется служебный символ 0x7E.
Передаче данных по протоколу PPP предшествует этап тестирования и конфигурирования соединения с помощью протокола LCP (Link Control Protocol), являющегося частью PPP. LCP используется и для завершения соединения.
Кроме того, для обмена управляющей информацией используется протокол NCP (Network Control Protocol). Каждый протокол, лежащий выше PPP, имеет свою версию протокола NCP. NCP, определенный для протокола IP, носит название IPCP (Internet Protocol Control Protocol).
Данное пособие содержит описание технологии организации взаимодействия прикладных программ, параллельно функционирующих на различных узлах вычислительной сети в среде ОС UNIX. Формулируются требования к такой технологии, описывается предлагаемая модель взаимодействия. Приведен пример использования технологии.
Одной из наиболее актуальных проблем в разработке современных систем автоматизированного проектирования (САПР) и иных сложных программных систем является организация кооперативного решения прикладных задач параллельно функционирующими на различных узлах локальной вычислительной сети (ЛВС) подсистемами.
Решение проблемы невозможно без создания технологии и средств, обеспечивающих взаимодействие параллельно выполняющихся прикладных программ (ПП).
Такая технология должна отвечать набору, по крайней мере, следующих требований.
Ниже дается краткий обзор существующих технологий и средств организации взаимодействия ПП в ОС UNIX, предлагается модель взаимодействия, отвечающая перечисленным требованиям, кратко описывается ее реализация в среде ОС UNIX и приводится пример использования.
Современные версии ОС UNIX стандартно предоставляют прикладным программистам следующие средства разработки распределенных приложений:
Наибольшее распространение при построении ЛВС из UNIX-машин получил комплекс протоколов TCP/IP.
Socket-интерфейс был первоначально разработан для версий BSD4.2 и BSD4.3 ОС UNIX. Socket представляет собой канал двунаправленной связи с коммуникационной средой ЛВС, для манипулирования которым используется обычный файловый дескриптор ОС UNIX. Библиотека socket-интерфейса включает в себя функции открытия и закрытия socket'а, привязки к socket'у сетевого идентификатора, "прослушивания" сети и приема запросов на соединение через socket (на стороне сервера), инициализации соединения через socket (на стороне клиента), обмена данными и др.
Интерфейс транспортного уровня (TLI - Transport Level Interface) идет на смену socket-интерфейсу, используя ту же идеологию, но обеспечивая более высокую степень независимости прикладных программ от используемых сетевых протоколов.
Оба интерфейса поддерживают модель взаимодействия "клиент-сервер" и различные типы связи (с установлением соединения - потоковый тип, без установления - дэйтаграммный), различные режимы (синхронный и асинхронный) и дают возможность использовать стандартные операции ввода-вывода ОС UNIX.
Однако, недостатком средств, предоставляемых двумя этими интерфейсами, с точки зрения прикладного программиста является их "низкоуровневость", возлагающая на него необходимость разработки средств:
Наиболее удобны для прикладного программиста высокоуровневые RPC-средства, первоначально разработанные фирмой Sun Microsystems и ставшие международным промышленным стандартом. Основные достоинства этих средств составляют:
Проведенный анализ средств взаимодействия показал, что именно RPC-средства, хотя они и реализуют классическую модель "клиент-сервер", в наибольшей степени удобны для создания технологии, отвечающей требованиям, перечисленным выше.
В основе этой технологии лежит описываемая далее модель взаимодействия.
Используемая в технологии модель взаимодействия построена на основе модели, предложенной организацией CAD Framework Initiative в ее стандарте DIS-92-5-3 1.0 Inter-Tool Communication Programming Interface.
Любая ПП, нуждающаяся в обмене информацией с другими ПП, выполняющимися одновременно с ней, должна подключиться к коммуникационной среде.
Коммуникационная среда покрывает одну локальную ЭВМ или несколько узлов вычислительной сети. Границы среды динамически меняются с подключением/отключением от нее ПП. Работой коммуникационной среды управляет коммуникационный сервер, резидентный на одном из узлов сети.
Каждая ПП для работы с коммуникационной средой обязана зарегистрировать себя и создать один или несколько каналов связи со средой. Перед завершением своей работы ПП должна отключиться от среды, предварительно закрыв (разрушив) все созданные каналы связи. Подключение к коммуникационной среде (отключение от нее), открытие (закрытие) каналов связи может выполнено ПП в любой необходимый ей момент времени, определяемый логикой ее работы.
ПП, подключенные к коммуникационной среде, получают возможность посылать и принимать через открытые каналы связи сообщения.
Сообщения характеризуются именем (играющим роль, по сути дела, типа сообщения) и имеют тело. Имена (типы) сообщений и содержащаяся в них информация никак рассматриваемой моделью не регламентируются.
Тело сообщения состоит из конечного числа слотов. Слот характеризуется именем, типом содержащегося в нем значения и самим значением. Допустимыми типами слотов являются:
Доступ к слоту тела сообщения может осуществляться как по его имени, так и по номеру в теле. При пересылке сообщения с ЭВМ одного типа на ЭВМ другого типа гарантируется корректное преобразование значений слотов первых трех типов в машинный формат ЭВМ-приемника из формата ЭВМ-источника. Массив же байт пересылается без каких-либо преобразований.
Допустимы сообщения следующих трех видов:
Сообщение, направляемое ПП в коммуникационную среду через канал связи, является широковещательным по определению, т.е. направляется всем без исключения ПП, подключенным в данный момент к коммуникационной среде (в том числе и самой посылающей ПП). Нет способа послать сообщение целенаправленно какой-либо ПП.
Для того, чтобы получать сообщения определенного типа и содержания, ПП должна зарегистрировать свой интерес, создав для соответствующего канала связи объект, называемый приемником.
При создании приемника ПП обязана специфицировать:
Допустимы два режима обработки запросов - пассивный и активный. Активный режим подразумевает намерение ответить на полученный запрос, пассивный - оставить запрос без ответа. Зарегистрировать свой интерес в получении запроса одного и того же типа может любое количество ПП, но ответ должен быть только один.
Каждое сообщение любого вида, поступившее в коммуникационную среду от любого ПП, "фильтруется" через все имеющиеся приемники.
Считается, что сообщение интересует ПП, если:
а) тело сообщения содержит в себе слоты одноименные и однотипные всем слотам контекста (при этом в соспоставлении участвуют только слоты типов "строка символов" и "длинное целое число");
б) значения всех слотов совпадают.
В случае удачного сопоставления автоматически и асинхронно вызывается функция-обработчик, ассоциированная с данным приемником. Функции в качестве одного из аргументов передается для обработки полученное сообщение.
В течение времени выполнения функции-обработчика никакие обращения к ПП через коммуникационную среду невозможны. Кроме необходимости учета данного обстоятельства никакие другие особые требования к функциям-обработчикам не предъявляются.
В ситуациях, когда вычисление ответа требует заметного времени и/или связано с обращениями, в свою очередь, с запросами через коммуникационную среду к третьим ПП, функция имеет возможность отложить запрос на неопределенное время. ПП может позже в любой удобный момент инициировать "посылку" ей отложенного запроса.
Сформированный функцией-обработчиком ответ рассылается через коммуникационную среду всем заинтересованным получателям (в том числе, конечно, и ПП-источнику запроса).
Если для получения запроса определенного типа в среде имеется несколько приемников для разных ПП, то запрос последовательно будет пересылаться к ПП через каждый из них, пока не будет получен первый ответ. Нет средств управления порядком перебора приемников.
Если для данного запроса в среде нет ни одного приемника или ни одна ПП не смогла дать ответ на запрос, то ПП, пославшая запрос, получит соответствующее уведомление.
С точки зрения организации взаимодействия с ПП-партнерами все ПП могут быть условно разделены на две группы.
Типичная последовательность действий ПП первой группы при работе с коммуникационной средой выглядит следующим образом.
ПП второй группы много сложнее. Для них типичная последовательность действий в части взаимодействия через коммуникационную среду выглядит следующим образом.
Рассмотренная модель взаимодействия не накладывает никаких ограничений ни на логику функционирования ПП, ни на тип рассылаемых сообщений. В задачу прикладных программистов входит разработка прикладных протоколов, требуемых для кооперативного решения прикладных задач, и создание необходимых словарей сообщений.
Для реализации рассмотренной выше модели взаимодействия были использованы RPC-средства в силу их достоинств, перечисленных в разделе 2.
Реализация включает в себя коммуникационный сервер (сервер сообщений), управляющий работой коммуникационной среды, и клиентную часть, обеспечивающую прикладным программам процедурный интерфейс к средствам взаимодействия.
Коммуникационный сервер регистрирует подключаемые к среде ПП, организует с ними каналы связи, диспетчеризует и распределяет сообщения. Коммуникационный сервер функционирует как фоновый процесс (демон, в терминологии ОС UNIX) на одном из узлов вычислительной сети.
Клиентная часть реализована в виде библиотеки объектных файлов функций языка программирования СИ, образуюших процедурный интерфейс к средствам взаимодействия. Эта библиотека болжна быть подключена при компоновке исполняемого файла прикладной программы.
Библиотека включает в себя функции (около 30) следующих пяти групп:
Тестирование разработанных средств было выполнено в локальных сетях на базе протоколов TCP/IP с использованием операционных систем SunOS 4.1 (ЭВМ SPARCstation), Ultrix 3.0 (ЭВМ mVAX II) и SunOS 5.2 (ЭВМ типа IBM PC и SPARCstation).
Примером использования описанной технологии может служить распределенная система моделирования на базе программно-методического комплекса (ПМК) ПА8.
Распределенная система моделирования состоит из трех подсистем.
Первоначально каждая из указанных подсистем создавалась для автономной работы и поддерживала обмен данными с другими подсистемами через файлы. Однако создание средств организации взаимодействия подсистем дало возможность обеспечить их совместную работу как на отдельной ЭВМ, так и в локальной вычислительной сети.
Были разработаны два следующих прикладных протокола:
Таким образом пользователь распределенной системы получил возможность оперативно "оптимизировать" численные значения параметров объектов проектирования, без задержки наблюдая результат своих изменений в окне ГВ.
Возможность выполнения параллельно функционирующих ГРС, ПА8 и ГВ на различных узлах ЛВС обеспечивает рациональное использование вычислительных ресурсов сети и повышает "реактивность" системы.
Средства вызова удаленных процедур (RPC) является составной частью более общего средства, называемого Open Network Computing (ONC), разработанного фирмой Sun Microsystems и получившего всеобщее признание в качестве промышленного стандарта.
ONC помимо RPC включает в себя средства внешнего представления данных (XDR), необходимые для организации обмена информацией в гетерогенных сетях, включающих в себя ЭВМ различной архитектуры, и средства монтирования удаленных файловых систем (NFS), обеспечивающее доступ пользователям локального узла сети к файлам, физически расположенным на удаленных узлах, как к файлам локальным.
Средство RPC реализует модель "клиент-сервер", где роль клиента играет прикладная программа, обращающаяся к набору процедур (функций), исполняемых на удаленном узле в качестве сервера. RPC предоставляет прикладным программистам сервис более высокого уровня, чем ранее рассмотренных два, т.к. обращение за услугой к удаленным процедурам выполняется в привычной для программиста манере вызова "локальной" функции языка программирования СИ. RPC реализовано, как правило, на базе socket-интерфейса и/или TLI. При пересылке данных между узлами сети в RPC для их внешнего представления используется стандарт XDR.
Средство RPC предоставляет программистам сервис трех уровней:
В данном учебном учебном пособии рассматриваются только средства RPC среднего уровня.
Согласно идеологии RPC все процедуры (функции) некоторого распределенного приложения, планируемые к исполнению на одном и том же удаленном узле вычислительной сети, объединяются в единый модуль, оформляемый в виде исполняемого файла и характеризующегося уникальным "номером программы". Допустимо иметь несколько вариантов такого модуля, идентифицируемых уникальным "номером версии". Каждая процедура в составе модуля имеет уникальный "номер процедуры". Таким образом, для однозначной идентификации конкретной процедуры-сервера используется четверка:
Каждая процедура-сервер прежде, чем она станет доступной для обращения к ней, должна быть зарегистрирована на соответствующем узле сети. Регистрация процедуры делает ее известной под соответствующими номерами (программы, версии и, собственно, процедуры) сетевому демону portmapper на локальном узле сети. Удаленный RPC-клиент, обращаясь скрытно от пользователя к этому демону, может получить точный сетевой адрес процедуры, который и будет использовать в дальнейшем для прямых обращений к процедуре-серверу.
Процедура-сервер, создаваемая средствами RPC любого уровня, должна иметь единственный аргумент и единственный результат. Это ограничение заставляет прикладного программиста в случае необходимости передачи в процедуру (или возврата из нее) нескольких аргументов (результатов) компоновать их в сложные агрегаты данных (структуры, массивы, списки и т.п.).
Для создания распределенных приложений средствами RPC среднего уровня достаточно использовать три функции: registerrpc, svc_run (на стороне сервера) и callrpc (на стороне клиента).
Примечание. Столь малое количество функций объясняется тем, что средний уровень средств RPC беден с точки зрения возможностей выбора используемого транспорта данных, управления количеством ретрансляций данных, назначения тайм-аутов, организации асинхронной обработки и т.п.
Программисты распределенных приложений кроме собственно функций RPC обязаны также использовать функции преобразования данных во внешнее представление согласно стандарту XDR (так называемые XDR-функции).
Регистрация процедуры в качестве сервера на узле сети выполняется функцией registerrpc, имеющей следующий вид
#include <sys/types.h> #include <rpc/rpc.h> int registerrpc (prognum, vernum, procnum, procname, inproc, outproc) u_long prognum; u_long vernum; u_long procnum; char *(*procname) (); xdrproc_t inproc; xdrproc_t outproc;
Аргументы prognum, vernum и procnum задают номера программы, версии и процедуры соответственно. Номера версии и процедуры назначаются программистом произвольно. Номер же программы, находящейся в стадии разработки, должен назначаться из диапазона 0x20000000...0x3fffffff.
Аргумент procname задает функцию языка программирования СИ, регистрируемую в качестве сервера. Эта функция (процедура) вызывается с указателем на ее аргумент и должна возвращать указатель на свой результат, располагаемый в статической или динамически выделенной (функциями malloc или calloc) памяти. Для хранения результата нельзя использовать автоматически выделяемую память (напоминаем, что локальные переменные функций располагаются именно в такой памяти).
Аргументы inproc и outproc задают XDR-функции преобразования, соответственно, аргумента и ее результата.
При успешном выполнении функция registerrpc возвращает 0, иначе - число "-1".
Для приема запросов к процедурам-серверам от клиентов и диспетчеризации их используется функция svc_run, имеющая следующий вид
#include <rpc/rpc.h> void svc_run ();
Не имеющая аргументов функция svc_run должна вызываться после регистрации всех диспетчируемых ею процедур-серверов. При успешном выполнении svc_run никогда не возвращает управление в вызвавшую ее программу.
Для запроса к удаленной процедуре-серверу из программы-клиента используется функция callrpc, имеющая следующий вид
#include <sys/types.h> #include <rpc/rpc.h> int callrpc (host, prognum, vernum, procnum, inproc, in, outproc, out) char *host; u_long prognum; u_long vernum; u_long procnum; xdrproc_t inproc; char *in; xdrproc_t outproc; char *out;
Аргумент host задает имя узла, на котором функционирует вызы- ваемая процедура-сервер.
Аргументы prognum, vernum и procnum задают номера программы, версии и, собственно, вызываемой процедуры-сервера. К моменту вызова процедуры она должна быть зарегистрирована на узле сети, определяемом аргументом host.
Аргумент in должен указывать на данные, передаваемые процеду- ре-серверу в качестве аргумента.
Аргумент out должен указывать на область памяти, предназначенную для размещения в ней результата работы процедуры-сервера.
Аргументы inproc и outproc задают XDR-функции преобразования, соответственно, аргумента процедуры-сервера и ее результата.
На время обработки процедурой-сервером запроса к ней программа-клиент переходит в состояние ожидания результата.
При успешном выполнении вызова удаленной процедуры-сервера функция registerrpc возвращает 0, иначе - число "-1".
Для преобразования данных в/из XDR-формат библиотека функций RPC содержит ряд функций, некоторые из них перечислены ниже:
В ситуациях, когда в процедуру-сервер аргумент не передается (или от нее не возвращается результат), используется функция-"заглушка" xdr_void.
XDR-функции универсальны: в зависимости от контекста их использования они могут преобразовывать данные из внутреннего представления, специфичного для ЭВМ данной архитектуры, во внешнее согласно стандарту XDR и наоборот, а также динамически выделять/освобождать память под сложные агрегаты данных.
При необходимости прикладной программист может легко расширить имеющийся набор XDR-функций, создав свои собственные на базе уже имеющихся.
В данном разделе рассматривается использование средств RPC на очень простом примере взаимодействия программы-клиента с одной удаленной процедурой-сервером.
Содержательная часть программ примитивна:
При разработке прикладных программ средствами RPC целесообразно для номеров программ, версий и процедур определять символические имена в едином файле, включаемом в исходный текст как процедур-серверов, так и клиентов. В нашем примере таким файлом будет файл common.h, имеющий следующий вид
#define MY_PROG (u_long) 0x20000001 #define MY_VER (u_long) 1 #define MY_PROC1 (u_long) 1
Текст программы-сервера на языке программирования СИ выглядит следующим образом
1 #include <rpc/rpc.h> 2 #include <stdio.h> 3 #include "common.h" 4 char **proc1 (); 5 main () 6 { 7 registerrpc (MY_PROG, MY_VER, MY_PROC1, proc1, xdr_int, xdr_wrapstring); 8 svc_run(); 9 fprintf (stderr, "Error: svc_run returned\n"); 10 exit (1); 11 } 12 char **proc1 (indata_p) 13 int *indata_p; 14 { 15 static char *res; 16 static char even[] = {"even"}; 17 static char odd[] = {"odd"}; 18 printf ("Number recieved is %d\n", *indata_p); 19 if (*indata_p % 2) { 20 res = odd; } 21 else { 22 res = even; }; 23 return &res; 24 }
Строки 1...3 описывают включаемые файлы, содержащие определения для всех необходимых структур данных и символических констант.
Строка 4 объявляет тип функции proc1. Это объявление необходимо, поскольку в нашей программе функция proc1 используется (в строке 7) раньше по тексту, чем определяется (в строке 12).
В строке 7 функция proc1 регистрируется в качестве процедуры-сервера, имеющей аргумент в виде целого числа и результат в виде строки символов. Таким образом можно зарегистрировать произвольное количество процедур-серверов с одинаковыми номерами программы и версии.
В строке 8 программа посредством обращения к функции svc_run переходит в состояние ожидания запросов клиентов. Нормально функционирующая программа никогда к выполнению следующих за вызовом svc_run операторов не переходит.
В строках 9 и 10 выводится сообщение об ошибке и завершается выполнение программы с кодом ошибки 1.
Строки 12...24 содержат описание функции proc1. Необходимо еще раз подчеркнуть, что функция работает с указателями на ее аргумент и результат.
Переменные res, even и odd объявлены статическими, чтобы они сохранялись в памяти программы и после выхода из функции proc1.
Текст программы-клиента на языке программирования СИ выглядит следующим образом
1 #include <rpc/rpc.h> 2 #include <stdio.h> 3 #include "common.h" 4 main (argc, argv) 5 int argc; 6 char **argv; 7 { 8 int arg; 9 char *answer; 10 int stat; 11 if (argc < 3) exit (1); 12 arg = strlen (argv[2]); 13 if (stat = callrpc (argv[1], MY_PROG, MY_VER, MY_PROC1, xdr_int, &arg, xdr_wrapstring, &answer) != 0) { 14 clnt_perrno (stat); 15 exit (2); 16 }; 17 printf ("Number of letters in %s is %s\n", argv[2], answer); 18 exit (0); 19 }
В строке 11 проверяется количество аргументов командной строки, с помощью которой программа-клиент вызывается на выполнение (аргументов не должно быть меньше двух).
В строке 12 переменной целого типа arg присваивается число, равное количеству символов второго аргумента командной строки.
В строке 13 вызывается удаленная процедура-сервер, резидентная на узле сети, имя которого задается первым аргументом командной строки. Процедуре передается указатель на ее аргумент (целое число), сама же она возвращает также указатель на свой результат (при этом память под возвращаемую строку символов нужного размера автоматически выделяется XDR-функцией xdr_wrapstring).
Строка 14 содержит обращение к функции clnt_perrno, которая выводит в stderr текст сообщения, характеризующего ошибку, которая по разным причинам может возникнуть при выполнении вызова удаленной процедуры.
Строка 17 выводит в stdout сообщение, содержащее ответ удаленной процедуры-сервера.
В серию учебных пособий "TCP/IP для программиста" входят следующие три пособия:
Пособие предназначено для программистов, разрабатывающих "нетривиальные" сетевые приложения, и для администраторов сетей на основе TCP/IP, интересующихся предметом своей деятельности. Изложение материала ориентировано на обучающихся, знакомых с идеологией и терминологией в области информационно-вычислительных сетей.
Приведены спецификации наиболее часто используемых функций, объяснено их назначение, даны простые примеры сетевых программ, реализующих модель взаимодействия "клиент-сервер".
Освоение материала требует от обучающегося знания основ языка программирования СИ.
Подробные описания технологии взаимодействия, программных средств и процедурного интерфейса, а также исходные тексты на языке СИ, инструкция по установке в различных версиях ОС UNIX доступны по адресу: ftp://wwwcdl.bmstu.ru/pub/tools/cfitc.tar.gz.
Данное учебное пособие посвящено изучению средств программирования сетевых приложений в среде операционной системы UNIX.
Основным средством построения информационно-вычислительных сетей из ЭВМ, работающих под управлением ОС UNIX, является комплекс протоколов TCP/IP. Программное обеспечение, реализующее данные протоколы информационного обмена, как правило, входит в состав базового варианта любой современной версии этой ОС.
Для создания распределенных приложений в среде ОС UNIX наибольшее распространение получили следующие три средства:
Причем socket-интерфейс и TLI обеспечивают возможность прикладным программам взаимодействовать, используя стандартные (или очень похожие на них) средства ввода-вывода ОС UNIX. RPC позволяет одной прикладной программе обращаться к другой (но функционирующей на другом узле сети) фактически так же, как одна функция традиционной программы на языке СИ обращается к другой.
Примечание. Перечисленные средства программирования универcальны и могут быть использованы для работы с различными протоколами сетевого взаимодействия, однако в данном учебном пособии они рассматриваются применительно только к протоколам TCP/IP.
Рассматриваемые средства предназначены для создания распределенных приложений, функционирующих, в первую очередь, согласно модели взаимодействия "клиент-сервер". Эта модель подразумевает, что из двух (в простейшем случае) параллельно выполняющихся на разных (а, возможно, на одном и том же) узлах сети программ одна является сервером (поставщиком услуг), способным решать некоторую задачу вычислительного или информационного характера, а другая - клиентом (потребителем услуг), который в ходе решения собственной проблемы сталкивается с необходимостью получения ответа на задачи, решаемые сервером. Программа-сервер пассивна - непосредственно к решению своей задачи она приступает только при поступлении к ней запроса от клиента, остальное же время она находится в состоянии ожидания такого запроса. После решения поставленной задачи сервер возвращает клиенту найденный ответ. В типичной ситуации программа-клиент приостанавливает свою работу после выдачи запроса серверу в ожидании получения ответа.
Одна программа-сервер может обслуживать (последовательно) несколько клиентов, организуя очередь запросов от них. Клиент и сервер могут меняться местами ролями (если того, конечно, требует логика решаемой проблемы) в разные моменты времени. Сервер, обрабатывая запрос какого-либо клиента, в свою очередь может стать клиентом, обращаясь за некоторой услугой к другому серверу.
Двумя наиболее общими режимами взаимодействия прикладных программ в вычислительной сети являются:
Первый режим подразумевает, что взаимодействие осуществляется в три этапа:
В режиме без установления соединения обмен информацией ведется отдельными блоками данных, часто называемых дейтаграммами и содержащими в себе помимо собственно данных еще и адрес их получателя (соответственно, клиента или сервера). В этом режиме, как правило, не гарантируется надежность доставки данных (они могут быть потеряны или продублированы), может быть нарушена правильная последовательность дейтаграмм на принимающей стороне, но, очевидно, явно присутствуют границы в передаваемых данных.
Программист имеет возможность выбрать режим взаимодействия, отвечающий специфике создаваемого приложения.
Одним из аспектов сетевого программирования является манипулирование адресами, идентифицирующими узлы в вычислительной сети и прикладные программы на этих узлах. К сожалению, способы адресации в сетях, построенных на базе различных протоколов, также различны. Ниже рассматриваются механизмы адресации, принятые в стеке протоколов TCP/IP.
В сетях на основе TCP/IP для идентификации отдельного узла используется уникальное четырехбайтовое число. Только для удобства пользователей сети этим адресам (числам) в соответствие могут быть поставлены символические имена узлов сети. Информация о таком соответствии хранится в одной или нескольких специальных базах данных. Система программирования ОС UNIX предоставляет библиотеку вспомогательных функций, позволяющих работать с этими база- ми (транслируя, например, имена узлов в адреса и обратно).
Каналом выхода в коммуникационную среду, образуемую вычислительной сетью TCP/IP, для любой прикладной программы является так называемый "порт" - чисто абстрактное ("программное") понятие, не имеющее какого-либо соответствия в аппаратуре ЭВМ. Прикладная программа может использовать для общения с другими программами в сети любое количество портов. Каждый порт должен иметь уникальный номер. Номера портов от 1 до 1024 зарезервированы для "широко известных" приложений. Любой номер, больший 1024, может быть использован программистом для идентификации порта его приложения, необходимо лишь следить за его уникальностью в рамках отдельного узла сети.
В сетях TCP/IP для организации режима взаимодействия с логическим соединением используется протокол транспортного уровня TCP, а режима без установления соединения - протокол UDP. Причем два порта с одинаковым номером, но открытые для взаимодействия по разным протоколам транспортного уровня, считаются различными.
Таким образом: для идентификации партнера по взаимодействию любая сетевая прикладная программа должна специфицировать:
Ниже кратко рассматриваются перечисленные ранее средства программирования сетевых приложений.
Данное средство было первоначально разработано для обеспечения прикладным программистам в среде ОС UNIX доступа к транспортному уровню стека протоколов TCP/IP. Позже оно было адаптировано для использования и иных протоколов (например, DECnet), а также реализовано в других операционных системах.
Socket (гнездо, разъем) - абстрактное программное понятие, используемое для обозначения в прикладной программе конечной точки канала связи с коммуникационной средой, образованной вычислительной сетью. При использовании протоколов TCP/IP можно говорить, что socket является средством подключения прикладной программы к порту (см. выше) локального узла сети.
Socket-интерфейс представляет собой просто набор системных вызовов и/или библиотечных функций языка программирования СИ, разделенных на четыре группы:
Ниже рассматривается подмножество функций socket-интерфейса, достаточное для написания сетевых приложений, реализующих модель "клиент-сервер" в режиме с установлением соединения.
Функции локального управления используются, главным образом, для выполнения подготовительных действий, необходимых для организации взаимодействия двух программ-партнеров. Функции носят такое название, поскольку их выполнение носит локальный для программы характер.
Создание socket'а осуществляется следующим системным вызовом
#include <sys/socket.h> int socket (domain, type, protocol) int domain; int type; int protocol;
Аргумент domain задает используемый для взаимодействия набор протоколов (вид коммуникационной области), для стека протоколов TCP/IP он должен иметь символьное значение AF_INET (определено в sys/socket.h).
Аргумент type задает режим взаимодействия:
Аргумент protocolзадает конкретный протокол транспортного уровня (из нескольких возможных в стеке протоколов). Если этот аргумент задан равным 0, то будет использован протокол "по умолчанию" (TCP для SOCK_STREAM и UDP для SOCK_DGRAM при использовании комплекта протоколов TCP/IP).
При удачном завершении своей работы данная функция возвращает дескриптор socket'а - целое неотрицательное число, однозначно его идентифицирующее. Дескриптор socket'а аналогичен дескриптору файла ОС UNIX.
При обнаружении ошибки в ходе своей работы функция возвращает число "-1".
Для подключения socket'а к коммуникационной среде, образованной вычислительной сетью, необходимо выполнить системный вызов bind, определяющий в принятом для сети формате локальный адрес канала связи со средой. В сетях TCP/IP socket связывается с локальным портом. Системный вызов bind имеет следующий синтаксис:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int bind (s, addr, addrlen) int s; struct sockaddr *addr; int addrlen;
Аргумент s задает дескриптор связываемого socket'а.
Аргумент addr в общем случае должен указывать на структуру данных, содержащую локальный адрес, приписываемый socket'у. Для сетей TCP/IP такой структурой является sockaddr_in.
Аргумент addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr.
Структура sockaddr_in используется несколькими системными вызовами и функциями socket-интерфейса и определена в include-файле in.h следующим образом:
struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; };
Поле sin_family определяет используемый формат адреса (набор протоколов), в нашем случае (для TCP/IP) оно должно иметь значение AF_INET.
Поле sin_addr содержит адрес (номер) узла сети.
Поле sin_port содержит номер порта на узле сети.
Поле sin_zero не используется.
Определение структуры in_addr (из того же include-файла) таково:
struct in_addr { union { u_long S_addr; /* другие (не интересующие нас) члены объединения */ } S_un; #define s_addr S_un.S_addr };
Структура sockaddr_in должна быть полностью заполнена перед выдачей системного вызова bind. При этом, если поле sin_addr.s_addr имеет значение INADDR_ANY, то системный вызов будет привязывать к socket'у номер (адрес) локального узла сети.
В случае успеха bind возвращает 0, в противном случае - "-1".
Для установления связи "клиент-сервер" используются системные вызовы listen и accept (на стороне сервера), а также connect (на стороне клиента). Для заполнения полей структуры socaddr_in, используемой в вызове connect, обычно используется библиотечная функция gethostbyname, транслирующая символическое имя узла сети в его номер (адрес).
Системный вызов listen выражает желание выдавшей его программы-сервера ожидать запросы к ней от программ-клиентов и имеет следующий вид:
#include <sys/socket.h> int listen (s, n) int s; int n;
Аргумент s задает дескриптор socket'а, через который программа будет ожидать запросы к ней от клиентов. Socket должен быть предварительно создан системным вызовом socket и обеспечен адресом с помощью системного вызова bind.
Аргумент n определяет максимальную длину очереди входящих запросов на установление связи. Если какой-либо клиент выдаст запрос на установление связи при полной очереди, то этот запрос будет отвергнут.
Признаком удачного завершения системного вызова listen служит нулевой код возврата.
Для обращения программы-клиента к серверу с запросом на установление логической соединения используется системный вызов connect, имеющий следующий вид
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> int connect (s, addr, addrlen) int s; struct sockaddr_in *addr; int addrlen;
Аргумент s задает дескриптор socket'а, через который программа обращается к серверу с запросом на соединение. Socket должен быть предварительно создан системным вызовом socket и обеспечен адресом с помощью системного вызова bind.
Аргумент addr должен указывать на структуру данных, содержащую адрес, приписанный socket'у программы-сервера, к которой делается запрос на соединение. Для сетей TCP/IP такой структурой является sockaddr_in. Для формирования значений полей структуры sockaddr_in удобно использовать функцию gethostbyname.
Аргумент addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr.
Для того, чтобы запрос на соединение был успешным, необходимо, по крайней мере, чтобы программа-сервер выполнила к этому моменту системный вызов listen для socket'а с указанным адресом.
При успешном выполнении запроса системный вызов connect возвращает 0, в противном случае - "-1" (устанавливая код причины неуспеха в глобальной переменной errno).
Примечание. Если к моменту выполнения connect используемый им socket не был привязан к адресу посредством bind ,то такая привязка будет выполнена автоматически.
Примечание. В режиме взаимодействия без установления соединения необходимости в выполнении системного вызова connect нет. Однако, его выполнение в таком режиме не является ошибкой - просто меняется смысл выполняемых при этом действий: устанавливается адрес "по умолчанию" для всех последующих посылок дейтаграмм.
Для приема запросов от программ-клиентов на установление связи в программах-серверах используется системный вызов accept, имеющий следующий вид:
#include <sys/socket.h> #include <netinet/in.h> int accept (s, addr, p_addrlen) int s; struct sockaddr_in *addr; int *p_addrlen;
Аргумент s задает дескриптор socket'а, через который программа-сервер получила запрос на соединение (посредством системного запроса listen ).
Аргумент addr должен указывать на область памяти, размер которой позволял бы разместить в ней структуру данных, содержащую адрес socket'а программы-клиента, сделавшей запрос на соединение. Никакой инициализации этой области не требуется.
Аргумент p_addrlen должен указывать на область памяти в виде целого числа, задающего размер (в байтах) области памяти, указываемой аргументом addr.
Системный вызов accept извлекает из очереди, организованной системным вызовом listen, первый запрос на соединение и возвращает дескриптор нового (автоматически созданного) socket'а с теми же свойствами, что и socket, задаваемый аргументом s. Этот новый дескриптор необходимо использовать во всех последующих операциях обмена данными.
Кроме того после удачного завершения accept:
Если очередь запросов на момент выполнения accept пуста, то программа переходит в состояние ожидания поступления запросов от клиентов на неопределенное время (хотя такое поведение accept можно и изменить).
Признаком неудачного завершения accept служит отрицательное возвращенное значение (дескриптор socket'а отрицательным быть не может).
Примечание. Системный вызов accept используется в программах-серверах, функционирующих только в режиме с установлением соединения.
Для получения адреса узла сети TCP/IP по его символическому имени используется библиотечная функция
#include <netinet/in.h> #include <netdb.h> struct hostent *gethostbyname (name) char *name;
Аргумент name задает адрес последовательности литер, образующих символическое имя узла сети.
При успешном завершении функция возвращает указатель на структуру hostent, определенную в include-файле netdb.h и имеющую следующий вид
struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_lenght; char *h_addr; };
Поле h_name указывает на официальное (основное) имя узла.
Поле h_aliases указывает на список дополнительных имен узла (синонимов), если они есть.
Поле h_addrtype содержит идентификатор используемого набора протоколов, для сетей TCP/IP это поле будет иметь значение AF_INET.
Поле h_lenght содержит длину адреса узла.
Поле h_addr указывает на область памяти, содержащую адрес узла в том виде, в котором его используют системные вызовы и функции socket-интерфейса.
Пример обращения к функции gethostbyname для получения адреса удаленного узла в программе-клиенте, использующей системный вызов connect для формирования запроса на установления соединения с программой-сервером на этом узле, рассматривается ниже.
В режиме с установлением логического соединения после удачного выполнения пары взаимосвязанных системных вызовов connect (в клиенте) и accept (в сервере) становится возможным обмен данными.
Этот обмен может быть реализован обычными системными вызовами read и write, используемыми для работы с файлами (при этом вместо дескрипторов файлов в них задаются дескрипторы socket'ов).
Кроме того могут быть дополнительно использованы системные вызовы send и recv, ориентированные специально на работу с socket'ами.
Примечание. Для обмена данными в режиме без установления логического соединения используются, как правило, системные вызовы sendtoи recvfrom. Sendto позволяет специфицировать вместе с передаваемыми данными (составляющими дейтаграмму) адрес их получателя. Recvfrom одновременно с доставкой данных получателю информирует его и об адресе отправителя.
Для посылки данных партнеру по сетевому взаимодействию используется системный вызов send, имеющий следующий вид
#include <sys/types.h> #include <sys/socket.h> int send (s, buf, len, flags) int s; char *buf; int len; int flags;
Аргумент s задает дескриптор socket'а, через который посылаются данные.
Аргумент buf указывает на область памяти, содержащую передаваемые данные.
Аргумент len задает длину (в байтах) передаваемых данных.
Аргумент flags модифицирует исполнение системного вызова send. При нулевом значении этого аргумента вызов send полностью аналогичен системному вызову write.
При успешном завершении send возвращает количество переданных из области, указанной аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается "переполненным", то send переводит программу в состояние ожидания до момента его освобождения.
Для получения данных от партнера по сетевому взаимодействию используется системный вызов recv, имеющий следующий вид
#include <sys/types.h> #include <sys/socket.h> int recv (s, buf, len, flags) int s; char *buf; int len; int flags;
Аргумент s задает дескриптор socket'а, через который принимаются данные.
Аргумент buf указывает на область памяти, предназначенную для размещения принимаемых данных.
Аргумент len задает длину (в байтах) этой области.
Аргумент flags модифицирует исполнение системного вызова recv. При нулевом значении этого аргумента вызов recv полностью аналогичен системному вызову read.
При успешном завершении recv возвращает количество принятых в область, указанную аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается "пустым", то recv переводит программу в состояние ожидания до момента появления в нем данных.
Для закрытия связи с партнером по сетевому взаимодействию используются системные вызовы close и shutdown.
Для закрытия ранее созданного socket'а используется обычный системный вызов close, применяемый в ОС UNIX для закрытия ранее открытых файлов и имеющий следующий вид
int close (s) int s;
Аргумент s задает дескриптор ранее созданного socket'а.
Однако в режиме с установлением логического соединения (обеспечивающем, как правило, надежную доставку данных) внутрисистемные механизмы обмена будут пытаться передать/принять данные, оставшиеся в канале передачи на момент закрытия socket'а. На это может потребоваться значительный интервал времени, неприемлемый для некоторых приложений. В такой ситуации необходимо использовать описываемый далее системный вызов shutdown.
Для "экстренного" закрытия связи с партнером (путем "сброса" еще не переданных данных) используется системный вызов shutdown, выполняемый перед close и имеющий следующий вид
int shutdown (s, how) int s; int how;
Аргумент s задает дескриптор ранее созданного socket'а.
Аргумент how задает действия, выполняемые при очистке системных буферов socket'а:
В данном разделе рассматривается использование socket-интерфейса в режиме взаимодействия с установлением логического соединения на очень простом примере взаимодействия двух программ (сервера и клиента), функционирующих на разных узлах сети TCP/IP.
Содержательная часть программ примитивна:
Примечание. Предлагаемые ниже тексты программ предназначены только для иллюстрации логики взаимодействия программ через сеть, поэтому в них отсутствуют такие атрибуты программ, предназначенных для практического применения, как обработка кодов возврата системных вызовов и функций, анализ кодов ошибок в глобальной переменной errno, реакция на асинхронные события и т.п.
Текст программы-сервера на языке программирования СИ выглядит следующим образом
1 #include <sys/types.h> 2 #include <sys/socket.h> 3 #include <netinet/in.h> 4 #include <netdb.h> 5 #include <memory.h> 6 #define SRV_PORT 1234 7 #define BUF_SIZE 64 8 #define TXT_QUEST "Who are you?\n" 9 main () { 10 int s, s_new; 11 int from_len; 12 char buf[BUF_SIZE]; 13 struct sockaddr_in sin, from_sin; 14 s = socket (AF_INET, SOCK_STREAM, 0); 15 memset ((char *)&sin, '\0', sizeof(sin)); 16 sin.sin_family = AF_INET; 17 sin.sin_addr.s_addr = INADDR_ANY; 18 sin.sin_port = SRV_PORT; 19 bind (s, (struct sockaddr *)&sin, sizeof(sin)); 20 listen (s, 3); 21 while (1) { 22 from_len = sizeof(from_sin); 23 s_new = accept (s, &from_sin, &from_len); 24 write (s_new, TXT_QUEST, sizeof(TXT_QUEST)); 25 from_len = read (s_new, buf, BUF_SIZE); 26 write (1, buf, from_len); 27 shutdown (s_new, 0); 28 close (s_new); 29 }; 30 }
Строки 1...5 описывают включаемые файлы, содержащие определения для всех необходимых структур данных и символических констант.
Строка 6 приписывает целочисленной константе 1234 символическое имя SRV_PORT. В дальнейшем эта константа будет использована в качестве номера порта сервера. Значение этой константы должно быть известно и программе-клиенту.
Строка 7 приписывает целочисленной константе 64 символическое имя BUF_SIZE. Эта константа будет определять размер буфера, используемого для размещения принимаемых от клиента данных.
Строка 8 приписывает последовательности символов, составляющих текст вопроса клиенту, символическое имя TXT_QUEST. Последним символом в последовательности является символ перехода на новую строку '\n'. Сделано это для упрощения вывода текста вопроса на стороне клиента.
В строке 14 создается (открывается) socket для организации режима взаимодействия с установлением логического соединения (SOCK_STREAM) в сети TCP/IP (AF_INET), при выборе протокола транспортного уровня используется протокол "по умолчанию" (0).
В строках 15...18 сначала обнуляется структура данных sin, а затем заполняются ее отдельные поля. Использование константы INADDR_ANY упрощает текст программы, избавляя от необходимости использовать функцию gethostbyname для получения адреса локального узла, на котором запускается сервер.
Строка 19 посредством системного вызова bind привязывает socket, задаваемый дескриптором s, к порту с номером SRV_PORT на локальном узле. Bind завершится успешно при условии, что в момент его выполнения на том же узле уже не функционирует программа, использующая этот номер порта.
Строка 20 посредством системного вызова listen организует очередь на три входящих к серверу запроса на соединение.
Строка 21 служит заголовком бесконечного цикла обслуживания запросов от клиентов.
На строке 23, содержащей системный вызов accept, выполнение программы приостанавливается на неопределенное время, если очередь запросов к серверу на установление связи оказывается пуста. При появлении такого запроса accept успешно завершается, возвращая в переменной s_new дескриптор socket'а для обмена информацией с клиентом.
В строке 24 сервер с помощью системного вызова write отправляет клиенту вопрос.
В строке 25 с помощью системного вызова read читается ответ клиента.
В строке 26 ответ направляется в стандартный вывод, имеющий дескриптор файла номер 1. Так как строка ответа содержит в себе символ перехода на новую строку, то текст ответа будет размещен на отдельной строке дисплея.
Строка 27 содержит системный вывод shutdown, обеспечивающий очистку системных буферов socket'а, содержащих данные для чтения ("лишние" данные могут там оказаться в результате неверной работы клиента).
В строке 28 закрывается (удаляется) socket, использованный для обмена данными с очередным клиентом.
Примечание. Данная программа (как и большинство реальных программ-серверов) самостоятельно своей работы не завершает, находясь в бесконечном цикле обработки запросов клиентов. Ее выполнение может быть прервано только извне путем посылки ей сигналов (прерываний) завершения. Правильно разработанная программа-сервер должна обрабатывать такие сигналы, корректно завершая работу (закрывая, в частности, посредством close socket с дескриптором s).
Текст программы-клиента на языке программирования СИ выглядит следующим образом
1 #include <sys/types.h> 2 #include <sys/socket.h> 3 #include <netinet/in.h> 4 #include <netdb.h> 5 #include <memory.h> 6 #define SRV_HOST "delta" 7 #define SRV_PORT 1234 8 #define CLNT_PORT 1235 9 #define BUF_SIZE 64 10 #define TXT_ANSW "I am your client\n" 11 main () { 12 int s; 13 int from_len; 14 char buf[BUF_SIZE]; 15 struct hostent *hp; 16 struct sockaddr_in clnt_sin, srv_sin; 17 s = socket (AF_INET, SOCK_STREAM, 0); 18 memset ((char *)&clnt_sin, '\0', sizeof(clnt_sin)); 19 clnt_sin.sin_family = AF_INET; 20 clnt_sin.sin_addr.s_addr = INADDR_ANY; 21 clnt_sin.sin_port = CLNT_PORT; 22 bind (s, (struct sockaddr *)&clnt_sin, sizeof(clnt_sin)); 23 memset ((char *)&srv_sin, '\0', sizeof(srv_sin)); 24 hp = gethostbyname (SRV_HOST); 25 srv_sin.sin_family = AF_INET; 26 memcpy ((char *)&srv_sin.sin_addr,hp->h_addr,hp->h_length); 27 srv_sin.sin_port = SRV_PORT; 28 connect (s, &srv_sin, sizeof(srv_sin)); 29 from_len = recv (s, buf, BUF_SIZE, 0); 30 write (1, buf, from_len); 31 send (s, TXT_ANSW, sizeof(TXT_ANSW), 0); 32 close (s); 33 exit (0); 34 }
В строках 6 и 7 описываются константы SRV_HOST и SRV_PORT, определяющие имя удаленного узла, на котором функционирует программа-сервер, и номер порта, к которому привязан socket сервера.
Строка 8 приписывает целочисленной константе 1235 символическое имя CLNT_PORT. В дальнейшем эта константа будет использована в качестве номера порта клиента.
В строках 17...22 создается привязанный к порту на локальном узле socket.
В строке 24 посредством библиотечной функции gethostbyname транслируется символическое имя удаленного узла (в данном случае "delta"), на котором должен функционировать сервер, в адрес этого узла, размещенный в структуре типа hostent.
В строке 26 адрес удаленного узла копируется из структуры типа hostent в соответствующее поле структуры srv_sin, которая позже (в строке 28) используется в системном вызове connect для идентификации программы-сервера.
В строках 29...31 осуществляется обмен данными с сервером и вывод вопроса, поступившего от сервера, в стандартный вывод.
Строка 32 посредством системного вызова close закрывает (удаляет) socket.
Интерфейс транспортного уровня (TLI) был разработан как альтернатива более раннему socket-интерфейсу. Он базируется на средстве ввода-вывода STREAMS, первоначально реализованном в версиях System V операционной системы UNIX. Основное достоинство STREAMS заключается в гибкой, управляемой пользователем многослойности модулей, по конвейерному принципу обрабатывающих информацию, передаваемую от прикладной программы к физической среде хранения/пересылки и обратно. Это делает STREAMS удобным инструментом для реализации стеков протоколов сетевого взаимодействия различной архитектуры (OSI, TCP/IP, DECnet, SNA, XNS и т.п.).
Хотя все современные реализации и версии ОС UNIX поддерживают socket-интерфейс по крайней мере для TCP/IP, для вновь разрабатываемых сетевых приложений настоятельно рекомендуется использовать TLI, что обеспечит их независимость от используемых сетевых протоколов.
С точки зрения прикладного программиста логика TLI очень похожа на логику socket-интерфейса (даже имена функций первого образованы от имен системных вызовов второго добавлением префикса "t_"). TLI реализован в виде библиотеки функций языка программирования СИ, разделенных (как и в случае с socket-интерфейсом) на четыре группы:
Основу концепции TLI составляют три базовых понятия:
Поставщиком транспортных услуг (transport provider) называется набор модулей, реализующих какой-либо конкретный стек протоколов сетевого взаимодействия (в данном учебном пособии - TCP/IP) и обеспечивающий сервис транспортного уровня модели OSI [REF].
Пользователем транспорта (transport user) является любая прикладная программа, использующая сервис, предоставляемый ПТС на локальном узле сети.
Транспортная точка (transport endpoint) - абстрактное понятие (аналогичное socket'у), используемое для обозначения канала связи между пользователем транспорта и поставщиком транспортных услуг на локальном узле сети. Транспортная точка имеем уникальный для всей сети транспортный адрес (для сетей TCP/IP этот адрес образуется триадой: адрес узла сети, номер порта, используемый протокол транспортного уровня). Для ссылки на транспортные точки в функциях TLI используются их дескрипторы, подобные дескрипторам обычных файлов и socket'ов ОС UNIX.
Ниже рассматривается подмножество функций TLI и приводится пример его использования для создания учебного приложения, функционирующего согласно модели "клиент-сервер" в режиме без установления соединения.
Функции TLI работают с нескольким универсальными структурами данных. При этом структура одного и того же типа может использоваться как для передачи данных в функции, так и для получения информации из них.
Ниже дается описание некоторых структур данных, используемых TLI.
Структура netbuf служит составляющей более сложных ("законченных") структур данных TLI. Она используется как для передачи данных в функции TLI, так и для размещения в ней возвращаемой из функций информации. Эта структура имеет следующий вид
struct netbuf { unsigned int maxlen; unsigned int len; char *buf; };
Поле buf указывает на область оперативной памяти (буфер), предназначенную для размещения в ней данных, передаваемых в функцию TLI или получаемых от нее. Семантика этих данных зависит от типа "вмещающей" структуры (см. ниже).
Поле len в ситуации, когда netbuf используется для передачи информации в функцию TLI, должно содержать длину (в байтах) данных, указываемых полем buf.
Поле maxlen в ситуациях, когда netbuf используется для получения информации от функции TLI, должно содержать длину (в байтах) области памяти, отводимой для этой цели и указываемой полем buf. Len после завершения функции будет содержать действительную длину данных, размещенных в буфере.
Структура t_bind определена в файле tiuser.h следующим образом
struct t_bind { struct netbuf addr; unsigned int qlen; };
Поле addr типа struct netbuf используется для размещения транспортного адреса транспортной точки.
Назначение поля qlen зависит от использующей эту структуру функции TLI.
Структура t_call определена в файле tiuser.h следующим образом
struct t_call { struct netbuf addr; struct netbuf opt; struct netbuf udata; int sequence; };
Поле addr типа struct netbuf используется для размещения транспортного адреса транспортной точки.
Поле opt типа struct netbuf используется для размещения необязательной информации, модифицирующей или описывающей характеристики используемого конкретного поставщика транспортных услуг. Приложения, проектируемые как независимые от поставщика транспортных услуг, этим полем структуры t_call пользоваться не должны.
Поле udata типа struct netbuf используется для размещения передаваемых к партнеру или принимаемых от него в ходе взаимодействия через сеть данных.
Назначение поля sequence зависит от использующей эту структуру функции TLI.
Структура t_unitdata определена в файле tiuser.h следующим образом
struct t_unitdata { struct netbuf addr; struct netbuf opt; struct netbuf udata; };
Поле addr типа struct netbuf используется для размещения транспортного адреса транспортной точки.
Поле opt типа struct netbuf используется для размещения необязательной информации, модифицирующей или описывающей характеристики используемого конкретного поставщика транспортных услуг. Приложения, проектируемые как независимые от поставщика транспортных услуг, этим полем структуры t_unitdata пользоваться не должны.
Поле udata типа struct netbuf используется для размещения передаваемых к партнеру или принимаемых от него в ходе взаимодействия через сеть данных.
Структура данных типа struct t_unitdata используется в функциях посылки/приема данных в режиме взаимодействия без установления соединения.
К функциям локального управления относятся функции создания/удаления транспортной точки (t_open/t_close), назначения/снятия транспортного адреса для транспортной точки (t_bind/t_unbind), выделения/освобождения оперативной памяти под структуры данных, используемые TLI (t_alloc/t_free) и другие.
Динамическое выделение оперативной памяти под различные структуры данных, используемые TLI, удобно осуществлять функцией t_alloc, имеющей следующий вид
#include <tiuser.h> char *t_alloc (fd, structType, fields) int fd; int structType; int fields;
Аргумент fd задает дескриптор ранее созданной функцией t_open транспортной точки.
Аргумент structType задает тип структуры данных, под которую необходимо выделить память, и может принимать следующие значения:
Каждая из указанных структур (исключая struct t_info) содержит одно или несколько полей типа struct netbuf. Для каждого из таких полей можно также потребовать динамического выделения памяти. Аргумент fields конкретизирует это требование, допуская задание fields в виде побитового ИЛИ из следующих значений:
При успешном завершении функция возвращает указатель на размещенную структуру данных, в противном случае - NULL.
Для освобождения оперативной памяти, динамически выделенной под различные структуры данных, используемые TLI, удобно использовать функцию t_free, имеющую следующий вид
#include<tiuser.h> int t_free (ptr, structType) char *ptr; int structType;
Аргумент ptr указывает освобождаемую область памяти.
Аргумент structType задает тип структуры данных, занимающей память. Этот аргумент может принимать те же значения, что и аналогичный аргумент функции t_alloc.
Функция t_free освобождает оперативную память, занятую собственно структурой и всеми ее буферами типа struct netbuf.
При успешном завершении функция t_free возвращает ноль, иначе - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Создание транспортной точки осуществляется функцией t_open, имеющей следующий вид
#include <tiuser.h> #include <fcntl.h> int t_open (path, oflags, info) char *path; int oflags; struct t_info *info;
Аргумент path задает имя файла (располагающегося, как правило, в каталоге /dev), определяющего используемого поставщика транспортных услуг. Для стека протоколов TCP/IP такими файлами могут быть /dev/tcp (режим с установлением логического соединения) и /dev/udp (режим без установления логического соединения).
Аргумент oflags задает флаги открытия транспортной точки. Допустимые значения флагов - те же, что и для обычного системного вызова open. Если транспортная точка создается для двустороннего обмена информацией через нее, то значением oflags должно быть O_RDWR.
Аргумент info должен указывать на структуру данных типа struct t_info, поля которой заполняются функцией t_open при ее удачном завершении информацией о характеристиках используемого поставщика транспортных услуг. Если info задан как NULL, то информация о протоколе не возвращается. Для выделения памяти под структуру удобно использовать функцию t_alloc [REF].
При успешном завершении функция t_open возвращает дескриптор транспортной точки, используемый для ссылки на нее в большинстве функций TLI. Дескриптор транспортной точки аналогичен дескриптору socket'а. При обнаружении ошибки в ходе своей работы функция возвращает число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Для назначения транспортного адреса транспортной точке и ее активизации используется функция t_bind, имеющая следующий вид
#include <tiuser.h> int t_bind (fd, req, ret) int fd; struct t_bind *req; struct t_bind *ret;
Аргумент fd задает дескриптор транспортной точки, созданной ранее с помощью функции t_open.
Аргумент req указывает на структуру t_bind, которая должна определять требуемый транспортный адрес для точки (поле req->addr) и максимальное количество запросов на соединение (поле req->qlen), одновременно обрабатываемых программой. Для программы-клиента поле req->qlen должно быть нулевым, а для программ-серверов, работающих в режиме с установлением логического соединения, оно, как правило, содержит 1 (необходимо учитывать, что не все поставщики транспортных услуг могут обеспечивать одновременную обработку сразу нескольких соединений к одной транспортной точке). Для программ-серверов, функционирующих в режиме без установления логического соединения, поле req->qlen смысла не имеет.
Если аргумент req имеет значение NULL, то функция t_bind сама назначит произвольный транспортный адрес для точки.
Аргумент ret должен указывать на область памяти под структуру t_bind, в которой после успешного выполнения функции будет размещена информация о транспортном адресе, назначенном транспортной точке. Если этот аргумент равен NULL, то информация о назначенном транспортном адресе возвращена не будет.
При успешном завершении функция t_bind возвращает ноль, иначе - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Для снятия транспортного адреса у транспортной точки используется функция t_unbind, имеющая следующий вид
#include <tiuser.h> int t_unbind (fd) int fd;
Аргумент fd задает дескриптор транспортной точки, которой ранее с помощью функции t_bind был назначен транспортный адрес.
После выполнения функции t_unbind для транспортной точки обмен данными через нее становится невозможным.
При успешном завершении функция t_unbind возвращает ноль, иначе - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Удаление транспортной точки осуществляется функцией t_close, имеющей следующий вид
#include <tiuser.h> int t_close (fd) int fd;
Аргумент fd задает дескриптор транспортной точки, созданной ранее с помощью функции t_open.
При успешном завершении функция t_close возвращает ноль, иначе - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Для установления логического соединения "клиент-сервер" в TLI используются функции t_listen, t_accept (на стороне сервера), t_connect (на стороне клиента), а также ряд других.
Ожидание в программе-сервере запроса от клиента на соединение реализуется функцией t_listen, имеющей следующий вид
#include <tiuser.h> int t_listen (fd, call) int fd; struct t_call *call;
Аргумент call должен указывать на область памяти под структуру t_call, в которой после успешного выполнения функции будет размещена следующая информация: транспортный адрес (call->addr) транспортной точки програм- мы-клиента, через которую она делает запрос на установление соединения; необязательные характеристики соединения (call->opt); необязательные данные (call->udata), передаваемые клиентом серверу вместе с запросом на соединение (однако, не любой поставщик транспортных услуг обеспечивает возможность передачи данных вместе с запросом на соединение); уникальный идентификатор соединения (call->sequence), имеющий смысл для программы-сервера только, если она допускает обслуживание одновременно нескольких соединений с нею.
Функция t_listen извлекает из очереди запросов на установление соединения первый запрос и возвращает в области памяти, указываемой аргументом call, описанную выше информацию клиента. Если очередь запросов на момент выполнения t_listen пуста, то программа переходит в состояние ожидания поступления запросов от клиентов на неопределенное время (хотя такое поведение t_listen можно и изменить).
При успешном завершении функция t_listen возвращает ноль, иначе - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Примечание. Обратите внимание: схожие по названию функция t_listen и системный вызов listen из socket-интерфейса имеют различный смысл.
Прием в программе-сервере запроса от клиента на соединение, "услышанного" функцией t_listen, реализуется функцией t_accept, имеющей следующий вид
#include <tiuser.h> int t_accept (fd, resfd, call) int fd; int resfd; struct t_call *call;
Аргумент fd задает дескриптор транспортной точки, через которую ранее выполненная функция t_listen получила запрос на соединение.
Аргумент resfd задает дескриптор еще одной транспортной точки, созданной с теми же свойствами, что и точка, задаваемая аргументом fd, но имеющей другой транспортный адрес.
Аргумент call указывает на структуру данных типа t_call, поля которой должны содержать следующую информацию: транспортный адрес (call->addr) транспортной точки программы-клиента, через которую она сделала запрос на установление соединения; необязательные характеристики соединения (call->opt); необязательные данные (call->udata), возвращаемые сервером клиенту вместе с подтверждением установления соединения (однако, не любой поставщик транспортных услуг обеспечивает возможность такой передачи данных); уникальный идентификатор (call->sequence), присвоенный соединению функцией t_listen.
После успешного выполнения в программе-сервере функции t_accept устанавливается логическое соединение с клиентом и становится возможным обмен данными с ним через дескриптор resfd.
В типичной программе сервере транспортная точка с дескриптором resfd создается и активизируется после успешного завершения функции t_listen с помощью функций t_open и t_bind. Допустимой является ситуация, когда resfd = fd, но тогда программа-сервер до момента закрытия соединения с клиентом теряет возможность получать и ставить в очередь запросы на соединение от других клиентов.
При успешном завершении функция t_accept возвращает ноль, иначе - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Программа-сервер может отказаться от установления соединения с клиентом, используя функцию t_snddis.
Примечание. Обратите внимание: схожие по названию функция t_accept и системный вызов accept из socket-интерфейса имеют различный смысл.
Программа-сервер может отвергнуть запрос клиента на соединение, "услышанный" функцией t_listen, используя функцию t_snddis, имеющую следующий вид
#include <tiuser.h> int t_snddis (fd, call) int fd; struct t_call *call;
Аргумент fd задает дескриптор транспортной точки, через которую ранее выполненная функция t_listen получила запрос на соединение.
Аргумент call указывает на структуру данных типа t_call, поля которой должны содержать следующую информацию: уникальный идентификатор (call->sequence), присвоенный соединению функцией t_listen; необязательные данные (call->udata), возвращаемые сервером клиенту вместе с информацией об отклонении запроса на соединения (однако, не любой поставщик транспортных услуг обеспечивает возможность такой передачи данных); поля call->addr и call->opt не используются.
При успешном завершении функция t_snddis возвращает ноль, иначе - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Примечание. Функция t_snddis используется также для "экстренного" закрытия ранее установленного соединения, при этом аргумент call формируется несколько иначе.
Для обращения программы-клиента к серверу с запросом на установление логической соединения используется функция t_connect, имеющая следующий вид
#include <tiuser.h> int t_connect (fd, sndcall, rcvcall) int fd; struct t_call *sndcall; struct t_call *rcvcall;
Аргумент fd задает дескриптор транспортной точки, созданной ранее с помощью функции t_open и активизированной функцией t_bind.
Аргумент sndcall указывает на структуру данных типа t_call, в которой функции передается следующая информация: транспортный адрес (sndcall->addr) транспортной точки программы-сервера, к которой клиент делает запрос на установление соединения; необязательные характеристики соединения (sndcall->opt); необязательные данные (sndcall->udata), передаваемые клиентом серверу вместе с запросом на соединение (однако, не любой постав- щик транспортных услуг обеспечивает возможность передачи данных вместе с запросом на соединение).
Поле sndcall->sequence не используется и может принимать произвольное значение.
Аргумент rcvcall должен указывать на область памяти под структуру t_call, в которой после успешного выполнения функции будет размещена следующая информация: транспортный адрес (rcvcall->addr) транспортной точки в программе-сервере, с которой установлено соединение; необязательные характеристики соединения (rcvcall->opt); необязательные данные (rcvcall->udata), передаваемые клиенту сервером (посредством функции t_accept) вместе с подтверждением соединения (однако, не любой поставщик транспортных услуг обеспечивает возможность такой передачи данных); поле rcvcall->sequence не используется.
При успешном установлении соединения функция t_connect возвращает ноль. Если же сервер отверг запрос на соединение, то t_connect возвращает "-1" и устанавливает код ошибки TLOOK в глобальной переменной t_errno.
В режиме с установлением логического соединения для обмена данными используются функции t_snd и t_rcv.
В режиме без установления логического соединения для обмена данными используются функции t_sndudata и t_rcvudata.
Для посылки данных партнеру по сетевому взаимодействию в режиме с установлением логического соединения используется функция t_snd, имеющая следующий вид
#include <tiuser.h> int t_snd (fd, buf, len, flags) int fd; char *buf; unsigned int len; int flags;
Аргумент fd задает дескриптор транспортной точки, через которую посылаются данные.
Аргумент buf указывает на область памяти, содержащую передаваемые данные.
Аргумент len задает длину (в байтах) передаваемых данных.
Аргумент flags модифицирует исполнение функции t_snd. При нулевом значении этого аргумента функция t_snd полностью аналогична системному вызову write.
При успешном завершении t_snd возвращает количество переданных из области, указанной аргументом buf, байт данных. Если канал данных, определяемый дескриптором fd, оказывается "переполненным", то t_snd переводит программу в состояние ожидания до момента его освобождения.
Для получения данных от партнера по сетевому взаимодействию в режиме с установлением логического соединения используется функция t_rcv, имеющая следующий вид
#include <tiuser.h> int t_rcv (fd, buf, len, flags) int fd; char *buf; unsigned int len; int flags;
Аргумент fd задает дескриптор транспортной точки, через которую принимаются данные.
Аргумент buf указывает на область памяти, предназначенную для размещения принимаемых данных.
Аргумент len задает длину (в байтах) этой области.
Аргумент flags модифицирует исполнение системного вызова recv. При нулевом значении этого аргумента вызов t_rcv полностью аналогичен системному вызову read.
При успешном завершении t_rcv возвращает количество принятых в область, указанную аргументом buf, байт данных. Если канал данных, определяемый дескриптором fd, оказывается "пустым", то t_rcv переводит программу в состояние ожидания до момента появления в нем данных.
Для посылки данных, составляющих дейтаграмму, партнеру по сетевому взаимодействию в режиме без установления логического соединения используется функция t_sndudata, имеющая следующий вид
#include <tiuser.h> int t_sndudata (fd, unitdata) int fd; struct t_unitdata *unitdata;
Аргумент fd задает дескриптор транспортной точки, через которую посылаются данные.
Аргумент unitdata указывает на структуру данных типа t_unitdata, в которой функции передается следующая информация: транспортный адрес (unitdata->addr) транспортной точки программы-партнера по взаимодействию, которой посылается дейтаграмма; необязательные характеристики соединения (unitdata->opt); собственно данные (unitdata->udata), составляющие дейтаграмму, передаваемую партнеру по взаимодействию.
Если канал данных, определяемый дескриптором fd, оказывается "переполненным", то t_sndudata переводит программу в состояние ожидания до момента его освобождения.
При успешном выполнении функция t_sndudata возвращает ноль, в противном случае - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
Для получения данных, составляющих дейтаграмму, от партнера по сетевому взаимодействию в режиме без установления логического соединения используется функция t_rcvudata, имеющая следующий вид
#include <tiuser.h> int t_rcvudata (fd, unitdata, flags) int fd; struct t_unitdata *unitdata; int *flags;
Аргумент fd задает дескриптор транспортной точки, через которую посылаются данные.
Аргумент unitdata указывает на структуру данных типа t_unitdata, в которой функции передается следующая информация: транспортный адрес (unitdata->addr) транспортной точки программы-партнера по взаимодействию, которой посылается дейтаграмма; необязательные характеристики соединения (unitdata->opt); собственно данные (unitdata->udata), составляющие дейтаграмму, передаваемую партнеру по взаимодействию.
Аргумент unitdata должен указывать на область памяти под структуру t_unitdata, в которой после успешного выполнения функции будет размещена следующая информация: транспортный адрес (unitdata->addr) транспортной точки в программе-партнере по взыимодействию, отправившей дейтаграмму; необязательные характеристики соединения (unitdata->opt); собственно данные (unitdata->udata), составляющие дейтаграмму, принимаемую от партнера по взаимодействию.
Аргумент flags должен указывать область памяти (типа int), в которой функция t_rcvudata может установить флаг T_MORE, сигнализирующий о том, что в канале передачи остались еще данные, составляющие дейтаграмму. Такая ситуация может возникнуть в случае, если размер буфера в unitdata->udata недостаточен для размещения в нем сразу всей дейтаграммы.
Если канал данных, определяемый дескриптором fd, оказывается "пустым", то t_rcvudata переводит программу в состояние ожидания до момента появления в нем данных.
При успешном выполнении функция t_rcvudata возвращает ноль, в противном случае - число "-1" и устанавливает код ошибки в глобальной переменной t_errno.
TLI поддерживает две процедуры закрытия связи в режиме с установлением логического соединения: упорядоченную и экстренную.
Упорядоченная процедура реализуется парой функций t_sndrel и t_rcvrel и обеспечивает надежную доставку к партнеру по взаимодействию всех данных, планируемых для передачи. Однако не все поставщики транспортных услуг поддерживают эту процедура закрытия связи.
Экстренная процедура закрытия логического соединения реализуется функциями t_snddis и t_rcvdis.
В данном учебном пособии процедуры закрытия логического соединения не рассматриваются.
В данном разделе рассматривается использование TLI в режиме взаимодействия без установления логического соединения на очень простом примере взаимодействия двух программ (сервера и клиента), функционирующих на разных узлах сети TCP/IP.
Содержательная часть программ примитивна:
Примечание. Предлагаемые ниже тексты программ предназначены только для иллюстрации логики взаимодействия программ через сеть, поэтому в них отсутствуют такие атрибуты программ, предназначенных для практического применения, как обработка кодов возврата системных вызовов и функций, анализ кодов ошибок в глобальных переменных errno и t_errno, реакция на асинхронные события и т.п.
Текст программы-сервера на языке программирования СИ выглядит следующим образом
1 #include <tiuser.h> 2 #include <fcntl.h> 3 #include <stdio.h> 4 #include <sys/socket.h> 5 #include <netinet/in.h> 6 #include <netdb.h> 7 #include <memory.h> 8 #include <sys/time.h> 9 #define SRV_PORT 1234 10 #define CONT_TXT "Continue\n" 11 #define CANC_TXT "Cancel\n" 12 main () { 13 int fd; 14 int flags; 15 time_t secs; 16 struct t_bind *bind; 17 struct t_unitdata *ud; 18 struct sockaddr_in *p_addr; 19 extern int t_errno; 20 fd = t_open("/dev/udp", O_RDWR, NULL); 21 bind = (struct t_bind *) t_alloc (fd, T_BIND, T_ADDR); 22 memset (bind->addr.buf, '\0', bind->addr.maxlen); 23 p_addr = (struct sockaddr_in *) bind->addr.buf; 24 p_addr->sin_family = AF_INET; 25 p_addr->sin_addr.s_addr = INADDR_ANY; 26 p_addr->sin_port = SRV_PORT; 27 bind->addr.len = sizeof(struct sockaddr_in); 28 bind->qlen = 0; 29 t_bind (fd, bind, bind); 30 ud = (struct t_unitdata *)t_alloc(fd,T_UNITDATA,T_ALL); 31 while (1) { 32 t_rcvudata (fd, ud, &flags); 33 write (1, ud->udata.buf, ud->udata.len); 34 secs = time (NULL); 35 if (secs % 3) { 36 strcpy (ud->udata.buf, CONT_TXT); 37 ud->udata.len = sizeof(CONT_TXT); 38 } 39 else { 40 strcpy (ud->udata.buf, CANC_TXT); 41 ud->udata.len = sizeof(CANC_TXT); 42 }; 43 t_sndudata (fd, ud); 44 }; 45 }
Строки 1...8 описывают включаемые файлы, содержащие определения для всех необходимых структур данных и символических констант.
Строка 9 приписывает целочисленной константе 1234 символическое имя SRV_PORT. В дальнейшем эта константа будет использована в качестве номера порта сервера. Значение этой константы должно быть известно и программе-клиенту.
Строки 10 и 11 приписывают последовательностям символов, составляющих тексты возможных ответов клиенту, символические имена CONT_TXT и CANC_TXT. Последним символом в последовательностях является символ перехода на новую строку '\n'. Сделано это для упрощения вывода текста ответа на стороне клиента.
В строке 20 создается (открывается) транспортная точка для организации режима взаимодействия без установления логического соединения с помощью протокола транспортного уровня UDP в сети TCP/IP (/dev/udp). Транспортная точка будет использоваться для двустороннего обмена информацией (O_RDWR). Третий аргумент функции t_open задан как NULL, поскольку данную программу особые характеристики поставщика транспортных услуг не интересуют.
В строке 21 выделяется оперативная память под структуру данных типа struct t_bind и под буфер данных, определяемый полем addr этой структуры (размер памяти, выделяемой под этот буфер, функция t_alloc вычисляет самостоятельно на основе информации о конкретном поставщике транспортных услуг). Поле addr этой структуры в своем буфере будет содержать транспортный адрес транспортной точки, который для поставщиков транспортных услуг UDP и TCP имеет тот же формат, что и адрес socket'а.
В строках 22...27 сначала обнуляется структура данных типа struct sockaddr_in, на которую указывает bind->addr.buf а затем заполняются ее отдельные поля. Использование константы INADDR_ANY упрощает текст программы, избавляя от необходимости использовать функцию gethostbyname для получения адреса локального узла, на котором запускается сервер.
В строке 28 переменной bind->qlen присваивается значение 0, поскольку наш сервер предназначен для работы в режиме без установления соединения.
Строка 29 посредством функции t_bind привязывает к транспортной точке транспортный адрес, описанный в структуре, на которую указывает bind. T_bind завершится успешно при условии, что в момент его выполнения на том же узле уже не функционирует программа, использующая этот же транспортный адрес.
Строка 31 служит заголовком бесконечного цикла обслуживания запросов от клиентов.
В строке 32 с помощью функции t_rcvudata читается запрос клиента.
В строке 33 текст запроса направляется в стандартный вывод, имеющий дескриптор файла номер 1. Так как строка ответа содержит в себе символ перехода на новую строку, то текст ответа будет размещен на отдельной строке дисплея.
Строка 34 содержит обращение к функции time, возвращающей количество секунд времени, прошедших с 1 января 1970 г. до текущего момента. Это значение используется в программе-сервере для выбора варианта ответа клиенту.
В строке 43 сервер с помощью функции t_sndudata отправляет клиенту ответ на его запрос, выбранный из двух возможных вариантов псевдослучайным образом.
Примечание. Данная программа (как и большинство реальных программ-серверов) самостоятельно своей работы не завершает, находясь в бесконечном цикле обработки запросов клиентов. Ее выполнение может быть прервано только извне путем посылки ей сигналов (прерываний) завершения. Правильно разработанная программа-сервер должна обрабатывать такие сигналы, корректно завершая работу.
Текст программы-клиента на языке программирования СИ выглядит следующим образом
1 #include <tiuser.h> 2 #include <fcntl.h> 3 #include <stdio.h> 4 #include <sys/socket.h> 5 #include <netinet/in.h> 6 #include <netdb.h> 7 #include <memory.h> 8 #define SRV_HOST "delta" 9 #define SRV_PORT 1234 10 #define ASK_TXT "What must I do?\n" 11 #define CONT_TXT "Continue\n" 12 #define CANC_TXT "Cancel\n" 13 main () { 14 int fd; 15 int flags; 16 struct t_unitdata *ud; 17 struct sockaddr_in *p_addr; 18 struct hostent *hp; 19 extern int t_errno; 20 fd = t_open ("/dev/udp", O_RDWR, NULL); 21 t_bind (fd, NULL, NULL); 22 ud = (struct t_unitdata *) t_alloc (fd, T_UNITDATA, T_ALL); 23 memset (ud->addr.buf, '\0', ud->addr.maxlen); 24 p_addr = (struct sockaddr_in *) ud->addr.buf; 25 hp = gethostbyname (SRV_HOST); 26 p_addr->sin_family = AF_INET; 27 memcpy((char *)&(p_addr->sin_addr),hp->h_addr,hp->h_length); 28 p_addr->sin_port = SRV_PORT; 29 ud->addr.len = sizeof(struct sockaddr_in); 30 while (1) { 31 strcpy (ud->udata.buf, ASK_TXT); 32 ud->udata.len = sizeof(ASK_TXT); 33 t_sndudata (fd, ud); 34 t_rcvudata (fd, ud, &flags); 35 write (1, ud->udata.buf, ud->udata.len); 36 if ( strcmp(ud->udata.buf, CONT_TXT) ) 37 break; 38 }; 39 t_free ((char *) ud, T_UNITDATA); 40 t_close (fd); 41 exit (0); 42 }
В строках 8 и 9 описываются константы SRV_HOST и SRV_PORT, определяющие имя удаленного узла, на котором функционирует программа-сервер, и номер порта, к которому привязана транспортная точка сервера.
В строках 20 и 21 создается транспортная точка, имеющая не интересующий нас в этой программе транспортный адрес.
В строке 22 выделяется оперативная память под структуру данных типа struct t_unitdata и под три буфера, определяемых полями addr, opt и udata этой структуры (размер памяти, выделяемой под эти буфера, функция t_alloc вычисляет самостоятельно на основе информации о конкретном поставщике транспортных услуг).
В строке 25 посредством библиотечной функции gethostbyname транслируется символическое имя удаленного узла (в данном случае "delta"), на котором должен функционировать сервер, в адрес этого узла, размещенный в структуре типа hostent.
В строке 27 адрес удаленного узла копируется из структуры типа struct hostent в соответствующее поле структуры типа struct sockaddr_in, которая размещена в буфере ud->addr.
В строках 31 и 32 заполняются поля структуры ud->udata передаваемой серверу информацией и длиной этой информации.
В строке 33 с помощью функции t_sndudata посылается запрос серверу.
В строке 34 с помощью функции t_rcvudata принимается ответ от сервера. При этом транспортный адрес транспортной точки отправителя ответа (сервера) размещается функцией в ud->addr, а сами данные, составляющие ответ, - в ud->udata.
В строке 39 освобождается оперативная память, занимавшаяся структурой типа struct t_unitdata.
Строка 40 посредством функции t_close закрывает (удаляет) транспортную точку.