Java. HTTP протокол и работа с WEB. Использование сокетов для работы с UDP

35 ответов

Резюме

Сокет TCP является экземпляром конечной точки , определяемым IP-адресом и портом в контексте определенного TCP-соединения или состояния прослушивания.

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

Сокет TCP не является соединением , это конечная точка определенного соединения.

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

Для данной комбинации адресов и портов может быть только один разъем-слушатель.

Экспозиция

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

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

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

Комбинация IP-адреса и порта строго известна как конечная точка и иногда называется сокетом. Это использование связано с RFC793, оригинальной спецификацией TCP.

TCP-соединение определяется двумя конечными точками aka сокетами.

Конечная точка (сокет) определяется комбинацией сетевого адреса и идентификатора порта. Обратите внимание, что адрес/порт не полностью идентифицирует сокет (подробнее об этом позже).

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

Это пара сокетов (4-кортеж состоящий из IP-адреса клиента, номер порта клиента, IP-адрес сервера, и номер порта сервера), который указывает две конечные точки, которые однозначно идентифицирует каждое TCP-соединение в интернет. (TCP-IP Illustrated Volume 1, W. Richard Stevens)

В большинстве языков, основанных на C, TCP-соединения устанавливаются и обрабатываются с использованием методов в экземпляре класса Socket. Хотя распространено работать на более высоком уровне абстракции, обычно это экземпляр класса NetworkStream, он обычно предоставляет ссылку на объект сокета. К кодеру этот объект сокета, как представляется, представляет соединение, потому что соединение создается и управляется с помощью методов объекта сокета.

В С#, чтобы установить TCP-соединение (к существующему слушателю), вы сначала создаете TcpClient. Если вы не укажете конечную точку для конструктора TcpClient, она использует значения по умолчанию - так или иначе определяется локальная конечная точка. Затем вы вызываете Connect метод на созданном экземпляре. Этот метод требует параметра, описывающего другую конечную точку.

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

Проделав много чтения и мышления, я теперь убежден, что было бы гораздо разумнее иметь класс TcpConnection с конструктором, который принимает два аргумента: LocalEndpoint и RemoteEndpoint. Вероятно, вы могли бы поддержать один аргумент RemoteEndpoint, когда допустимы значения по умолчанию для локальной конечной точки. Это неоднозначно для многоъядерных компьютеров, но неоднозначность может быть решена с использованием таблицы маршрутизации путем выбора интерфейса с кратчайшим маршрутом к удаленной конечной точке.

Ясность будет повышена и в других отношениях. Сокет не идентифицируется комбинацией IP-адреса и порта:

[...] TCP демультиплексирует входящие сегменты, используя все четыре значения, которые содержат локальные и внешние адреса: IP-адрес получателя, номер порта назначения, IP-адрес источника и номер порта источника. TCP не может определить, какой процесс получает входящий сегмент, только глядя на порт назначения. Кроме того, единственная из [различных] конечных точек в [данном номере порта], которая будет принимать входящие запросы на соединение, является одной в состоянии прослушивания. (p255, TCP-IP Illustrated Volume 1, W. Richard Stevens)

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

Хаграваль не верит мне (см. комментарии), так что здесь настоящий образец. Я подключил веб-браузер к http://dilbert.com , а затем запустил netstat -an -p tcp . Последние шесть строк вывода содержат два примера того, что адреса и порта недостаточно, чтобы однозначно идентифицировать сокет. Существует два разных соединения между 192.168.1.3 (моя рабочая станция) и 54.252.92.236:80

TCP 192.168.1.3:63240 54.252.94.236:80 SYN_SENT TCP 192.168.1.3:63241 54.252.94.236:80 SYN_SENT TCP 192.168.1.3:63242 207.38.110.62:80 SYN_SENT TCP 192.168.1.3:63243 207.38.110.62:80 SYN_SENT TCP 192.168.1.3:64161 65.54.225.168:443 ESTABLISHED

Поскольку сокет является конечной точкой соединения, есть два сокета с комбинацией адресов/портов 207.38.110.62:80 и еще два с комбинацией адресов/портов 54.252.94.236:80 .

Я думаю, что неправильное понимание Хаграваля возникает из моего очень осторожного использования слова "идентифицирует". Я имею в виду "полностью, однозначно и однозначно идентифицировать". В приведенном выше примере имеются две конечные точки с комбинацией адрес/порт 54.252.94.236:80 . Если у вас есть адрес и порт, у вас недостаточно информации, чтобы разделить эти разъемы. Недостаточно информации для идентификации сокета.

Добавление

В абзаце втором раздела 2.7 RFC793 говорится:

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

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

Ссылки

    TCP-IP Иллюстрированный том 1 Протоколы, W. Richard Stevens, 1994 Addison Wesley

    Сокет представляет собой одно соединение между двумя сетевыми приложениями. Эти два приложения номинально работают на разных компьютерах, но сокеты также могут использоваться для межпроцессного взаимодействия на одном компьютере. Приложения могут создавать несколько сокетов для связи друг с другом. Сокеты являются двунаправленными, что означает, что обе стороны соединения способны отправлять и получать данные. Поэтому сокет может быть создан теоретически на любом уровне модели OSI с 2 вверх. Программисты часто используют сокеты в сетевом программировании, хотя и косвенно. Библиотеки программирования, такие как Winsock, скрывают многие низкоуровневые детали программирования сокетов. Розетки широко используются с начала 1980-х годов.

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

    С некоторой аналогией

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

    Рассмотрим сервер S ,

    и скажем, человеку X, Y, Z нужна служба (скажем, служба чата) с этого сервера S

    IP-адрес говорит кто? тот сервер чата "S", с которым X, Y, Z хочет связаться

    хорошо, вы получили "кто сервер"

    но предположим, что сервер "S" также предоставляет некоторые другие услуги другим людям, скажем, "S" предоставляет услуги хранения для лиц A, B, C

    порт говорит ---> который? сервис, который вам (X, Y, Z) нужен, т.е. сервис чата, а не сервис хранения

    ладно.., вы заставляете сервер знать, что вам нужен чат-сервис, а не хранилище

    вам три года, и сервер может захотеть идентифицировать все три по-разному

    приходит розетка

    теперь сокет говорит какой? конкретное соединение

    то есть, скажем,

    розетка 1 для человека X

    розетка 2 для человека Y

    и розетка 3 для человека Z

    Я надеюсь, что это помогает кому-то, кто был все еще смущен:)

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

    Общим определением для сети является использование модели OSI , которая разделяет сеть на несколько слоев в соответствии с назначением. Есть несколько важных, которые мы рассмотрим здесь:

    • Уровень канала передачи данных. Этот уровень отвечает за получение пакетов данных от одного сетевого устройства к другому и находится чуть выше уровня, фактически передающего. Он говорит о MAC-адресах и знает, как найти хосты на основе их MAC (аппаратного) адреса, но не более того.
    • Сетевой уровень - это уровень, который позволяет переносить данные по машинам и по физическим границам, таким как физические устройства. Сетевой уровень должен по существу поддерживать дополнительный механизм, основанный на адресе, который как-то связан с физическим адресом; введите IP-адрес (IPv4). IP-адрес может получить ваш пакет от A до B через Интернет, но ничего не знает о том, как проходить индивидуальные перелеты. Это обрабатывается уровнем выше в соответствии с информацией о маршрутизации.
    • Транспортный уровень. Этот уровень отвечает за определение способа получения информации от A до B и любых ограничений, проверок или ошибок в этом поведении. Например, TCP добавляет дополнительную информацию в пакет, так что можно вывести, если пакеты были потеряны.

    TCP содержит, помимо прочего, концепцию ports . Это фактически разные конечные точки данных на том же IP-адресе, к которому может привязываться Internet Socket (AF_INET).

    Короткий короткий ответ.

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

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

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

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

    Например, HTTP как служба работает на порту 80.

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

    Кажется, что есть много ответов, сравнивающих сокет с соединением между двумя ПК. Я думаю, что это абсолютно неверно. Сокет всегда был конечной точкой на 1 ПК, который может быть или не быть подключен - конечно, мы все использовали приемник или UDP-сокеты * в какой-то момент. Важная часть заключается в том, что он адресный и активный. Отправка сообщения в файл 1.1.1.1:1234 вряд ли будет работать, поскольку для этой конечной точки нет сокета.

    Сокеты специфичны для протокола - поэтому реализация уникальности заключается в том, что TCP / и UDP / использует * (ipaddress: порт), отличается от, например, IPX (Сеть, Node и... гейм, сокет - но другой сокет, чем под общим термином "сокет". Номера сокетов IPX эквивалентны IP-портам). Но все они предлагают уникальную адресуемую конечную точку.

    Поскольку IP стал доминирующим протоколом, порт (в сетевых терминах) стал сингулярным с номером порта UDP или TCP, который является частью адреса сокета.

    • UDP не имеет отношения к подключению - это означает, что виртуальная схема между двумя конечными точками никогда не создается. Однако в качестве конечной точки мы по-прежнему относимся к UDP-сокетам . Функции API дают понять, что оба являются просто разными типами сокетов. SOCK_DGRAM - это UDP (просто отправка сообщения), а SOCK_STREAM - TCP (создание виртуальной схемы).

      Технически, заголовок IP содержит IP-адрес, а протокол поверх IP (UDP или TCP) содержит номер порта. Это позволяет использовать другие протоколы (например, ICMP , которые не имеют номеров портов, но имеют информацию о IP-адресе).

      Это термины из двух разных доменов: "порт" - это концепция из сетей TCP/IP, "сокет" - это API (программирование). "Сокет" создается (в коде), беря порт, имя хоста или сетевой адаптер и объединяя их в структуру данных, которую вы можете использовать для отправки или получения данных.

      Соединения TCP-IP - это двунаправленные пути, соединяющие один адрес: комбинация портов с другим адресом: комбинация портов. Поэтому, всякий раз, когда вы открываете соединение с локальной машины на порт на удаленном сервере (например, www.google.com:80), вы также связываете новый номер порта на вашем компьютере с подключением, чтобы сервер мог отправлять вещи назад к вам (например, 127.0.0.1:65234). Полезно использовать netstat для просмотра ваших соединений с компьютером:

      > netstat -nWp tcp (on OS X) Active Internet connections Proto Recv-Q Send-Q Local Address Foreign Address (state) tcp4 0 0 192.168.0.6.49871 17.172.232.57.5223 ESTABLISHED ...

      Сокет - это особый тип дескриптора файла, который используется процессом для запроса сетевых сервисов из операционной системы. Адрес сокета - это тройной: {protocol, local-address, local-process}, где локальный процесс идентифицируется номером порта.

      В наборе TCP/IP, например:

      {tcp, 193.44.234.3, 12345}

      Разговор - это линия связи между двумя процессами, таким образом изображающая связь между двумя. Ассоциацией является 5-кортеж, который полностью определяет два процесса, которые содержат соединение: {protocol, local-address, local-process, foreign-address, foreign-process}

      В наборе TCP/IP, например:

      {tcp, 193.44.234.3, 1500, 193.44.234.5, 21}

      может быть допустимой ассоциацией.

      Полу-ассоциация: {протокол, локальный адрес, локальный процесс}

      {protocol, foreign-address, foreign-process}

      которые определяют каждую половину соединения.

      Полусвязь также называется сокет или транспортный адрес. То есть, сокет является конечной точкой для связи, которая может быть названа и адресована в сети. Интерфейс сокета является одним из нескольких интерфейсов прикладного программирования (API) для протоколов связи. Разработанный как универсальный коммуникационный интерфейс программирования, он впервые был внедрен системой UNIX 4.2BSD. Хотя он не был стандартизирован, он стал фактическим промышленным стандартом.

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

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

      Сокеты являются частью транспортного уровня, который обеспечивает логическую связь с приложениями. Это означает, что с точки зрения приложения оба узла напрямую связаны друг с другом, хотя между ними существует множество маршрутизаторов и/или переключателей. Таким образом, сокет не является самим соединением, это конечная точка соединения. Протоколы транспортного уровня реализуются только на хостах, а не на промежуточных маршрутизаторах.
      Порты предоставляют средства внутренней адресации для машины. Основная цель - позволить нескольким процессам отправлять и получать данные по сети без вмешательства в другие процессы (их данные). Все сокеты снабжены номером порта. Когда сегмент поступает на хост, транспортный уровень исследует номер порта назначения сегмента. Затем он переводит сегмент в соответствующий сокет. Это задание доставки данных в сегменте транспортного уровня в правильный сокет называется демпплексированием . Затем данные сегмента передаются процессу, присоединенному к сокету.

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

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

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

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

      Относительная терминология TCP/IP, которую я предполагаю, подразумевает этот вопрос. В условиях неспециалиста:

      PORT - это номер телефона определенного дома в определенном почтовом индексе. Почтовый код города можно рассматривать как IP-адрес города и всех домов в этом городе.

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

      Порт и сокет можно сравнить с отделением банка.

      Номер здания "Банка" аналогичен IP-адресу. Банк имеет различные разделы, такие как:

      1. Отдел сберегательного счета
      2. Отдел персональных кредитов
      3. Отдел ипотечного кредитования
      4. Отдел рассмотрения жалоб

      Таким образом, 1 (отдел сберегательного счета), 2 (отдел персональных кредитов), 3 (отдел жилищных кредитов) и 4 (отдел рассмотрения жалоб) являются портами.

      Теперь позвольте нам сказать, что вы идете, чтобы открыть сберегательный счет, вы идете в банк (IP-адрес), затем вы идете в "отдел сберегательного счета" (порт № 1), затем вы встречаете одного из сотрудников, работающих в "отдел сберегательного счета" ". Давайте позвоним ему SAVINGACCOUNT_EMPLOYEE1 для открытия счета.

      SAVINGACCOUNT_EMPLOYEE1 - ваш дескриптор сокета, поэтому может быть от SAVINGACCOUNT_EMPLOYEE1 до SAVINGACCOUNT_EMPLOYEEN. Это все дескрипторы сокетов.

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

      Сокет является конечной точкой связи. Сокет не имеет прямого отношения к семейству протоколов TCP/IP, его можно использовать с любым протоколом, поддерживаемым вашей системой. API сокета C ожидает, что вы сначала получите пустой объект сокета из системы, который затем можно будет привязать к локальному адресу сокета (чтобы напрямую получать входящий трафик для протоколов без установления соединения или принимать входящие запросы на соединение для протоколов, ориентированных на установление соединения) или что вы можете подключиться к адресу удаленного сокета (для любого типа протокола). Вы можете даже сделать и то и другое, если хотите контролировать оба: локальный адрес сокета, к которому привязан сокет, и адрес удаленного сокета, к которому подключен сокет. Для протоколов без установления соединения подключение сокета даже необязательно, но если вы этого не сделаете, вам также придется передавать адрес назначения с каждым пакетом, который вы хотите отправить через сокет, как иначе, как бы сокет знал, куда отправить эти данные для? Преимущество заключается в том, что вы можете использовать один сокет для отправки пакетов на разные адреса сокетов. После того, как вы настроили сокет и, возможно, даже подключили его, считайте его двунаправленным каналом связи. Вы можете использовать его для передачи данных в какой-либо пункт назначения, а другой пункт назначения может использовать его для передачи данных вам. То, что вы пишете в сокет, отправляется, а то, что было получено, доступно для чтения.

      С другой стороны, порты - это то, что есть только у определенных протоколов стека протоколов TCP/IP. TCP и UDP пакеты имеют порты. Порт - это просто число. Комбинация порта источника и порта назначения определяет канал связи между двумя хостами. Например, у вас может быть сервер, который должен быть и простым HTTP-сервером, и простым FTP-сервером. Если сейчас приходит пакет для адреса этого сервера, как он узнает, является ли это пакет для HTTP или FTP-сервера? Что ж, он будет знать, так как HTTP-сервер будет работать на порту 80, а FTP-сервер - на порту 21, поэтому, если пакет поступает с портом назначения 80, он предназначен для HTTP-сервера, а не для FTP-сервера. Также пакет имеет порт источника, поскольку без такого порта источника сервер может иметь только одно подключение к одному IP-адресу за раз. Порт источника позволяет серверу различать идентичные соединения: все они имеют один и тот же порт назначения, например, порт 80, один и тот же IP-адрес назначения, всегда один и тот же адрес сервера и один и тот же IP-адрес источника, поскольку все они исходят от одного и того же клиент, но так как они имеют разные исходные порты, сервер может отличить их друг от друга. И когда сервер отправляет обратно ответы, он делает это с портом, с которого поступил запрос, таким образом, клиент также может различать разные ответы, которые он получает.

      Один порт может иметь один или несколько разъемов, подключенных к другому внешнему IP-адресу, например, к нескольким розеткам.

      TCP 192.168.100.2:9001 155.94.246.179:39255 ESTABLISHED 1312 TCP 192.168.100.2:9001 171.25.193.9:61832 ESTABLISHED 1312 TCP 192.168.100.2:9001 178.62.199.226:37912 ESTABLISHED 1312 TCP 192.168.100.2:9001 188.193.64.150:40900 ESTABLISHED 1312 TCP 192.168.100.2:9001 198.23.194.149:43970 ESTABLISHED 1312 TCP 192.168.100.2:9001 198.49.73.11:38842 ESTABLISHED 1312

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

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

    В джаве для этого существует специальный пакет "java.net", содержащий класс java.net.Socket. Socket в переводе означает "гнездо", название это было дано по аналогии с гнёздами на аппаратуре, теми самыми, куда подключают штепсели. Соответственно этой аналогии, можно связать два "гнезда", и передавать между ними данные. Каждое гнездо принадлежит определённому хосту (Host - хозяин, держатель). Каждый хост имеет уникальный IP (Internet Packet) адрес. На данный момент интернет работает по протоколу IPv4, где IP адрес записывается 4 числами от 0 до 255 - например, 127.0.0.1 (подробнее о распределении IP адресов тут - RFC 790 , RFC 1918 , RFC 2365 , о версии IPv6 читайте тут - RFC 2373)

    Гнёзда монтируются на порт хоста (port). Порт обозначается числом от 0 до 65535 и логически обозначает место, куда можно пристыковать (bind) сокет. Если порт на этом хосте уже занят каким-то сокетом, то ещё один сокет туда пристыковать уже не получится. Таким образом, после того, как сокет установлен, он имеет вполне определённый адрес, символически записывающийся так :, к примеру - 127.0.0.1:8888 (означает, что сокет занимает порт 8888 на хосте 127.0.0.1)

    Для того, чтобы облегчить жизнь, чтобы не использовать неудобозапоминаемый IP адрес, была придумана система DNS (DNS - Domain Name Service). Цель этой системы - сопоставлять IP адресам символьные имена. К примеру, адресу "127.0.0.1" в большинстве компьютеров сопоставленно имя "localhost" (в просторечье - "локалхост").

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

    Клиентский сокет

    Итак, вернёмся к классу java.net.Socket Наиболее удобно инициализировать его следующим образом:

    Public Socket(String host, int port) throws UnknownHostException, IOException В строковой константе host можно указать как IP адрес сервера, так и его DNS имя. При этом программа автоматически выберет свободный порт на локальном компьютере и "привинтит" туда ваш сокет, после чего будет предпринята попытка связаться с другим сокетом, адрес которого указан в параметрах инициализации. При этом могут возникнуть два вида исключений: неизвестный адрес хоста - когда в сети нет компьютера с таким именем или ошибка отсутствия связи с этим сокетом.

    Так же полезно знать функцию

    Public void setSoTimeout(int timeout) throws SocketException Эта функция устанавливает время ожидания (timeout) для работы с сокетом. Если в течение этого времени никаких действий с сокетом не произведено (имеется ввиду получение и отправка данных), то он самоликвидируется. Время задаётся в секундах, при установке timeout равным 0 сокет становится "вечным".

    Для некоторых сетей изменение timeout невозможно или установлено в определённых интервалах (к примеру от 20 до 100 секунд). При попытке установить недопустимый timeout, будет выдано соответственное исключение.

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

    Сокет сервера

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

    Public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException Как видно, в качестве третьего параметра используется объект ещё одного класса - java.net.InetAddress Этот класс обеспечивает работу с DNS и IP именами, по этому вышеприведённый инициализатор в программах можно использовать так: ServerSocket(port, 0, InetAddress.getByName(host)) throws IOException Для этого типа сокета порт установки указывается прямо, поэтому, при инициализации, может возникнуть исключение, говорящее о том, что данный порт уже используется либо запрещён к использованию политикой безопасности компьютера.

    После установки сокета, вызывается функция

    Public Socket accept() throws IOException Эта функция погружает программу в ожидание того момента, когда клиент будет присоединяться к сокету сервера. Как только соединение установлено, функция возвратит объект класса Socket для общения с клиентом.

    Клиент-сервер через сокеты. Пример

    Как пример - простейшая программа, реализующая работу с сокетами.

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

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


    Логическая структура работы программ-примеров

    Программа простого TCP/IP клиента

    (SampleClient.java) import java. io.* ; import java. net.* ; class SampleClient extends Thread { public static void main(String args) { try { // открываем сокет и коннектимся к localhost:3128 // получаем сокет сервера Socket s = new Socket("localhost" , 3128 ); // берём поток вывода и выводим туда первый аргумент // заданный при вызове, адрес открытого сокета и его порт args[ 0 ] = args[ 0 ] + "\n" + s. getInetAddress() . getHostAddress() + ":" + s. getLocalPort(); s. getOutputStream() . write(args[ 0 ] . getBytes()); // читаем ответ byte buf = new byte [ 64 * 1024 ]; int r = s. getInputStream() . read(buf); String data = new String(buf, 0 , r); // выводим ответ в консоль System. out. println(data); } catch (Exception e) { System. out. println("init error: " + e);} // вывод исключений } }

    Программа простого TCP/IP сервера

    (SampleServer.java) import java. io.* ; import java. net.* ; class SampleServer extends Thread { Socket s; int num; public static void main(String args) { try { int i = 0 ; // счётчик подключений // привинтить сокет на локалхост, порт 3128 ServerSocket server = new ServerSocket(3128 , 0 , InetAddress. getByName("localhost" )); System. out. println("server is started" ); // слушаем порт while (true) { // ждём нового подключения, после чего запускаем обработку клиента // в новый вычислительный поток и увеличиваем счётчик на единичку new SampleServer(i, server. accept()); i++ ; } } catch (Exception e) { System. out. println("init error: " + e);} // вывод исключений } public SampleServer(int num, Socket s) { // копируем данные this. num = num; this. s = s; // и запускаем новый вычислительный поток (см. ф-ю run()) setDaemon(true); setPriority(NORM_PRIORITY); start(); } public void run() { try { // из сокета клиента берём поток входящих данных InputStream is = s. getInputStream(); // и оттуда же - поток данных от сервера к клиенту OutputStream os = s. getOutputStream(); // буффер данных в 64 килобайта byte buf = new byte [ 64 * 1024 ]; // читаем 64кб от клиента, результат - кол-во реально принятых данных int r = is. read(buf); // создаём строку, содержащую полученную от клиента информацию String data = new String(buf, 0 , r); // добавляем данные об адресе сокета: data = "" + num+ ": " + "\n" + data; // выводим данные: os. write(data. getBytes()); // завершаем соединение s. close(); } catch (Exception e) { System. out. println("init error: " + e);} // вывод исключений } }

    После компиляции, получаем файлы SampleServer.class и SampleClient.class (все программы здесь и далее откомпилированы с помощью JDK v1.4) и запускаем вначале сервер:

    Java SampleServer а потом, дождавшись надписи "server is started", и любое количество клиентов: java SampleClient test1 java SampleClient test2 ... java SampleClient testN

    Если во время запуска программы-сервера, вместо строки "server is started" выдало строку типа

    Init error: java.net.BindException: Address already in use: JVM_Bind то это будет обозначать, что порт 3128 на вашем компьютере уже занят какой-либо программой или запрещён к применению политикой безопасности.

    Заметки

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

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

    Стоит упомянуть, что абстракцию Socket - ServerSocket и работу с потоками данных используют C/C++, Perl, Python, многие другие языки программирования и API операционных систем, так что многое из сказанного подходит к применению не только для платформы Java .

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

    Я полагаю, вы уже знаете, как устроена сеть, что такое Internet Protocol , User Datagram Protocol и Transmission Control Protocol . Эта тема большинству программистов известна. Но если вы почему-то ее упустили, то придется сперва наверстать упущенное, и потом вернуться к этому уроку.

    UDP сокет

    Вспомним в общих чертах, что такое UDP:

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

    Для работы с UDP используется модуль gen_udp .

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

    На 1-й ноде откроем UDP на порту 2000:

    1> {ok, Socket} = gen_udp:open(2000, ). {ok,#Port<0.587>}

    Вызываем gen_udp:open/2 , передаем номер порта и список опций. Список всех возможных опций довольно большой, но нас интересуют две из них:

    binary -- сокет открыт в бинарном режиме. Как вариант, сокет можно открыть в текстовом режиме, указав опцию list . Разница в том, как мы интерпретируем данные, полученные из сокета -- как поток байт, или как текст.

    {active, true} -- сокет открыт в активном режиме, значит данные, приходящие в сокет, будут посылаться в виде сообщений в почтовый ящик потока, владельца сокета. Подробнее об этом ниже.

    На 2-й ноде откроем UDP на порту 2001:

    1> {ok, Socket} = gen_udp:open(2001, ). {ok,#Port<0.587>}

    И пошлем сообщение с 1-й ноды на 2-ю:

    2> gen_udp:send(Socket, {127,0,0,1}, 2001, <<"Hello from 2000">>). ok

    Вызываем gen_udp:send/4 , передаем сокет, адрес и порт получателя, и само сообщение.

    Адрес может быть доменным именем в виде строки или атома, или адресом IPv4 в виде кортежа из 4-х чисел, или адресом IPv6 в виде кортежа из 8 чисел.

    На 2-й ноде убедимся, что сообщение пришло:

    2> <0.587>,{127,0,0,1},2000,<<"Hello from 2000">>} ok

    Сообщение приходит в виде кортежа {udp, Socket, SenderAddress, SenderPort, Packet} .

    Пошлем сообщение с 2-й ноды на 1-ю:

    3> gen_udp:send(Socket, {127,0,0,1}, 2000, <<"Hello from 2001">>). ok

    На 1-й ноде убедимся, что сообщение пришло:

    3> flush(). Shell got {udp,#Port<0.587>,{127,0,0,1},2001,<<"Hello from 2001">>} ok

    Как видим, тут все просто.

    Активный и пассивный режим сокета

    И gen_udp , и gen_tcp , оба имеют одну важную настройку: режим работы с входящими данными. Это может быть либо активный режим {active, true} , либо пассивный режим {active, false} .

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

    Для udp сокета это сообщения вида:

    {udp, Socket, SenderAddress, SenderPort, Packet}

    мы их уже видели:

    {udp,#Port<0.587>,{127,0,0,1},2001,<<"Hello from 2001">>}

    Для tcp сокета аналогичные сообщения:

    {tcp, Socket, Packet}

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

    В пассивном режиме данные нужно забрать самому вызовами gen_udp:recv/3 и gen_tcp:recv/3 :

    Gen_udp:recv(Socket, Length, Timeout) -> {ok, {Address, Port, Packet}} | {error, Reason} gen_tcp:recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason}

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

    Однако, gen_udp:recv игнорирует аргумент Length, и возвращает все данные, которые есть в сокете. Или блокируется и ждет каких-нибудь данных, если в сокете ничего нет. Непонятно, зачем вообще аргумент Length присутствует в АПИ.

    Для gen_tcp:recv аргумент Length работает как надо. Если только не указана опция {packet, Size} , о которой речь пойдет ниже.

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

    TCP сокет

    Вспомним в общих чертах, что такое TCP:

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

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

    Именно поэтому так популярен протокол HTTP, который хоть и работает поверх TCP сокета, но подразумевает короткое время взаимодействия. Это позволяет относительно небольшим числом потоков (десятки-сотни) обслуживать значительно большее число клиентов (тысячи, десятки тысяч).

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

    Для работы с TCP используется модуль gen_tcp .

    Работать с TCP сокетом сложнее, чем с UDP. У нас появляются роли клиента и сервера, требующие разной реализации. Рассмотрим вариант реализации сервера.

    Module(server). -export(). start() -> start(1234). start(Port) -> spawn(?MODULE, server, ), ok. server(Port) -> io:format("start server at port ~p~n", ), {ok, ListenSocket} = gen_tcp:listen(Port, ), ) || Id <- lists:seq(1, 5)], timer:sleep(infinity), ok. accept(Id, ListenSocket) -> io:format("Socket #~p wait for client~n", ), {ok, _Socket} = gen_tcp:accept(ListenSocket), io:format("Socket #~p, session started~n", ), handle_connection(Id, ListenSocket). handle_connection(Id, ListenSocket) -> receive {tcp, Socket, Msg} -> io:format("Socket #~p got message: ~p~n", ), gen_tcp:send(Socket, Msg), handle_connection(Id, ListenSocket); {tcp_closed, _Socket} ->

    Есть два вида сокета: Listen Socket и Accept Socket . Listen Socket один, он принимает все запросы на соединение. Accept Socket нужно много, по одному для каждого соединения. Поток, в котором создается сокет, становится владельцем сокета. Если поток-владелец завершается, то сокет автоматически закрывается. Поэтому для каждого сокета мы создаем отдельный поток.

    Listen Socket должен работать всегда, а для этого его поток-владелец не должен завершаться. Поэтому в server/1 мы добавили вызов timer:sleep(infinity) . Это заблокирует поток и не даст ему завершиться. Такая реализация, конечно, учебная. По хорошему нужно предусмотреть возможность корректно остановить сервер, а здесь этого нет.

    Accept Socket и поток для него можно было бы создавать динамически, по мере появления клиентов. В начале можно создать один такой поток, вызвать в нем gen_tcp:accept/1 и ждать клиента. Этот вызов является блокирующим. Он завершается, когда появляется клиент. Дальше можно обслуживать текущего клиента в этом потоке, и создать новый поток, ожидающий нового клиента.

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

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

    Потоки принадлежат эрланговской ноде, и мы можем создавать их сколько угодно. Но сокеты принадлежат операционной системе. Их количество лимитировано, хотя и довольно большое. (Речь идет о лимите на количество файловых дескрипторов, которое операционная система позволяет открыть пользовательскому процессу, обычно это 2 10 - 2 16).

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

    Текущая сессия с клиентом обрабатывается в функции handle_connection/2 . Видно, что сокет работает в активном режиме, и поток получает сообщения вида {tcp, Socket, Msg} , где Msg -- это бинарные данные, пришедшие от клиента. Эти данные мы отравляет обратно клиенту, то есть, реализуем банальный эхо-сервис:)

    Когда клиент закрывает соединение, поток получает сообщение {tcp_closed, _Socket} , возвращается обратно в accept/2 и ждет следующего клиента.

    Вот как выглядит работа такого сервера с двумя telnet-клиентами:

    $ telnet localhost 1234 Trying 127.0.0.1... Connected to localhost. Escape character is "^]". hello from client 1 hello from client 1 some message from client 1 some message from client 1 new message from client 1 new message from client 1 client 1 is going to close connection client 1 is going to close connection ^] telnet> quit Connection closed.

    $ telnet localhost 1234 Trying 127.0.0.1... Connected to localhost. Escape character is "^]". hello from client 2 hello from client 2 message from client 2 message from client 2 client 2 is still active client 2 is still active but client 2 is still active but client 2 is still active and now client 2 is going to close connection and now client 2 is going to close connection ^] telnet> quit Connection closed.

    2> server:start(). start server at port 1234 ok Socket #1 wait for client Socket #2 wait for client Socket #3 wait for client Socket #4 wait for client Socket #5 wait for client Socket #1, session started Socket #1 got message: <<"hello from client 1\r\n">> Socket #1 got message: <<"some message from client 1\r\n">> Socket #2, session started Socket #2 got message: <<"hello from client 2\r\n">> Socket #2 got message: <<"message from client 2\r\n">> Socket #1 got message: <<"new message from client 1\r\n">> Socket #2 got message: <<"client 2 is still active\r\n">> Socket #1 got message: <<"client 1 is going to close connection\r\n">> Socket #1, session closed Socket #1 wait for client Socket #2 got message: <<"but client 2 is still active\r\n">> Socket #2 got message: <<"and now client 2 is going to close connection\r\n">> Socket #2, session closed Socket #2 wait for client

    Сервер в пассивном режиме

    Это все хорошо, но хороший сервер должен работать в пассивном режиме. То есть, он должен получать данные от клиента не в виде сообщений в почтовый ящик, а вызовом gen_tcp:recv/2,3 .

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

    Теперь нужно решить, сколько байт должен занимать этот служебный пакет. Если это будет 1 байт, то в него нельзя упаковать число больше 255. В 2 байта можно упаковать число 65535, в 4 байта 4294967295. 1 байт, очевидно, мало. Вполне вероятно, что клиенту будет нужно послать данных больше, чем 255 байт. Заголовок в 2 байта вполне подходит. Заголовок в 4 байта иногда бывает нужен.

    Итак, клиент посылает служебный пакет размером в 2 байта, где указано, сколько данных последуют за ним, а затем сами эти данные:

    Msg = <<"Hello">>, Size = byte_size(Msg), Header = <>, gen_tcp:send(Socket, <

    >),

    Полный код клиента:

    Module(client2). -export(). start() -> start("localhost", 1234). start(Host, Port) -> spawn(?MODULE, client, ). send(Pid, Msg) -> Pid ! {send, Msg}, ok. stop(Pid) -> Pid ! stop, ok. client(Host, Port) -> io:format("Client ~p connects to ~p:~p~n", ), {ok, Socket} = gen_tcp:connect(Host, Port, ), loop(Socket). loop(Socket) -> receive {send, Msg} -> io:format("Client ~p send ~p~n", ), Size = byte_size(Msg), Header = <>, gen_tcp:send(Socket, <

    >), loop(Socket); {tcp, Socket, Msg} -> io:format("Client ~p got message: ~p~n", ), loop(Socket); stop -> io:format("Client ~p closes connection and stops~n", ), gen_tcp:close(Socket) after 200 -> loop(Socket) end.

    Сервер сперва читает 2 байта, определяет размер данных и затем читает все данные:

    {ok, Header} = gen_tcp:recv(Socket, 2), <> = Header, {ok, Msg} = gen_tcp:recv(Socket, Size),

    В коде сервера функции start/0 и start/1 не изменились, остальное немного поменялось:

    Server(Port) -> io:format("start server at port ~p~n", ), {ok, ListenSocket} = gen_tcp:listen(Port, ), ) || Id <- lists:seq(1, 5)], timer:sleep(infinity), ok. accept(Id, ListenSocket) -> io:format("Socket #~p wait for client~n", ), {ok, Socket} = gen_tcp:accept(ListenSocket), io:format("Socket #~p, session started~n", ), handle_connection(Id, ListenSocket, Socket). handle_connection(Id, ListenSocket, Socket) -> case gen_tcp:recv(Socket, 2) of {ok, Header} -> <> = Header, {ok, Msg} = gen_tcp:recv(Socket, Size), io:format("Socket #~p got message: ~p~n", ), gen_tcp:send(Socket, Msg), handle_connection(Id, ListenSocket, Socket); {error, closed} -> io:format("Socket #~p, session closed ~n", ), accept(Id, ListenSocket) end.

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

    2> Pid = client2:start(). Client <0.40.0> connects to "localhost":1234 <0.40.0> 3> client2:send(Pid, <<"Hello">>). Client <0.40.0> send <<"Hello">> ok Client <0.40.0> got message: <<"Hello">> 4> client2:send(Pid, <<"Hello again">>). Client <0.40.0> send <<"Hello again">> ok Client <0.40.0> got message: <<"Hello again">> 5> client2:stop(Pid). Client <0.40.0> closes connection and stops ok

    И со стороны сервера:

    2> server2:start(). start server at port 1234 ok Socket #1 wait for client Socket #2 wait for client Socket #3 wait for client Socket #4 wait for client Socket #5 wait for client Socket #1, session started Socket #1 got message: <<"Hello">> Socket #1 got message: <<"Hello again">> Socket #1, session closed Socket #1 wait for client

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

    {ok, Socket} = gen_tcp:connect(Host, Port, ),

    и на стороне сервера:

    {ok, ListenSocket} = gen_tcp:listen(Port, ),

    и необходимость самому формировать и разбирать эти заголовки пропадает.

    На стороне клиента упрощается отправка:

    Gen_tcp:send(Socket, Msg),

    и на стороне сервера упрощается получение:

    Handle_connection(Id, ListenSocket, Socket) -> case gen_tcp:recv(Socket, 0) of {ok, Msg} -> io:format("Socket #~p got message: ~p~n", ), gen_tcp:send(Socket, Msg), handle_connection(Id, ListenSocket, Socket); {error, closed} -> io:format("Socket #~p, session closed ~n", ), accept(Id, ListenSocket) end.

    Теперь при вызове gen_tcp:recv/2 мы указываем Length = 0. gen_tcp сам знает, сколько байт нужно прочитать из сокета.

    Работа с текстовыми протоколами

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

    Такой вариант характерен для текстовых протоколов (SMTP, POP3, FTP).

    Писать свою реализацию чтения из сокета нет необходимости, все уже реализовано в gen_tcp . Нужно только указать в настройках сокета вместо {packet, 2} опцию {packet, line} .

    {ok, ListenSocket} = gen_tcp:listen(Port, ),

    В остальном код сервера остается без изменений. Но теперь мы можем опять вернуться к telnet-клиенту.

    $ telnet localhost 1234 Trying 127.0.0.1... Connected to localhost. Escape character is "^]". hello hello hello again hello again ^] telnet> quit Connection closed.

    TCP-сервер, текстовый протокол и telnet-клиент нам понадобятся в курсовой работе.

    Socket vs Socket часть 2, или скажем “нет” протоколу TCP — Архив WASM.RU

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

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

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

    Слышу, слышу хор подсказок, что мол, используй встроенные возможности Windows, типа:

    net send 192.168.0.4 Женя шлет тебе привет!

    net send Node4 Жду ответа!

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

    Для меня самым главным в постановке этой задачи было обеспечить возможность передачи чего-либо всем компьютерам сети сразу. Представьте, что мы написали некую программу... Кто сказал - троян? Нет, нет и нет! Никаких троянов. Просто маленькую (очень) бухгалтерскую программу, например. Которая смогла таки через некоторое время поселиться на многих компьютерах нашей локальной сети. И вот приходит назначенное время, пора свести сальдо с бульдо, подвести, так сказать, итог за квартал... Надо сделать все быстро и желательно одновременно. Как это сделать в рамках того материала, что мы изучили в первой части, оставалось неясным.

    Ответ, как всегда, дает WindowsAPI. Ищем и находим. Функция sendto () – посылает данные по указанному адресу. А в чем же тогда ее отличие от уже изученной в первой части функции send () ? Оказывается, что sendto () может осуществлять широковещательную передачу по специальному IP – адресу. Но, внимание, это работает только для сокетов типа SOCK_DGRAM! А сокеты, при открытии которых в качестве параметра типа сокета использовалось значение SOCK_DGRAM работают через протокол UDP, а не TCP! Отсюда становится ясно значение подзаголовка этой статьи… Конечно, это всего лишь литературный прием, ни один протокол не лучше и не хуже другого, просто они… разные, вот и все. Хотя оба – это протоколы транспортного уровня, которые “…обеспечивают передачу данных между прикладными процессами”. Оба обращаются к протоколу сетевого уровня, такому, как IP для передачи (приема) данных. Через который далее они (данные) попадают на физический уровень, т.е. в среду передачи… А что там за среда, кто его знает. Может это медный кабель, а может и не среда вовсе, а четверг, и не медный кабель, а эфир…

    Схема взаимодействия сетевых протоколов.

    UDP U ser D atagram P rotocol

    TCP – T ransmission C ontrol P rotocol

    ICMP – I nternet C ontrol M essage P rotocol (протоколобменауправляющимисообщениями)

    ARP – A ddress R esolution P rotocol (протокол определения адресов)

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

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

    Для демонстрации всего материала, как обычно, используется учебный пример, который можно скачать < >. Пропускаем общую для всех Windows приложений часть и описываем только то, что касается работы сокетов. Сначала необходимо инициализировать Windows Sockets DLL с помощью функции WSAStartup () , которая вернет ноль в случае успешного выполнения, либо, в противном случае, один из кодов ошибки. Затем при инициализации главного окна приложения открываем сокет для приема cообщений:

      invoke socket, AF_INET, \

      SOCK_DGRAM, \ ; задает тип сокета - протокол UDP!

      0 ; тип протокола

      If eax != INVALID_SOCKET ; если нет ошибки

      mov hSocket, eax ; запомнить дескриптор

    После этого, как обычно, надо указать Windows посылать сообщения заданному окну от открытого нами сокета:

      invoke WSAAsyncSelect, hSocket, hWnd, WM_SOCKET, FD_READ

    где hSocket - дескриптор сокета
    hWnd - дескриптор окна, процедуре которого будут посылаться сообщения
    WM_SOCKET - сообщение, нами же определенное в секции.const
    FD_READ – маска, задающая интересующие нас события, в данном случае это готовность данных от сокета для чтения.

    Слышу, слышу удивленный хор с отчаянием в голосе: обещали скрытое приложение, а тут главное окно и все такое… Дело в том, что без него не обойтись, т.к. операционная система посылает все сообщения нашему приложению чеpез пpоцедуpу его окна. Выход прост. При необходимости сделайте скрытым это самое главное окно приложения. Как? Например, закомментируйте строку:

      invoke ShowWindow, hwnd, SW_SHOWNORMAL

    или, что более правильно, используйте:

      invoke ShowWindow, hwnd, SW_HIDE

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

    Для этого преобразуем номер порта в сетевой порядок байт с помощью специальной функции АPI:

      invoke htons, Port

      mov sin.sin_port, ax

      mov sin.sin_family, AF_INET

      mov sin.sin_addr, INADDR_ANY

    Небольшое лирическое отступление, необязательное для понимания смысла этой статьи .

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

    через протоколTCP : 20, 21 – ftp; 23 – telnet; 25 – smtp; 80 – http; 139 - NetBIOS session service;

    через протоколUDP : 53 – DNS; 137, 138 – NetBIOS; 161 – SNMP;

    Конечно, в составе API есть специальная функция getservbyport () , которая по заданному номеру порта возвращает имя соответствующего ему сервиса. Вернее, сама функция возвращает указатель на структуру, внутри которой есть указатель на это имя...

    Вызвать ее можно так:

      invoke htons, Port; преобразуем номер порта в сетевой порядок байт

      invoke getservbyport, ax, 0;

    Обратите внимание на то, что сообщает Win32 Programmer’sReference по поводу getservbyport :

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

    А вот и сама структура:

    1. s_name DWORD ?; указатель на строку с именем сервиса

      s_aliases DWORD ?;

      s_port WORD ?; номер порта

      s_proto DWORD ?;

    Есть в API, так сказать, и “парная” функция: getservbyname() , которая по имени сервиса возвращает информацию о номере используемого порта.

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

      invoke bind, hSocket, addr sin, sizeof sin

      If eax == SOCKET_ERROR; если ошибка

      invoke MessageBox, NULL, addr …

    На этом подготовительную работу по созданию и настройке принимающего сокета с использованием датаграмм можно считать законченной. Нет необходимости устанавливать сокет в состояние cлушания порта функцией invoke listen , как мы это делали для сокета типа SOCK_STREAM в первой части. Теперь в процедуре главного окна нашего приложения мы можем добавить код, который будет выполняться при поступлении сообщения WM_SOCKET от сокета:

      ; если получено сообщение от сокета (hSocket)

      Elseif uMsg == WM_SOCKET

    1. If ax == FD_READ;

    2. If ax == NULL ; отсутствует ошибка

      ; принять данные (64 байта) от сокета в буфер BytRecu

      invoke recv, hSocket, addr BytRecu, 64, 0;

    Теперь о том, как открыть сокет для передачи сообщений. Вот все необходимые действия программы:

      invoke socket, AF_INET, SOCK_DGRAM, 0

        invoke htons, Port

        mov sin_to.sin_port, ax

        mov sin_to.sin_family, AF_INET

        invoke inet_addr, addr AdresIP

        mov sin_to.sin_addr, eax

      Когда дело доходит до передачи данных, достаточно сделать cледующее:

        invoke sendto, hSocket1, addr BytSend1, 64, 0, \

        addr sin_to, sizeof sin_to

      Значения параметров при вызове этой функции APIследующие:

      hSocket1 - дескриптор ранее открытого сокета
      addrBytSend1 - адрес буфера, содержащего данные на передачу
      64 - размер данных вбуфере, в байтах
      0 - индикатор…, в примере MSDNэто просто 0
      addrsin_to - указатель наструктуру, которая содержит адрес назначения
      sizeofsin_to – размер этой структуры в байтах.

      Если при выполнении функции sendto () не возникло ошибок, то она возвращает число переданных байт, иначе на выходе имеем в eaxзначение SOCKET_ERROR.

      Теперь самое время поговорить о том самом широковещательном адресе, о котором упоминалось вначале. В структуре мы предварительно заполнили поле с IP - адресом назначения, указывая, куда, собственно, отправлять данные. Если это адрес 127.0.0.1 – естественно, никуда дальше собственного компьютера наши данные не уйдут. В литературе четко сказано, что пакет, посланный в сеть с адресом 127.x.x.x, не будет передаваться ни по какой сети. Более того, маршрутизатор или шлюз никогда не должен распространять информацию о маршрутах для сети с номером 127 - этот адрес не является адресом сети. Чтобы отправить “передачку” сразу всем компьютерам локальной сети нужно использовать адрес, сформированный из нашего собственного IP – адреса, но имеющий все единицы в младшем октете, что-нибудь типа 192.168.0.255.

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

        invoke closesocket, hSocket

        invoke closesocket, hSocket1

        invoke WSACleanup

      Для мультипотоковых приложений после WSACleanup завершаются операции с сокетами для всех потоков.

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

      Безэтогофункцияsend() выдаст SOCKET_ERROR!

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

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

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

      Какой способ выбрать - решайте сами. И еще одно. На время экспериментов, возможно, придется отключить ваш персональный firewall. Так, например, Outpost Pro 2.1.275 в режиме обучения реагировал на попытку передачи в сокет, но, когда передача вручную разрешалась, данные все равно не доходили. Вот вам и UDP. Хотя дело может быть и не в этом. Проблем с моим ZoneAlarmPro 5.0.590 в такой же ситуации не было.

      P. S. Заканчивая вторую часть статьи случайно наткнулся в сети на исходники трояна на нашем любимом языке MASM. Все компилируется и запускается, одно но, клиент не хочет коннектиться с сервером, да еще под Windows 2000 sp4 иногда вылетает с ошибкой, мол, приложение будет закрыто и все такое… Лично мне в этом трояне нравится, что программа не просто там ведет лог нажатий, или “выдирает” файл с паролями и отсылает его по электронке, а имеет широкий набор управляемых дистанционно функций, дольно оригинально реализованных. Если получится привести все это хозяйство в чувство, то, возможно, скоро появится и третья часть, посвященная описанию конкретной реализации… Для тех, кто внимательно прочитал обе статьи и разобрался с работой функций сокет API, там нет ничего сложного. Вроде бы… Кстати, сам автор пишет в readme, что написал его (троян) в образовательных целях. Ну-ну. Этим и воспользуемся.

      DirectOr

    Приложения, использующие TCP и UDP, фундаментально отличаются друг от друга, потому что UDP является ненадежным протоколом дейтаграмм, не ориентированным на установление соединения, и этим принципиально непохож на ориентированный на установление соединения и надежную передачу потока байтов TCP. Тем не менее есть случаи, когда имеет смысл использовать UDP вместо TCP. Подобные случаи мы рассматриваем в разделе 22.4. Некоторые популярные приложения построены с использованием UDP, например DNS (Domain Name System - система доменных имен), NFS (сетевая файловая система - Network File System) и SNMP (Simple Network Management Protocol - простой протокол управления сетью).

    На рис. 8.1 показаны вызовы функций для типичной схемы клиент-сервер UDP. Клиент не устанавливает соединения с сервером. Вместо этого клиент лишь отправляет серверу дейтаграмму, используя функцию sendto (она описывается в следующем разделе), которой нужно задать адрес получателя (сервера) в качестве аргумента. Аналогично, сервер не устанавливает соединения с клиентом. Вместо этого сервер лишь вызывает функцию recvfrom , которая ждет, когда придут данные от какого-либо клиента. Функция recvfrom возвращает адрес клиента (для данного протокола) вместе с дейтаграммой, и таким образом сервер может отправить ответ именно тому клиенту, который прислал дейтаграмму.

    Рис. 8.1 . Функции сокета для модели клиент-сервер UDP

    Рисунок 8.1 иллюстрирует временную диаграмму типичного сценария обмена UDP-дейтаграммами между клиентом и сервером. Мы можем сравнить этот пример с типичным обменом по протоколу TCP, изображенным на рис. 4.1.

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

    8.2. Функции recvfrom и sendto

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

    ssize_t recvfrom(int sockfd , void * buff , size_t nbytes , int flags ,

    struct sockaddr * from , socklen_t * addrlen);

    ssize_t sendto(int sockfd , const void * buff , size_t nbytes , int flags ,

    const struct sockaddr * to , socklen_t addrlen);

    Обе функции возвращают количество записанных или прочитанных байтов в случае успешного выполнения, -1 в случае ошибки

    Первые три аргумента, sockfd , buff и nbytes , идентичны первым трем аргументам функций read и write: дескриптор, указатель на буфер, из которого производится чтение или в который происходит запись, и число байтов для чтения или записи.

    Мы расскажем об аргументе flags в главе 14, где мы рассматриваем функции recv , send , recvmsg и sendmsg , поскольку сейчас в нашем простом примере они не нужны. Пока мы всегда будем устанавливать аргумент flags в нуль.

    Аргумент to для функции sendto - это структура адреса сокета, содержащая адрес протокола (например, IP-адрес и номер порта) адресата. Размер этой структуры адреса сокета задается аргументом addrlen . Функция recvform заполняет структуру адреса сокета, на которую указывает аргумент from, записывая в нее протокольный адрес отправителя дейтаграммы. Число байтов, хранящихся в структуре адреса сокета, также возвращается вызывающему процессу в целом числе, на которое указывает аргумент addrlen . Обратите внимание, что последний аргумент функции sendto является целочисленным значением, в то время как последний аргумент функции recvfrom - это указатель на целое значение (аргумент типа «значение-результат»).

    Последние два аргумента функции recvfrom аналогичны двум последним аргументам функции accept: содержимое структуры адреса сокета по завершении сообщает нам, кто отправил дейтаграмму (в случае UDP) или кто инициировал соединение (в случае TCP). Последние два аргумента функции sendto аналогичны двум последним аргументам функции connect: мы заполняем структуру адреса сокета протокольным адресом получателя дейтаграммы (в случае UDP) или адресом узла, с которым будет устанавливаться соединение (в случае TCP).

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

    Дейтаграмма может иметь нулевую длину. В случае UDP при этом возвращается дейтаграмма IP, содержащая заголовок IP (обычно 20 байт для IPv4 или 40 байт для IPv6), 8-байтовый заголовок UDP и никаких данных. Это также означает, что возвращаемое из функции recvfrom нулевое значение вполне приемлемо для протокола дейтаграмм: оно не является признаком того, что собеседник закрыл соединение, как это происходит при возвращении нулевого значения из функции read на сокете TCP. Поскольку протокол UDP не ориентирован на установление соединения, то в нем и не существует такого события, как закрытие соединения.

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

    И функция recvfrom , и функция sendto могут использоваться с TCP, хотя обычно в этом нет необходимости.

    8.3. Эхо-сервер UDP: функция main

    Теперь мы переделаем нашу простую модель клиент-сервер из главы 5, используя UDP. Диаграмма вызовов функций в программах наших клиента и сервера UDP показана на рис. 8.1. На рис. 8.2 представлены используемые функции. В листинге 8.1 показана функция сервера main .

    Рис. 8.2 . Простая модель клиент-сервер, использующая UDP

    Листинг 8.1 . Эхо-сервер UDP

    //udpcliserv/udpserv01.с

    1 #include "unp.h"

    3 intmain(int argc, char **argv)

    6 struct sockaddr_in servaddr, cliaddr;

    7 sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    8 bzero(&servaddr, sizeof(servaddr));

    9 servaddr.sin_family = AF_INET;

    10 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    12 Bind(sockfd, (SA*)&servaddr, sizeof(servaddr));

    13 dg_echo(sodkfd, (SA*)&cliaddr, sizeof(cliaddr));

    Создание сокета UDP, связывание с заранее известным портом при помощи функции bind

    7-12 Мы создаем сокет UDP, задавая в качестве второго аргумента функции socket значение SOCK_DGRAM (сокет дейтаграмм в протоколе IPv4). Как и в примере сервера TCP, адрес IPv4 для функции bind задается как INADDR_ANY , а заранее известный номер порта сервера - это константа SERV_PORT из заголовка unp.h .

    13 Затем вызывается функция dg_echo для обработки клиентского запроса сервером.

    8.4. Эхо-сервер UDP: функция dg_echo

    В листинге 8.2 показана функция dg_echo .

    Листинг 8.2 . Функция dg_echo: отражение строк на сокете дейтаграмм

    1 #include "unp.h"

    3 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

    6 socklen_t len;

    7 char mesg;

    10 n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

    11 Sendto(sockfd, mesg, n, 0, pcliaddr, len);

    Чтение дейтаграммы, отражение отправителю

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

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

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

    Для сокета на уровне UDP происходит неявная буферизация дейтаграмм в виде очереди. Действительно, у каждого сокета UDP имеется буфер приема, и каждая дейтаграмма, приходящая на этот сокет, помещается в его буфер приема. Когда процесс вызывает функцию recvfrom , очередная дейтаграмма из буфера возвращается процессу в порядке FIFO (First In, First Out - первым пришел, первым обслужен). Таким образом, если множество дейтаграмм приходит на сокет до того, как процесс может прочитать данные, уже установленные в очередь для сокета, то приходящие дейтаграммы просто добавляются в буфер приема сокета. Но этот буфер имеет ограниченный размер. Мы обсуждали этот размер и способы его увеличения с помощью параметра сокета SO_RCVBUF в разделе 7.5.

    На рис. 8.3 показано обобщение нашей модели TCP клиент-сервер из главы 5, когда два клиента устанавливают соединения с сервером.

    Рис. 8.3 . Обобщение модели TCP клиент-сервер с двумя клиентами

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

    Рис. 8.4 . Обобщение модели UDP клиент-сервер с двумя клиентами

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

    Функция main в листинге 8.1 является зависящей от протокола (она создает сокет семейства AF_INET , а затем выделяет и инициализирует структуру адреса сокета IPv4), но функция dg_echo от протокола не зависит. Причина, по которой функция dg_echo не зависит от протокола, заключается в том, что вызывающий процесс (в нашем случае функция main) должен разместить в памяти структуру адреса сокета корректного размера, и указатель на эту структуру вместе с ее размером передаются в качестве аргументов функции dg_echo . Функция dg_echo никогда не углубляется в эту структуру: она просто передает указатель на нее функциям recvfrom и sendto . Функция recvfrom заполняет эту структуру, вписывая в нее IP-адрес и номер порта клиента, и поскольку тот же указатель (pcliaddr) затем передается функции sendto в качестве адреса получателя, таким образом дейтаграмма отражается обратно клиенту, отправившему дейтаграмму.

    8.5. Эхо-клиент UDP: функция main

    Функция main клиента UDP показана в листинге 8.3.

    Листинг 8.3 . Эхо-клиент UDP

    //udpcliserv/udpcli01.c

    1 #include "unp.h"

    3 main(int argc, char **argv)

    6 struct sockaddr_in servaddr;

    7 if (argc != 2)

    8 err_quit("usage: udpcli ");

    9 bzero(&servaddr, sizeof(servaddr));

    10 servaddr.sin_family = AF_INET;

    11 servaddr.sin_port = htons(SERV_PORT);

    12 Inet_pton(AF_INET, argv, &servaddr.sin_addr);

    13 sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    14 dg_cli(stdin, sockfd, (SA*)&servaddr, sizeof(servaddr));

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

    9-12 Структура адреса сокета IPv4 заполняется IP-адресом и номером порта сервера. Эта структура будет передана функции dg_cli . Она определяет, куда отправлять дейтаграммы.

    13-14 Создается сокет UDP и вызывается функция dg_cli .

    8.6. Эхо-клиент UDP: функция dg_cli

    В листинге 8.4 показана функция dg_cli , которая выполняет большую часть работы на стороне клиента.

    Листинг 8.4 . Функция dg_cli: цикл обработки клиента

    1 #include "unp.h"

    7 while (Fgets(sendline, MAXLINE, fp) != NULL) {

    8 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

    9 n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);

    10 recvline[n] = 0; /* завершающий нуль */

    11 Fputs(recvline, stdout);

    7-12 В цикле обработки на стороне клиента имеется четыре шага: чтение строки из стандартного потока ввода при помощи функции fgets , отправка строки серверу с помощью функции sendto , чтение отраженного ответа сервера с помощью функции recvfrom и помещение отраженной строки в стандартный поток вывода с помощью функции fputs .

    Наш клиент не запрашивал у ядра присваивания динамически назначаемого порта своему сокету (тогда как для клиента TCP это имело место при вызове функции connect). В случае сокета UDP при первом вызове функции sendto ядро выбирает динамически назначаемый порт, если с этим сокетом еще не был связан никакой локальный порт. Как и в случае TCP, клиент может вызвать функцию bind явно, но это делается редко.

    Обратите внимание, что при вызове функции recvfrom в качестве пятого и шестого аргументов задаются пустые указатели. Таким образом мы сообщаем ядру, что мы не заинтересованы в том, чтобы знать, кто отправил ответ. Существует риск, что любой процесс, находящийся как на том же узле, так и на любом другом, может отправить на IP-адрес и порт клиента дейтаграмму, которая будет прочитана клиентом, предполагающим, что это ответ сервера. Эту ситуацию мы рассмотрим в разделе 8.8.

    Как и в случае функции сервера dg_echo , функция клиента dg_cli является не зависящей от протокола, но функция main клиента зависит от протокола. Функция main размещает в памяти и инициализирует структуру адреса сокета, относящегося к определенному типу протокола, а затем передает функции dg_cli указатель на структуру вместе с ее размером.

    8.7. Потерянные дейтаграммы

    Клиент и сервер UDP в нашем примере являются ненадежными. Если дейтаграмма клиента потеряна (допустим, она проигнорирована неким маршрутизатором между клиентом и сервером), клиент навсегда заблокируется в своем вызове функции recvfrom внутри функции dg_cli , ожидая от сервера ответа, который никогда не придет. Аналогично, если дейтаграмма клиента приходит к серверу, но ответ сервера потерян, клиент навсегда заблокируется в своем вызове функции recvfrom . Единственный способ предотвратить эту ситуацию - поместить тайм-аут в клиентский вызов функции recvfrom . Мы рассмотрим это в разделе 14.2.

    Простое помещение тайм-аута в вызов функции recvfrom - еще не полное решение. Например, если заданное время ожидания истекло, а ответ не получен, мы не можем сказать точно, в чем дело - или наша дейтаграмма не дошла до сервера, или же ответ сервера не пришел обратно. Если бы запрос клиента содержал требование типа «перевести определенное количество денег со счета А на счет Б» (в отличие от случая с нашим простым эхо-сервером), то тогда между потерей запроса и потерей ответа существовала бы большая разница. Более подробно о добавлении надежности в модель клиент-сервер UDP мы расскажем в разделе 22.5.

    8.8. Проверка полученного ответа

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

    Сначала мы изменяем функцию клиента main (см. листинг 8.3) для работы со стандартным эхо-сервером (см. табл. 2.1). Мы просто заменяем присваивание

    servaddr.sin_port = htons(SERV_PORT);

    присваиванием

    servaddr.sin_port = htons(7);

    Теперь мы можем использовать с нашим клиентом любой узел, на котором работает стандартный эхо-сервер.

    Затем мы переписываем функцию dg_cli , с тем чтобы она размещала в памяти другую структуру адреса сокета для хранения структуры, возвращаемой функцией recvfrom . Мы показываем ее в листинге 8.5.

    Листинг 8.5 . Версия функции dg_cli, проверяющая возвращаемый адрес сокета

    //udpcliserv/dgcliaddr.c

    1 #include "unp.h"

    3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

    6 char sendline, recvline;

    7 socklen_t len;

    8 struct sockaddr *preply_addr;

    9 preply_addr = Malloc(servlen);

    10 while (Fgets(sendline, MAXLINE, fp) != NULL) {

    11 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

    12 len = servlen;

    13 n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);

    14 if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) {

    15 printf("reply from %s (ignored)\n",

    18 recvline[n] = 0; /* завершающий нуль */

    19 Fputs(recvline, stdout);

    Размещение другой структуры адреса сокета в памяти

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

    Сравнение возвращаемых адресов

    12-13 В вызове функции recvfrom мы сообщаем ядру, что нужно возвратить адрес отправителя дейтаграммы. Сначала мы сравниваем длину, возвращаемую функцией recvfrom в аргументе типа «значение-результат», а затем сравниваем сами структуры адреса сокета при помощи функции memcmp .

    Новая версия нашего клиента работает замечательно, если сервер находится на узле с одним единственным IP-адресом. Но эта программа может не сработать, если сервер имеет несколько сетевых интерфейсов (multihomed server). Запускаем эту программу, обращаясь к узлу freebsd4 , у которого имеется два интерфейса и два IP-адреса:

    macosx % host freebsd4

    freebsd4.unpbook.com has address 172.24.37.94

    freebsd4.unpbook.com has address 135.197.17.100

    macosx % udpcli02 135.197.17.100

    reply from 172.24.37.94:7 (ignored)

    По рис. 1.7 видно, что мы задали IP-адрес из другой подсети. Обычно это допустимо. Большинство реализаций IP принимают приходящую IP-дейтаграмму, предназначенную для любого из IP-адресов узла, независимо от интерфейса, на который она приходит . Документ RFC 1122 называет это моделью системы с гибкой привязкой (weak end system model). Если система должна реализовать то, что в этом документе называется моделью системы с жесткой привязкой (strong end system model), она принимает приходящую дейтаграмму, только если дейтаграмма приходит на тот интерфейс, которому она адресована.

    IP-адрес, возвращаемый функцией recvfrom (IP-адрес отправителя дейтаграммы UDP), не является IP-адресом, на который мы посылали дейтаграмму. Когда сервер отправляет свой ответ, IP-адрес получателя - это адрес 172.24.37.94. Функция маршрутизации внутри ядра на узле freebsd4 выбирает адрес 172.24.37.94 в качестве исходящего интерфейса. Поскольку сервер не связал IP-адрес со своим сокетом (сервер связал со своим сокетом универсальный адрес, что мы можем проверить, запустив программу netstat на узле freebsd4), ядро выбирает адрес отправителя дейтаграммы IP. Этим адресом становится первичный IP-адрес исходящего интерфейса . Если мы отправляем дейтаграмму не на первичный IP-адрес интерфейса (то есть на альтернативное имя, псевдоним), то наша проверка, показанная в листинге 8.5, также окажется неудачной.

    Одним из решений будет проверка клиентом доменного имени отвечающего узла вместо его IP-адреса. Для этого имя сервера ищется в DNS (см. главу 11) на основе IP-адреса, возвращаемого функцией recvfrom . Другое решение - сделать так, чтобы сервер UDP создал по одному сокету для каждого IP-адреса, сконфигурированного на узле, связал с помощью функции bind этот IP-адрес с сокетом, вызвал функцию select для каждого из всех этих сокетов (ожидая, когда какой-либо из них станет готов для чтения), а затем ответил с сокета, готового для чтения. Поскольку сокет, используемый для ответа, связан с IP-адресом, который являлся адресом получателя клиентского запроса (иначе дейтаграмма не была бы доставлена на сокет), мы можем быть уверены, что адреса отправителя ответа и получателя запроса совпадают. Мы показываем эти примеры в разделе 22.6.

    ПРИМЕЧАНИЕ

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

    8.9. Запуск клиента без запуска сервера

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

    Сначала мы запускаем программу tcpdump на узле macosx , а затем - клиент на том же узле, задав в качестве узла сервера freebsd4. Потом мы вводим одну строку, но эта строка не отражается сервером.

    macosx % udpcli01 172.24.37.94

    hello, world мы вводим эту строку,

    но ничего не получаем в ответ

    В листинге 8.6 показан вывод программы tcpdump .

    Листинг 8.6 . Вывод программы tcpdump, когда процесс сервера не запускается на узле сервера

    01 0.0 arp who-has freebsd4 tell macosx

    02 0.003576 (0.0036) arp reply freebsd4 is-at 0:40:5:42:d6:de

    03 0.003601 (0.0000) macosx.51139 > freebsd4.9877: udp 13

    04 0.009781 (0.0062) freebsd4 >

    В первую очередь мы замечаем, что запрос и ответ ARP получены до того, как узел клиента смог отправить дейтаграмму UDP узлу сервера. (Мы оставили этот обмен в выводе программы, чтобы еще раз подчеркнуть, что до отправки IP-дейтаграммы всегда следует отправка запроса и получение ответа по протоколу ARP.)

    В строке 3 мы видим, что дейтаграмма клиента отправлена, но узел сервера отвечает в строке 4 сообщением ICMP о недоступности порта. (Длина 13 включает 12 символов плюс символ новой строки.) Однако эта ошибка ICMP не возвращается клиентскому процессу по причинам, которые мы кратко перечислим чуть ниже. Вместо этого клиент навсегда блокируется в вызове функции recvfrom в листинге 8.4. Мы также отмечаем, что в ICMPv6 имеется ошибка «Порт недоступен», аналогичная ошибке ICMPv4 (см. табл. А.5 и А.6), поэтому результаты, представленные здесь, аналогичны результатам для IPv6.

    Эта ошибка ICMP является асинхронной ошибкой. Ошибка была вызвана функцией sendto , но функция sendto завершилась нормально. Вспомните из раздела 2.9, что нормальное возвращение из операции вывода UDP означает только то, что дейтаграмма была добавлена к очереди вывода канального уровня. Ошибка ICMP не возвращается, пока не пройдет определенное количество времени (4 мс для листинга 8.6), поэтому она и называется асинхронной.

    Основное правило состоит в том, что асинхронные ошибки не возвращаются для сокета UDP, если сокет не был присоединен. Мы показываем, как вызвать функцию connect для сокета UDP, в разделе 8.11. Не все понимают, почему было принято это решение, когда сокеты были впервые реализованы. (Соображения о реализациях обсуждаются на с. 748-749 .) Рассмотрим клиент UDP, последовательно отправляющий три дейтаграммы трем различным серверам (то есть на три различных IP-адреса) через один сокет UDP. Клиент входит в цикл, вызывающий функцию recvfrom для чтения ответов. Две дейтаграммы доставляются корректно (то есть сервер был запущен на двух из трех узлов), но на третьем узле не был запущен сервер, и третий узел отвечает сообщением ICMP о недоступности порта. Это сообщение об ошибке ICMP содержит IP-заголовок и UDP-заголовок дейтаграммы, вызвавшей ошибку. (Сообщения об ошибках ICMPv4 и ICMPv6 всегда содержат заголовок IP и весь заголовок UDP или часть заголовка TCP, чтобы дать возможность получателю сообщения определить, какой сокет вызвал ошибку. Это показано на рис. 28.5 и 28.6.) Клиент, отправивший три дейтаграммы, должен знать получателя дейтаграммы, вызвавшей ошибку, чтобы точно определить, какая из трех дейтаграмм вызвала ошибку. Но как ядро может сообщить эту информацию процессу? Единственное, что может возвратить функция recvfrom , - это значение переменной errno . Но функция recvfrom не может вернуть в ошибке IP-адрес и номер порта получателя UDP-дейтаграммы. Следовательно, было принято решение, что эти асинхронные ошибки возвращаются процессу, только если процесс присоединил сокет UDP лишь к одному определенному собеседнику.

    ПРИМЕЧАНИЕ

    Linux возвращает большинство ошибок ICMP о недоступности порта даже для неприсоединенного сокета, если не включен параметр сокета SO_DSBCOMPAT. Возвращаются все ошибки о недоступности получателя, показанные в табл. А.5, за исключением ошибок с кодами 0, 1, 4, 5, 11 и 12.

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

    8.10. Итоговый пример клиент-сервера UDP

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

    Рис. 8.5 . Обобщение модели клиент-сервер UDP с точки зрения клиента

    Клиент должен задать IP-адрес сервера и номер порта для вызова функции sendto . Обычно клиентский IP-адрес и номер порта автоматически выбираются ядром, хотя мы отмечали, что клиент может вызвать функцию bind . Мы также отмечали, что если эти два значения выбираются для клиента ядром, то динамически назначаемый порт клиента выбирается один раз - при первом вызове функции sendto , и более никогда не изменяется. Однако IP-адрес клиента может меняться для каждой дейтаграммы UDP, которую отправляет клиент, если предположить, что клиент не связывает с сокетом определенный IP-адрес при помощи функции bind . Причину объясняет рис. 8.5: если узел клиента имеет несколько сетевых интерфейсов, клиент может переключаться между ними (на рис. 8.5 один адрес относится к канальному уровню, изображенному слева, другой - к изображенному справа). В худшем варианте этого сценария IP-адрес клиента, выбираемый ядром на основе исходящего канального уровня, будет меняться для каждой дейтаграммы.

    Что произойдет, если клиент с помощью функции bind свяжет IP-адрес со своим сокетом, но ядро решит, что исходящая дейтаграмма должна быть отправлена с какого-то другого канального уровня? В этом случае дейтаграмма IP будет содержать IP-адрес отправителя, отличный от IP-адреса исходящего канального уровня (см. упражнение 8.6).

    На рис. 8.6 представлены те же четыре значения, но с точки зрения сервера.

    Рис. 8.6 . Обобщение модели клиент-сервер UDP с точки зрения сервера

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

    Таблица 8.1 . Информация, доступная серверу из приходящей дейтаграммы IP

    У сервера TCP всегда есть простой доступ ко всем четырем фрагментам информации для присоединенного сокета, и эти четыре значения остаются постоянными в течение всего времени жизни соединения. Однако в случае соединения UDP IP-адрес получателя можно получить только с помощью установки параметра сокета IP_RECVDSTADDR для IPv4 или IPV6_PKTINFO для IPv6 и последующего вызова функции recvmsg вместо функции recvfrom . Поскольку протокол UDP не ориентирован на установление соединения, IP-адрес получателя может меняться для каждой дейтаграммы, отправляемой серверу. Сервер UDP может также получать дейтаграммы, предназначенные для одного из широковещательных адресов узла или для адреса многоадресной передачи, что мы обсуждаем в главах 20 и 21. Мы покажем, как определить адрес получателя дейтаграммы UDP, в разделе 20.2, после того как опишем функцию recvmsg .

    8.11. Функция connect для UDP
    ПРИМЕЧАНИЕ
    ПРИМЕЧАНИЕ
    ПРИМЕЧАНИЕ

    Таблица 8.2

    ПРИМЕЧАНИЕ

    Рис. 8.7 . Присоединенный сокет UDP

    Рис. 8.8

    Многократный вызов функции connect для сокета UDP

    Процесс с присоединенным сокетом UDP может снова вызвать функцию connect Для этого сокета, чтобы:

    в– задать новый IP-адрес и порт;

    в– отсоединить сокет.

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

    Чтобы отсоединить сокет UDP, мы вызываем функцию connect , но присваиваем элементу семейства структуры адреса сокета (sin_family для IPv4 или sin6_family для IPv6) значение AF_UNSPEC . Это может привести к ошибке EAFNOSUPPORT , но это нормально. Именно процесс вызова функции connect на уже присоединенном сокете UDP позволяет отсоединить сокет .

    ПРИМЕЧАНИЕ

    В руководстве BSD по поводу функции connect традиционно говорилось: «Сокеты дейтаграмм могут разрывать связь, соединяясь с недействительными адресами, такими как пустые адреса». К сожалению, ни в одном руководстве не сказано, что представляет собой «пустой адрес», и не упоминается, что в результате возвращается ошибка (что нормально). Стандарт POSIX явно указывает, что семейство адресов должно быть установлено в AF_UNSPEC, но затем сообщает, что этот вызов функции connect может возвратить, а может и не возвратить ошибку EAFNOSUPPORT.

    Производительность

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

    в– присоединение сокета;

    в– вывод первой дейтаграммы;

    в– отсоединение сокета;

    в– присоединение сокета;

    в– вывод второй дейтаграммы;

    в– отсоединение сокета.

    ПРИМЕЧАНИЕ

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

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

    в– присоединение сокета;

    в– вывод первой дейтаграммы;

    в– вывод второй дейтаграммы.

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

    8.12. Функция dg_cli (продолжение)

    Вернемся к функции dg_cli , показанной в листинге 8.4, и перепишем ее, с тем чтобы она вызывала функцию connect . В листинге 8.7 показана новая функция.

    Листинг 8.7 . Функция dg_cli, вызывающая функцию connect

    //udpcliserv/dgcliconnect.c

    1 #include "unp.h"

    3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

    6 char sendline, recvline;

    7 Connect(sockfd, (SA*)pservaddr, servlen);

    8 while (Fgets(sendline, MAXLINE, fp) != NULL) {

    9 Write(sockfd, sendline, strlen(sendline));

    10 n = Read(sockfd, recvline, MAXLINE);

    11 recvline[n] = 0; /* завершающий нуль */

    12 Fputs(recvline, stdout);

    Изменения по сравнению с предыдущей версией - это добавление вызова функции connect и замена вызовов функций sendto и recvfrom вызовами функций write и read . Функция dg_cli остается не зависящей от протокола, поскольку она не вникает в структуру адреса сокета, передаваемую функции connect . Наша функция main клиента, показанная в листинге 8.3, остается той же.

    Если мы запустим программу на узле macosx , задав IP-адрес узла freebsd4 (который не запускает наш сервер на порте 9877), мы получим следующий вывод:

    macosx % udpcli04 172.24.37.94

    hello, world

    read error: Connection refused

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

    В листинге 8.8 показан вывод программы tcpdump .

    Листинг 8.8 . Вывод программы tcpdump при запуске функции dg_cli

    macosx % tcpdump

    01 0.0 macosx.51139 > freebsd4 9877:udp 13

    02 0.006180 (0.0062) freebsd4 > macosx: icmp: freebsd4 udp port 9877 unreachable

    В табл. A.5 мы также видим, что возникшую ошибку ICMP ядро сопоставляет ошибке ECONNREFUSED , которая соответствует выводу строки сообщения Connection refused (В соединении отказано) функцией err_sys .

    ПРИМЕЧАНИЕ

    К сожалению, не все ядра возвращают сообщения ICMP присоединенному сокету UDP, как мы показали в этом разделе. Обычно ядра реализаций, происходящих от Беркли, возвращают эту ошибку, а ядра System V - не возвращают. Например, если мы запустим тот же клиент на узле Solaris 2.4 и с помощью функции connect соединимся с узлом, на котором не запущен наш сервер, то с помощью программы tcpdump мы сможем убедиться, что ошибка ICMP о недоступности порта возвращается узлом сервера, но вызванная клиентом функция read никогда не завершается. Эта ситуация была исправлена в Solaris 2.5. UnixWare не возвращает ошибку, в то время как AIX, Digital Unix, HP-UX и Linux возвращают.

    8.13. Отсутствие управления потоком в UDP

    Листинг 8.9

    //udpcliserv/dgcliloop1.c

    1 #include "unp.h"

    8 char sendline;

    Листинг 8.10

    //udpcliserv/dgecholoop1.c

    1 #include "unp.h"

    3 static int count;

    7 socklen_t len;

    8 char mesg;

    11 len = clilen;

    17 recvfrom_int(int signo)

    Листинг 8.11 . Вывод на узле сервера

    freebsd % netstat -s -p udp

    71208 datagrams received

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 with no checksum

    832 dropped due to no socket

    0 not for hashed pcb

    137685 datagrams output

    freebsd % udpserv06 запускаем наш сервер

    клиент посылает дейтаграммы

    ^C

    freebsd % netstat -s -р udp

    73208 datagrams received

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 with no checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams dropped due to no socket

    0 not for hashed pcb

    137685 datagrams output

    aix % udpserv06

    ^?

    received 2000 datagrams

    Приемный буфер сокета UDP

    Число дейтаграмм UDP, установленных в очередь UDP, для данного сокета ограничено размером его приемного буфера. Мы можем изменить его с помощью параметра сокета SO_RCVBUF , как мы показали в разделе 7.5. В FreeBSD по умолчанию размер приемного буфера сокета UDP равен 42 080 байт, что допускает возможность хранения только 30 из наших 1400-байтовых дейтаграмм. Если мы увеличим размер приемного буфера сокета, то можем рассчитывать, что сервер получит дополнительные дейтаграммы. В листинге 8.12 представлена измененная функция dg_echo из листинга 8.10, которая увеличивает размер приемного буфера сокета до 240 Кбайт. Если мы запустим этот сервер в системе Sun, а клиент - в системе RS/6000, то счетчик полученных дейтаграмм будет иметь значение 103. Поскольку это лишь немногим лучше, чем в предыдущем примере с размером буфера, заданным по умолчанию, ясно, что мы пока не получили решения проблемы.

    Листинг 8.12 . Функция dg_echo, увеличивающая размер приемного буфера сокета

    //udpcliserv/dgecholоор2.c

    1 #include "unp.h"

    2 static void recvfrom_int(int);

    3 static int count;

    5 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

    8 socklen_t len;

    9 char mesg;

    10 Signal(SIGINT, recvfrom_int);

    11 n = 240 * 1024;

    12 Setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

    14 len = clilen;

    15 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

    20 recvfrom_int(int signo)

    22 printf("\nreceived %d datagrams\n", count);

    ПРИМЕЧАНИЕ

    Почему мы устанавливаем размер буфера приема сокета равным 240Г-1024 байт в листинге 8.12? Максимальный размер приемного буфера сокета в BSD/OS 2.1 по умолчанию равен 262 144 байта (256Г-1024), но из-за способа размещения буфера в памяти (описанного в главе 2 ) он в действительности ограничен до 246 723 байт. Многие более ранние системы, основанные на 4.3BSD, ограничивали размер буфера приема сокета примерно до 52 000 байт.

    8.14. Определение исходящего интерфейса для UDP

    С помощью присоединенного сокета UDP можно также задавать исходящий интерфейс, который будет использован для отправки дейтаграмм к определенному получателю. Это объясняется побочным эффектом функции connect , примененной к сокету UDP: ядро выбирает локальный IP-адрес (предполагается, что процесс еще не вызвал функцию bind для явного его задания). Локальный адрес выбирается в процессе поиска адреса получателя в таблице маршрутизации, причем берется основной IP-адрес интерфейса, с которого, согласно таблице, будут отправляться дейтаграммы.

    В листинге 8.13 показана простая программа UDP, которая с помощью функции connect соединяется с заданным IP-адресом и затем вызывает функцию getsockname , выводя локальный IP-адрес и порт.

    Листинг 8.13 . Программа UDP, использующая функцию connect для определения исходящего интерфейса

    //udpcliserv/udpcli09.c

    1 #include "unp.h"

    3 main(int argc, char **argv)

    6 socklen_t len;

    7 struct sockaddr_in cliaddr, servaddr;

    8 if (argc != 2)

    9 err_quit("usage: udpcli ");

    10 sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    11 bzero(&servaddr, sizeof(servaddr));

    12 servaddr.sin_family = AF_INET;

    13 servaddr.sin_port = htons(SERV_PORT);

    14 Inet_pton(AF_INET, argv, &servaddr.sin_addr);

    15 Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));

    16 len = sizeof(cliaddr);

    17 Getsockname(sockfd, (SA*)&cliaddr, &len);

    18 printf("local address %s\n", Sock_ntop((SA*)&cliaddr, len));

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

    freebsd % udpcli09 206.168.112.96

    local address 12.106.32.254:52329

    freebsd % udpcli09 192.168.42.2

    local address 192.168.42.1:52330

    freebsd % udpcli09 127.0.0.1

    local address 127.0.0.1:52331

    По рис. 1.7 видно, что когда мы запускаем программу первые два раза, аргументом командной строки является IP-адрес в разных сетях Ethernet. Ядро присваивает локальный IP-адрес первичному адресу интерфейса в соответствующей сети Ethernet. При вызове функции connect на сокете UDP ничего не отправляется на этот узел - это полностью локальная операция, которая сохраняет IP-адрес и порт собеседника. Мы также видим, что вызов функции connect на неприсоединенном сокете UDP также присваивает сокету динамически назначаемый порт.

    ПРИМЕЧАНИЕ

    К сожалению, эта технология действует не во всех реализациях, что особенно касается ядер, происходящих от SVR4. Например, это не работает в Solaris 2.5, но работает в AIX, Digital Unix, Linux, MacOS X и Solaris 2.6.

    8.15. Эхо-сервер TCP и UDP, использующий функцию select

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

    Листинг 8.14 . Первая часть эхо-сервера, обрабатывающего сокеты TCP и UDP при помощи функции select

    //udpcliserv/udpservselect01.c

    1 #include "unp.h"

    3 main(int argc, char **argv)

    5 int listenfd, connfd, udpfd, nready, maxfdp1;

    6 char mesg;

    7 pid_t childpid;

    10 socklen_t len;

    11 const int on = 1;

    12 struct sockaddr_in cliaddr, servaddr;

    13 void sig_chld(int);

    14 /* создание прослушиваемого сокета TCP */

    15 listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    16 bzero(&servaddr, sizeof(servaddr));

    17 servaddr.sin_family = AF_INET;

    18 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    19 servaddr.sin_port = htons(SERV_PORT);

    20 Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    21 Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));

    22 Listen(listenfd, LISTENQ);

    23 /* создание сокета UDP */

    24 udpfd = Socket(AF_INET, SOCK_DGRAM, 0);

    25 bzero(&servaddr, sizeof(servaddr));

    26 servaddr.sin_family = AF_INET;

    27 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    28 servaddr.sin_port = htons(SERV_PORT);

    29 Bind(udpfd, (SA*)&servaddr, sizeof(servaddr));

    Создание прослушиваемого сокета TCP

    14-22 Создается прослушиваемый сокет TCP, который связывается с заранее известным портом сервера. Мы устанавливаем параметр сокета SO_REUSEADDR в случае, если на этом порте существуют соединения.

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

    23-29 Также создается сокет UDP и связывается с тем же портом. Даже если один и тот же порт используется для сокетов TCP и UDP, нет необходимости устанавливать параметр сокета SO_REUSEADDR перед этим вызовом функции bind , поскольку порты TCP не зависят от портов UDP.

    В листинге 8.15 показана вторая часть нашего сервера.

    Листинг 8.15 . Вторая половина эхо-сервера, обрабатывающего TCP и UDP при помощи функции select

    udpcliserv/udpservselect01.c

    30 Signal(SIGCHLD, sig_chld); /* требуется вызвать waitpid() */

    31 FD_ZERO(&rset);

    32 maxfdp1 = max(listenfd, udpfd) + 1;

    34 FD_SET(listenfd, &rset);

    35 FD_SET(udpfd, &rset);

    36 if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL))

    37 if (errno == EINTR)

    38 continue; /* назад в for() */

    40 err_sys("select error");

    42 if (FD_ISSET(listenfd, &rset)) {

    43 len = sizeof(cliaddr);

    44 connfd = Accept(listenfd, (SA*)&cliaddr, &len);

    45 if ((childpid = Fork()) == 0) { /* дочерний процесс */

    46 Close(listenfd); /* закрывается прослушиваемый сокет */

    47 str_echo(connfd); /* обработка запроса */

    50 Close(connfd); /* родитель закрывает присоединенный сокет */

    52 if (FD_ISSET(udpfd, &rset)) {

    53 len = sizeof(cliaddr);

    54 n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA*)&cliaddr, &len);

    55 Sendto(udpfd, mesg, n, 0, (SA*)&cliaddr, len);

    Установка обработчика сигнала SIGCHLD

    30 Для сигнала SIGCHLD устанавливается обработчик, поскольку соединения TCP будут обрабатываться дочерним процессом. Этот обработчик сигнала мы показали в листинге 5.8.

    Подготовка к вызову функции select

    31-32 Мы инициализируем набор дескрипторов для функции select и вычисляем максимальный из двух дескрипторов, готовности которого будем ожидать.

    Вызов функции select

    34-41 Мы вызываем функцию select , ожидая только готовности к чтению прослушиваемого сокета TCP или сокета UDP. Поскольку наш обработчик сигнала sig_chld может прервать вызов функции select , обрабатываем ошибку EINTR .

    Обработка нового клиентского соединения

    42-51 С помощью функции accept мы принимаем новое клиентское соединение, а когда прослушиваемый сокет TCP готов для чтения, с помощью функции fork порождаем дочерний процесс и вызываем нашу функцию str_echo в дочернем процессе. Это та же последовательность действий, которую мы выполняли в главе 5.

    Обработка приходящей дейтаграммы

    52-57 Если сокет UDP готов для чтения, дейтаграмма пришла. Мы читаем ее с помощью функции recvfrom и отправляем обратно клиенту с помощью функции sendto .

    8.16. Резюме

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

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

    В UDP отсутствует возможность управления потоком, что очень легко продемонстрировать. Обычно это не создает проблем, поскольку многие приложения UDP построены с использованием модели «запрос-ответ» и не предназначены для передачи большого количества данных.

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

    Упражнения

    1. Допустим, у нас имеется два приложения, одно использует TCP, а другое - UDP. В приемном буфере сокета TCP находится 4096 байт данных, а в приемном буфере для сокета UDP - две дейтаграммы по 2048 байт. Приложение TCP вызывает функцию read с третьим аргументом 4096, а приложение UDP вызывает функцию recvfrom с третьим аргументом 4096. Есть ли между этими вызовами какая-нибудь разница?

    2. Что произойдет в листинге 8.2, если мы заменим последний аргумент функции sendto (который мы обозначили len) аргументом clilen ?

    3. Откомпилируйте и запустите сервер UDP из листингов 8.1 и 8.4, а затем - клиент из листингов 8.3 и 8.4. Убедитесь в том, что клиент и сервер работают вместе.

    4. Запустите программу ping в одном окне, задав параметр -i 60 (отправка одного пакета каждые 60 секунд; некоторые системы используют ключ I вместо i), параметр -v (вывод всех полученных сообщений об ошибках ICMP) и задав адрес закольцовки на себя (обычно 127.0.0.1). Мы будем использовать эту программу, чтобы увидеть ошибку ICMP недоступности порта, возвращаемую узлом сервера. Затем запустите наш клиент из предыдущего упражнения в другом окне, задав IP-адрес некоторого узла, на котором не запущен сервер. Что происходит?

    5. Рассматривая рис. 8.3, мы сказали, что каждый присоединенный сокет TCP имеет свой собственный буфер приема. Как вы думаете, есть ли у прослушиваемого сокета свой собственный буфер приема?

    6. Используйте программу sock (см. раздел В.3) и такое средство, как, например, tcpdump (см. раздел В.5), чтобы проверить утверждение из раздела 8.10: если клиент с помощью функции bind связывает IP-адрес со своим сокетом, но отправляет дейтаграмму, исходящую от другого интерфейса, то результирующая дейтаграмма содержит IP-адрес, который был связан с сокетом, даже если он не соответствует исходящему интерфейсу.

    7. Откомпилируйте программы из раздела 8.13 и запустите клиент и сервер на различных узлах. Помещайте printf в клиент каждый раз, когда дейтаграмма записывается в сокет. Изменяет ли это процент полученных пакетов? Почему? Вызывайте printf из сервера каждый раз, когда дейтаграмма читается из сокета. Изменяет ли это процент полученных пакетов? Почему?

    8. Какова наибольшая длина, которую мы можем передать функции sendto для сокета UDP/IPv4, то есть каково наибольшее количество данных, которые могут поместиться в дейтаграмму UDP/IPv4? Что изменяется в случае UDP/IPv6?

    Измените листинг 8.4, с тем чтобы отправить одну дейтаграмму UDP максимального размера, считать ее обратно и вывести число байтов, возвращаемых функцией recvfrom .

    9. Измените листинг 8.15 таким образом, чтобы он соответствовал RFC 1122: для сокета UDP следует использовать параметр IP_RECVDSTADDR .

    В конце разделе 8.9 мы упомянули, что асинхронные ошибки не возвращаются на сокете UDP, если сокет не был присоединен. На самом деле мы можем вызвать функцию connect для сокета UDP (см. раздел 4.3). Но это не приведет ни к чему похожему на соединение TCP: здесь не существует трехэтапного рукопожатия. Ядро просто проверяет, нет ли сведений о заведомой недоступности адресата, после чего записывает IP-адрес и номер порта собеседника, которые содержатся в структуре адреса сокета, передаваемой функции connect, и немедленно возвращает управление вызывающему процессу.

    ПРИМЕЧАНИЕ

    Перегрузка функции connect этой новой возможностью для сокетов UDP может внести путаницу. Если используется соглашение о том, что sockname - это адрес локального протокола, a peername - адрес удаленного протокола, то лучше бы эта функция называлась setpeername. Аналогично, функции bind больше подошло бы название setsockname.

    С учетом этого необходимо понимать разницу между двумя видами сокетов UDP.

    в– Неприсоединенный (unconnected) сокет UDP - это сокет UDP, создаваемый по умолчанию.

    в– Присоединенный {connected) сокет UDP - результат вызова функции connect для сокета UDP.

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

    1. Мы больше не можем задавать IP-адрес получателя и порт для операции вывода. То есть мы используем вместо функции sendto функцию write или send . Все, что записывается в присоединенный сокет UDP, автоматически отправляется на адрес (например, IP-адрес и порт), заданный функцией connect .

    ПРИМЕЧАНИЕ

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

    2. Вместо функции recvfrom мы используем функцию read или recv . Единственные дейтаграммы, возвращаемые ядром для операции ввода через присоединенный сокет UDP, - это дейтаграммы, приходящие с адреса, заданного в функции connect . Дейтаграммы, предназначенные для адреса локального протокола присоединенного сокета UDP (например, IP-адрес и порт), но приходящие с адреса протокола, отличного от того, к которому сокет был присоединен с помощью функции connect , не передаются присоединенному сокету. Это ограничивает присоединенный сокет UDP, позволяя ему обмениваться дейтаграммами с одним и только одним собеседником.

    ПРИМЕЧАНИЕ

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

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

    В табл. 8.2 сводятся воедино свойства, перечисленные в первом пункте, применительно к 4.4BSD.

    Таблица 8.2 . Сокеты TCP и UDP: может ли быть задан адрес протокола получателя

    ПРИМЕЧАНИЕ

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

    Solaris 2.5 допускает функцию sendto, которая задает адрес получателя для присоединенного сокета UDP. POSIX определяет, что в такой ситуации должна возвращаться ошибка EISCONN.

    На рис. 8.7 обобщается информация о присоединенном сокете UDP.

    Рис. 8.7 . Присоединенный сокет UDP

    Приложение вызывает функцию connect , задавая IP-адрес и номер порта собеседника. Затем оно использует функции read и write для обмена данными с собеседником.

    Дейтаграммы, приходящие с любого другого IP-адреса или порта (который мы обозначаем как «???» на рис. 8.7), не передаются на присоединенный сокет, поскольку либо IP-адрес, либо UDP-порт отправителя не совпадают с адресом протокола, с которым сокет соединяется с помощью функции connect . Эти дейтаграммы могут быть доставлены на какой-то другой сокет UDP на узле. Если нет другого совпадающего сокета для приходящей дейтаграммы, UDP проигнорирует ее и сгенерирует ICMP-сообщение о недоступности порта.

    Обобщая вышесказанное, мы можем утверждать, что клиент или сервер UDP может вызвать функцию connect , только если этот процесс использует сокет UDP для связи лишь с одним собеседником. Обычно именно клиент UDP вызывает функцию connect , но существуют приложения, в которых сервер UDP связывается с одним клиентом на длительное время (например, TFTP), и в этом случае и клиент, и сервер вызывают функцию connect .

    Еще один пример долгосрочного взаимодействия - это DNS (рис. 8.8).

    Рис. 8.8 . Пример клиентов и серверов DNS и функции connect

    Клиент DNS может быть сконфигурирован для использования одного или более серверов, обычно с помощью перечисления IP-адресов серверов в файле /etc/resolv.conf . Если в этом файле указан только один сервер (на рисунке этот клиент изображен в крайнем слева прямоугольнике), клиент может вызвать функцию connect, но если перечислено множество серверов (второй справа прямоугольник на рисунке), клиент не может вызвать функцию connect . Обычно сервер DNS обрабатывает также любые клиентские запросы, следовательно, серверы не могут вызывать функцию connect .

    Теперь мы проверим, как влияет на работу приложения отсутствие какого-либо управления потоком в UDP. Сначала мы изменим нашу функцию dg_cli так, чтобы она отправляла фиксированное число дейтаграмм. Она больше не будет читать из стандартного потока ввода. В листинге 8.9 показана новая версия функции. Эта функция отправляет серверу 2000 дейтаграмм UDP по 1400 байт каждая.

    Листинг 8.9 . Функция dg_cli, отсылающая фиксированное число дейтаграмм серверу

    //udpcliserv/dgcliloop1.c

    1 #include "unp.h"

    2 #define NDG 2000 /* количество дейтаграмм для отправки */

    3 #define DGLEN 1400 /* длина каждой дейтаграммы */

    5 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

    8 char sendline;

    10 Sendto(sockfd, sendline, DGLEN, 0, pservaddr, servlen);

    Затем мы изменяем сервер так, чтобы он получал дейтаграммы и считал число полученных дейтаграмм. Сервер больше не отражает дейтаграммы обратно клиенту. В листинге 8.10 показана новая функция dg_echo . Когда мы завершаем процесс сервера нажатием клавиши прерывания на терминале (что приводит к отправке сигнала SIGINT процессу), сервер выводит число полученных дейтаграмм и завершается.

    Листинг 8.10 . Функция dg_echo, считающая полученные дейтаграммы

    //udpcliserv/dgecholoop1.c

    1 #include "unp.h"

    2 static void recvfrom_int(int);

    3 static int count;

    5 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

    7 socklen_t len;

    8 char mesg;

    9 Signal (SIGINT, recvfrom_int);

    11 len = clilen;

    12 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

    17 recvfrom_int(int signo)

    19 printf("\nreceived %d datagrams\n", count);

    Теперь мы запускаем сервер на узле freebsd , который представляет собой медленный компьютер SPARCStation. Клиент мы запускаем в значительно более быстрой системе RS/6000 с операционной системой aix . Они соединены друг с другом напрямую каналом Ethernet на 100 Мбит/с. Кроме того, мы запускаем программу netstat -s на узле сервера и до, и после запуска клиента и сервера, поскольку выводимая статистика покажет, сколько дейтаграмм мы потеряли. В листинге 8.11 показан вывод сервера.

    Листинг 8.11 . Вывод на узле сервера

    freebsd % netstat -s -p udp

    71208 datagrams received

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 with no checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams dropped due to no socket

    1971 dropped due to full socket buffers

    0 not for hashed pcb

    137685 datagrams output

    freebsd % udpserv06 запускаем наш сервер

    клиент посылает дейтаграммы

    ^C для окончания работы клиента вводим наш символ прерывания

    freebsd % netstat -s -р udp

    73208 datagrams received

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 with no checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams dropped due to no socket

    3941 dropped due to full socket buffers

    0 not for hashed pcb

    137685 datagrams output

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

    Если мы посмотрим на вывод программы netstat , то увидим, что общее число дейтаграмм, полученных узлом сервера (не приложением-сервером) равно 2000 (73 208 – 71 208). Счетчик dropped due to full socket buffers (отброшено из-за переполнения буферов сокета) показывает, сколько дейтаграмм было получено UDP и проигнорировано из-за того, что приемный буфер принимающего сокета был полон . Это значение равно 1970 (3941 – 1971), что при добавлении к выводу счетчика дейтаграмм, полученных приложением (30), дает 2000 дейтаграмм, полученных узлом. К сожалению, счетчик дейтаграмм, отброшенных из-за заполненного буфера, в программе netstat распространяется на всю систему. Не существует способа определить, на какие приложения (например, какие порты UDP) это влияет.

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

    Если мы запустим тот же клиент и тот же сервер, но на этот раз клиент на медленной системе Sun, а сервер на быстрой системе RS/6000, никакие дейтаграммы не теряются.

    aix % udpserv06

    ^? после окончания работы клиента вводим наш символ прерывания

    received 2000 datagrams

    В конце разделе 8.9 мы упомянули, что асинхронные ошибки не возвращаются на сокете UDP, если сокет не был присоединен. На самом деле мы можем вызвать функцию connect для сокета UDP (см. раздел 4.3). Но это не приведет ни к чему похожему на соединение TCP: здесь не существует трехэтапного рукопожатия. Ядро просто проверяет, нет ли сведений о заведомой недоступности адресата, после чего записывает IP-адрес и номер порта собеседника, которые содержатся в структуре адреса сокета, передаваемой функции connect, и немедленно возвращает управление вызывающему процессу.

    ПРИМЕЧАНИЕ

    Перегрузка функции connect этой новой возможностью для сокетов UDP может внести путаницу. Если используется соглашение о том, что sockname - это адрес локального протокола, a peername - адрес удаленного протокола, то лучше бы эта функция называлась setpeername. Аналогично, функции bind больше подошло бы название setsockname.

    С учетом этого необходимо понимать разницу между двумя видами сокетов UDP.

    в– Неприсоединенный (unconnected) сокет UDP - это сокет UDP, создаваемый по умолчанию.

    в– Присоединенный {connected) сокет UDP - результат вызова функции connect для сокета UDP.

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

    1. Мы больше не можем задавать IP-адрес получателя и порт для операции вывода. То есть мы используем вместо функции sendto функцию write или send . Все, что записывается в присоединенный сокет UDP, автоматически отправляется на адрес (например, IP-адрес и порт), заданный функцией connect .

    ПРИМЕЧАНИЕ

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

    2. Вместо функции recvfrom мы используем функцию read или recv . Единственные дейтаграммы, возвращаемые ядром для операции ввода через присоединенный сокет UDP, - это дейтаграммы, приходящие с адреса, заданного в функции connect . Дейтаграммы, предназначенные для адреса локального протокола присоединенного сокета UDP (например, IP-адрес и порт), но приходящие с адреса протокола, отличного от того, к которому сокет был присоединен с помощью функции connect , не передаются присоединенному сокету. Это ограничивает присоединенный сокет UDP, позволяя ему обмениваться дейтаграммами с одним и только одним собеседником.

    ПРИМЕЧАНИЕ

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

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

    В табл. 8.2 сводятся воедино свойства, перечисленные в первом пункте, применительно к 4.4BSD.

    Таблица 8.2 . Сокеты TCP и UDP: может ли быть задан адрес протокола получателя

    ПРИМЕЧАНИЕ

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

    Solaris 2.5 допускает функцию sendto, которая задает адрес получателя для присоединенного сокета UDP. POSIX определяет, что в такой ситуации должна возвращаться ошибка EISCONN.

    На рис. 8.7 обобщается информация о присоединенном сокете UDP.

    Рис. 8.7 . Присоединенный сокет UDP

    Приложение вызывает функцию connect , задавая IP-адрес и номер порта собеседника. Затем оно использует функции read и write для обмена данными с собеседником.

    Дейтаграммы, приходящие с любого другого IP-адреса или порта (который мы обозначаем как «???» на рис. 8.7), не передаются на присоединенный сокет, поскольку либо IP-адрес, либо UDP-порт отправителя не совпадают с адресом протокола, с которым сокет соединяется с помощью функции connect . Эти дейтаграммы могут быть доставлены на какой-то другой сокет UDP на узле. Если нет другого совпадающего сокета для приходящей дейтаграммы, UDP проигнорирует ее и сгенерирует ICMP-сообщение о недоступности порта.

    Обобщая вышесказанное, мы можем утверждать, что клиент или сервер UDP может вызвать функцию connect , только если этот процесс использует сокет UDP для связи лишь с одним собеседником. Обычно именно клиент UDP вызывает функцию connect , но существуют приложения, в которых сервер UDP связывается с одним клиентом на длительное время (например, TFTP), и в этом случае и клиент, и сервер вызывают функцию connect .

    Еще один пример долгосрочного взаимодействия - это DNS (рис. 8.8).

    Рис. 8.8 . Пример клиентов и серверов DNS и функции connect

    Клиент DNS может быть сконфигурирован для использования одного или более серверов, обычно с помощью перечисления IP-адресов серверов в файле /etc/resolv.conf . Если в этом файле указан только один сервер (на рисунке этот клиент изображен в крайнем слева прямоугольнике), клиент может вызвать функцию connect, но если перечислено множество серверов (второй справа прямоугольник на рисунке), клиент не может вызвать функцию connect . Обычно сервер DNS обрабатывает также любые клиентские запросы, следовательно, серверы не могут вызывать функцию connect .

    Теперь мы проверим, как влияет на работу приложения отсутствие какого-либо управления потоком в UDP. Сначала мы изменим нашу функцию dg_cli так, чтобы она отправляла фиксированное число дейтаграмм. Она больше не будет читать из стандартного потока ввода. В листинге 8.9 показана новая версия функции. Эта функция отправляет серверу 2000 дейтаграмм UDP по 1400 байт каждая.

    Листинг 8.9 . Функция dg_cli, отсылающая фиксированное число дейтаграмм серверу

    //udpcliserv/dgcliloop1.c

    1 #include "unp.h"

    2 #define NDG 2000 /* количество дейтаграмм для отправки */

    3 #define DGLEN 1400 /* длина каждой дейтаграммы */

    5 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)

    8 char sendline;

    10 Sendto(sockfd, sendline, DGLEN, 0, pservaddr, servlen);

    Затем мы изменяем сервер так, чтобы он получал дейтаграммы и считал число полученных дейтаграмм. Сервер больше не отражает дейтаграммы обратно клиенту. В листинге 8.10 показана новая функция dg_echo . Когда мы завершаем процесс сервера нажатием клавиши прерывания на терминале (что приводит к отправке сигнала SIGINT процессу), сервер выводит число полученных дейтаграмм и завершается.

    Листинг 8.10 . Функция dg_echo, считающая полученные дейтаграммы

    //udpcliserv/dgecholoop1.c

    1 #include "unp.h"

    2 static void recvfrom_int(int);

    3 static int count;

    5 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)

    7 socklen_t len;

    8 char mesg;

    9 Signal (SIGINT, recvfrom_int);

    11 len = clilen;

    12 Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);

    17 recvfrom_int(int signo)

    19 printf("\nreceived %d datagrams\n", count);

    Теперь мы запускаем сервер на узле freebsd , который представляет собой медленный компьютер SPARCStation. Клиент мы запускаем в значительно более быстрой системе RS/6000 с операционной системой aix . Они соединены друг с другом напрямую каналом Ethernet на 100 Мбит/с. Кроме того, мы запускаем программу netstat -s на узле сервера и до, и после запуска клиента и сервера, поскольку выводимая статистика покажет, сколько дейтаграмм мы потеряли. В листинге 8.11 показан вывод сервера.

    Листинг 8.11 . Вывод на узле сервера

    freebsd % netstat -s -p udp

    71208 datagrams received

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 with no checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams dropped due to no socket

    1971 dropped due to full socket buffers

    0 not for hashed pcb

    137685 datagrams output

    freebsd % udpserv06 запускаем наш сервер

    клиент посылает дейтаграммы

    ^C для окончания работы клиента вводим наш символ прерывания

    freebsd % netstat -s -р udp

    73208 datagrams received

    0 with incomplete header

    0 with bad data length field

    0 with bad checksum

    0 with no checksum

    832 dropped due to no socket

    16 broadcast/multicast datagrams dropped due to no socket

    3941 dropped due to full socket buffers

    0 not for hashed pcb

    137685 datagrams output

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

    Если мы посмотрим на вывод программы netstat , то увидим, что общее число дейтаграмм, полученных узлом сервера (не приложением-сервером) равно 2000 (73 208 – 71 208). Счетчик dropped due to full socket buffers (отброшено из-за переполнения буферов сокета) показывает, сколько дейтаграмм было получено UDP и проигнорировано из-за того, что приемный буфер принимающего сокета был полон . Это значение равно 1970 (3941 – 1971), что при добавлении к выводу счетчика дейтаграмм, полученных приложением (30), дает 2000 дейтаграмм, полученных узлом. К сожалению, счетчик дейтаграмм, отброшенных из-за заполненного буфера, в программе netstat распространяется на всю систему. Не существует способа определить, на какие приложения (например, какие порты UDP) это влияет.

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

    Если мы запустим тот же клиент и тот же сервер, но на этот раз клиент на медленной системе Sun, а сервер на быстрой системе RS/6000, никакие дейтаграммы не теряются.

    aix % udpserv06

    ^? после окончания работы клиента вводим наш символ прерывания

    received 2000 datagrams



Понравилась статья? Поделиться с друзьями: