учебники, программирование, основы, введение в,

 

Семейство протоколов TCP/IP. Сокеты (sockets) в UNIX и основы работы с ними

Краткая история семейства протоколов TCP/IP
Мы приступаем к последней теме наших семинарских и практических занятий – введению в сетевое программирование в операционной системе UNIX.
Все многообразие сетевых приложений и многомиллионная всемирная компьютерная сеть выросли из четырехкомпьютерной сети ARPANET, созданной по заказу Министерства Обороны США и связавшей вычислительные комплексы в Стэндфордском исследовательском институте, Калифорнийском университете в Санта-Барбаре, Калифорнийском университете в Лос-Анджелесе и университете Юты. Первая передача информации между двумя компьютерами сети ARPANET состоялась в октябре 1969 года, и эту дату принято считать датой рождения нелокальных компьютерных сетей. (Необходимо отметить, что дата является достаточно условной, так как первая связь двух удаленных компьютеров через коммутируемые телефонные линии была осуществлена еще в 1965 году, а реальные возможности для разработки пользователями ARPANET сетевых приложений появились только в 1972 году.) Эта сеть росла и почковалась, закрывались ее отдельные части, появлялись ее гражданские аналоги, они сливались вместе, и в результате "что выросло – то выросло".
При создании ARPANET был разработан протокол сетевого взаимодействия коммуникационных узлов – Network Control Protocol (NCP), осуществлявший связь посредством передачи датаграмм (см. лекцию 14, раздел "Связь с установлением логического соединения и передача данных с помощью сообщений"). Этот протокол был предназначен для конкретного архитектурного построения сети и базировался на предположении, что сеть является статической и настолько надежной, что компьютерам не требуется умения реагировать на возникающие ошибки. По мере роста ARPANET и необходимости подключения к ней сетей, построенных на других архитектурных принципах (пакетные спутниковые сети, наземные пакетные радиосети), от этого предположения пришлось отказаться и искать другие подходы к построению сетевых систем. Результатом исследований в этих областях стало появление семейства протоколов TCP/IP, на базе которого обеспечивалась надежная доставка информации по неоднородной сети. Это семейство протоколов до сих пор занимает ведущее место в качестве сетевой технологии, используемой в операционной системе UNIX. Именно поэтому мы и выбрали его для практической иллюстрации общих сетевых решений, изложенных в лекции 14.
Общие сведения об архитектуре семейства протоколов TCP/IP
Семейство протоколов TCP/IP построено по "слоеному" принципу, подробно рассмотренному в лекции (лекция 14, раздел "Многоуровневая модель построения сетевых вычислительных систем"). Хотя оно и имеет многоуровневую структуру, его строение отличается от строения эталонной модели OSI, предложенной стандартом ISO. Это и неудивительно, так как основные черты семейства TCP/IP были заложены до появления эталонной модели и во многом послужили толчком для ее разработки. В семействе протоколов TCP/IP можно выделить четыре уровня:

  • Уровень сетевого интерфейса.
  • Уровень Internet.
  • Транспортный уровень.
  • Уровень приложений/процессов.

Соотношение уровней семейства TCP/IP и уровней модели OSI/ISO приведено.
На каждом уровне семейства TCP/IP присутствует несколько протоколов. Связь между наиболее употребительными протоколами и их принадлежность уровням изображены на.
Давайте кратко охарактеризуем каждый уровень семейства.
Уровень сетевого интерфейса
Уровень сетевого интерфейса составляют протоколы, которые обеспечивают передачу данных между узлами связи, физически напрямую соединенными друг с другом, или, иначе говоря, подключенными к одному сегменту сети, и соответствующие физические средства передачи данных. К этому уровню относятся протоколы Ethernet, Token Ring, SLIP, PPP и т.д. и такие физические средства как витая пара, коаксиальный кабель, оптоволоконный кабель и т.д. Формально протоколы уровня сетевого интерфейса не являются частью семейства TCP/IP, но существующие стандарты определяют, каким образом должна осуществляться передача данных семейства TCP/IP с использованием этих протоколов. На уровне сетевого интерфейса в операционной системе UNIX обычно функционируют драйверы различных сетевых плат.
Передача информации на уровне сетевого интерфейса производится на основании физических адресов, соответствующих точкам входа сети в узлы связи (например, физических адресов сетевых карт). Каждая точка входа имеет свой уникальный адрес – MAC-адрес (Media Access Control), физически зашитый в нее на этапе изготовления. Так, например, каждая сетевая плата Ethernet имеет собственный уникальный 48-битовый номер.

Уровень Internet. Протоколы IP, ICMP, ARP, RARP. Internet–адреса
Из многочисленных протоколов уровня Internet мы перечислим только те, которые будут в дальнейшем упоминаться в нашем курсе:

  • ICMP – Internet Control Message Protocol. Протокол обработки ошибок и обмена управляющей информацией между узлами сети.
  • IP – Internet Protocol. Это протокол, который обеспечивает доставку пакетов информации для протокола ICMP и протоколов транспортного уровня TCP и UDP.
  • ARP – Address Resolution Protocol. Это протокол для отображения адресов уровня Internet в адреса уровня сетевого интерфейса.
  • RARP – Reverse Address Resolution Protocol. Этот протокол служит для решения обратной задачи: отображения адресов уровня сетевого интерфейса в адреса уровня Internet.

Два последних протокола используются не для всех сетей; только некоторые сети требуют их применения.
Уровень Internet обеспечивает доставку информации от сетевого узла отправителя к сетевому узлу получателя без установления виртуального соединения с помощью датаграмм и не является надежным.
Центральным протоколом уровня является протокол IP. Вся информация, поступающая к нему от других протоколов, оформляется в виде IP-пакетов данных (IP datagrams). Каждый IP-пакет содержит адреса компьютера отправителя и компьютера получателя, поэтому он может передаваться по сети независимо от других пакетов и, возможно, по своему собственному маршруту. Любая ассоциативная связь между пакетами, предполагающая знания об их содержании, должна осуществляться на более высоком уровне семейства протоколов.
IP-уровень семейства TCP/IP не является уровнем, обеспечивающим надежную связь, так как он не гарантирует ни доставку отправленного пакета информации, ни то, что пакет будет доставлен без ошибок. IP вычисляет и проверяет контрольную сумму, которая покрывает только его собственный 20-байтовый заголовок для пакета информации (включающий, например, адреса отправителя и получателя). Если IP-заголовок пакета при передаче оказывается испорченным, то весь пакет просто отбрасывается. Ответственность за повторную передачу пакета тем самым возлагается на вышестоящие уровни.
IP протокол, при необходимости, осуществляет фрагментацию и дефрагментацию данных, передаваемых по сети. Если размер IP-пакета слишком велик для дальнейшей передачи по сети, то полученный пакет разбивается на несколько фрагментов, и каждый фрагмент оформляется в виде нового IP-пакета с теми же адресами отправителя и получателя. Фрагменты собираются в единое целое только в конечной точке своего путешествия. Если при дефрагментации пакета обнаруживается, что хотя бы один из фрагментов был потерян или отброшен, то отбрасывается и весь пакет целиком.
Уровень Internet отвечает за маршрутизацию пакетов. Для обмена информацией между узлами сети в случае возникновения проблем с маршрутизацией пакетов используется протокол ICMP. С помощью сообщений этого же протокола уровень Internet умеет частично управлять скоростью передачи данных – он может попросить отправителя уменьшить скорость передачи.
Поскольку на уровне Internet информация передается от компьютера-отправителя к компьютеру-получателю, ему требуются специальные IP-адреса компьютеров (а точнее, их точек подсоединения к сети – сетевых интерфейсов) – удаленные части полных адресов процессов (см. лекцию 14, раздел "Удаленная адресация и разрешение адресов"). Мы будем далее работать с IP версии 4 (IPv4), которая предполагает наличие у каждого сетевого интерфейса уникального 32-битового адреса. Когда разрабатывалось семейство протоколов TCP/IP, казалось, что 32 битов адреса будет достаточно для всех нужд сети, однако не прошло и 30 лет, как выяснилось, что этого мало. Поэтому была разработана версия 6 для IP (IPv6), предполагающая наличие 128-битовых адресов. С точки зрения сетевого программиста IPv6 мало отличается от IPv4, но имеет более сложный интерфейс передачи параметров, поэтому для практических занятий был выбран IPv4.
Все IP-адреса версии 4 принято делить на 5 классов. Принадлежность адреса к некоторому классу определяют по количеству последовательных 1 в старших битах адреса. Адреса классов A, B и C используют собственно для адресации сетевых интерфейсов. Адреса класса D применяются для групповой рассылки информации (multicast addresses) и далее нас интересовать не будут. Класс E (про который во многих книгах по сетям забывают) был зарезервирован для будущих расширений.
Каждый из IP-адресов классов A–C логически делится на две части: идентификатор или номер сети и идентификатор или номер узла в этой сети. Идентификаторы сетей в настоящее время присваиваются локальным сетям специальной международной организацией – корпорацией Internet по присвоению имен и номеров (ICANN). Присвоение адреса конкретному узлу сети, получившей идентификатор, является заботой ее администратора. Класс A предназначен для небольшого количества сетей, содержащих очень много компьютеров, класс C – напротив, для большого количества сетей с малым числом компьютеров. Класс B занимает среднее положение. Надо отметить, что все идентификаторы сетей классов A и B к настоящему моменту уже задействованы.
Любая организация, которой был выделен идентификатор сети из любого класса, может произвольным образом разделить имеющееся у нее адресное пространство идентификаторов узлов для создания подсетей.
Допустим, что вам выделен адрес сети класса C, в котором под номер узла сети отведено 8 бит. Если нужно присвоить IP-адреса 100 компьютерам, которые организованы в 10 Ethernet-сегментов по 10 компьютеров в каждом, можно поступить по-разному. Можно присвоить номера от 1 до 100 компьютерам, игнорируя их принадлежность к конкретному сегменту – воспользовавшись стандартной формой IP-адреса. Или же можно выделить несколько младших бит из адресного пространства идентификаторов узлов для идентификации сегмента сети, например 4 бита, а для адресации узлов внутри сегмента использовать оставшиеся 4 бита. Последний способ получил название адресации с использованием подсетей.
Запоминать четырехбайтовые числа для человека достаточно сложно, поэтому принято записывать IP-адреса в символической форме, переводя значение каждого байта в десятичный вид по отдельности и разделяя полученные десятичные числа в записи точками, начиная со старшего байта: 192.168.253.10.
Допустим, что мы имеем дело с сегментом сети, использующим Ethernet на уровне сетевого интерфейса и состоящим из компьютеров, где применяются протоколы TCP/IP на более высоких уровнях. Тогда у нас в сети есть два вида адресов: 48-битовые физические адреса Ethernet (MAC-адреса) и 32-битовые IP-адреса. Для нормальной передачи информации необходимо, чтобы Internet уровень семейства протоколов, обращаясь к уровню сетевого интерфейса, знал, какой физический адрес соответствует данному IP-адресу и наоборот, т.е. умел "разрешать адреса". В очередной раз мы сталкиваемся с проблемой разрешения адресов, которая в различных постановках разбиралась в материалах лекций. При разрешении адресов может возникнуть две сложности:

  • Если мы знаем IP-адреса компьютеров, которым или через которые мы хотим передать данные, то каким образом Internet уровень семейства протоколов TCP/IP сможет определить соответствующие им MAC-адреса? Эта проблема получила название address resolution problem (проблема разрешения адресов).
  • Пусть у нас есть бездисковые рабочие станции или рабочие станции, на которых операционные системы сгенерированы без назначения IP-адресов (это часто делается, когда один и тот же образ операционной системы загружается на ряд компьютеров, например, в учебных классах). Тогда при старте операционной системы на каждом таком компьютере операционная система знает только MAC-адреса, соответствующие данному компьютеру. Как можно определить, какой Internet-адрес был выделен данной рабочей станции? Эта проблема называется reverse address resolution problem (обратная проблема разрешения адресов).

Первая задача решается с использованием протокола ARP, вторая – с помощью протокола RARP.
Протокол ARP позволяет компьютеру разослать специальное сообщение по всему сегменту сети, которое требует от компьютера, имеющего содержащийся в сообщении IP-адрес, откликнуться и указать свой физический адрес. Это сообщение поступает всем компьютерам в сегменте сети, но откликается на него только тот, кого спрашивали. После получения ответа запрашивавший компьютер может установить необходимое соответствие между IP-адресом и MAC-адресом.
Для решения второй проблемы один или несколько компьютеров в сегменте сети должны выполнять функции RARP-сервера и содержать набор физических адресов для рабочих станций и соответствующих им IP-адресов. Когда рабочая станция с операционной системой, сгенерированной без назначения IP-адреса, начинает свою работу, она получает MAC-адрес от сетевого оборудования и рассылает соответствующий RARP-запрос, содержащий этот адрес, всем компьютерам сегмента сети. Только RARP-сервер, содержащий информацию о соответствии указанного физического адреса и выделенного IP-адреса, откликается на данный запрос и отправляет ответ, содержащий IP-адрес.

Транспортный уровень. Протоколы TCP и UDP. TCP и UDP сокеты. Адресные пространства портов. Понятие encapsulation
Мы не будем вдаваться в детали реализации протоколов транспортного уровня, а лишь кратко рассмотрим их основные характеристики. К протоколам транспортного уровня относятся протоколы TCP и UDP.
Протокол TCP реализует потоковую модель передачи информации, хотя в его основе, как и в основе протокола UDP, лежит обмен информацией через пакеты данных. Он представляет собой ориентированный на установление логической связи (connection-oriented), надежный (обеспечивающий проверку контрольных сумм, передачу подтверждения в случае правильного приема сообщения, повторную передачу пакета данных в случае неполучения подтверждения в течение определенного промежутка времени, правильную последовательность получения информации, полный контроль скорости передачи данных) дуплексный способ связи между процессами в сети. Протокол UDP, наоборот, является способом связи ненадежным, ориентированным на передачу сообщений (датаграмм). От протокола IP он отличается двумя основными чертами: использованием для проверки правильности принятого сообщения контрольной суммы, насчитанной по всему сообщению, и передачей информации не от узла сети к другому узлу, а от отправителя к получателю.
На лекции 14 (раздел "Полные адреса. Понятие сокета (socket)") мы говорили, что полный адрес удаленного процесса или промежуточного объекта для конкретного способа связи с точки зрения операционных систем определяется парой адресов: <числовой адрес компьютера в сети, локальный адрес>.
Такая пара получила название socket (гнездо, панель), так как по сути дела является виртуальным коммуникационным узлом (можно представить себе виртуальный разъем или ящик для приема/отправки писем), ведущим от объекта во внешний мир и наоборот. При непрямой адресации сами промежуточные объекты для организации взаимодействия процессов также именуются сокетами.
Поскольку уровень Internet семейства протоколов TCP/IP умеет доставлять информацию только от компьютера к компьютеру, данные, полученные с его помощью, должны содержать тип использованного протокола транспортного уровня и локальные адреса отправителя и получателя. И протокол TCP, и протокол UDP используют непрямую адресацию.
Для того чтобы избежать путаницы, мы в дальнейшем термин "сокет" будем употреблять только для обозначения самих промежуточных объектов, а полные адреса таких объектов будем называть адресами сокетов.
Для каждого транспортного протокола в стеке TCP/IP существуют собственные сокеты: UDP сокеты и TCP сокеты, имеющие различные адресные пространства своих локальных адресов – портов. В семействе протоколов TCP/IP адресные пространства портов представляют собой положительные значения целого 16-битового числа. Поэтому, говоря о локальном адресе сокета, мы часто будем использовать термин "номер порта". Из различия адресных пространств портов следует, что порт 1111 TCP – это совсем не тот же самый локальный адрес, что и порт 1111 UDP. О том, как назначаются номера портов различным сокетам, мы поговорим позже.
Итак, мы описали иерархическую систему адресации, используемую в семействе протоколов TCP/IP, которая включает в себя несколько уровней:

  • Физический пакет данных, передаваемый по сети, содержит физические адреса узлов сети (MAC-адреса) с указанием на то, какой протокол уровня Internet должен использоваться для обработки передаваемых данных (поскольку пользователя интересуют только данные, доставляемые затем на уровень приложений/процессов, то для него это всегда IP).
  • IP-пакет данных содержит 32-битовые IP-адреса компьютера-отправителя и компьютера-получателя, и указание на то, какой вышележащий протокол (TCP, UDP или еще что-нибудь) должен использоваться для их дальнейшей обработки.
  • Служебная информация транспортных протоколов (UDP-заголовок к данным и TCP-заголовок к данным) должна содержать 16-битовые номера портов для сокета отправителя и сокета получателя.

Добавление необходимой информации к данным при переходе от верхних уровней семейства протоколов к нижним принято называть английским словом encapsulation (дословно: герметизация). Поскольку между MAC-адресами и IP-адресами существует взаимно однозначное соответствие, известное семейству протоколов TCP/IP, то фактически для полного задания адреса доставки и адреса отправления, необходимых для установления двусторонней связи, нужно указать пять параметров:
<транспортный протокол, IP-адрес отправителя, порт отправителя, IP-адрес получателя, порт получателя>
Уровень приложений/процессов
К этому уровню можно отнести протоколы TFTP (Trivial File Transfer Protocol), FTP (File Transfer Protocol), telnet, SMTP (Simple Mail Transfer Protocol) и другие, которые поддерживаются соответствующими системными утилитами. Об их использовании подробно рассказано в UNIX Manual, и останавливаться на них мы не будем.
Нас будет интересовать в дальнейшем программный интерфейс между уровнем приложений/процессов и транспортным уровнем для того, чтобы мы могли создавать собственные процессы, общающиеся через сеть. Но прежде чем заняться программным интерфейсом, нам необходимо вспомнить особенности взаимодействия процессов в модели клиент-сервер.

Использование модели клиент-сервер для взаимодействия удаленных процессов
В материалах семинара 9 при обсуждении мультиплексирования сообщений (раздел "Понятие мультиплексирования. Мультиплексирование сообщений. Модель взаимодействия процессов клиент-сервер. Неравноправность клиента и сервера") говорилось об использовании модели клиент-сервер для организации взаимодействия локальных процессов. Эта же модель, изначально предполагающая неравноправность взаимодействующих процессов, наиболее часто используется для организации сетевых приложений. Напомним основные отличия процессов клиента и сервера применительно к удаленному взаимодействию:

  • Сервер, как правило, работает постоянно, на всем протяжении жизни приложения, а клиенты могут работать эпизодически.
  • Сервер ждет запроса от клиентов, инициатором же взаимодействия выступает клиент.
  • Как правило, клиент обращается к одному серверу за раз, в то время как к серверу могут одновременно поступить запросы от нескольких клиентов.
  • Клиент должен знать полный адрес сервера (его локальную и удаленную части) перед началом организации запроса (до начала общения), в то время как сервер может получить информацию о полном адресе клиента из пришедшего запроса (после начала общения).
  • И клиент, и сервер должны использовать один и тот же протокол транспортного уровня.

Неравноправность процессов в модели клиент-сервер, как мы увидим далее, накладывает свой отпечаток на программный интерфейс, используемый между уровнем приложений/процессов и транспортным уровнем.
Поступающие запросы сервер может обрабатывать последовательно – запрос за запросом – или параллельно, запуская для обработки каждого из них свой процесс или thread. Как правило, серверы, ориентированные на связь клиент-сервер с помощью установки логического соединения (TCP-протокол), ведут обработку запросов параллельно, а серверы, ориентированные на связь клиент-сервер без установления соединения (UDP-протокол), обрабатывают запросы последовательно.

Рассмотрим основные действия, которые нам необходимы в терминах абстракции socket для того, чтобы организовать взаимодействие между клиентом и сервером, используя транспортные протоколы стека TCP/IP.

Организация связи между удаленными процессами с помощью датаграмм
Как уже упоминалось в лекциях, более простой для взаимодействия удаленных процессов является схема организации общения клиента и сервера с помощью датаграмм, т. е. использование протокола UDP.
Рассмотрение этой схемы мы начнем с некоторой житейской аналогии, а затем убедимся, что каждому житейски обоснованному действию в операционной системе UNIX соответствует определенный системный вызов.
С точки зрения обычного человека общение процессов посредством датаграмм напоминает общение людей в письмах. Каждое письмо представляет собой законченное сообщение, содержащее адрес получателя, адрес отправителя и указания, кто написал письмо и кто должен его получить. Письма могут теряться, доставляться в неправильном порядке, быть поврежденными в дороге и т.д.
Что в первую очередь должен сделать человек, проживающий в отдаленной местности, для того чтобы принимать и отправлять письма? Он должен изготовить почтовый ящик, который одновременно будет служить и для приема корреспонденции, и для ее отправки. Пришедшие письма почтальон будет помещать в этот ящик и забирать из него письма, подготовленные к отправке.
Изготовленный почтовый ящик нужно где-то прикрепить. Это может быть парадная дверь дома или вход со двора, изгородь, столб, дерево и т.п. Потенциально может быть изготовлено несколько почтовых ящиков и размещено в разных местах с тем, чтобы письма от различных адресатов прибывали в различные ящики. Этим ящикам будут соответствовать разные адреса: "г. Иванову, почтовый ящик на конюшне", "г. Иванову, почтовый ящик, что на дубе".
После закрепления ящика мы готовы к обмену корреспонденцией. Человек-клиент пишет письмо с запросом по заранее известному ему адресу человека-сервера и ждет получения ответного письма. После получения ответа он читает его и перерабатывает полученную информацию.
Человек-сервер изначально находится в состоянии ожидания запроса. Получив письмо, он читает текст запроса и определяет адрес отправителя. После обработки запроса он пишет ответ и отправляет его по обратному адресу, после чего начинает ждать следующего запроса.
Все эти модельные действия имеют аналоги при общении удаленных процессов по протоколу UDP.
Процесс-сервер должен сначала совершить подготовительные действия: создать UDP-сокет (изготовить почтовый ящик) и связать его с определенным номером порта и IP-адресом сетевого интерфейса (прикрепить почтовый ящик в определенном месте) – настроить адрес сокета. При этом сокет может быть привязан к конкретному сетевому интерфейсу (к конюшне, к дубу) или к компьютеру в целом, то есть в полном адресе сокета может быть либо указан IP-адрес конкретного сетевого интерфейса, либо дано указание операционной системе, что информация может поступить через любой сетевой интерфейс, имеющийся в наличии. После настройки адреса сокета операционная система начинает принимать сообщения, пришедшие на этот адрес и складывать их в сокет. Сервер дожидается поступления сообщения, читает его, определяет, от кого оно поступило и через какой сетевой интерфейс, обрабатывает полученную информацию и отправляет результат по обратному адресу. После чего процесс готов к приему новой информации от того же или другого клиента.
Процесс-клиент должен сначала совершить те же самые подготовительные действия: создать сокет и настроить его адрес. Затем он передает сообщение, указав, кому оно предназначено (IP-адрес сетевого интерфейса и номер порта сервера), ожидает от него ответа и продолжает свою деятельность.
Схематично эти действия выглядят так, как показано на. Каждому из них соответствует определенный системный вызов. Названия вызовов написаны справа от блоков соответствующих действий.
Создание сокета производится с помощью системного вызова socket(). Для привязки созданного сокета к IP-адресу и номеру порта (настройка адреса) служит системный вызов bind(). Ожиданию получения информации, ее чтению и, при необходимости, определению адреса отправителя соответствует системный вызов recvfrom(). За отправку датаграммы отвечает системный вызов sendto().
Прежде чем приступить к подробному рассмотрению этих системных вызовов и примеров программ нам придется остановиться на нескольких вспомогательных функциях, которые мы должны будем использовать при программировании.
Сетевой порядок байт. Функции htons(), htonl(), ntohs(), ntohl()
Передача от одного вычислительного комплекса к другому символьной информации, как правило (когда один символ занимает один байт), не вызывает проблем. Однако для числовой информации ситуация усложняется.
Как известно, порядок байт в целых числах, представление которых занимает более одного байта, может быть для различных компьютеров неодинаковым. Есть вычислительные системы, в которых старший байт числа имеет меньший адрес, чем младший байт (big-endian byte order), а есть вычислительные системы, в которых старший байт числа имеет больший адрес, чем младший байт (little-endian byte order). При передаче целой числовой информации от машины, имеющей один порядок байт, к машине с другим порядком байт мы можем неправильно истолковать принятую информацию. Для того чтобы этого не произошло, было введено понятие сетевого порядка байт, т.е. порядка байт, в котором должна представляться целая числовая информация в процессе передачи ее по сети (на самом деле – это big-endian byte order). Целые числовые данные из представления, принятого на компьютере-отправителе, переводятся пользовательским процессом в сетевой порядок байт, в таком виде путешествуют по сети и переводятся в нужный порядок байт на машине-получателе процессом, которому они предназначены. Для перевода целых чисел из машинного представления в сетевое и обратно используется четыре функции: htons(), htonl(), ntohs(), ntohl().



Прототипы функций
#include <netinet/in.h>
unsigned long int htonl(
unsigned long int hostlong);
unsigned short int htons(
unsigned short int hostshort);
unsigned long int ntohl(
unsigned long int netlong);
unsigned short int ntohs(
unsigned short int netshort);
Описание функций
Функция htonl осуществляет перевод целого длинного числа из порядка байт, принятого на компьютере, в сетевой порядок байт.
Функция htons осуществляет перевод целого короткого числа из порядка байт, принятого на компьютере, в сетевой порядок байт.
Функция ntohl осуществляет перевод целого длинного числа из сетевого порядка байт в порядок байт, принятый на компьютере.
Функция ntohs осуществляет перевод целого короткого числа из сетевого порядка байт в порядок байт, принятый на компьютере.
В архитектуре компьютеров i80x86 принят порядок байт, при котором младшие байты целого числа имеют младшие адреса. При сетевом порядке байт, принятом в Internet, младшие адреса имеют старшие байты числа.

Параметр у них – значение, которое мы собираемся конвертировать. Возвращаемое значение – то, что получается в результате конвертации. Направление конвертации определяется порядком букв h (host) и n (network) в названии функции, размер числа – последней буквой названия, то есть htons – это host to network short, ntohl – network to host long.
Для чисел с плавающей точкой все обстоит гораздо хуже. На разных машинах могут различаться не только порядок байт, но и форма представления такого числа. Простых функций для их корректной передачи по сети не существует. Если требуется обмениваться действительными данными, то либо это нужно делать на гомогенной сети, состоящей из одинаковых компьютеров, либо использовать символьные и целые данные для передачи действительных значений.
Функции преобразования IP-адресов inet_ntoa(), inet_aton()
Нам также понадобятся функции, осуществляющие перевод IP-адресов из символьного представления (в виде четверки чисел, разделенных точками) в числовое представление и обратно. Функция inet_aton() переводит символьный IP-адрес в числовое представление в сетевом порядке байт.
Функция возвращает 1, если в символьном виде записан правильный IP-адрес, и 0 в противном случае – для большинства системных вызовов и функций это нетипичная ситуация. Обратите внимание на использование указателя на структуру struct in_addr в качестве одного из параметров данной функции. Эта структура используется для хранения IP-адресов в сетевом порядке байт. То, что используется структура, состоящая из одной переменной, а не сама 32-битовая переменная, сложилось исторически, и авторы в этом не виноваты.
Для обратного преобразования применяется функция inet_ntoa().


Функции преобразования IP-адресов
Прототипы функций
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int inet_aton(const char *strptr,
struct in_addr *addrptr);
char *inet_ntoa(struct in_addr *addrptr);
Описание функций
Функция inet_aton переводит символьный IP-адрес, расположенный по указателю strptr, в числовое представление в сетевом порядке байт и заносит его в структуру, расположенную по адресу addrptr. Функция возвращает значение 1, если в строке записан правильный IP-адрес, и значение 0 в противном случае. Структура типа struct in_addr используется для хранения IP-адресов в сетевом порядке байт и выглядит так:
struct in_addr {
in_addr_t s_addr;
};
То, что используется адрес такой структуры, а не просто адрес переменной типа in_addr_t, сложилось исторически.
Функция inet_ntoa применяется для обратного преобразования. Числовое представление адреса в сетевом порядке байт должно быть занесено в структуру типа struct in_addr, адрес которой addrptr передается функции как аргумент. Функция возвращает указатель на строку, содержащую символьное представление адреса. Эта строка располагается в статическом буфере, при последующих вызовах ее новое содержимое заменяет старое содержимое.

Функция bzero()
Функция 2 настолько проста, что про нее нечего рассказывать. Все видно из описания.


Функция bzero
Прототип функции
#include <string.h>
void bzero(void *addr, int n);
Описание функции
Функция bzero заполняет первые n байт, начиная с адреса addr, нулевыми значениями. Функция ничего не возвращает.

Теперь мы можем перейти к системным вызовам, образующим интерфейс между пользовательским уровнем стека протоколов TCP/IP и транспортным протоколом UDP.


Создание сокета. Системный вызов socket()
Для создания сокета в операционной системе служит системный вызов socket(). Для транспортных протоколов семейства TCP/IP существует два вида сокетов: UDP-сокетсокет для работы с датаграммами, и TCP сокет – потоковый сокет. Однако понятие сокета (см. лекцию 14, раздел "Полные адреса. Понятие сокета (socket)") не ограничивается рамками только этого семейства протоколов. Рассматриваемый интерфейс сетевых системных вызовов (socket(), bind(), recvfrom() , sendto() и т. д.) в операционной системе UNIX может применяться и для других стеков протоколов (и для протоколов, лежащих ниже транспортного уровня).
При создании сокета необходимо точно специфицировать его тип. Эта спецификация производится с помощью трех параметров вызова socket(). Первый параметр указывает, к какому семейству протоколов относится создаваемый сокет, а второй и третий параметры определяют конкретный протокол внутри данного семейства.
Второй параметр служит для задания вида интерфейса работы с сокетом – будет это потоковый сокет, сокет для работы с датаграммами или какой-либо иной. Третий параметр указывает протокол для заданного типа интерфейса. В стеке протоколов TCP/IP существует только один протокол для потоковых сокетов – TCP и только один протокол для датаграммных сокетов – UDP, поэтому для транспортных протоколов TCP/IP третий параметр игнорируется.
В других стеках протоколов может быть несколько протоколов с одинаковым видом интерфейса, например, датаграммных, различающихся по степени надежности.
Для транспортных протоколов TCP/IP мы всегда в качестве первого параметра будем указывать предопределенную константу AF_INET (Address family – Internet) или ее синоним PF_INET (Protocol family – Internet).
Второй параметр будет принимать предопределенные значения SOCK_STREAM для потоковых сокетов и SOCK_DGRAM – для датаграммных.
Поскольку третий параметр в нашем случае не учитывается, в него мы будем подставлять значение 0.
Ссылка на информацию о созданном сокете помещается в таблицу открытых файлов процесса подобно тому, как это делалось для pip’ов и FIFO (см. семинар 5). Системный вызов возвращает пользователю файловый дескриптор, соответствующий заполненному элементу таблицы, который далее мы будем называть дескриптором сокета. Такой способ хранения информации о сокете позволяет, во-первых, процессам-детям наследовать ее от процессов-родителей, а, во-вторых, использовать для сокетов часть системных вызовов, которые уже знакомы нам по работе с pip’ами и FIFO: close(), read(), write().

Системный вызов для создания сокета
Прототип системного вызова
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type,
int protocol);
Описание системного вызова
Системный вызов socket служит для создания виртуального коммуникационного узла в операционной системе. Данное описание не является полным описанием системного вызова, а предназначено только для использования в нашем курсе. За полной информацией обращайтесь к UNIX Manual.
Параметр domain определяет семейство протоколов, в рамках которого будет осуществляться передача информации. Мы рассмотрим только два таких семейства из нескольких существующих. Для них имеются предопределенные значения параметра:
  • PF_INET – для семейства протоколов TCP/IP;
  • PF_UNIX – для семейства внутренних протоколов UNIX, иначе называемого еще UNIX domain.

Параметр type определяет семантику обмена информацией: будет ли осуществляться связь через сообщения (datagrams), с помощью установления виртуального соединения или еще каким-либо способом. Мы будем пользоваться только двумя способами обмена информацией с предопределенными значениями для параметра type:

  • SOCK_STREAM – для связи с помощью установления виртуального соединения;
  • SOCK_DGRAM – для обмена информацией через сообщения.

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

Адреса сокетов. Настройка адреса сокета. Системный вызов bind()
Когда сокет создан, необходимо настроить его адрес. Для этого используется системный вызов bind(). Первый параметр вызова должен содержать дескриптор сокета, для которого производится настройка адреса. Второй и третий параметры задают этот адрес.
Во втором параметре должен быть указатель на структуру struct sockaddr, содержащую удаленную и локальные части полного адреса.
Указатели типа struct sockaddr * встречаются во многих сетевых системных вызовах; они используются для передачи информации о том, к какому адресу привязан или должен быть привязан сокет. Рассмотрим этот тип данных подробнее. Структура struct sockaddr описана в файле <sys/socket.h> следующим образом:
struct sockaddr {
short sa_family;
char sa_data[14];
};
Такой состав структуры обусловлен тем, что сетевые системные вызовы могут применяться для различных семейств протоколов, которые по-разному определяют адресные пространства для удаленных и локальных адресов сокета. По сути дела, этот тип данных представляет собой лишь общий шаблон для передачи системным вызовам структур данных, специфических для каждого семейства протоколов. Общим элементом этих структур остается только поле short sa_family (которое в разных структурах, естественно, может иметь разные имена, важно лишь, чтобы все они были одного типа и были первыми элементами своих структур) для описания семейства протоколов. Содержимое этого поля системный вызов анализирует для точного определения состава поступившей информации.
Для работы с семейством протоколов TCP/IP мы будем использовать адрес сокета следующего вида, описанного в файле <netinet/in.h>:
struct sockaddr _in{
short sin_family;
/* Избранное семейство протоколов
– всегда AF_INET */
unsigned short sin_port;
/* 16-битовый номер порта в сетевом
порядке байт */
struct in_addr sin_addr;
/* Адрес сетевого интерфейса */
char sin_zero[8];
/* Это поле не используется, но должно
всегда быть заполнено нулями */
};
Первый элемент структуры – sin_family задает семейство протоколов. В него мы будем заносить уже известную нам предопределенную константу AF_INET (см. предыдущий раздел).
Удаленная часть полного адреса – IP-адрес – содержится в структуре типа struct in_addr, с которой мы встречались в разделе "Функции преобразования IP-адресов inet_ntoa(), inet_aton()" .
Для указания номера порта предназначен элемент структуры sin_port, в котором номер порта должен храниться в сетевом порядке байт. Существует два варианта задания номера порта: фиксированный порт по желанию пользователя и порт, который произвольно назначает операционная система. Первый вариант требует указания в качестве номера порта положительного заранее известного числа и для протокола UDP обычно используется при настройке адресов сокетов и при передаче информации с помощью системного вызова sendto() (см. следующий раздел). Второй вариант требует указания в качестве номера порта значения 0. В этом случае операционная система сама привязывает сокет к свободному номеру порта. Этот способ обычно используется при настройке сокетов программ клиентов, когда заранее точно знать номер порта для программисту необязательно.
Какой номер порта может задействовать пользователь при фиксированной настройке? Номера портов с 1 по 1023 могут назначать сокетам только процессы, работающие с привилегиями системного администратора. Как правило, эти номера закреплены за системными сетевыми службами независимо от вида используемой операционной системы, для того чтобы пользовательские клиентские программы могли запрашивать обслуживание всегда по одним и тем же локальным адресам. Существует также ряд широко применяемых сетевых программ, которые запускают процессы с полномочиями обычных пользователей (например, X-Windows). Для таких программ корпорацией Internet по присвоению имен и номеров (ICANN) выделяется диапазон адресов с 1024 по 49151, который нежелательно использовать во избежание возможных конфликтов. Номера портов с 49152 по 65535 предназначены для процессов обычных пользователей. Во всех наших примерах при фи ксированном задании номера порта у сервера мы будем использовать номер 51000.
IP–адрес при настройке также может быть определен двумя способами. Он может быть привязан к конкретному сетевому интерфейсу (т.е. сетевой плате), заставляя операционную систему принимать/передавать информацию только через этот сетевой интерфейс, а может быть привязан и ко всей вычислительной системе в целом (информация может быть получена/отослана через любой сетевой интерфейс). В первом случае в качестве значения поля структуры sin_addr.s_addr используется числовое значение IP-адреса конкретного сетевого интерфейса в сетевом порядке байт. Во втором случае это значение должно быть равно значению предопределенной константы INADDR_ANY, приведенному к сетевому порядку байт.
Третий параметр системного вызова bind() должен содержать фактическую длину структуры, адрес которой передается в качестве второго параметра. Эта длина меняется в зависимости от семейства протоколов и даже различается в пределах одного семейства протоколов. Размер структуры, содержащей адрес сокета, для семейства протоколов TCP/IP может быть определен как sizeof(struct sockaddr_in).


Системный вызов для привязки сокета к конкретному адресу
Прототип системного вызова
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockd,
struct sockaddr *my_addr,
int addrlen);
Описание системного вызова
Системный вызов bind служит для привязки созданного сокета к определенному полному адресу вычислительной сети.
Параметр sockd является дескриптором созданного ранее коммуникационного узла, т. е. значением, которое вернул системный вызов socket().
Параметр my_addr представляет собой адрес структуры, содержащей информацию о том, куда именно мы хотим привязать наш сокет – то, что принято называть адресом сокета. Он имеет тип указателя на структуру-шаблон struct sockaddr, которая должна быть конкретизирована в зависимости от используемого семейства протоколов и заполнена перед вызовом.
Параметр addrlen должен содержать фактическую длину структуры, адрес которой передается в качестве второго параметра. Эта длина в разных семействах протоколов и даже в пределах одного семейства протоколов может быть различной (например, для UNIX Domain).
Возвращаемое значение
Системный вызов возвращает значение 0 при нормальном завершении и отрицательное значение – в случае ошибки.

Системные вызовы sendto() и recvfrom()
Для отправки датаграмм применяется системный вызов sendto(). В число параметров этого вызова входят:

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

Системный вызов возвращает отрицательное значение при возникновении ошибки и количество реально отосланных байт при нормальной работе. Нормальное завершение системного вызова не означает, что датаграмма уже покинула ваш компьютер! Датаграмма сначала помещается в системный сетевой буфер, а ее реальная отправка может произойти после возврата из системного вызова. Вызов sendto() может блокироваться, если в сетевом буфере не хватает места для датаграммы.
Для чтения принятых датаграмм и определения адреса получателя (при необходимости) служит системный вызов recvfrom(). В число параметров этого вызова входят:

  • Дескриптор сокета, через который принимается датаграмма.
  • Адрес области памяти, куда следует положить данные, составляющие содержательную часть датаграммы.
  • Максимальная длина, допустимая для датаграммы. Если количество данных датаграммы превышает заданную максимальную длину, то вызов по умолчанию рассматривает это как ошибочную ситуацию.
  • Флаги, определяющие поведение системного вызова (в нашем случае они будут полагаться равными 0).
  • Указатель на структуру, в которую при необходимости может быть занесен адрес сокета отправителя. Если этот адрес не требуется, то можно указать значение NULL.
  • Указатель на переменную, содержащую максимально возможную длину адреса отправителя. После возвращения из системного вызова в нее будет занесена фактическая длина структуры, содержащей адрес отправителя. Если предыдущий параметр имеет значение NULL, то и этот параметр может иметь значение NULL.

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


Системные вызовы sendto и recvfrom
Прототипы системных вызовов
#include <sys/types.h>
#include <sys/socket.h>
int sendto(int sockd, char *buff,
int nbytes, int flags,
struct sockaddr *to, int addrlen);
int recvfrom(int sockd, char *buff,
int nbytes, int flags,
struct sockaddr *from, int *addrlen);
Описание системных вызовов
Системный вызов sendto предназначен для отправки датаграмм. Системный вызов recvfrom предназначен для чтения пришедших датаграмм и определения адреса отправителя. По умолчанию при отсутствии пришедших датаграмм вызов recvfrom блокируется до тех пор, пока не появится датаграмма. Вызов sendto может блокироваться при отсутствии места под датаграмму в сетевом буфере. Данное описание не является полным описанием системных вызовов, а предназначено только для использования в нашем курсе. За полной информацией обращайтесь к UNIX Manual.
Параметр sockd является дескриптором созданного ранее сокета, т. е. значением, возвращенным системным вызовом socket(), через который будет отсылаться или получаться информация.
Параметр buff представляет собой адрес области памяти, начиная с которого будет браться информация для передачи или размещаться принятая информация.
Параметр nbytes для системного вызова sendto определяет количество байт, которое должно быть передано, начиная с адреса памяти buff. Параметр nbytes для системного вызова recvfrom определяет максимальное количество байт, которое может быть размещено в приемном буфере, начиная с адреса buff.
Параметр to для системного вызова sendto определяет ссылку на структуру, содержащую адрес сокета получателя информации, которая должна быть заполнена перед вызовом. Если параметр from для системного вызова recvfrom не равен NULL, то для случая установления связи через пакеты данных он определяет ссылку на структуру, в которую будет занесен адрес сокета отправителя информации после завершения вызова. В этом случае перед вызовом эту структуру необходимо обнулить.
Параметр addrlen для системного вызова sendto должен содержать фактическую длину структуры, адрес которой передается в качестве параметра to. Для системного вызова recvfrom параметр addrlen является ссылкой на переменную, в которую будет занесена фактическая длина структуры адреса сокета отправителя, если это определено параметром from. Заметим, что перед вызовом этот параметр должен указывать на переменную, содержащую максимально допустимое значение такой длины. Если параметр from имеет значение NULL, то и параметр addrlen может иметь значение NULL.
Параметр flags определяет режимы использования системных вызовов. Рассматривать его применение мы в данном курсе не будем, и поэтому берем значение этого параметра равным 0.
Возвращаемое значение
В случае успешного завершения системный вызов возвращает количество реально отосланных или принятых байт. При возникновении какой-либо ошибки возвращается отрицательное значение.

Определение IP-адресов для вычислительного комплекса
Для определения IP-адресов на компьютере можно воспользоваться утилитой /sbin/ifconfig. Эта утилита выдает всю информацию о сетевых интерфейсах, сконфигурированных в вычислительной системе. Пример выдачи утилиты показан ниже:
Сетевой интерфейс eth0 использует протокол Ethernet. Физический 48-битовый адрес, зашитый в сетевой карте, – 00:90:27:A7:1B:FE. Его IP-адрес – 192.168.253.12.
Сетевой интерфейс lo не относится ни к какой сетевой карте. Это так называемый локальный интерфейс, который через общую память эмулирует работу сетевой карты для взаимодействия процессов, находящихся на одной машине по полным сетевым адресам. Наличие этого интерфейса позволяет отлаживать сетевые программы на машинах, не имеющих сетевых карт. Его IP-адрес обычно одинаков на всех компьютерах – 127.0.0.1.
Пример программы UDP-клиента
Рассмотрим, наконец, простой пример программы 15–16-1.с. Эта программа является UDP-клиентом для стандартного системного сервиса echo. Стандартный сервис принимает от клиента текстовую датаграмму и, не изменяя ее, отправляет обратно. За сервисом зарезервирован номер порта 7. Для правильного запуска программы необходимо указать символьный IP-адрес сетевого интерфейса компьютера, к сервису которого нужно обратиться, в качестве аргумента командной строки, например:
a.out 192.168.253.12
Ниже следует текст программы
Наберите и откомпилируйте программу. Перед запуском "узнайте у своего системного администратора", запущен ли в системе стандартный UDP-сервис echo и если нет, попросите стартовать его. Запустите программу с запросом к сервису своего компьютера, к сервисам других компьютеров. Если в качестве IP-адреса указать несуществующий адрес, адрес выключенной машины или машины, на которой не работает сервис echo, то программа бесконечно блокируется в вызове recvfrom(), ожидая ответа. Протокол UDP не является надежным протоколом. Если датаграмму доставить по назначению не удалось, то отправитель никогда об этом не узнает!
Пример программы UDP-сервера
Поскольку UDP-сервер использует те же самые системные вызовы, что и UDP-клиент, мы можем сразу приступить к рассмотрению примера UDP-сервера (программа 15–16-2.с) для сервиса echo.
Наберите и откомпилируйте программу. Запустите ее на выполнение. Модифицируйте текст программы UDP-клиента, заменив номер порта с 7 на 51000. Запустите клиента с другого виртуального терминала или с другого компьютера и убедитесь, что клиент и сервер взаимодействуют корректно.

Организация связи между процессами с помощью установки логического соединения

Теперь посмотрим, какие действия нам понадобятся для организации взаимодействия процессов с помощью протокола TCP , то есть при помощи создания логического соединения. И начнем, как и в разделе "Использование модели клиент-сервер для взаимодействия удаленных процессов" текущего семинара, с простой жизненной аналогии. Если взаимодействие процессов через датаграммы напоминает общение людей по переписке, то для протокола TCP лучшей аналогией является общение людей по телефону.
Какие действия должен выполнить клиент для того, чтобы связаться по телефону с сервером? Во-первых, необходимо приобрести телефон (создать сокет), во-вторых, подключить его на АТС – получить номер (настроить адрес сокета). Далее требуется позвонить серверу (установить логическое соединение). После установления соединения можно неоднократно обмениваться с сервером информацией (писать и читать из потока данных). По окончании взаимодействия нужно повесить трубку (закрыть сокет).
Первые действия сервера аналогичны действиям клиента. Он должен приобрести телефон и подключить его на АТС (создать сокет и настроить его адрес). А вот дальше поведение клиента и сервера различно. Представьте себе, что телефоны изначально продаются с выключенным звонком. Звонить по ним можно, а вот принять звонок – нет. Для того чтобы вы могли пообщаться, необходимо включить звонок. В терминах сокетов это означает, что TCP-сокет по умолчанию создается в активном состоянии и предназначен не для приема, а для установления соединения. Для того чтобы соединение принять, сокет требуется перевести в пассивное состояние.
Если два человека беседуют по телефону, то попытка других людей дозвониться до них окажется неудачной. Будет идти сигнал "занято", и соединение не установится. В то же время хотелось бы, чтобы клиент в такой ситуации не получал отказ в обслуживании, а ожидал своей очереди. Подобное наблюдается в различных телефонных справочных, когда вы слышите "Ждите, пожалуйста, ответа. Вам обязательно ответит оператор". Поэтому следующее действие сервера – это создание очереди для обслуживания клиентов. Далее сервер должен дождаться установления соединения, прочитать информацию, переданную по линии связи, обработать ее и отправить полученный результат обратно. Обмен информацией может осуществляться неоднократно. Заметим, что сокет, находящийся в пассивном состоянии, не предназначен для операций приема и передачи информации. Для общения на сервере во время установления соединения автоматически создается новый потоковый сокет, через который и производится обмен данными с клиентами. По окончании общения сервер "кладет трубку" (закрывает этот новый сокет) и отправляется ждать очередного звонка.
Схематично эти действия выглядят так, как показано на. Как и в случае протокола UDP отдельным действиям или их группам соответствуют системные вызовы, частично совпадающие с вызовами для протокола UDP. Их названия написаны справа от блоков соответствующих действий.
Для протокола TCP неравноправность процессов клиента и сервера видна особенно отчетливо в различии используемых системных вызовов. Для создания сокетов и там, и там по-прежнему используется системный вызов socket(). Затем наборы системных вызовов становятся различными.
Для привязки сервера к IP-адресу и номеру порта, как и в случае UDP- протокола, используется системный вызов bind(). Для процесса клиента эта привязка объединена с процессом установления соединения с сервером в новом системном вызове connect() и скрыта от глаз пользователя. Внутри этого вызова операционная система осуществляет настройку сокета на выбранный ею порт и на адрес любого сетевого интерфейса. Для перевода сокета на сервере в пассивное состояние и для создания очереди соединений служит системный вызов listen(). Сервер ожидает соединения и получает информацию об адресе соединившегося с ним клиента с помощью си стемного вызова accept(). Поскольку установленное логическое соединение выглядит со стороны процессов как канал связи, позволяющий обмениваться данными с помощью потоковой модели, для передачи и чтения информации оба системных вызова используют уже известные нам системные вызовы read() и write(), а для завершения соединения – системный вызов close(). Необходимо отметить, что при работе с сокетами вызовы read() и write() обладают теми же особенностями поведения, что и при работе с pip’ами и FIFO (см. семинар 5).


Установление логического соединения. Системный вызов connect()
Среди системных вызовов со стороны клиента появляется только один новый – connect(). Системный вызов connect() при работе с TCP-сокетами служит для установления логического соединения со стороны клиента. Вызов connect() скрывает внутри себя настройку сокета на выбранный системой порт и произвольный сетевой интерфейс (по сути дела, вызов bind() с нулевым номером порта и IP-адресом INADDR_ANY). Вызов блокируется до тех пор, пока не будет установлено логическое соединение, или пока не пройдет определенный промежуток времени, который может регулироваться системным администратором.
Для установления соединения необходимо задать три параметра: дескриптор активного сокета, через который будет устанавливаться соединение, полный адрес сокета сервера и его длину.

Системный вызов connect()
Прототип системного вызова
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockd,
struct sockaddr *servaddr,
int addrlen);
Описание системного вызова
Системный вызов connect служит для организации связи клиента с сервером. Чаще всего он используется для установления логического соединения, хотя может быть применен и при связи с помощью датаграмм (connectionless). Данное описание не является полным описанием системного вызова, а предназначено только для использования в нашем курсе. Полную информацию можно найти в UNIX Manual.
Параметр sockd является дескриптором созданного ранее коммуникационного узла, т. е. значением, которое вернул системный вызов socket().
Параметр servaddr представляет собой адрес структуры, содержащей информацию о полном адресе сокета сервера. Он имеет тип указателя на структуру-шаблон struct sockaddr, которая должна быть конкретизирована в зависимости от используемого семейства протоколов и заполнена перед вызовом.
Параметр addrlen должен содержать фактическую длину структуры, адрес которой передается в качестве второго параметра. Эта длина меняется в зависмости от семейства протоколов и различается даже в пределах одного семейства протоколов (например, для UNIX Domain).
При установлении виртуального соединения системный вызов не возвращается до его установления или до истечения установленного в системе времени – timeout. При использовании его в connectionless связи вызов возвращается немедленно.
Возвращаемое значение
Системный вызов возвращает значение 0 при нормальном завершении и отрицательное значение, если в процессе его выполнения возникла ошибка.

Пример программы TCP-клиента
Рассмотрим пример – программу 15–16-3.с. Это простой TCP-клиент, обращающийся к стандартному системному сервису echo. Стандартный сервис принимает от клиента текстовую датаграмму и, не изменяя ее, отправляет обратно. За сервисом зарезервирован номер порта 7. Заметим, что это порт 7 TCP – не путать с портом 7 UDP из примера в разделе "Пример программы UDP-клиента"! Для правильного запуска программы необходимо указать символьный IP-адрес сетевого интерфейса компьютера, к сервису которого требуется обратиться, в качестве аргумента командной строки, например:
a.out 192.168.253.12
Для того чтобы подчеркнуть, что после установления логического соединения клиент и сервер могут обмениваться информацией неоднократно, клиент трижды запрашивает текст с экрана, отсылает его серверу и печатает полученный ответ. Ниже представлен текст программы.
Наберите и откомпилируйте программу. Перед запуском "узнайте у своего системного администратора", запущен ли в системе стандартный TCP-сервис echo и, если нет, попросите это сделать. Запустите программу с запросом к сервису своего компьютера, к сервисам других компьютеров. Если в качестве IP-адреса указать несуществующий адрес или адрес выключенной машины, то программа сообщит об ошибке при работе вызова connect() (правда, возможно, придется подождать окончания timeout’а). При задании адреса компьютера, на котором не работает сервис echo, об ошибке станет известно сразу же. Протокол TCP является надежным протоколом. Если логическое соединение установить не удалось, то отправитель будет знать об этом.
Как происходит установление виртуального соединения
Протокол TCP является надежным дуплексным протоколом. С точки зрения пользователя работа через протокол TCP выглядит как обмен информацией через поток данных. Внутри сетевых частей операционных систем поток данных отправителя нарезается на пакеты данных, которые, собственно, путешествуют по сети и на машине-получателе вновь собираются в выходной поток данных. В лекции 4 речь шла о том, каким образом может обеспечиваться надежность передачи информации в средствах связи, использующих в своей основе передачу пакетов данных. В протоколе TCP используются приемы нумерации передаваемых пакетов и контроля порядка их получения, подтверждения о приеме пакета со стороны получателя и насчет контрольных сумм по передаваемой информации. Для правильного порядка получения пакетов получатель должен знать начальный номер первого пакета отправителя. Поскольку связь является дуплексной, и в роли отправителя пакетов данных могут выступать обе взаимодействующие стороны, они до передачи пакетов данных должны обменяться, по крайней мере, информацией об их начальных номерах. Согласование начальных номеров происходит по инициативе клиента при выполнении системного вызова connect().
Для такого согласования клиент посылает серверу специальный пакет информации, который принято называть SYN (от слова synchronize – синхронизировать). Он содержит, как минимум, начальный номер для пакетов данных, который будет использовать клиент. Сервер должен подтвердить получение пакета SYN от клиента и отправить ему свой пакет SYN с начальным номером для пакетов данных, в виде единого пакета с сегментами SYN и ACK (от слова acknowledgement – подтверждение). В ответ клиент пакетом данных ACK должен подтвердить прием пакета данных от сервера.
Описанная выше процедура, получившая название трехэтапного рукопожатия (three-way handshake), схематично изображена на. При приеме на машине-сервере пакета SYN, направленного на пассивный (слушающий) сокет, сетевая часть операционной системе создает копию этого сокетаприсоединенный сокет – для последующего общения, отмечая его как сокет с не полностью установленным соединением. После приема от клиента пакета ACK этот сокет переводится в состояние полностью установленного соединения, и тогда он готов к дальнейшей работе с использованием вызовов read() и write().

Системный вызов listen()
Системный вызов listen() является первым из еще неизвестных нам вызовов, применяемым на TCP–сервере. В его задачу входит перевод TCP–сокета в пассивное (слушающее) состояние и создание очередей для порождаемых при установлении соединения присоединенных сокетов, находящихся в состоянии не полностью установленного соединения и полностью установленного соединения. Для этого вызов имеет два параметра: дескриптор TCP–сокета и число, определяющее глубину создаваемых очередей.


Системный вызов listen()
Прототип системного вызова
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockd, int backlog);
Описание системного вызова
Системный вызов listen используется сервером, ориентированным на установление связи путем виртуального соединения, для перевода сокета в пассивный режим и установления глубины очереди для соединений.
Параметр sockd является дескриптором созданного ранее сокета, который должен быть переведен в пассивный режим, т. е. значением, которое вернул системный вызов socket(). Системный вызов listen требует предварительной настройки адреса сокета с помощью системного вызова bind().
Параметр backlog определяет максимальный размер очередей для сокетов, находящихся в состояниях полностью и не полностью установленных соединений.
Возвращаемое значение
Системный вызов возвращает значение 0 при нормальном завершении и значение -1 при возникновении ошибки.

Последний параметр на разных UNIX-подобных операционных системах и даже на разных версиях одной и той же системы может иметь различный смысл. Где-то это суммарная длина обеих очередей, где-то он относится к очереди не полностью установленных соединений (например, Linux до версии ядра 2.2) где-то – к очереди полностью установленных соединений (например, Linux, начиная с версии ядра 2.2), где-то – вообще игнорируется.
Системный вызов accept()
Системный вызов accept() позволяет серверу получить информацию о полностью установленных соединениях. Если очередь полностью установленных соединений не пуста, то он возвращает дескриптор для первого присоединенного сокета в этой очереди, одновременно удаляя его из очереди. Если очередь пуста, то вызов ожидает появления полностью установленного соединения. Системный вызов также позволяет серверу узнать полный адрес клиента, установившего соединение. У вызова есть три параметра: дескриптор слушающего сокета, через который ожидается установление соединения; указатель на структуру, в которую при необходимости будет занесен полный адрес сокета клиента, установившего соединение; указатель на целую переменную, содержащую максимально допустимую длину этого адреса. Как и в случае вызова recvfrom(), последний параметр является модернизируемым, а если нас не интересует, кто с нами соединился, то вместо второго и третьего параметров можно указать значение NULL.


Системный вызов accept()
Прототип системного вызова
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockd,
struct sockaddr *cliaddr,
int *clilen);
Описание системного вызова
Системный вызов accept используется сервером, ориентированным на установление связи путем виртуального соединения, для приема полностью установленного соединения.
Параметр sockd является дескриптором созданного и настроенного сокета, предварительного переведенного в пассивный (слушающий) режим с помощью системного вызова listen().
Системный вызов accept требует предварительной настройки адреса сокета с помощью системного вызова bind().
Параметр cliaddr служит для получения адреса клиента, установившего логическое соединение, и должен содержать указатель на структуру, в которую будет занесен этот адрес.
Параметр clilen содержит указатель на целую переменную, которая после возвращения из вызова будет содержать фактическую длину адреса клиента. Заметим, что перед вызовом эта переменная должна содержать максимально допустимое значение такой длины. Если параметр cliaddr имеет значение NULL, то и параметр clilen может иметь значение NULL.
Возвращаемое значение
Системный вызов возвращает при нормальном завершении дескриптор присоединенного сокета, созданного при установлении соединения для последующего общения клиента и сервера, и значение -1 при возникновении ошибки.

Пример простого TCP-сервера
Рассмотрим программу 15–16-4.c, реализующую простой TCP-сервер для сервиса echo.
Наберите и откомпилируйте программу. Запустите ее на выполнение. Модифицируйте текст программы TCP-клиента, заменив номер порта с 7 на 51000. Запустите клиента с другого виртуального терминала или с другого компьютера и убедитесь, что клиент и сервер взаимодействуют корректно.

Создание программы с параллельной обработкой запросов клиентов
В приведенном выше примере сервер осуществлял последовательную обработку запросов от разных клиентов. При таком подходе клиенты могут подолгу простаивать после установления соединения, ожидая обслуживания. Поэтому обычно применяется схема псевдопараллельной обработки запросов. После приема установленного соединения сервер порождает процесс-ребенок, которому и поручает дальнейшую работу с клиентом. Процесс-родитель закрывает присоединенный сокет и уходит на ожидание нового соединения. Схематично организация такого сервера изображена на Напишите, откомпилируйте и запустите такой параллельный сервер. Убедитесь в его работоспособности. Не забудьте о необходимости удаления зомби-процессов.
Применение интерфейса сетевых вызовов для других семейств протоколов. UNIX Domain протоколы. Файлы типа "сокет"
Рассмотренный нами интерфейс умеет работать не только со стеком протоколов TCP/IP, но и с другими семействами протоколов. При этом требуется лишь незначительное изменение написанных с его помощью программ. Рассмотрим действия, которые необходимо выполнить для модернизации написанных для TCP/IP программ под другое семейство протоколов.

  1. Изменяется тип сокета, поэтому для его точной спецификации нужно задавать другие параметры в системном вызове socket().
  2. В различных семействах протоколов применяются различные адресные пространства для удаленных и локальных адресов сокетов. Поэтому меняется состав структуры для хранения полного адреса сокета, название ее типа, наименования полей и способ их заполнения.
  3. Описание типов данных и предопределенных констант будет находиться в других include-файлах, поэтому потребуется заменить include-файлы <netinet/in.h> и <arpa/inet.h> на файлы, относящиеся к выбранному семейству протоколов.
  4. Может измениться способ вычисления фактической длины полного адреса сокета и указания его максимального размера.

И все!!!
Давайте подробнее рассмотрим эти изменения на примере семейства UNIX Domain протоколов. Семейство UNIX Domain протоколов предназначено для общения локальных процессов с использованием интерфейса системных вызовов. Оно содержит один потоковый и один датаграммный протокол. Никакой сетевой интерфейс при этом не используется, а вся передача информации реально происходит через адресное пространство ядра операционной системы. Многие программы, взаимодействующие и с локальными, и с удаленными процессами (например, X-Windows), для локального общения используют этот стек протоколов.
Поскольку общение происходит в рамках одной вычислительной системы, в полном адресе сокета его удаленная часть отсутствует. В качестве адресного пространства портов – локальной части адреса – выбрано адресное пространство, совпадающее с множеством всех допустимых имен файлов в файловой системе.
При этом в качестве имени сокета требуется задавать имя несуществующего еще файла в директории, к которой у вас есть права доступа как на запись, так и на чтение. При настройке адреса (системный вызов bind()) под этим именем будет создан файл типа "сокет" – последний еще неизвестный нам тип файла. Этот файл для сокетов играет роль файла-метки типа FIFO для именованных pip’ов. Если на вашей машине функционируют X-Windows, то вы сможете обнаружить такой файл в директории с именем /tmp/.X11-unix – это файл типа "сокет", служащий для взаимодействия локальных процессов с оконным сервером.
Для хранения полного адреса сокета используется структура следующего вида, описанного в файле <sys/un.h>:
struct sockaddr _un{
short sun_family;
/* Избранное семейство
протоколов – всегда AF_UNIX */

char sun_path[108];
/* Имя файла типа "сокет" */
};
Выбранное имя файла мы будем копировать внутрь структуры, используя функцию strcpy().
Фактическая длина полного адреса сокета, хранящегося в структуре с именем my_addr, может быть вычислена следующим образом: sizeof(short)+strlen(my_addr.sun_path). В Linux для этих целей можно использовать специальный макрос языка С
SUN_LEN(struct sokaddr_un*)
Ниже приведены тексты переписанных под семейство UNIX Domain протоколов клиента и сервера для сервиса echo (программы 15–16-5.c и 15–16-6.c), общающиеся через датаграммы. Клиент использует сокет с именем AAAA в текущей директории, а сервер – сокет с именем BBBB. Как следует из описания типа данных, эти имена (полные или относительные) не должны по длине превышать 107 символов. Комментарии даны лишь для изменений по сравнению с программами 15–16-1.c и 15–16-2.c.
Наберите программы, откомпилируйте их и убедитесь в работоспособности.
Создание потоковых клиента и сервера для стека UNIX Domain протоколов
По аналогии с программами в предыдущем примере модифицируйте тексты программ TCP клиента и сервера для сервиса echo  для потокового общения в семействе UNIX Domain протоколов. Откомпилируйте их и убедитесь в правильном функционировании.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика