C сокеты. Как работают сокеты в C? Ожидание установления связи

На iOS - iPhone, iPod touch 22.03.2019
На iOS - iPhone, iPod touch

Всем привет сегодня расскажу чем отличается протокол TCP от UDP. Протоколы транспортного уровня, следующие в иерархии за IP, используются для передачи данных между прикладными процессами, реализующимися в сетевых узлах. Пакет данных, поступивший от одного компьютера другому через Интернет, должен быть передан процессу-обработчику, и именно по конкретному назначению. Транспортный уровень принимает на себя ответственность за это. На этом уровне два основных протокола – TCP и UDP.

Что означают TCP и UDP

TCP – транспортный протокол передачи данных в сетях TCP/IP, предварительно устанавливающий соединение с сетью.

UDP – транспортный протокол, передающий сообщения-датаграммы без необходимости установки соединения в IP-сети.

Напоминаю, что оба протокола работают на транспортном уровне модели OSI или TCP/IP, и понимание того чем они отличаются очень важно.

Разница между протоколами TCP и UDP

Разница между протоколами TCP и UDP – в так называемой “гарантии доставки”. TCP требует отклика от клиента, которому доставлен пакет данных, подтверждения доставки, и для этого ему необходимо установленное заранее соединение. Также протокол TCP считается надежным, тогда как UDP получил даже именование “протокол ненадежных датаграмм. TCP исключает потери данных, дублирование и перемешивание пакетов, задержки. UDP все это допускает, и соединение для работы ему не требуется. Процессы, которым данные передаются по UDP, должны обходиться полученным, даже и с потерями. TCP контролирует загруженность соединения, UDP не контролирует ничего, кроме целостности полученных датаграмм.

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

Давайте рассмотрим основные отличия tcp от udp.

  1. TCP гарантирует доставку пакетов данных в неизменных виде, последовательности и без потерь, UDP ничего не гарантирует.
  2. TCP нумерует пакеты при передаче, а UDP нет
  3. TCP работает в дуплексном режиме, в одном пакете можно отправлять информацию и подтверждать получение предыдущего пакета.
  4. TCP требует заранее установленного соединения, UDP соединения не требует, у него это просто поток данных.
  5. UDP обеспечивает более высокую скорость передачи данных.
  6. TCP надежнее и осуществляет контроль над процессом обмена данными.
  7. UDP предпочтительнее для программ, воспроизводящих потоковое видео, видеофонии и телефонии, сетевых игр.
  8. UPD не содержит функций восстановления данных

Примерами UDP приложений, например можно привести, передачу DNS зон, в Active Directory, там не требуется надежность. Очень часто такие вопросы любят спрашивать на собеседованиях, так, что очень важно знать tcp и udp отличия.

Заголовки TCP и UDP

Давайте рассмотрим как выглядят заголовки двух транспортных протоколов, так как и тут отличия кардинальные.

Заголовок UDP

  • 16 битный порт источника > Указание порта источника для UDP необязательно. Если это поле используется, получатель может отправить ответ этому порту.
  • 16 битный порт назначения > Номер порта назначения
  • 16 битная длина UDP > Длина сообщения, включая заголовок и данные.
  • 16 битная контрольная сумма > Контрольная сумма заголовка и данных для проверки

Заголовок TCP

  • 16 битный порт источника > Номер порта источника
  • 16 битный порт назначения > Номер порта назначения
  • 32 битный последовательный номер > Последовательный номер генерируется источником и используется назначением, чтобы переупорядочить пакеты для создания исходного сообщения и отправить подтверждение источнику.
  • 32 битный номер подтверждения > Если установлен бит АСК поля "Управление", в данном поле содержит следующий ожидаемый последовательный номер.
  • 4 бита длина заголовка > Информация о начале пакета данных.
  • резерв > Резервируются для будущего использования.
  • 16 битная контрольная сумма > Контрольная сумма заголовка и данных; по ней определяется, был ли искажен пакет.
  • 16 битный указатель срочности > В этом поле целевое устройство получает информацию о срочности данных.
  • Параметры > Необязательные значения, которые указываются при необходимости.

Размер окна позволяет экономить трафик, рассмотрим когда его значение равно 1, тут на каждый отправленный ответ, отправитель ждет подтверждения, не совсем рационально.

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

Надеюсь у вас теперь есть представления об отличиях tcp udp протоколов.

Протоколы TCP и UDP

TCP- Transmission Control Protocol

Обмен данными, ориентированный на соединения, может использовать надежную связь, для обеспечения которой протокол уровня 4 посылает подтверждения о получении данных и запрашивает повторную передачу, если данные не получены или искажены. Протокол TCP использует именно такую надежную связь. TCP используется в таких прикладных протоколах, как HTTP, FTP, SMTP и Telnet.

Протокол TCP требует, чтобы перед отправкой сообщения было открыто соединение. Серверное приложение должно выполнить так называемое пассивное открытие (passive open) , чтобы создать соединение с известным номером порта, и, вместо того чтобы отправлять вызов в сеть, сервер переходит в ожидание поступления входящих запросов. Клиентское приложение должно выполнить активное открытие (active open) , отправив серверному приложению синхронизирующий порядковый номер (SYN), идентифицирующий соединение. Клиентское приложение может использовать динамический номер порта в качестве локального порта.

Сервер должен отправить клиенту подтверждение (ACK) вместе с порядковым номером (SYN) сервера. В свою очередь клиент отвечает АСК, и соединение устанавливается.

После этого может начаться процесс отправки и получения сообщений. При получении сообщения в ответ всегда отправляется сообщение АСК. Если до получения АСК отправителем истекает тайм-аут, сообщение помещается в очередь на повторную передачу.

Поля заголовка TCP перечислены в следующей таблице:

Заголовок TCP
Поле Длина Описание
Порт источника 2 байта Номер порта источника
Порт назначения 2 байта Номер порта назначения
Последовательный номер 4 байта Последовательный номер генерируется источником и используется назначением, чтобы переупорядочить пакеты для создания исходного сообщения и отправить подтверждение источнику.
Номер подтверждения 4 байта Если установлен бит АСК поля "Управление", в данном поле содержится следующий ожидаемый последовательный номер.
Смещение данных 4 бита Информация о начале пакета данных.
Резерв 6 битов Резервируются для будущего использования.
Управление 6 битов Биты управления содержат флаги, указывающие, верны ли поля подтверждения (АСК), указателя срочности (URG), следует ли сбрасывать соединение (RST), послан ли синхронизирующий последовательный номер (SYN) и т. д.
Размер окна 2 байта В этом поле указывается размер приемного буфера. Используя подтверждающие сообщения, получатель может информировать отправителя о максимальном размере данных, которые тот может отправить.
Контрольная сумма 2 байта Контрольная сумма заголовка и данных; по ней определяется, был ли искажен пакет.
Указатель срочности 2 байта В этом поле целевое устройство получает информацию о срочности данных.
Опции переменная Необязательные значения, которые указываются при необходимости.
Дополнение переменная В поле дополнения добавляется столько нулей, чтобы заголовок заканчивался на 32-битной границе.

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

Протокол TCP имеет встроенную возможность надежной доставки. Если сообщение не отправлено корректно, мы получим сообщение об ошибке. Протокол TCP определен в RFC 793.

UDP - User Datagram Protocol

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

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

Так в чем же преимущества UDP, зачем может понадобиться такой ненадежный протокол? Чтобы понять причину использования UDP, нужно различать однонаправленную передачу, широковещательную передачу и групповую рассылку.

Однонаправленное (unicast) сообщение отправляется из одного узла только в один другой узел. Это также называется связью "точка-точка". Протокол TCP поддерживает лишь однонаправленную связь. Если серверу нужно с помощью TCP взаимодействовать с несколькими клиентами, каждый клиент должен установить соединение, поскольку сообщения могут отправляться только одиночным узлам.

Широковещательная передача (broadcast) означает, что сообщение отправляется всем узлам сети. Групповая рассылка (multicast) - это промежуточный механизм: сообщения отправляются выбранным группам узлов.

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

Протокол UDP

User Datagram Protocol (UDP) - это простой, ориентированный на дейтаграммы протокол без организации соединения, предоставляющий быстрое, но необязательно надежное транспортное обслуживание. Он поддерживает взаимодействия "один со многими" и поэтому часто применяется для широковещательной и групповой передачи дейтаграмм.

Internet Protocol (IP) является основным протоколом Интернета. Transmission Control Protocol (TCP) и UDP - это протоколы транспортного уровня, построенные поверх лежащего в основе протокола.

TCP/IP - это набор протоколов, называемый также "пакетом протоколов Интернета" (Internet Protocol Suite), состоящий из четырех уровней. Запомните, что TCP/IP не просто один протокол, а семейство или набор протоколов, который состоит из других низкоуровневых протоколов, таких, как IP, TCP и UDP. UDP располагается на транспортном уровне поверх IP (протокола сетевого уровня). Транспортный уровень обеспечивает взаимодействие между сетями через шлюзы. В нем используются IP-адреса для отправки пакетов данных через Интернет или другую сеть с помощью разнообразных драйверов устройств.

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

Пакеты

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

Дейтаграммы

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

MTU (Maximum Transmission Unit)

MTU характеризует канальный уровень и соответствует максимальному числу байтов, которое можно передать в одном пакете. Другими словами MTU - это самый большой пакет, который может переносить данная сетевая среда. Например, Ethernet имеет фиксированный MTU, равный 1500 байтам. В UDP, если размер дейтаграммы больше MTU, протокол IP выполняет фрагментацию, разбивая дейтаграмму на более мелкие части (фрагменты) так, чтобы каждый фрагмент был меньше MTU.

Порты

Чтобы поставить в соответствие входящим данным конкретный процесс, выполняемый в компьютере, UDP использует порты. UDP направляет пакет в соответствующее место, используя номер порта, указанный в UDP-заголовке дейтаграммы. Порты представлены 16-битными номерами и, следовательно, принимает значения в диапазоне от 0 до 65 535. Порты, которые также называют конечными точками логических соединений, разделены на три категории:

    Хорошо известные порты - от 0 до 1023

    Регистрируемые порты - от 1024 до 49151

    Динамические / частные порты - от 49152 до 65535

Заметим, что порты UDP могут получать более одного сообщения в каждый промежуток времени. В некоторых случаях сервисы TCP и UDP могут использовать одни и те же номера портов, например 7 (Echo) или 23 (Telnet).

UDP использует следующие известные порты:

Перечень портов UDP и TCP поддерживается агентством IANA (Internet Assigned Numbers Authority) .

IP-адреса

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

Однонаправленный IP-адрес уникально определяет хост в сети, тогда как групповой IP-адрес определяет конкретную группу адресов в сети. Широковещательные IP-адреса получаются и обрабатываются всеми хостами локальной сети или конкретной подсети.

TTL

Значение времени жизни, или TTL (time-to-live), позволяет установить верхний предел числа маршрутизаторов, через которые может пройти дейтаграмма. Значение TTL не дает пакетам попасть в бесконечные циклы. Оно инициализируется отправителем и уменьшается на единицу каждым маршрутизатором, обрабатывающим дейтаграмму. Когда значение TTL становится нулевым, дейтаграмма отбрасывается.

Групповая рассылка

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

Принцип работы UDP

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

На предыдущем рисунке указано, что общая длина заголовка UDP составляет восемь байтов. Теоретически максимальный размер дейтаграммы IP равен 65 535 байтам. С учетом 20 байтов заголовка IP и 8 байтов заголовка UDP длина данных пользователя может достигать 65 507 байтов. Однако большинство программ работают с данными меньшего размера. Так, для большинства приложений по умолчанию установлена длина приблизительно 8192 байта, поскольку именно такой объем данных считывается и записывается сетевой файловой системой (NFS). Можно устанавливать размеры входного и выходного буферов.

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

На следующем шаге уровень IP добавляет 20 байтов заголовка, включающего TTL, IP-адреса источника и получателя и другую информацию. Это действие называют IP-инкапсуляцией.

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

Когда приложение-отправитель "выбрасывает" дейтаграмму в сеть, она направляется по IP-адресу назначения, указанному в заголовке IP. При проходе через маршрутизатор значение времени жизни (TTL) в заголовке IP уменьшается на единицу.

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

Недостатки UDP

По сравнению с TCP UDP имеет следующие недостатки:

    Отсутствие сигналов квитирования . Перед отправкой пакета UDP, отправляющая сторона не обменивается с получающей стороной квитирующими сигналами. Следовательно, у отправителя нет способа узнать, достигла ли дейтаграмма конечной системы. В результате UDP не может гарантировать, что данные будут действительно доставлены адресату (например, если не работает конечная система или сеть).

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

    Использование сессий . Ориентированность TCP на соединения поддерживается сеансами между хостами. TCP использует идентификатор сеанса, позволяющий отслеживать соединения между двумя хостами. UDP не имеет поддержки сеансов из-за своей природы, не ориентированной на соединения.

    Надежность . UDP не гарантирует, что адресату будет доставлена только одна копия данных. Чтобы отправить конечной системе большой объем данных, UDP разбивает его на небольшие части. UDP не гарантирует, что эти части будут доставлены по назначению в том же порядке, в каком они создавались в источнике. Напротив, TCP вместе с номерами портов использует порядковые номера и регулярно отправляемые подтверждения, гарантирующие упорядоченную доставку данных.

    Безопасность . TCP более защищен, чем UDP. Во многих организациях брандмауэры и маршрутизаторы не пропускают пакеты UDP. Это связано с тем, что хакеры могут воспользоваться портами UDP, не устанавливая явных соединений.

    Управление потоком . В UDP управление потоком отсутствует, в результате плохо спроектированное UDP-приложение может захватить значительную часть пропускной способности сети.

Преимущества UDP

По сравнению с TCP UDP имеет следующие преимущества:

    Нет установки соединения . UDP является протоколом без организации соединений, поэтому он освобождает от накладных расходов, связанных с установкой соединений. Поскольку UDP не пользуется сигналами квитирования, то задержек, вызванных установкой соединений, также удается избежать. Именно поэтому DNS отдает предпочтение UDP перед TCP - DNS работала бы гораздо медленнее, если бы она выполнялась через TCP.

    Скорость . UDP работает быстрее TCP. По этой причине многие приложения предпочитают не TCP, a UDP. Те же средства, которые делают TCP более устойчивым (например сигналы квитирования), замедляют его работу.

    Топологическое разнообразие . UDP поддерживает взаимодействия "один с одним" и "один с многими", в то время как TCP поддерживает лишь взаимодействие "один с одним".

    Накладные расходы . Работа с TCP означает повышенные накладные расходы, издержки, налагаемые UDP, существенно ниже. TCP по сравнению с UDP использует значительно больше ресурсов операционной системы, и, как следствие, в таких средах, где серверы одновременно обслуживают многих клиентов, широко используют UDP.

    Размер заголовка . Для каждого пакета заголовок UDP имеет длину всего лишь восемь байтов, в то время как TCP имеет 20-байтовые заголовки, и поэтому UDP потребляет меньше пропускной способности сети.

User Datagram Protocol (UDP) (протокол пользовательских дейтаграмм) - является протоколом стандарта TCP/IP , определенный в стандарте RFC 768, "User Datagram Protocol (UDP)". UDP используется вместо TCP для быстрой и ненадежной транспортировки данных между TCP/IP хостами.

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

Чувствительные ко времени приложения часто используют UDP (видеоданные), так как предпочтительнее сбросить пакеты, чем ждать задержавшиеся пакеты, что может оказаться невозможным в системах реального времени. Также потеря одного или нескольких кадров, при передаче видеоданных по UDP, не так критична, в отличии от передачи бинарных файлов, где потеря одно пакета может привести к искажению всего файла. Еще одним преимуществом протокола UDP является то, что длина заголовка UDP составляет 4 байта, а у TCP протокола - 20 байт.

UDP сообщения инкапсулируются и передаются в IP дейтаграммы.

UDP заголовок

На рисунке показаны поля, присутствующие в UDP заголовке.

  • Порт отправителя - в этом поле указывается номер порта отправителя. Предполагается, что это значение задает порт, на который при необходимости будет посылаться ответ. В противном же случае, значение должно быть равным 0. Если хостом-источником является клиент, то номер порта будет, скорее всего, эфемерным. Если источником является сервер, то его порт будет одним из "хорошо известных".
  • Порт получателя - это поле обязательно и содержит порт получателя. Аналогично порту отправителя, если клиент - хост-получатель, то номер порта эфемерный, иначе (сервер - получатель) это "хорошо известный порт".
  • Длина дейтаграммы - поле, задающее длину всей дейтаграммы (заголовка и данных) в байтах. Минимальная длина равна длине заголовка - 8 байт. Теоретически, максимальный размер поля - 65535 байт для UDP-дейтаграммы (8 байт на заголовок и 65527 на данные). Фактический предел для длины данных при использовании IPv4 - 65507 (помимо 8 байт на UDP-заголовок требуется еще 20 на IP-заголовок).
  • Контрольная сумма - поле контрольной суммы используется для проверки заголовка и данных на ошибки. Если сумма не сгенерирована передатчиком, то поле заполняется нулями.

Рассмотрим структуру заголовка UDP с помощью сетевого анализатора Wireshark:

UDP порты

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

Номер порта - это условное 16-битное число от 1 до 65535, указывающее, какой программе предназначается пакет.

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

Все номера портов UDP, которые меньше чем 1024 - зарезервированы и зарегистрированы в Internet Assigned Numbers Authority (IANA).
Номера портов UDP и TCP не пересекаются.

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


Глава 15

В этой главе вы познакомитесь с еще одним способом взаимодействия процессов, существенно отличающимся от тех, которые мы обсуждали в главах 13 и 14. До настоящего момента все рассматриваемые нами средства основывались на совместно используемых ресурсах одного компьютера. Ресурсы могли быть разными: областью файловой системы, сегментами совместно используемой памяти или очередями сообщений, но использовать их могли только процессы, выполняющиеся на одной машине.

В версию ОС Berkeley UNIX было включено новое средство коммуникации - интерфейс сокетов, - являющееся расширением концепции канала, обсуждавшейся в главе 13. В системах Linux также есть интерфейсы сокетов.

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

Кроме того, интерфейс сокетов стал доступен в ОС Windows благодаря общедоступной спецификации Windows Sockets или WinSock. Сервисы сокетов в ОС Windows предоставляются системным файлом Winsock.dll. Стало быть, программы под управлением Windows могут взаимодействовать по сети с компьютерами под управлением Linux и UNIX и наоборот, реализуя, таким образом, клиент-серверные системы. Несмотря на то, что программный интерфейс для WinSock не совпадает полностью с интерфейсом сокетов в UNIX, в основе его лежат те же сокеты.

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

Более подробно мы рассмотрим следующие темы:

□ как действует соединение с помощью сокетов;

□ атрибуты сокетов, адреса и обмен информацией;

□ сетевая информация и интернет-демон (inetd/xinetd);

□ клиенты и серверы.

Что такое сокет?

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

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

Сокеты создаются и используются не так, как каналы, потому что они подчеркивают явное отличие между клиентом и сервером. Механизм сокетов позволяет создавать множество клиентов, присоединенных к единственному серверу.

Соединения на базе сокетов

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

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

, и этот сокет не может использоваться совместно с другими процессами.

Далее сервер присваивает сокету имя. Локальные сокеты с заданными именами файлов в файловой системе Linux часто размещаются в каталоге /tmp или /usr/tmp. У сетевых сокетов имя файла будет идентификатором сервиса (номер порта/точка доступа), относящегося к конкретной сети, к которой могут подключаться клиенты. Этот идентификатор, задавая определенный номер порта, соответствующий корректному серверному процессу, позволяет Linux направлять входящие подключения по определенному маршруту. Например, Web-сервер обычно создает сокет для порта 80, идентификатор, зарезервированный для этой цели. Web-обозреватели знают о необходимости применять порт 80 для своих HTTP-подключений к Web- сайтам, которые пользователь хочет читать. Именуется сокет с помощью системного вызова

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

Когда сервер вызывает

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

Клиентская сторона системы с применением сокетов гораздо проще. Клиент создает неименованный сокет с помощью вызова

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

Будучи установлены, сокеты могут применяться как низкоуровневые файловые дескрипторы, обеспечивая двунаправленный обмен данными.

Выполните упражнения 15.1 и 15.2.

Упражнение 15.1. Простой локальный клиент . Системный вызов мы подробно рассмотрим чуть позже, когда будем обсуждать некоторые проблемы адресации.

1. Включите нужные заголовочные файлы и задайте переменные:


2. Создайте сокет для клиента:

sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

3. Назовите сокет по согласованию с сервером:

address.sun_family = AF_UNIX;
strcpy(address.sun_path, "server_socket");

4. Соедините ваш сокет с сокетом сервера:

:
printf("char from server = %c\n", ch);

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

oops: client1: No such file or directory
Упражнение 15.2. Простой локальный сервер

Далее приведена программа простого сервера server1.с, которая принимает запрос на соединение от клиента. Она создает сокет сервера, присваивает ему имя, создает очередь ожидания и принимает запросы на соединения.

1. Включите необходимые заголовочные файлы и задайте переменные:


struct sockaddr_un server_address;
struct sockaddr_un client_address;

2. Удалите все старые сокеты и создайте неименованный сокет для сервера:

server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

3. Присвойте имя сокету:

server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, "server_socket");

4. Создайте очередь запросов на соединение и ждите запроса клиента:

5. Примите запрос на соединение:

(struct sockaddr *)&client_address, &client_len);

6. Читайте и записывайте данные клиента с помощью

:

Как это работает

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

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

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

.

Хорошо взять за правило удалять сокет после окончания работы с ним, даже в случае аварийного завершения программы из-за получения сигнала. Это убережет файловую систему от загромождения неиспользуемыми файлами.

$ ls -lF server socket
srwxr-xr-x 1 neil users 0 2007-06-23 11:41 server_socket=

Здесь тип устройства - сокет, на что указывает символ

перед правами доступа и символ в конце имени. Сокет был создан как обычный файл с правами доступа, модифицированными текущей . Если применить команду , то можно увидеть сервер, выполняющийся в фоновом режиме. Он показан спящим (параметр равен ) и, следовательно, не потребляющим ресурсы ЦП.
F UID PID PPID PRI NI VSZ RSS WCHAN STAT TTY TIME COMMAND
0 1000 23385 10689 17 0 1424 312 361800 S pts/1 0:00 ./server1

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

server waiting char from server = В

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

$ ./client1 & ./client1 & ./client1 &

Атрибуты сокета

Для того чтобы до конца понять системные вызовы, применявшиеся в рассмотренном примере, необходимо узнать кое-что об организации сети в системах UNIX.

Сокеты характеризуются тремя атрибутами: доменом, типом и протоколом. У них также есть адрес, используемый как имя сокета. Форматы адресов меняются в зависимости от домена, также называемого семейством протоколов (protocol family). Каждое семейство протоколов может применять одно или несколько семейств адресов, определяющих формат адреса.

Домены сокетов

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

, ссылающийся на сеть Интернет и применяемый во многих локальных сетях Linux и, конечно, в самом Интернете. Низкоуровневый протокол Internet Protocol (IP), у которого только одно адресное семейство, накладывает определенный способ задания компьютеров, входящих в сеть. Он называется IP-адресом.
Примечание

Для преодоления некоторых проблем стандартного протокола IP существенно ограниченного количества доступных адресов был разработан интернет-протокол нового поколения IPv6. Он использует другой домен сокетов

и иной формат адресов. Ожидается, что со временем IPv6 заменит IP, но для этого потребуется много лет. Несмотря на то, что уже есть реализации IPv6 для Linux, их обсуждение выходит за рамки этой книги.

Несмотря на то, что у машин в Интернете почти всегда есть имена, их преобразуют в IP-адреса. Пример IP-адреса - 192.168.1.99. Все IP-адреса представлены четырьмя числами, каждое из которых меньше 256, и образуют так называемые четверки с точками. Когда клиент подключается по сети с помощью сокетов, ему нужен IP- адрес компьютера сервера.

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

Серверы ожидают запросов на соединения от определенных клиентов. У хорошо известных сервисов есть выделенные номера портов, которые используются всеми машинами под управлением ОС Linux и UNIX. Обычно, но не всегда, эти номера меньше 1024. Примерами могут служить буфер печати принтера (515),

(513), (21) и (80). Последний из названных - стандартный порт для Web-серверов. Обычно номера портов, меньшие 1024, зарезервированы для системных сервисов и могут обслуживаться процессами с правами суперпользователя. Стандарт X/Open определяет в заголовочном файле netdb.h константу для указания наибольшего номера зарезервированных портов.

Поскольку для стандартных сервисов есть стандартный набор номеров портов, компьютеры могут легко соединяться друг с другом, не угадывая правильный номер порта. Локальный сервисы могут применять адреса нестандартных портов.

Домен в первом упражнении,

, - это домен файловой системы UNIX, который может использоваться сокетами, находящимися на единственном компьютере, возможно, даже не входящем в сеть. Если это так, то низкоуровневый протокол - это файловый ввод/вывод, а адреса - имена файлов. Для сокета сервера применялся адрес , который, как вы видели, появлялся в текущем каталоге, когда вы выполняли серверное приложение.

Кроме того, могут применяться и другие домены:

для сетей на основе стандартных протоколов ISO и для Xerox Network System (сетевая система Xerox). В этой книге мы их не будем обсуждать. Типы сокетов

У домена сокетов может быть несколько способов обмена данными, у каждого из которых могут быть разные характеристики. В случае сокетов домена

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

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

Потоковые сокеты

Потоковые сокеты (в чем-то подобные стандартным потокам ввода/вывода) обеспечивают соединение, представляющее собой последовательный и надежный двунаправленный поток байтов. Следовательно, гарантируется, что без указания возникшей ошибки данные не будут потеряны, продублированы или переупорядочены. Сообщения большого объема фрагментируются, передаются и снова собираются воедино. Это напоминает файловый поток, который принимает большие объемы данных и делит их на меньшие блоки для записи на физический диск. У потоковых сокетов предсказуемое поведение.

Потоковые сокеты, описываемые типом

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

TCP/IP - сокращение для протоколов Transmission Control Protocol/Internet Protocol. Протокол IP - низкоуровневый протокол передачи пакетов, обеспечивающий выбор маршрута при пересылке данных в сети от одного компьютера к другому. Протокол TCP обеспечивает упорядочивание, управление потоком и ретрансляцию, гарантирующие полную и корректную передачу больших объемов данных или же сообщение о соответствующей ошибочной ситуации.

Дейтаграммные сокеты

В отличие от потоковых дейтаграммные сокеты, описываемые типом

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

Дейтаграммные сокеты реализованы в домене

с помощью соединений UDP/IP и предоставляют неупорядоченный ненадежный сервис. (UDP сокращенное название протокола User Datagram Protocol.) Однако они относительно экономичны с точки зрения расходования ресурсов, поскольку не нуждаются в поддержке сетевых соединений. Они быстры, т.к. не тратится время на установку сетевого соединения.

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

На этом мы закончим обсуждение дейтаграмм, дополнительную информацию см. в разд. "Дейтаграммы" в конце данной главы.

Протоколы сокетов

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

Создание сокета

Системный вызов socket создает сокет и возвращает дескриптор, который может применяться для доступа к сокету:

#include
#include
int socket(int domain, int type, int protocol);

Созданный сокет - это одна конечная точка линии передачи. Параметр

задает семейство адресов, параметр определяет тип используемого с этим сокетом обмена данными, a - применяемый протокол.

В табл. 15.1 приведены имена доменов.


Таблица 15.1

К наиболее популярным доменам сокетов относятся

, применяемый для локальных сокетов, реализуемых средствами файловых систем UNIX и Linux, и , используемый для сетевых сокетов UNIX. Сокеты домена могут применяться программами, взаимодействующими в сетях на базе протоколов TCP/IP, включая Интернет. Интерфейс ОС Windows Winsock также предоставляет доступ к этому домену сокетов.

Параметр сокета type задает характеристики обмена данными, применяемые для нового сокета. Возможными значениями могут быть

и . - это упорядоченный, надежный, основанный на соединении, двунаправленный поток байтов. В случае домена сокетов этот тип обмена данными по умолчанию обеспечивается TCP-соединением, которое устанавливается между двумя конечными точками потоковых сокетов при подключении. Данные могут передаваться в двух направлениях по линии связи сокетов. Протоколы TCP включают в себя средства фрагментации и последующей повторной сборки сообщений больших объемов и повторной передачи любых их частей, которые могли быть потеряны в сети. - дейтаграммный сервис. Вы можете использовать такой сокет для отправки сообщений с фиксированным (обычно небольшим) максимальным объемом, но при этом нет гарантии, что сообщение будет доставлено или что сообщения не будут переупорядочены в сети. В случае сокетов домена этот тип передачи данных обеспечивается дейтаграммами UDP (User Datagram Protocol, пользовательский протокол дейтаграмм).

Протокол, применяемый для обмена данными, обычно определяется типом сокета и доменом. Как правило, выбора нет. Параметр

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

Системный вызов

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

Адреса сокетов

Каждый домен сокетов требует своего формата адресов. В домене

адрес описывается структурой , объявленной в заголовочном файле sys/un.h:
sa_family_t sun_family; /* AF_UNIX */
char sun_path; /* Путь к файлу */

Для того чтобы адреса разных типов могли передаваться в системные вызовы для обработки сокетов, все адресные форматы описываются похожей структурой, которая начинается с поля (в данном случае

), задающего тип адреса (домен сокета). В домене адрес задается именем файла в поле структуры .

В современных системах Linux тип

, описанный в стандарте X/Open как объявляемый в заголовочном файле sys/un.h, интерпретируется как тип . Кроме того, размер , задаваемого в поле , ограничен (в Linux указывается 108 символов; в других системах может применяться именованная константа, например, ). Поскольку размер адресной структуры может меняться, многие системные вызовы сокетов требуют или предоставляют на выходе длину, которая будет использоваться для копирования конкретной адресной структуры. адрес задается с помощью структуры с именем , определенной в файле netinet/in.h, которая содержит как минимум следующие элементы:
short int sin_family; /* AF_INET */
unsigned short int sin_port; /* Номер порта */
struct in_addr sin_addr; /* Интернет-адрес */

Структура IP-адреса типа in_addr определена следующим образом:

unsigned long int s_addr;

Четыре байта IP-адреса образуют одно 32-разрядное значение. Сокет домена

полностью описывается IP-адресом и номером порта. С точки зрения приложения все сокеты действуют как файловые дескрипторы, и их адреса задаются уникальными целочисленными значениями.

Именование сокета

Для того чтобы сделать сокет (созданный с помощью вызова

) доступным для других процессов, серверная программа должна присвоить сокету имя. Сокеты домена связаны с полным именем файла в файловой системе, как вы видели в программе-примере server1. Сокеты домена связаны с номером IP-порта.
#include
int bind(int socket, const struct sockaddr *address, size_t address len);

Системный вызов

присваивает адрес, заданный в параметре , неименованному сокету, связанному с дескриптором сокета . Длина адресной структуры передается в параметре :

Длина и формат адреса зависят от адресного семейства. В системном вызове

указатель конкретной адресной структуры должен быть приведен к обобщенному адресному типу .

В случае успешного завершения

возвращает 0. Если он завершается аварийно, возвращается -1, и переменной присваивается одно из значений, перечисленных в табл. 15.2.

Таблица 15.2

Создание очереди сокетов

Для приема запросов на входящие соединения на базе сокетов серверная программа должна создать очередь для хранения ждущих обработки запросов. Формируется она с помощью системного вызова

.
#include
int listen(int socket, int backlog);

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

задает длину очереди, равной . Входящие соединения, не превышающие максимальной длины очереди, сохраняются в ожидании сокета; последующим запросам на соединение будет отказано, и клиентская попытка соединения завершится аварийно. Этот механизм реализуется вызовом для того, чтобы можно было сохранить ждущие соединения запросы, пока серверная программа занята обработкой запроса предыдущего клиента. Очень часто параметр равен 5. вернет 0 в случае успешного завершения и -1 в случае ошибки. Как и для системного вызова , ошибки могут обозначаться константами , И .

Прием запросов на соединение

После создания и именования сокета серверная программа может ждать запросы на выполнение соединения с сокетом с помощью системного вызова

:
#include
int accept(int socket, struct sockaddr *address, size_t *address_len);

Системный вызов

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

Предварительно сокету должно быть присвоено имя с помощью системного вызова

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

Параметр

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

Если нет запросов на соединение, ждущих в очереди сокета, вызов accept будет заблокирован (так что программа не сможет продолжить выполнение) до тех пор, пока клиент не сделает запрос на соединение. Вы можете изменить это поведение, применив флаг

в файловом дескрипторе сокета с помощью вызова в вашей программе следующим образом:
int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, O_NONBLOCK | flags);
возвращает файловый дескриптор нового сокета, если есть запрос клиента, ожидающего соединения, и -1 в случае ошибки. Возможные значения ошибок такие же, как у вызовов и плюс дополнительная константа в случае, когда задан флаг и нет ждущих запросов на соединение. Ошибка возникнет, если процесс прерван во время блокировки в функции .

Запросы соединений

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

:
#include
int connect(int socket, const struct sockaddr *address, size_t address_len);

Сокет, заданный в параметре

, соединяется с сокетом сервера, заданным в параметре , длина которого равна . Сокет должен задаваться корректным файловым дескриптором, полученным из системного вызова .

Если функция

завершается успешно, она возвращает 0, в случае ошибки вернется -1. Возможные ошибки на этот раз включают значения, перечисленные в табл. 15.3.

Таблица 15.3

Если соединение не может быть установлено немедленно, вызов

будет заблокирован на неопределенный период ожидания. Когда допустимое время ожидания будет превышено, соединение разорвется и вызов завершится аварийно. Однако, если вызов прерван сигналом, который обрабатывается, connect завершится аварийно (со значением errno, равным ), но попытка соединения не будет прервана - соединение будет установлено асинхронно и программа должна будет позже проверить, успешно ли оно установлено.

Как и в случае вызова

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

Хотя асинхронные соединения трудно обрабатывать, вы можете применить вызов

к файловому дескриптору сокета, чтобы убедиться в том, что сокет готов к записи. Мы обсудим вызов чуть позже в этой главе.

Закрытие сокета

Вы можете разорвать сокетное соединение в серверной или клиентской программах, вызвав функцию

, так же как в случае низкоуровневых файловых дескрипторов. Сокеты следует закрывать на обоих концах. На сервере это нужно делать, когда вернет ноль. Имейте в виду, что вызов может быть заблокирован, если сокет, у которого есть непереданные данные, обладает типом, ориентированным на соединение, и установленным параметром . Дополнительную информацию об установке параметров сокета вы узнаете позже в этой главе.

Обмен данными с помощью сокетов

Теперь, когда мы описали основные системные вызовы, связанные с сокетами, давайте повнимательнее рассмотрим программы-примеры. Вы попытаетесь переработать их, заменив сокет файловой системы сетевым сокетом. Недостаток сокета файловой системы состоит в том, что если автор не использует полное имя файла, он создается в текущем каталоге серверной программы. Для того чтобы сделать его полезным в большинстве случаев, следует создать сокет в общедоступном каталоге (например, /tmp), подходящем для сервера и его клиентов. В случае сетевых серверов достаточно выбрать неиспользуемый номер порта.

Для примера выберите номер порта 9734. Это произвольный выбор, позволяющий избежать использования портов стандартных сервисов (вы не должны применять номера портов, меньшие 1024, поскольку они зарезервированы для системного использования). Другие номера портов с обеспечиваемыми ими сервисами часто приводятся в системном файле /etc/services. При написании программ, использующих сокеты, всегда выбирайте номер порта, которого нет в этом файле конфигурации.

Примечание

Вам следует знать, что в программах client2.c и server2.c умышленно допущена ошибка, которую вы устраните в программах client3.c и server3.c. Пожалуйста, не используйте текст примеров client2.c и server2.c в собственных программах.

Вы будете выполнять ваши серверную и клиентскую программы в локальной сети, но сетевые сокеты полезны не только в локальной сети, любая машина с подключением к Интернету (даже по модемной линии связи) может применять сетевые сокеты для обмена данными с другими компьютерами. Программу, основанную на сетевых подключениях, можно применять даже на изолированном компьютере с ОС UNIX, т. к. такой компьютер обычно настроен на использование виртуальной сети или внутренней петли (loopback network), включающей только его самого. Для демонстрационных целей данный пример использует виртуальную сеть, которая может быть также полезна для отладки сетевых приложений, поскольку она устраняет любые внешние сетевые проблемы.

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

, со стандартным IP-адресом 127.0.0.1. Это локальная машина. Ее адрес вы сможете найти в файле сетевых узлов etc/hosts наряду с именами и адресами других узлов, входящих в совместно используемые сети.

У каждой сети, с которой компьютер обменивается данными, есть связанный с ней аппаратный интерфейс. У компьютера в каждой сети может быть свое имя и конечно будут разные IP-адреса. Например, у машины Нейла с именем tilde три сетевых интерфейса и, следовательно, три адреса. Они записаны в файле /etc/hosts следующим образом.

127.0.0.1 localhost # Петля
192.168.1.1 tilde.localnet # Локальная частная сеть Ethernet
158.152.X.X tilde.demon.co.uk # Модемная линия связи

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

Выполните упражнения 15.3 и 15.4.

Упражнение 15.3. Сетевой клиент

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

1. Включите необходимые директивы

и задайте переменные:
#include
#include
struct sockaddr_in address;

2. Создайте сокет клиента:

3. Присвойте имя сокету по согласованию с сервером:

address.sin_addr.s_addr = inet_addr("127.0.0.1");
address.sin_port = 9734;

Оставшаяся часть программы такая же, как в приведенном ранее в этой главе примере. Когда вы выполните эту версию, она завершится аварийно, потому что на данном компьютере нет сервера, выполняющегося на порте 9734.

oops: client2: Connection refused

Как это работает

Клиентская программа использует структуру

из заголовочного файла netinet/in.h для задания адреса . Она пытается подключиться к серверу, размещенному на узле с IP-адресом 127.0.0.1. Программа применяет функцию для преобразования текстового представления IP-адреса в форму, подходящую для адресации сокетов. На страницах интерактивного справочного руководства для inet вы найдете дополнительную информацию о других функциях, преобразующих адреса. Упражнение 15.4. Сетевой сервер

Вам также нужно модифицировать серверную программу, ждущую подключений на выбранном вами номере порта. Далее приведена откорректированная программа сервера server2.c.

1. Вставьте необходимые заголовочные файлы и задайте переменные:

#include
#include

int server_sockfd, client_sockfd;

2. Создайте неименованный сокет для сервера:

3. Дайте имя сокету:

server_address.sin_port.s_addr = inet_addr("127.0.0.1");
server_address.sin_port = 9734;
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

Как это работает

Серверная программа создает сокет домена

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

Если вы хотите разрешить серверу устанавливать соединения с удаленными клиентами, необходимо задать набор IP-адресов, которые разрешены. Можно применить специальное значение

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

Порядок байтов на компьютере и в сети

Если запустить приведенные версии серверной и клиентской программ на машине на базе процессора Intel под управлением Linux, то с помощью команды

можно увидеть сетевые соединения. Эта команда есть в большинство систем UNIX, настроенных на работу в сети. Она отображает клиент-серверное соединение, ожидающее закрытия. Соединение закрывается после небольшой задержки. (Повторяем, что вывод в разных версиях Linux может отличаться.)
$ ./server2 & ./client2
Active Internet connections (w/o servers)
tcp 1 0 localhost:1574 localhost:1174 TIME_WAIT root
Примечание

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

killall server1 server2 server3 server4 server5

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

Отображается локальный адрес (сокет сервера) 1574 (или может выводиться имя сервиса

) и выбранный в примере порт 9734. Почему они отличаются? Дело в том, что номера портов и адреса передаются через интерфейсы сокета как двоичные числа. В разных компьютерах применяется различный порядок байтов для представления целых чисел. Например, процессор Intel хранит 32-разрядное целое в виде четырех последовательных байтов памяти в следующем порядке 1-2-3-4, где 1-й байт - самый старший. Процессоры IBM PowerPC будут хранить целое со следующим порядком следования байтов: 4-3-2-1. Если используемую для хранения целых память просто побайтно копировать, два компьютера не придут к согласию относительно целочисленных значений.

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

#include
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);

Эти функции преобразуют 16- и 32-разрядные целые из внутреннего формата в сетевой порядок следования байтов и обратно. Их имена соответствуют сокращенному названию выполняемых преобразований, например "host to network, long" (htonl, компьютерный в сетевой, длинные целые) и "host to network, short" (htons, компьютерный в сетевой, короткие целые). Компьютерам, у которых порядок следования байтов соответствует сетевому, эти функции предоставляют пустые операции.

Для обеспечения корректного порядка следования при передаче 16-разрядного целого числа ваши сервер и клиент должны применить эти функции к адресу порта. В программу server3.c следует внести следующие изменения:

server_address.sin_addr_s_addr = htonl(INADDR_ANY);

Результат, возвращаемый функцией

, преобразовывать не нужно, потому что в соответствии со своим определением она возвращает результат с сетевым порядком следования байтов. В программу client3.c необходимо внести следующее изменение:
address.sin_port = htons(9734);

В сервер, благодаря применению константы

, внесено изменение, позволяющее принимать запросы на соединение от любых IP-адресов.

Теперь, выполнив программы server3 и client3, вы увидите корректный номер порта, используемый для локального соединения:

Proto Recv-Q Send-Q Local Address Foreign Address (State) User
tcp 1 0 localhost:9734 localhost:1175 TIME_WAIT root
Примечание

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

Сетевая информация

До сих пор у клиентских и серверных программ были адреса и номера портов, компилируемые в них. В более универсальных серверных и клиентских программах для определения применяемых адресов и портов вы можете использовать данные сети.

Если у вас есть на это право, можно добавить свой сервер к списку известных сервисов в файл /etc/services, который назначает имена номерам портов, так что клиенты могут использовать вместо номеров символические имена сервисов.

Точно так же зная имя компьютера, можно определить IP-адрес, вызвав функции базы данных сетевых узлов (host database), которые найдут эти адреса. Делают они это, обращаясь за справкой к конфигурационным файлам, например, etc/hosts или к сетевым информационным сервисам, таким как NIS (Network Information Services (сервисы сетевой информации), ранее известным как Yellow Pages (желтые страницы)) и DNS (Domain Name Service, служба доменных имен).

Функции базы данных сетевых узлов или хостов (Host database) объявлены в заголовочном файле интерфейса netdb.h:

struct hostent *gethostbyaddr(const void* addr, size_t len, int type);
struct hostent* gethostbyname(const char* name);

Структура, возвращаемая этими функциями, должна как минимум содержать следующие элементы.

char *h_name; /* Имя узла */
char **h_aliases; /* Перечень псевдонимов (nicknames) */
int h_addrtype; /* Тип адреса */
int h_length; /* Длина адреса в байтах */
char **h_addr_list /* Перечень адреса (сетевой порядок байтов) */

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

Аналогично информацию, касающуюся сервисов и связанных номеров портов, можно получить с помощью информационных функций сервисов:

struct servent *getservbyname(const char *name, const char *proto);
struct servent *getservbyport(int port, const char *proto);

Параметр

задает протокол, который будет применяться для подключения к сервису, либо "tcp" для TCP-соединений типа , либо "udp" для UDP-дейтаграмм типа .

Структура

содержит как минимум следующие элементы:
char *s_name; /* Имя сервиса */
char **s_aliases; /* Список псевдонимов (дополнительных имен) */
int s_port; /* Номер IP-порта */
char *s_proto; /* Тип сервиса, обычно "tcp" или "udp" */

Вы можете собрать воедино информацию о компьютере из базы данных сетевых узлов, вызвав функцию

и выведя ее результаты. Учтите, что адрес необходимо преобразовать в соответствующий тип и перейти от сетевого упорядочивания к пригодной для вывода строке с помощью преобразования , определенного следующим образом:
#include
char *inet_ntoa(struct in_addr in);

Функция преобразует адрес интернет-узла в строку формата четверки чисел с точками. В случае ошибки она возвращает -1, но в стандарте POSIX не определены конкретные ошибки. Еще одна новая функция, которую вы примените, -

:
int gethostname(char *name, int name length);

Эта функция записывает имя текущего узла в строку, заданную параметром

. Имя узла будет нуль-терминированной строкой. Аргумент содержит длину строкового имени и, если возвращаемое имя узла превысит эту длину, оно будет обрезано. Функция возвращает 0 в случае успешного завершения и -1 в случае ошибки. И снова ошибки в стандарте POSIX не определены.

Выполните упражнение 15.5.

Упражнение 15.5. Сетевая информация

Данная программа getname.c получает сведения о компьютере.

1. Как обычно, вставьте соответствующие заголовочные файлы и объявите переменные:


char *host, **names, **addrs;

2. Присвойте переменной

значение аргумента, предоставляемого при вызове программы , или по умолчанию имя машины пользователя:

3. Вызовите функцию gethostbyname и сообщите об ошибке, если никакая информация не найдена:

fprintf(stderr, "cannot get info for host: %s\n", host);

4. Отобразите имя узла и любые псевдонимы, которые у него могут быть:

printf("results for host %s:\n", host);
printf("Name: %s\n", hostinfo->h_name);
printf(" %s", *names); names++;

5. Если запрашиваемый узел не является IP-узлом, сообщите об этом и завершите выполнение:

if (hostinfo->h_addrtype != AF_INET) {
fprintf(stderr, "not an IP host!\n");

6. В противном случае выведите IP-адрес (адреса):

addrs = hostinfo->h_addr_list;
printf(" %s", inet_ntoa(*(struct in_addr*)*addrs));

Для определения узла по заданному IP-адресу можно применить функцию

. Вы можете использовать ее на сервере для того, чтобы выяснить, откуда клиент запрашивает соединение.

Как это работает

Программа getname вызывает функцию gethostbyname для извлечения сведений об узле из базы данных сетевых узлов. Она выводит имя компьютера, его псевдонимы (другие имена, под которыми известен компьютер) и IP-адреса, которые он использует в своих сетевых интерфейсах. На одной из машин авторов выполнение примера и указание в качестве аргумента имени tilde привело к выводу двух интерфейсов: сети Ethernet и модемной линии связи.

Когда используется имя узла

, задается виртуальная сеть:

Теперь вы можете изменить свою программу-клиента для соединения с любым именованным узлом сети. Вместо подключения к серверу из вашего примера, вы соединитесь со стандартным сервисом и сможете извлечь номер порта.

Большинство систем UNIX и некоторые ОС Linux делают доступными свои системные время и дату в виде стандартного сервиса с именем

. Клиенты могут подключаться к этому сервису для выяснения мнения сервера о текущих времени и дате. В упражнении 15:6 приведена программа-клиент getdate.c, именно это и делающая. Упражнение 15.6. Подключение к стандартному сервису

1. Начните с обычных директив

и объявлений:
int main(int argc, char *argv) {

2. Найдите адрес узла и сообщите об ошибке, если адрес не найден:

hostinfo = gethostbyname(host);

3. Убедитесь, что на компьютере есть сервис

:
servinfo = getservbyname("daytime", "tcp");
printf("daytime port is %d\n", ntohs(servinfo->s_port));

4. Создайте сокет:

sockfd = socket(AF_INET, SOCK_STREAM, 0);

5. Сформируйте адрес для соединения:

address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *(struct in_addr *)*hostinfo->h_addr_list;

6. Затем подключитесь и получите информацию:

result = connect(sockfd, (struct sockaddr *)&address, len);
result = read(sockfd, buffer, sizeof(buffer));

Вы можете применять программу

для получения времени суток с любого известного узла сети.
read 26 bytes: 24 JUN 2007 06:03:03 BST

Если вы получаете сообщение об ошибке, такое как

oops: getdate: Connection refused
oops: getdate: No such file or directory

причина может быть в том, что на компьютере, к которому вы подключаетесь, не включен сервис

. Такое поведение стало стандартным для большинства современных систем Linux. В следующем разделе вы увидите, как включать этот и другие сервисы.

Как это работает

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

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

Интернет-демон (xinetd/inetd)

Системы UNIX, предоставляющие ряд сетевых сервисов, зачастую делают это с помощью суперсервера. Эта программа (интернет-демон xinetd или inetd) ожидает одновременно запросы на соединения с множеством адресов портов. Когда клиент подключается к сервису, программа-демон запускает соответствующий сервер. При таком подходе серверам не нужно работать постоянно, они могут запускаться по требованию.

Примечание

В современных системах Linux роль интернет-демона исполняет программа xinetd. Она заменила оригинальную UNIX-программу inetd, которую вы все еще можете встретить в более ранних системах Linux и других UNIX-подобных системах.

Программа xinetd обычно настраивается с помощью пользовательского графического интерфейса для управления сетевыми сервисами, но вы можете изменять и непосредственно файлы конфигурации программы. К ним относятся файл /etc/xinetd.conf и файлы в каталоге /etc/xinetd.d.

У каждого сервиса, предоставляемого программой xinetd, есть файл конфигурации в каталоге /etc/xinetd.d. Программа xinetd считает все эти файлы конфигурации во время запуска и повторно при получении соответствующей команды.

.
# По умолчанию: отключен
# Описание: сервер daytime. Это версия tcp.

Следующий файл конфигурации предназначен для сервиса передачи файлов.

# По умолчанию: отключен
# FTP-сервер vsftpd обслуживает FTP-соединения. Он использует
# для аутентификации обычные, незашифрованные имена пользователей и
# пароли, vsftpd спроектирован для безопасной работы.
# Примечание: этот файл содержит конфигурацию запуска vsftpd для xinetd.
# Файл конфигурации самой программы vsftpd находится в
# log_on_success += DURATION USERID
, к которому подключается программа , обычно обрабатывается самой программой xinetd (он помечен как внутренний) и может включаться с помощью как сокетов типа (tcp), так и сокетов типа (udp).

Сервис передачи файлов

подключается только сокетами типа и предоставляется внешней программой, в данном случае vsftpd. Демон будет запускать эту внешнюю программу, когда клиент подключится к порту .

Для активизации конфигурационных изменений сервиса можно отредактировать конфигурацию xinetd и отправить сигнал отбоя (hang-up) процессу-демону, но мы рекомендуем использовать более дружелюбный способ настройки сервисов. Для того чтобы разрешить вашему клиенту подключаться к сервису

, включите этот сервис с помощью средств, предоставляемых системой Linux. В системах SUSE и openSUSE сервисы можно настраивать из SUSE Control Center (Центр управления SUSE), как показано на рис. 15.1. У версий Red Hat (и Enterprise Linux, и Fedora) есть похожий интерфейс настройки. В нем сервис включается для TCP- и UDP-запросов.

Рис. 15.1


Для систем, применяющих программу inetd вместо xinetd, далее приведено эквивалентное извлечение из файла конфигурации inetd, /etc/inetd.conf, которое программа inetd использует для принятия решения о запуске серверов:

#
# Echo, discard, daytime и chargen используются в основном для
daytime stream tcp nowait root internal
daytime dgram udp wait root internal
# Это стандартные сервисы.
ftp stream tcp-nowait root /usr/sbin/tcpd /usr/sbin/wu.ftpd
telnet stream tcp nowait root /usr/sbin/tcpd /usr/sbin/in.telnetd
# Конец файла inetd.conf.

Обратите внимание на то, что в нашем примере сервис ftp предоставляется внешней программой wu.ftpd. Если в вашей системе выполняется демон inetd, вы можете изменить набор предоставляемых сервисов, отредактировав файл /etc/inetd.conf (знак # в начале строки указывает на то, что это строка комментария) и перезапустив процесс inetd. Сделать это можно, отправив сигнал отбоя (hang-up) с помощью команды

. Для облегчения этого процесса некоторые системы настроены так, что программа inetd записывает свой ID в файл. В противном случае можно применить команду :

Параметры сокета

Существует много параметров, которые можно применять для управления поведением соединений на базе сокетов - слишком много для подробного описания в этой главе. Для манипулирования параметрами используют функцию

:
#include
int setsockopt(int socket, int level, int option_name,
const void *option value, size_t option len);

Задавать параметры можно на разных уровнях иерархии протоколов. Для установки параметров на уровне сокета вы должны задать

равным . Для задания параметров на более низком уровне протоколов (TCP, UDP и т.д.) приравняйте параметр level номеру протокола (полученному либо из заголовочного файла netinet/in.h, либо из функции ).

В аргументе

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

Параметры уровня сокета определены в заголовочном файле sys/socket.h и включают приведенные в табл. 15.4 значения.


Таблица 15.5

Параметры

и принимают целое значение для установки или включения (1) и сброса или выключения (0). Для параметра нужна структура типа , определенная в файле sys/socket.h и задающая состояние параметра и величину интервала задержки. возвращает 0 в случае успеха и -1 в противном случае. На страницах интерактивного справочного руководства описаны дополнительные параметры и ошибки.

Множественные клиенты

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

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

Тот факт, что исходный сокет все еще доступен, и что сокеты ведут себя как файловые дескрипторы, дает нам метод одновременного обслуживания многих клиентов. Если сервер вызовет функцию

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

Поскольку вы создаете дочерние процессы, но не ждете их завершения, следует сделать так, чтобы сервер игнорировал сигналы

, препятствуя возникновению процессов-зомби. Упражнение 15.7. Сервер для многочисленных клиентов

1. Программа server4.c начинается так же, как последний рассмотренный сервер с важным добавлением директивы

для заголовочного файла signal.h. Переменные и процедуры создания и именования сокета остались прежними:
int server_sockfd, client_sockfd;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

2. Создайте очередь соединений, игнорируйте подробности завершения дочернего процесса и ждите запросов клиентов:

listen(server_sockfd, 5);
signal(SIGCHLD, SIG_IGN);
printf("server waiting\n");

3. Примите запрос на соединение:

client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct_sockaddr*)&client_address, &client_len);

4. Вызовите

с целью создания процесса для данного клиента и выполните проверку, чтобы определить, родитель вы или потомок: . Пятисекундная задержка нужна для того, чтобы это продемонстрировать:
read(client_sockfd, &ch, 1);
write(client_sockfd, &ch, 1);
close(client_sockfd);

6. В противном случае вы должны быть родителем и ваша работа с данным клиентом закончена:

close(client_socket);

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

$ ./client3 & ./client3 & ./client3 & ps x

Как это работает

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

(отредактированном) показан главный процесс server4 с PID, равным 26 566, который ожидает новых клиентов, в то время, как три клиентских процесса client3 обслуживаются тремя потомками сервера. После пятисекундной паузы все клиенты получают свои результаты и завершаются. Дочерние серверные процессы тоже завершаются, оставляя только один главный серверный процесс.

Серверная программа применяет вызов

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

select

Очень часто при разработке приложений Linux вам может понадобиться проверка состояния ряда вводов для того, чтобы определить следующее предпринимаемое действие. Например, программа обмена данными, такая как эмулятор терминала, нуждается в эффективном способе одновременного чтения с клавиатуры и с последовательного порта. В однопользовательской системе подойдет цикл "активного ожидания", многократно просматривающий ввод в поиске данных и читающий их, как только они появятся. Такое поведение очень расточительно в отношении времени ЦП.

Системный вызов

позволяет программе ждать прибытия данных (или завершения вывода) одновременно на нескольких низкоуровневых файловых дескрипторах. Это означает, что программа эмулятора терминала может блокироваться до тех пор, пока у нее не появится работа. Аналогичным образом сервер может иметь дело с многочисленными клиентами, ожидая запросы одновременно на многих открытых сокетах. оперирует структурами данных , представляющими собой множества открытых файловых дескрипторов. Для обработки этих множеств определен набор макросов:
#include #include
void FD_ZERO(fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);

Как и предполагается в соответствии с их именами, макрос

инициализирует структуру пустым множеством, и задают и очищают элементы множества, соответствующего файловому дескриптору, переданному как параметр , а макрос возвращает ненулевое значение, если файловый дескриптор, на который ссылается , является элементом структуры , на которую указывает параметр . Максимальное количество файловых дескрипторов в структуре типа задается константой . может также использовать значение для времени ожидания, чтобы помешать бесконечной блокировке. Это значение задается с помощью структуры . Она определена в файле sys/time.h и содержит следующие элементы:
time_t tv_sec; /* Секунды */
long tv_usec; /* Микросекунды */
, определенный в файле sys/types.h, - целочисленный. Системный вызов объявляется следующим образом:
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *errorfds, struct timeval *timeout);

Вызов . Если параметр - пустой указатель и нет активности на сокетах, вызов может быть заблокирован на неопределенное время. возвращает управление программе, множества дескрипторов будут модифицированы для того, чтобы указать на готовые к чтению или записи или имеющие ошибки дескрипторы. Для их проверки следует использовать макрос , позволяющий определить, какие дескрипторы требуют внимания. Можно изменить значение timeout для того, чтобы показать время, остающееся до следующего превышения времени ожидания, но такое поведение не задано стандартом X/Open. При превышении времени ожидания все множества дескрипторов будут очищены.

Вызов select возвращает общее количество дескрипторов в модифицированных множествах. В случае сбоя он вернет -1 и установит значение переменной

, описывающее ошибку. Возможные ошибки - Выполните упражнение 15.8. Упражнение 15.8. Функция

Далее для демонстрации применения функции select приведена программа select.c. Более сложный пример вы увидите чуть позже. Программа читает данные с клавиатуры (стандартный ввод - дескриптор 0) со временем ожидания 2,5 секунды. Данные читаются только тогда, когда ввод готов. Естественно расширить программу, включив в зависимости от характера приложения другие дескрипторы, такие как последовательные каналы (serial lines) и сокеты.

1. Начните как обычно с директив

и объявлений, а затем инициализируйте для обработки ввода с клавиатуры:

2. Подождите ввод из файла stdin в течение максимум 2,5 секунд:

result = select(FD_SETSIZE, &testfds, (fd_set *)NULL,

3. Спустя это время проверьте

. Если ввода не было, программа выполнит цикл еще раз. Если в нем возникла ошибка, программа завершается:

4. Если во время ожидания у вас наблюдаются некоторые действия, связанные с файловым дескриптором, читайте ввод из stdin и выводите его при каждом получении символа EOL (конец строки), до нажатой комбинации клавиш +:

$ Сервер может применять функцию

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

Если сокет, ожидающий запросов на подключение, готов к вводу, это означает, что клиент пытается подсоединиться, и вы можете вызывать функцию

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

Выполните упражнение 15.9.

Упражнение 15.9. Улучшенное клиент-серверное приложение

1. В финальный пример программы server5.с вы включите заголовочные файлы sys/time.h и sys/ioctl.h вместо signal.h, использованного в предыдущей программе, и объявите несколько дополнительных переменных для работы с вызовом

:
int server_sockfd, client_sockfd;
struct sockaddr_in server_address;
struct sockaddr_in client_address;

2. Создайте сокет для сервера и присвойте ему имя:

server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
bind(serversockfd, (struct sockaddr *)&server_address, server_len);

3. Создайте очередь запросов на соединение и инициализируйте множество

для обработки ввода с сокета
server_sockfd

6. Если зафиксирована активность на

, это может быть запрос на новое соединение, и вы добавляете в множество дескрипторов соответствующий :
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr*)&client_address, &client_len);
FD_SET(client_sockfd, &readfds);
printf("adding client on fd %d\n", client_sockfd);

Если активен не сервер, значит, активность проявляет клиент. Если получен

, клиент исчезает, и можно удалить его из множества дескрипторов. В противном случае вы "обслуживаете" клиента, как и в предыдущих примерах.
printf("removing client on fd %d\n", fd);

Для полноты аналогии, упомянутой в начале главы, в табл. 15.5 приведены параллели между соединениями на базе сокетов и телефонными переговорами.


Таблица 15.5

Дейтаграммы

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

Хорошим примером может служить сервис

, использованный ранее в программе getdate.c. Вы создаете сокет, выполняете соединение, читаете единственный ответ и разрываете соединение. Столько операций для простого получения даты! так же доступен с помощью UDP-соединений, применяющих дейтаграммы. Для того чтобы воспользоваться им, просто пошлите сервису одну дейтаграмму и получите в ответ единственную дейтаграмму, содержащую дату и время. Все просто.

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

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

Для доступа к сервису, обеспечиваемому UDP-протоколом, вам следует применять системные вызовы

и , но вместо использования вызовов и для сокета вы применяете два системных вызова, характерных для дейтаграмм: и .
/* Начните с обычных include и объявлений. */

int main(int argc, char *argv) {
if (argc == 1) host = "localhost";
/* Ищет адрес хоста и сообщает об ошибке, если не находит. */
hostinfo = gethostbyname(host);
fprintf(stderr, "no host: %s\n", host);
/* Проверяет наличие на компьютере сервиса daytime. */
servinfo = getservbyname("daytime", "udp");
fprintf(stderr, "no daytime service\n");
printf("daytime port is %d\n", ntohs(servinfo->s_port));
/* Создает UDP-сокет. */
sockfd = socket(AF_INEТ, SOCK_DGRAM, 0);
/* Формирует адрес для использования в вызовах sendto/recvfrom... */
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;
result = sendto(sockfd, buffer, 1, 0, (struct sockaddr *)&address, len);
result = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&address, &len);
printf("read %d bytes: %s", result, buffer);

Как видите, необходимы лишь незначительные изменения. Как и раньше, вы ищете сервис

с помощью вызова , но задаете дейтаграммный сервис, запрашивая UDP-протокол. Дейтаграммный сокет создается с помощью вызова с параметром . Адрес назначения задается, как и раньше, но теперь вместо чтения из сокета вы должны послать дейтаграмму.

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

Системный вызов

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

int sendto(int sockfd, void *buffer, size_t len, int flags,

struct sockaddr *to, socklen_t tolen);

В случае обычного применения параметр

можно оставлять нулевым.

Системный вызов recvfrom ожидает дейтаграмму в соединении сокета с заданным адресом и помещает ее в буфер. У этого вызова следующий прототип:

int recvfrom(int sockfd, void *buffer, size_t len, int flags, и времени ожидания, позволяющих определить, поступили ли данные, так же, как в случае серверов с устанавливаемыми соединениями. В противном случае можно применить сигнал тревоги для прерывания операции получения данных (см. главу 11).

Резюме

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

В заключение вы познакомились с системным вызовом

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



Рекомендуем почитать

Наверх