UNIX: разработка сетевых приложений

Стивенс Уильям Ричард

Феннер Билл

Рудофф Эндрю М.

Приложения

 

 

Приложение А

Протоколы IPv4, IPv6, ICMPv4 и ICMFV6

 

А.1. Введение

В этом приложении приведен обзор протоколов IPv4, IPv6, ICMPv4 и ICMPv6. Данный материал позволяет глубже понять рассмотренные в главе 2 протоколы TCP и UDP. Некоторые возможности IP и ICMP рассматриваются также более подробно и в других главах, например параметры IP (см. главу 27), и программы ping и traceroute (см. главу 28).

 

А.2. Заголовок IPv4

Уровень IP обеспечивает не ориентированную на установление соединения (connectionless) и ненадежную службу доставки дейтаграмм (RFC 791 [94]). Уровень IP делает все возможное для доставки IP-дейтаграммы определенному адресату, но не гарантирует, что дейтаграмма будет доставлена, прибудет в нужном порядке относительно других пакетов, а также будет доставлена в единственном экземпляре. Если требуется надежная доставка дейтаграммы, она должна быть обеспечена на более высоком уровне. В случае приложений TCP и SCTP надежность обеспечивается транспортным уровнем. Приложению UDP надежность должно обеспечивать само приложение, поскольку уровень UDP также не предоставляет гарантии надежной доставки дейтаграмм, что было показано на примере в разделе 22.5.

Одной из наиболее важных функций уровня IP является маршрутизация (routing). Каждая IP-дейтаграмма содержит адрес отправителя и адрес получателя. На рис. А.1 показан формат заголовка Ipv4.

Рис. А.1. Формат заголовка IPv4

■ Значение 4-разрядного поля версия (version) равно 4. Это версия протокола IP, используемая с начала 80-х.

■ В поле длина заголовка (header length) указывается полная длина IP-заголовка, включающая любые параметры, описанные 32-разрядными словами. Максимальное значение этого 4-разрядного поля равно 15, и это значение задает максимальную длину IP-заголовка 60 байт. Таким образом, если заголовок занимает фиксированные 20 байт, то 40 байт остается на различные параметры.

■ 16-разрядное поле кода дифференцированных сервисов (Differentiated Services Code Point, DSCP) (RFC 2474 [82]) и 2-разрядное поле явного уведомления о загруженности сети (Explicit Congestion Notification, ECN) (RFC 3168 [100]) заменили 8-разрядное поле тип службы (сервиса) (type-of-service, TOS), которое описывалось в RFC 1349 [5]. Все 8 разрядов этого поля можно установить с помощью параметра сокета IP_TOS (см. раздел 7.6), хотя ядро может перезаписать любое установленное нами значение при проведении политики Diffserv или реализации ECN.

■ Поле общая длина (total length) имеет размер 16 бит и задает полную длину IP- дейтаграммы в байтах, включая заголовок IPv4. Количество данных в дейтаграмме равно значению этого поля минус длина заголовка, умноженная на 4. Данное поле необходимо, поскольку некоторые каналы передачи данных заполняют кадр до некоторой минимальной длины (например, Ethernet) и возможна ситуация, когда размер действительной IP-дейтаграммы окажется меньше требуемого минимума.

■ 16-разрядное поле идентификации (identification) является уникальным для каждой IP-дейтаграммы и используется при фрагментации и последующей сборке в единое целое (см. раздел 2.11). Значение должно быть уникальным для каждого сочетания отправителя, получателя и протокола в течение того времени, пока дейтаграмма может находиться в пути. Если пакет ни при каких условиях не может подвергнуться фрагментации (например, установлен бит DF), нет необходимости устанавливать значение этого поля.

■ Бит DF (флаг запрета фрагментации), бит MF (указывающий, что есть еще фрагменты для обработки) и 13-разрядное поле смещения фрагмента (fragment offset) также используются при фрагментации и последующей сборке в единое целое. Бит DF полезен при обнаружении транспортной MTU (раздел 2.11).

■ 8-разрядное поле времени жизни (time-to-live, TTL) устанавливается отправителем и уменьшается на единицу каждым последующим маршрутизатором, через который проходит дейтаграмма. Дейтаграмма отбрасывается маршрутизатором, который уменьшает данное поле до нуля. При этом время жизни любой дейтаграммы ограничивается 255 пересылками. Обычно по умолчанию данное поле имеет значение 64, но можно сделать соответствующий запрос и изменить его с помощью параметров сокета IP_TTL и IP_MULTICAST_TTL (см. раздел 7.6).

■ 8-разрядное поле протокола (protocol) определяет тип данных, содержащихся в IP-дейтаграмме. Характерные значения этого поля — 1 (ICMPv4), 2 (IGMPv4), 6 (TCP) и 17 (UDP). Эти значения определены в реестре IANA «Номера протоколов».

■ 16-разрядная контрольная сумма заголовка (header checksum) вычисляется для IP-заголовка (включая параметры). В качестве алгоритма вычисления используется стандартный алгоритм контрольных сумм для Интернета — простое суммирование 16-разрядных обратных кодов, как показано в листинге 28.11.

■ Два поля — IPv4-адрес отправителя (source IPv4 address) и IPv4-адрес получателя (destination IPv4 address) — занимают по 32 бита.

■ Поле параметров (options) описывается в разделе 27.2, а пример IPv4-параметра маршрута от отправителя приведен в разделе 27.3.

 

А.3. Заголовок IPv6

На рис. А.2 показан формат заголовка IPv6 (RFC 2460 [27]).

Рис. А.2. Формат заголовка IPv6

■ Значение 4-разрядного поля номера версии (version) равно 6. Данное поле занимает первые 4 бита первого байта заголовка (так же как и в версии IPv4, см. рис. А.1), поэтому если получающий стек IP поддерживает обе версии, он имеет возможность определить, какая из версий используется.

Когда в начале 90-х развивался протокол IPv6 и еще не был принят номер версии 6, протокол назывался IPng (IP next generation — IP нового поколения). До сих пор можно встретить ссылки на IPng.

■ 6-разрядное поле кода дифференцированных сервисов (Differentiated Services Code Point, DSCP) (RFC 2474 [82]) и 2-разрядное поле явного уведомления о загруженности сети (Explicit Congestion Notification, ECN) (RFC 3168 [100]) заменили 8-разрядное поле класса трафика, которое описывалось RFC 2460. Все 8 бит этого поля можно установить при помощи параметра сокета IPV6_TCLASS (раздел 22.8), но ядро может перезаписать установленное нами значение, выполняя политику Diffserv или реализуя ECN.

■ Поле метки потока (flow label) занимает 20 разрядов и может заполняться приложением для данного сокета. Поток представляет собой последовательность пакетов от конкретного отправителя определенному получателю, для которых отправитель потребовал специальную обработку промежуточными маршрутизаторами. Если для данного потока отправитель назначил метку, она уже не изменяется. Метка потока, равная нулю (по умолчанию), обозначает пакеты, не принадлежащие потоку. Метка потока не меняется при передаче по сети. Подробное описание использования меток потока приводится в [99]. Интерфейс метки потока еще не определен до конца. Поле sin6_flowinfo структуры адреса сокета sockaddr_in6 (см. листинг 3.3) зарезервировано для будущего использования. Некоторые системы копируют младшие 28 разрядов sin6_flowinfo непосредственно в заголовок пакета IPv6, перезаписывая поля DSCP и ECN.

■ Поле длины данных (payload length) занимает 16 бит и содержит длину данных в байтах, которые следуют за 40 байтами IPv6-заголовка. Нулевое значение этого поля указывает, что длина требует больше 16 бит и содержится в параметре размера увеличенного поля данных (jumbo payload length option) (см. рис. 27.5). Данные с увеличенной таким образом длиной называются джумбограммой (jumbogram).

■ Следующее поле содержит 8 бит и называется полем следующего заголовка (next header). Оно аналогично полю протокола (protocol) IPv4. Действительно, когда верхний уровень в основном не меняется, используются те же значения, например, 6 для TCP и 17 для UDP. Но при переходе от ICMPv4 к ICMPv6 возникло так много изменений, что для последнего было принято новое значение 58. Дейтаграмма IPv6 может иметь множество заголовков, следующих за 40-байтовым заголовком IPv6. Поэтому поле и называется «полем следующего заголовка», а не полем протокола.

■ Поле ограничения пересылок или предельного количества транзитных узлов (hop limit) аналогично полю TTL IPv4. Значение этого поля уменьшается на единицу каждым маршрутизатором, через который проходит дейтаграмма, и дейтаграмма отбрасывается тем маршрутизатором, который уменьшает данное поле до нуля. Значение этого поля можно установить и получить с помощью параметров сокета IPV6_UNICAST_HOPS и IPV6_MULTICAST_HOPS (см. раздел 7.8 и 21.6). Параметр сокета IPV6_HOPLIMIT также позволяет установить это поле, а параметр IPV6_RECVHOPLIMIT — узнать его значение для полученной дейтаграммы.

ПРИМЕЧАНИЕ

В ранних спецификациях IPv4 говорилось, что маршрутизаторы должны уменьшать значение TTL либо на единицу, либо на количество секунд, в течение которых дейтаграмма находилась на маршрутизаторе, если это количество превышает единицу. Поэтому поле и называлось «время жизни». Однако на практике TTL всегда уменьшалось на единицу. IPv6 разрешает уменьшать поле количества транзитных узлов только на единицу, поэтому и название поля было изменено.

■ Два следующих поля IPv6-адрес отправителя (source IPv6 address) и IPv6-адрес получателя (destination IPv6 address) занимают по 128 бит.

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

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

■ Два адреса IPv6 выровнены по 64-разрядной границе, если заголовок также является 64-разрядным. Такой подход может увеличить скорость обработки на 64-разрядных архитектурах. Адреса IPv4 имеют 32-разрядное выравнивание в заголовке IPv4, который в целом выровнен по 64 разрядам.

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

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

Если это ваше первое знакомство с IPv6, также следует отметить главные отличия IPv6 от IPv4:

■ В IPv6 отсутствует многоадресная передача (см. главу 20). Групповая адресация (см. главу 21), не являющаяся обязательной для IPv4, требуется для IPv6.

■ В IPv6 маршрутизаторы не фрагментируют перенаправляемые пакеты. Если пакет слишком велик, маршрутизатор сбрасывает его и отправляет сообщение об ошибке ICMPv6 (раздел А.6). Фрагментация при использовании IPv6 осуществляется только узлом отправителя.

■ IPv6 требует поддержки обнаружения транспортной MTU (раздел 2.11). Технически эта поддержка не является обязательной и может не включаться в реализации, обладающие минимальной функциональностью, такие как сетевые загрузчики, но если узел не обнаруживает транспортную MTU, он не должен отсылать дейтаграммы, размер которых превышает минимальную канальную MTU IPv6 (1280 байт). В разделе 22.9 описываются параметры сокетов, управляющие поведением механизма обнаружения транспортной MTU.

■ IPv6 требует поддержки параметра аутентификации (подтверждения прав доступа) и параметра обеспечения безопасности. Эти параметры добавляются после основного заголовка.

 

А.4. Адресация IPv4

 

Адреса IPv4 состоят из 32 разрядов и обычно записываются в виде последовательности из четырех чисел в десятичной форме, разделенных точками. Такая запись называется точечно-десятичной. Первое из четырех чисел определяет тип адреса (табл. А.1). Исторически IP-адреса делились на пять классов. Три класса направленных адресов эквивалентны друг другу с функциональной точки зрения, поэтому мы показываем их как один диапазон.

Таблица А.1. Диапазоны и классы IP-адресов

Назначение Класс Диапазон
Направленная передача А, В, С 0.0.0.0–223.255.255.255
Многоадресная передача D 224.0.0.0–239.255.255.255
Экспериментальные Е 240.0.0.0–255.255.255.255

Под сетевым адресом IPv4 подразумевается 32-разрядный адрес и соответствующая ему 32-разрядная маска подсети. Биты маски, равные 1, указывают адрес сети, а нулевые биты — адрес узла. Поскольку биты со значением 1 всегда занимают места в маске непрерывно начиная с крайнего левого бита, а нулевые биты — начиная с крайнего правого бита, то маску адреса можно определить как префиксную длину (prefix length), указывающую на количество заполненных единицами битов начиная с крайнего левого бита. Например, маска 255.255.255.0 соответствует префиксной длине 24. Такая адресация называется бесклассовой (classless), потому что маска указывается явно, а не задается классом адреса. Пример вы можете увидеть на рис. 1.7.

ПРИМЕЧАНИЕ

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

Использование бесклассовых адресов подразумевает бесклассовую маршрутизацию, которую обычно называют бесклассовой междоменной маршрутизацией (classless interdomain routing — CIDR) (RFC 1519 [31]). Бесклассовая междоменная маршрутизация позволяет сократить размер таблиц маршрутизации опорной сети Интернета и снизить скорость расходования адресов IPv4. Все маршруты CIDR характеризуются маской или длиной префикса. Маска больше не может быть определена по классу адреса. Более подробно CIDR описывается в разделе 10.8 книги [111].

 

Адреса подсетей

Обычно IPv4-адреса разделяются на подсети (RFC 950 [79]). Такой подход добавляет еще один уровень иерархии адресов:

■ идентификатор сети (присваивается предприятию);

■ идентификатор подсети (выбирается предприятием);

■ идентификатор узла (выбирается предприятием).

Граница между идентификатором сети и идентификатором подсети фиксирована префиксной длиной присвоенного адреса сети. Эта префиксная длина присваивается организациям их интернет-провайдером. Граница же между идентификатором подсети и идентификатором узла выбирается предприятием. Все узлы данной подсети имеют одинаковую маску подсети, которая и определяет границу между идентификатором подсети и идентификатором узла. Биты, заполненные единицами в маске подсети, соответствуют идентификатору подсети, а биты, заполненные нулями — идентификатору узла.

В качестве примера рассмотрим предприятие, которому был выделен адрес 192.168.42.0/24. Если это предприятие будет использовать 3-разрядный идентификатор подсети, на идентификатор узла останется 5 разрядов (рис. А.3.)

Рис. А.3. 24-разрядный адрес сети с 3-разрядным адресом подсети и 5-разрядным адресом узла

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

Таблица А.2. Список подсетей для 3-разрядного адреса подсети и 5-разрядного адреса узла

Подсеть Префикс
0 192.168.42.0/27+
1 192.168.42.32/27+
2 192.168.42.64/27
3 192.168.42.96/27
4 192.168.42.128/27
5 192.168.42.160/27
6 192.168.42.192/27
7 192.168.42.224/27+

В результате мы получаем 6–8 подсетей (идентификаторы 1–6 или 0–7), в каждой из которых может находиться до 30 узлов (идентификаторы 1–30). RFC 950 не рекомендует использовать подсети, идентификаторы которых состоят из одних нулей и одних единиц (знак «+» в табл. А.2). В настоящее время большинство систем поддерживают и такие адреса подсетей. Максимальный идентификатор узла (в нашем случае 31) зарезервирован за широковещательным адресом. Идентификатор 0 используется для адресации сети в целом и зарезервирован во избежание конфликтов со старыми системами, в которых нулевой адрес узла использовался в качестве адреса широковещательной передачи. В полностью контролируемых сетях, где такие системы отсутствуют, идентификатор 0 использовать можно. Вообще говоря, сетевые приложения не должны заботиться об идентификаторах подсетей и узлов, рассматривая IP-адреса как непрозрачные объекты.

 

Адрес закольцовки

По соглашению адрес 127.0.0.1 присвоен интерфейсу закольцовки на себя (loopback interface). Все, что посылается на этот IP-адрес, получается самим узлом. Обычно этот адрес используется при тестировании клиента и сервера на одном узле. Этот адрес известен под именем INADDR_LOOPBACK.

ПРИМЕЧАНИЕ

Любой адрес из подсети 127/8 можно присвоить интерфейсу закольцовки, но обычно используется именно 127.0.0.1.

 

Неопределенный адрес

Адрес, состоящий из 32 нулевых битов, является в IPv4 неопределенным (unspecified) адресом. В пакете IPv4 он может появиться только как адрес получателя в тех пакетах, которые посланы узлом, находящимся в состоянии загрузки, когда узел еще не знает своего IP-адреса. В API сокетов этот адрес называется универсальным адресом (wildcard address) и обычно обозначается INADDR_ANY. Указание этого адреса при вызове bind для прослушиваемого сокета TCP говорит о том, что сокет будет принимать входящие соединения на любой адрес данного узла.

 

Частные адреса

RFC 1918 [101] выделяет три диапазона адресов для «частных интрасетей», то есть сетей, не имеющих прямого подключения к Интернету. Эти диапазоны представлены в табл. А.3.

Таблица А.3. Диапазоны частных IP-адресов

Количество адресов Префикс Диапазон
16777216 10/8 10.0.0.0–10.255.255.255
1 048 576 172.16/12 172.16.0.0–172.31.255.255
65 536 192.168/16 192.168.0.0–192.168.255.255

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

 

Многоинтерфейсность и псевдонимы адресов

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

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

Термин «многоинтерфейсность» является более общим и охватывает два различных сценария (раздел 3.3.4 RFC 1122 [10]).

1. Узел с несколькими интерфейсами является многоинтерфейсным, при этом каждый интерфейс должен иметь свой IP-адрес. Это традиционное определение.

2. Современные узлы имеют возможность присваивать одному физическому интерфейсу несколько IP-адресов. Каждый IP-адрес, созданный в дополнение к первичному, или основному (primary), называется альтернативным именем, псевдонимом (alias) или логическим интерфейсом. Часто альтернативные IP-адреса используют ту же маску подсети, что и основной адрес, но имеют другие идентификаторы узла. Но допустима также ситуация, когда псевдонимы имеют адрес сети или подсети, совершенно отличный от первичного адреса. В разделе 17.6 приведен пример альтернативных адресов.

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

ПРИМЕЧАНИЕ

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

ПРИМЕЧАНИЕ

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

 

А.5. Адресация IPv6

 

Адреса IPv6 содержат 128 бит и обычно записываются как восемь 16-разрядных шестнадцатеричных чисел. Старшие биты 128-разрядного адреса обозначают тип адреса (RFC 3513 [44]). В табл. А.4 приведены различные значения старших битов и соответствующие им типы адресов.

Таблица А.4. Значение старших битов адреса IPv6

Значение Размер идентификатора Префикс формата Документ
Не определен нет 0000 0000 … 0000 0000 (128 разрядов) RFC 3513
Закольцовка нет 0000 0000 … 0000 0001 (128 разрядов) RFC 3513
Глобальный адрес направленной передачи произвольный 000 RFC 3513
Глобальный адрес NSAP произвольный 0000001 RFC 1888
Объединяемый глобальный адрес направленной передачи 64 разряда 001 RFC 3587
Глобальный адрес направленной передачи 64 разряда все остальное RFC 3513
Локальный в пределах канала адрес направленной передачи 64 разряда 1111 111010 RFC 3513
Локальный в пределах сайта адрес направленной передачи 64 разряда 1111 111011 RFC 3513
Групповой адрес нет 1111 1111 RFC 3513

Эти старшие биты называются форматным префиксом. Например, если 3 старших бита — 001, адрес называется объединяемым глобальным индивидуальным адресом (aggregatable global unicast address). Если 8 старших битов — 11111111 (0xff), это групповой адрес.

 

Объединяемые глобальные индивидуальные адреса

Архитектура IPv6 корректировалась в процессе своего развития исходя из результатов внедрения новой версии протокола и из статистики применения старой версии. Согласно изначальному определению объединяемых глобальных индивидуальных адресов, они начинались с префикса 001 и имели фиксированную структуру, встроенную в сам адрес. Эта структура, однако, была отменена RFC 3587 [45]. Адреса, начинающиеся с префикса 001, будут и впредь выделяться в первую очередь, однако никаких отличий между ними и другими глобальными адресами больше не будет. Эти адреса будут использоваться в тех областях, где сейчас используются направленные адреса IPv4.

Формат объединяемых индивидуальных адресов определяется в RFC 3513 [44] и RFC 3587 [45] и содержит следующие поля, слева направо:

■ глобальный префикс маршрутизации (n разрядов);

■ идентификатор подсети (64 - n разрядов);

■ идентификатор интерфейса (64 разряда).

На рис. А.4 приведен пример объединяемого глобального индивидуального адреса.

Рис. А.4. Объединяемый глобальный индивидуальный адрес IPv6

Идентификатор интерфейса должен быть построен в модифицированном формате EUI-64 (Extended User Interface — расширенный интерфейс пользователя) [51]. Это расширение множества 48-разрядных адресов IEEE 802 MAC (Media Access Control — уровень управления доступом к среде передачи), которые присвоены большинству карт сетевых интерфейсов локальной сети. Этот идентификатор должен автоматически присваиваться интерфейсу и по возможности основываться на MAC-адресе карты. Более подробное описание построения идентификаторов интерфейса, основанных на EUI-64, описывается в приложении А RFC 3513 [44].

Поскольку модифицированный адрес EUI-64 может быть глобально уникальным идентификатором интерфейса, а сам интерфейс может однозначно идентифицировать пользователя, модифицированный формат EUI-64 создает определенные проблемы, связанные с конфиденциальностью. Может оказаться возможным отслеживать действия и перемещение конкретного пользователя, например путешествующего с портативным компьютером, просто по его IPv6-адресу. RFC 3041 [80] описывает расширения протокола, предназначенные для генерации идентификаторов интерфейса, меняющихся по несколько раз в день и, таким образом, устраняющие описанную проблему.

 

Тестовые адреса 6bone

6bone — это виртуальная сеть, используемая для тестирования протоколов IPv6 (см. раздел Б.3). Объединяемые глобальные индивидуальные адреса уже назначаются, но сайты, не имеющие права на адресное пространство согласно региональной политике назначения адресов, могут использовать адреса специального формата в сети 6bone RFC 2471 [46] (рис. А.5).

Рис. А.5. Тестовые адреса IPv6 для сети 6bone

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

Старшие три байта имеют значение 0x3ffe. Идентификатор сайта 6bone назначается председателем руководства 6bone. Назначение проводится в том же порядке, в котором оно будет проводиться для реальных адресов IPv6. Активность 6bone постепенно сворачивается по мере того, как начинается внедрение IPv6 (в 2002 году было выделено больше реальных адресов IPv6, чем во всей сети 6bone за 8 лет). Идентификаторы подсети и интерфейса используются, как и раньше, для обозначения подсети и узла.

В разделе 11.2 был показан IPv6-адрес 3ffe:b80:1f8d:1:a00:20ff:fea7:686b для узла freebsd (см. рис. 1.7). Идентификатор 6bone имеет значение 0x0b801f8d, а идентификатор подсети 0x1. Младшие 64 разряда представляют собой модифицированный адрес EUI-64, полученный из MAC-адреса Ethernet-карты узла.

 

Адреса IPv4, преобразованные к виду IPv6

Адреса IPv4, преобразованные к виду IPv6 (IPv4-mapped IPv6 addresses), позволяют приложениям, запущенным на узлах, поддерживающих как IPv4, так и IPv6, связываться с узлами, поддерживающими только IPv4, в процессе перехода сети Интернет на версию протокола IPv6. Такие адреса автоматически создаются на серверах DNS (см. табл. 11.3), когда приложением IPv6 запрашивается IPv6-адрес узла, который имеет только адреса IPv4.

Рисунок 12.3 показывает, что использование данного типа адресов с сокетом IPv6 приводит к отправке IPv4-дейтаграммы узлу. Такие адреса не хранятся ни в каких файлах данных DNS — при необходимости они создаются сервером.

Рис. А.6. Адреса IPv4, преобразованные к виду IPv6

На рис. А.6 приведен формат таких адресов. Младшие 32 бита содержат адрес IPv4.

При записи IPv6-адреса последовательная строка из нулей может быть сокращена до двух двоеточий. Вложенный IPv4-адрес представлен в точечно-десятичной записи. Например, преобразованный к виду IPv6 IPv4-адрес 0:0:0:0:0:FFFF:206.62.226.33 можно сократить до ::FFFF:206.62.226.33.

 

Адреса IPv6, совместимые с IPv4

Для перехода от версии IPv4 к IPv6 планировалось также использовать адреса IPv6, совместимые с IPv4 (IPv4-compatible IPv6 addresses). Администратор узла, поддерживающего как IPv4, так и IPv6, и не имеющего соседнего IPv6-маршрутизатора, должен создать DNS запись типа AAAA, содержащую адрес IPv6, совместимый с IPv4. Любой другой IPv6-узел, посылающий IPv6-дейтаграмму на адрес IPv6, совместимый с IPv4, должен упаковать (encapsulate) IPv6-дейтаграмму в заголовок IPv4 — такой способ называется автоматическим туннелированием (automatic tunnel). Однако после рассмотрения вопросов, связанных с внедрением IPv6, использование этой возможности заметно сократилось. Более подробно вопросы туннелирования будут рассмотрены в разделе Б.3, а на рис. Б.2 будет приведен пример IPv6-дейтаграмм такого типа, упакованных в заголовок IPv4.

На рис. А.7 показан формат адреса IPv4, совместимого с IPv6.

Рис. А.7. Адрес IPv6, совместимый с IPv4

В качестве примера такого адреса можно привести ::206.62.226.33.

Адреса IPv6, совместимые с IPv4 могут появляться и в пакетах IPv6, не передающихся по туннелю, если используется механизм перехода SIIT IPv4/IPv6 (RFC 2765 [83]).

 

Адрес закольцовки

Адрес IPv6 ::1, состоящий из 127 нулевых битов и единственного единичного бита, является адресом закольцовки IPv6. В API сокетов он называется in6addr_loopback или IN6ADDR_LOOPBACK_INIТ.

 

Неопределенный адрес

Адрес IPv6, состоящий из 128 нулевых битов, записываемый как 0::0 или просто ::, является неопределенным адресом IPv6 (unspecified address). В пакете IPv6 он может появиться только как адрес получателя в пакетах, посланных узлом, который находится в состоянии загрузки и еще не знает своего IPv6-адреса.

В API сокетов этот адрес называется универсальным адресом, и его использование, например, в функции bind для связывания прослушиваемого сокета TCP означает, что сокет будет принимать клиентские соединения, предназначенные любому из адресов узла. Этот адрес имеет имя in6addr_any или IN6ADDR_ANY_INIT.

 

Адрес локальной связи

Адрес локальной связи (link-local, локальный в пределах физической подсети) используется для соединения в пределах одной физической подсети, когда известно, что дейтаграмма не будет перенаправляться. Примерами использования таких адресов являются автоматическая конфигурация адреса во время загрузки и поиска соседних узлов (neighbor discovery) (подобно ARP для IPv4). На рис. А.8 приведен формат такого адреса.

Рис. А.8. IPv6-адрес локальной связи

Такие адреса всегда начинаются с fe80. Маршрутизатор IPv6 не должен перенаправлять дейтаграммы, у которых в поле отправителя или получателя указан адрес локальной связи, по другому соединению. В разделе 11.2 приведен адрес локальной связи, связанный с именем aiх-611.

 

Адрес, локальный на уровне сайта

На момент написания этой книги рабочей группой IETF по IPv6 было принято решение отменить локальные в пределах сайта адреса в их текущей форме. В тех адресах, которые придут им на замену, может использоваться тот же диапазон, который был отведен для локальных на уровне сайта адресов изначально (fec0/10).

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

Рис. А.9. IPv6-адрес, локальный в пределах сайта

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

 

А.6. ICMPv4 и ICMPv6: протоколы управляющих сообщений в сети Интернет

Протокол ICMP (Internet Control Message Protocol) является необходимой и неотъемлемой частью любой реализации IPv4 или IPv6. Протокол ICMP обычно используется для обмена сообщениями об ошибках между узлами, как маршрутизирующими, так и обычными, но иногда этот протокол используется и приложениями. Например, приложения ping и traceroute (см. главу 28) используют протокол ICMP.

Первые 32 бита сообщений совпадают для ICMPv4 и ICMPv6 и приведены на рис. А.10. ICMPv4 документируется в RFC 792 [95], а ICMPv6 — в RFC 2463 [21].

Рис. А.10. Формат сообщений ICMPv4 и ICMPv6

Восьмиразрядное поле тип (type) указывает тип сообщения ICMPv4 или ICMPv6, а некоторые типы имеют дополнительную 8-разрядную информацию, указанную в поле кода (code). Поле контрольной суммы (checksum) является стандартной контрольной суммой, используемой в сети Интернет. Отличия между ICMPv4 и ICMPv6 заключаются в том, какие именно поля используются при подсчете контрольной суммы.

С точки зрения сетевого программирования необходимо понимать, какие сообщения ICMP могут быть возвращены приложению, что именно вызывает ошибку и каким образом эта ошибка возвращается приложению. В табл. А.5 приведены все сообщения ICMPv4 и показано, как они обрабатываются операционной системой 4.4BSD. В последнем столбце приведены значения переменной errno — то есть те ошибки, которые возвращаются приложениям. В табл. А.6 приведен список сообщений ICMPv6. При использовании TCP ошибка не возвращается приложению немедленно. Если TCP разрывает соединение по тайм-ауту, все накопленные ошибки возвращаются приложению. При использовании UDP ошибка возвращается при очередной операции чтения или записи, но только на присоединенном сокете (раздел 8.9).

Таблица А.5. Обработка различных типов ICMP-сообщений в 4.4BSD

Тип Код Описание Обработчик или errno
0 0 Echo-reply (Эхо-ответ) Пользовательский процесс (Ping)
3 Destination unreachable (Получатель недоступен)
0 Network unreachable (Сеть недоступна) EHOSTUNREACH
1 Host unreachable (Узел недоступен) EHOSTUNREACH
2 Protocol unreachable (Протокол недоступен) ECONNREFUSED
3 Port unreachable (Порт недоступен) ECONNREFUSED
4 Fragmentation needed but DF bit set (Необходима фрагментация, но установлен бит DF) EMSGSIZE
5 Source route failed (Сбой маршрута отправителя) EHOSTUNREACH
6 Destination network unknown (Неизвестна сеть получателя) EHOSTUNREACH
7 Destination host unknown (Неизвестен узел получателя) EHOSTUNREACH
8 Source host isolated (Узел отправителя изолирован). Устаревший тип сообщений EHOSTUNREACH
9 Destination network administratively prohibited (Сеть получателя запрещена администратором) EHOSTUNREACH
10 Destination host administratively prohibited (Узел получателя запрещен администратором) EHOSTUNREACH
11 Network unreachable for TOS (Сеть недоступна для TOS) EHOSTUNREACH
12 Host unreachable for TOS (Узел недоступен для TOS) EHOSTUNREACH
13 Communication administratively prohibited (Связь запрещена администратором) (Игнорируется)
14 Host precedence violation (Нарушение порядка старшинства узлов) (Игнорируется)
15 Precedence cutoff in effect (Действует старшинство узлов) (Игнорируется)
4 0 Source quench (Отключение отправителя) Обрабатывается ядром в случае TCP, игнорируется в случае UDP
5 Redirect (Перенаправление)
0 Redirect for network (Перенаправление для сети) Ядро обновляет таблицу маршрутизации
1 Redirect for host (Перенаправление для узла) Ядро обновляет таблицу маршрутизации
2 Redirect for type-of-service and network (Перенаправление для типа сервиса и сети) Ядро обновляет таблицу маршрутизации
3 Redirect for type of service and host (Перенаправление для типа сервиса и узла) Ядро обновляет таблицу маршрутизации
8 0 Echo request (Эхо-запрос) Ядро генерирует ответ
9 0 Router advertisement (Извещение маршрутизатора) Пользовательский процесс
10 0 Router solicitation (Запрос маршрутизатору) Пользовательский процесс
11 Time exceeded (Превышено время передачи)
0 TTL equals 0 during transit (Время жизни равно 0 во время передачи) Пользовательский процесс
1 TTL equals 0 during reassembly (Время жизни равно 0 во время сборки) Пользовательский процесс
12 Parameter problem (Проблема с параметром)
0 IP header bad (Неправильный IP-заголовок). Типичная ошибка ENOPROTOOPT
1 Required option missed (Пропущен необходимый параметр) ENOPROTOOPT
13 0 Timestamp request (Запрос отметки времени) Ядро генерирует ответ
14 0 Timestamp reply (Ответ об отметке времени) Пользовательский процесс
15 0 Information request (Информационный запрос). Устаревший тип сообщений (игнорируется)
16 0 Information reply (Информационный ответ). Устаревший тип сообщений Пользовательский процесс
17 0 Address mask request (Запрос маски адреса) Ядро генерирует ответ
18 0 Address mask reply (Ответ маски адреса) Пользовательский процесс

Таблица А.6. Сообщения ICMPv6

Тип Код Описание Обработчик или errno
1 Administratively prohibited, firewall filter (Запрещено администратором, фильтр брандмауэра) EHOSTUNREACH
2 Not a neighbor, incorrect strict source route (He сосед, некорректный маршрут отправителя) EHOSTUNREACH
3 Address unreachable (Адрес недоступен) EHOSTDOWN
4 Port unreachable (Порт недоступен) ECONNREFUSED
2 0 Packet too big (Слишком большой пакет) Ядро выполняет обнаружение транспортной MTU
3 Time exceeded (Превышено время передачи)
0 Hop limit exceeded in transit (При передаче превышено значение предельного количества транзитных узлов) Пользовательский процесс
1 Fragment reassembly time exceeded (Истекло время сборки из фрагментов) Пользовательский процесс
4 Parameter problem (Проблема с параметром)
0 Erroneous header filed (Ошибочное поле заголовка) ENOPROTOOPT
1 Unrecognized next header (Следующий заголовок нераспознаваем) ENOPROTOOPT
2 Unrecognized option (Неизвестный параметр) ENOPROTOOPT
128 0 Echo request (Эхо-запрос (Ping)) Ядро генерирует ответ
129 0 Echo reply (Эхо-ответ (Ping)) Пользовательский процесс (Ping)
130 0 Group membership query (Запрос о членстве в группе) Пользовательский процесс
131 0 Group membership report (Отчет о членстве в группе) Пользовательский процесс
132 0 Group membership reduction (Сокращение членства в группе) Пользовательский процесс
133 0 Router solicitation (Запрос маршрутизатору) Пользовательский процесс
134 0 Router advertisement (Извещение маршрутизатора) Пользовательский процесс
135 0 Neighbor solicitation (Запрос соседу) Пользовательский процесс
136 0 Neighbor advertisement (Извещение соседа) Пользовательский процесс
137 0 Redirect (Перенаправление) Ядро обновляет таблицу маршрутизации

Запись «пользовательский процесс» в этой таблице означает, что ядро не обрабатывает сообщение и ждет обработки данного сообщения от пользовательского процесса с символьным сокетом. Также следует отметить, что различные реализации могут обрабатывать одни и те же сообщения по-разному. Например, в Unix сообщения типа Router solicitation (Запрос маршрутизатору) и Router advertisement (Извещение маршрутизатора) обычно обрабатываются как пользовательские процессы, но некоторые реализации могут обрабатывать эти сообщения в ядре.

Версия ICMPv6 сбрасывает старший бит поля тип для сообщения об ошибке (типы 1-4) и устанавливает этот бит для информационного сообщения (типы 128–137).

 

Приложение Б

Виртуальные сети

 

Б.1. Введение

Поддержка новых возможностей протокола TCP, например каналов с повышенной пропускной способностью (RFC 1323), требуется только на узле, использующем TCP, тогда как маршрутизаторы в модернизации не нуждаются. Эти изменения, описанные в RFC 1323, постепенно проявляются в реализациях TCP на узлах. Когда устанавливается новое TCP-соединение, каждая сторона может определить, поддерживает ли другая сторона новую возможность, и если для обоих узлов это так, ею можно воспользоваться.

Иная ситуация с изменениями IP-уровня, такими как многоадресная передача, появившаяся в конце 80-х, или новая версия протокола IPv6, возникшая в середине 90-х, поскольку они требуют изменений на всех узлах и на всех маршрутизаторах. Но люди хотят начать использовать новые возможности, не дожидаясь, когда все системы будут модернизированы. Для этого существующий протокол IPv4 был дополнен так называемыми виртуальными сетями (virtual network), использующими туннели (tunnels).

 

Б.2. MBone

Наш первый пример виртуальной сети, построенной с использованием туннелей, — это сеть MBone, которая начала использоваться примерно с 1992 года [29]. Если два или более узла в локальной сети поддерживают многоадресную передачу, то на всех этих узлах могут быть запущены приложения многоадресной передачи, которые могут общаться друг с другом. Для соединения одной локальной сети с другой локальной сетью, также содержащей узлы с возможностью многоадресной передачи, между двумя узлами из этих сетей конфигурируется туннель, как показано на рис. Б.1. На этом рисунке отмечены следующие шаги:

Рис. Б.1. Упаковка IPv4 в IPv4, применяемая в MBone

1. Приложение на узле отправителя MH1 посылает групповую дейтаграмму адресам класса D.

2. На рисунке эта дейтаграмма показана как UDP-дейтаграмма, поскольку большинство приложений многоадресной передачи используют протокол UDP. Более подробно о многоадресной передаче и о том, как посылать и получать многоадресные дейтаграммы, рассказано в главе 21.

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

4. MR2 добавляет перед дейтаграммой другой IPv4-заголовок, в котором в поле адреса получателя записан индивидуальный адрес конечного узла туннеля (tunnel endpoint) MR. Этот индивидуальный адрес конфигурируется администратором узла MR2 и считывается программой mrouted при ее запуске. Аналогичным образом индивидуальный адрес узла MR2 сконфигурирован на узле MR — на другом конце туннеля. В поле протокола нового IPv4-заголовка установлено значение 4, соответствующее упаковке IPv4 в IPv4. Дейтаграмма посылается следующему маршрутизатору, UR3, который явно указан как маршрутизатор направленной передачи, то есть не поддерживает многоадресную передачу, и поэтому приходится использовать туннель. Выделенная на рисунке серым цветом часть IPv4-дейтаграммы не изменяется по сравнению шагом 1, только значение поля TTL в выделенном цветом IPv4-заголовке уменьшается на 1.

5. UR3 узнает адрес получателя из самого внешнего IPv4-заголовка и перенаправляет дейтаграмму следующему маршрутизатору направленной передачи — UR4.

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

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

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

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

В данном примере показана функция маршрутизации многоадресной передачи, осуществляемая программой mrouted, запущенной на одном из узлов в каждой из локальных сетей. Таким образом начинала свою работу сеть MBone. Но, начиная примерно с 1996 г. большинство основных поставщиков маршрутизаторов стали включать функцию групповой маршрутизации в свои маршрутизаторы. Если бы два маршрутизатора направленной передачи UR3 и UR4 на рис. Б.1 имели возможность маршрутизации многоадресной передачи, то нам не пришлось бы запускать mrouted, а маршрутизаторы UR3 и UR4 работали бы как маршрутизаторы многоадресной передачи. Но если между UR3 и UR4 существуют другие маршрутизаторы, не поддерживающие многоадресную передачу, туннель все же необходим. Однако конечными пунктами туннеля в этом случае могут стать MR3 (новое имя для UR3, поддерживающего многоадресную передачу) и MR4 (новое имя для UR4, поддерживающего многоадресную передачу), а не MR2 и MR.

ПРИМЕЧАНИЕ

В сценарии, приведенном на рис. Б.1, каждый многоадресный пакет появляется дважды в локальной сети, расположенной вверху рисунка, и дважды в локальной сети, расположенной внизу. Один раз это многоадресный пакет, а второй раз — направленный пакет внутри туннеля, так как пакет идет между узлом, на котором запущена программа mrouted, и следующим маршрутизатором направленной передачи (то есть между MR2 и UR3, а затем между UR4 и MR). Лишняя копия — это цена туннелирования. Преимущество замены маршрутизаторов направленной передачи UR3 и UR4 на рис. Б.1 на маршрутизаторы многоадресной передачи (те, что мы назвали MR3 и MR4) заключается в том, что мы избежали появления этой дополнительной копии многоадресного пакета в каждой из сетей. Даже если MR3 и MR4 должны установить туннель между собой, поскольку некоторые промежуточные маршрутизаторы между ними (которые на рисунке не показаны) не поддерживают многоадресную передачу, такой вариант предпочтительнее, так как в этом случае не происходит дублирования пакетов в каждой из локальных сетей.

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

 

Б.3. 6bone

Виртуальная сеть 6bone была создана в 1996 году по тем же причинам, что и MBone: пользователи в группах узлов, поддерживающих версию протокола IPv6, хотели соединить их вместе с помощью виртуальной сети, не дожидаясь поддержки IPv6 всеми промежуточными маршрутизаторами. На момент написания этой книги сеть 6bone выходит из употребления по мере внедрения IPv6; полное прекращение функционирования 6bone ожидается в июне 2006 года [30]. Мы рассказываем о туннелях только потому, что до сих пор можно встретить настроенные туннели. О динамических туннелях мы расскажем в разделе Б.4.

Рис. Б.2. Упаковка IPv6 в IPv4, используемая в сети 6bone

На рис. Б.2 приведен пример двух локальных сетей, поддерживающих IPv6, соединенных с помощью туннеля только через маршрутизаторы IPv4. На рисунке отмечены следующие шаги:

1. Узел HI локальной сети, показанной на рисунке вверху, посылает IP-дейтаграмму, содержащую TCP-сегмент, узлу H4 из локальной сети, показанной внизу. Будем называть эти два узла IPv6-узлами, хотя, вероятно, оба они поддерживают и протокол IPv4. В таблице маршрутизации IPv6 на узле H1 записано, что следующим маршрутизатором является узел H2, и IPv6-дейтаграмма отсылается этому маршрутизатору.

2. На узле HR2 имеется сконфигурированный туннель до узла HR3. Этот туннель позволяет посылать IPv6-дейтаграммы между двумя конечными узлами туннеля через сеть IPv4 путем упаковки IPv6-дейтаграмм в IPv4-дейтаграммы (упаковка IPv6 в IPv4). В поле протокола указано значение 4. Отметим, что оба узла IPv4/IPv6 на концах туннеля — HR2 и HR3 — работают как маршрутизаторы IPv6, поскольку они перенаправляют IPv6-дейтаграммы, получаемые на один интерфейс, через другой интерфейс. Сконфигурированный туннель считается интерфейсом, хотя он является виртуальным, а не физическим интерфейсом.

3. Конечный узел туннеля (HR3) получает упакованную дейтаграмму, отбрасывает IPv4-заголовок и посылает IPv6-дейтаграмму в свою локальную сеть.

4. Дейтаграмма приходит по назначению на узел H4.

 

Б.4. Переход на IPv6: 6to4

Механизм перехода 6to4 (6на4) полностью описан в документе «Соединение доменов IPv6 через облака IPv4» (RFC 3056 [17]). Это метод динамического создания туннелей, подобных изображенному на рис. Б.2. В отличие от предыдущих механизмов динамического создания туннелей, которые требовали наличия у всех узлов адресов IPv4, а также явного задания механизма туннелирования, 6to4 реализует туннелирование исключительно через маршрутизаторы. Это упрощает конфигурацию и позволяет централизованно устанавливать политику безопасности. Кроме того, появляется возможность совмещать функциональность 6to4 с типичной функциональностью трансляции сетевых адресов и межсетевой защиты (например, это может быть сделано на устройстве NAT, расположенном на стороне клиента).

Адреса 6to4 лежат в диапазоне 2002/16. В следующих четырех байтах адреса записывается адрес IPv4 (рис. Б.3). 16-разрядный префикс 2002 и 32-разрядный адрес IPv4 создают общий 48-разрядный идентификатор. Для идентификатора подсети, идущего перед 64-разрядным идентификатором интерфейса, остается 2 байта. Например, нашему узлу freebsd с адресом IPv4 12.106.32.254 соответствует префикс 2002:c6a:20fe/48.

Рис. Б.3. Адреса 6to4

Преимущество 6to4 перед 6bone состоит в том, что туннели, формирующие инфраструктуру, образуются автоматически. Для их создания не требуется предварительное конфигурирование. Сайт, использующий 6to4, настраивает основной маршрутизатор на известный адрес передачи наиболее подходящему узлу (anycast) IPv4 192.88.99.1 (RFC 3068 [48]). Он соответствует адресу IPv6 2002: :с058:6301::. Маршрутизаторы инфраструктуры IPv6, готовые действовать в качестве шлюзов 6to4, объявляют о маршруте к сети 2002/16 и отправляют упакованный трафик на адрес IPv4, скрытый внутри адреса 6to4. Такие маршрутизаторы могут быть локальными, региональными или глобальными в зависимости от областей действия их маршрутов.

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

 

Приложение В

Техника отладки

 

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

 

В.1. Трассировка системных вызовов

 

Многие версии Unix предоставляют возможность трассировки (отслеживания) системных вызовов. Зачастую это может оказаться полезным методом отладки.

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

В этом разделе мы рассмотрим системные вызовы, задействованные в работе клиента времени и даты (см. листинг 1.1).

 

Сокеты ядра BSD

Мы начнем с FreeBSD, операционной системы с Беркли-ядром, в котором все функции сокетов являются системными вызовами. Программа трассировки системных вызовов имеет название ktrace. Она выводит информацию о трассировке в файл (по умолчанию имя этого файла ktrace.out), который можно вывести на экран с помощью kdump. Клиент сокета запускается следующим образом:

freebsd % ktrace daytimetcpcli 192.168.42.2

Tue Aug 19 23:35.10 2003

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

3211 daytimetcpcli CALL socket(0x2,0x1,0)

3211 daytimetcpcli RET socket 3

3211 daytimetcpcli CALL connect(0x3,0x7fdffffe820,0x10)

3211 daytimetcpcli RET connect 0

3211 daytimetcpcli CALL read(0x3,0x7fdffffe830,0x1000)

3211 daytimetcpcli GIO fd 3 read 26 bytes

     "Tue Aug 19 23:35:10 2003

     "

3211 daytimetcpcli RET read 26/0x1a

...

3211 daytimetcpcli CALL write(0x1,0x204000,0x1a)

3211 daytimetcpcli GIO fd 1 wrote 26 bytes

     "Tue Aug 19 23:35:10 2003

     "

3211 daytimetcpcli RET write 26/0x1a

3211 daytimetcpcli CALL read(0x3,0x7fdffffe830,0x1000)

3211 daytimetcpcli GIO fd 3 read 0 bytes

     ""

3211 daytimetcpcli RET read 0

3211 daytimetcpcli CALL exit(0)

Число 3211 является идентификатором процесса. CALL идентифицирует системный вызов, RET обозначает возвращение управления, GIO подразумевает общую операцию ввода-вывода. Мы видим системные вызовы socket и connect, за которыми следуют вызовы read, возвращающие 26 байт. Наш клиент записывает эти байты в стандартный поток вывода, и при следующем вызове read возвращает нулевое значение (конец файла).

 

Сокеты ядра Solaris 9

Операционная система Solaris 2.x основывается на SVR4, и во всех версиях ранее 2.6 сокеты реализуются так, как показано на рис. 31.3. Однако во всех версиях SVR4 с подобными реализациями сокетов существует одна проблема: они редко обеспечивают полную совместимость с сокетами Беркли-ядер. Для обеспечения дополнительной совместимости в Solaris 2.6 способ реализации изменен за счет использования файловой системы sockfs. Такой подход обеспечивает поддержку сокетов ядра, что можно проверить с помощью программы truss на нашем клиенте (использующем сокеты).

solaris % truss -v connect daytimetcpcli 127.0.0.1

Mon Sep 8 12:16:42 2003

После обычного подключения библиотеки осуществляется первый системный вызов so_socket — системный вызов, инициированный нашим вызовом socket.

so_socket(PF_INET, SOCK_STREAM, IPPROTO_IP, 1) = 3

connect(3, 0xFFBFDEF0, 16, 1) = 0

AF_INET name = 127.0.0.1 port = 13

read(3, " M o n S e p 8 1", ... 4096) = 26

Mon Sep 8 12:48:06 2003

write(1, " M o n S e p 8 1", ... 26) = 26

read(3, 0xFFBFDF03, 4096) = 0

_exit(0)

Первые три аргумента системного вызова so_socket являются нашими аргументами socket.

Далее мы видим, что connect является системным вызовом, a truss при вызове с флагом -v connect выводит на экран содержимое структуры адреса сокета, на которую указывает второй аргумент (IP-адрес и номер порта). Мы не показываем системные вызовы, относящиеся к стандартным потокам ввода и вывода.

 

В.2. Стандартные службы Интернета

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

ПРИМЕЧАНИЕ

В настоящее время многие сайты перекрывают доступ к этим службам с помощью брандмауэров, так как некоторые атаки типа «отказ в обслуживании» (DoS), имевшие место в 1996 году, были направлены именно на эти службы (см. упражнение 13.3). Тем не менее можно успешно использовать эти службы внутри локальной сети.

 

В.3. Программа sock

Программа sock, написанная Уильямом Стивенсом, впервые появилась в книге [111], где широко использовалась для генерации специальных условий, большинство которых затем проверялось с помощью программы tcpdump. Удобство этой программы заключается в том, что она генерирует такое множество различных сценариев, что нет необходимости писать специальные тестовые программы.

В этой книге исходный код программы не приведен (более 2000 строк на языке С), но он находится в свободном доступе (см. предисловие).

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

1. Клиент стандартного ввода и стандартного вывода (рис. В.1).

Рис. В.1. Клиент sock: стандартный ввод и стандартный вывод

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

2. Сервер стандартного ввода и стандартного вывода. Этот режим аналогичен предыдущему, за исключением того, что программа связывает заранее известный порт со своим сокетом и в случае TCP осуществляется пассивное открытие.

3. Клиент-отправитель (рис. В.2).

Рис. В.2. Программа sock в качестве клиента-отправителя

Программа осуществляет фиксированное количество передач пакетов некоторого определенного размера в сеть.

4. Сервер-получатель (рис. В.3).

Рис. В.3. Программа sock в качестве сервера-получателя

Программа осуществляет фиксированное количество считываний из сети.

Эти четыре рабочих режима соответствуют следующим четырем командам:

sock [ параметры ] узел служба

sock [ параметры ] -s [ узел ] служба

sock [ параметры ] -i узел служба

sock [ параметры ] -is [ узел ] служба

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

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

-b n связывает n в качестве клиентского локального номера порта

-с   конвертирует символ новой строки в CR/LF и наоборот

-f a.b.c.d.p удаленный IP-адрес = a.b.c.d, удаленный номер порта = р

-g a.b.c.d свободная маршрутизация

-h   половинное закрытие TCP при получении EOF из стандартного потока ввода

-i   отправка данных на сокет, прием данных с сокета (w/-s)

-j a.b.c.d присоединение к группе многоадресной передачи

-k   осуществляет write или writev порциями

-l a.b.c.d.p клиентский локальный IP-адрес = a.b.c.d. локальный номер порта = р

-n n размер буфера для записи клиентом "рассылки" (по умолчанию 1024)

-о   НЕ присоединять UDP-клиент

-р n время ожидания (в мс) перед каждым считыванием или записью (рассылка/прием)

-q n размер очереди на прослушиваемом сокете для сервера TCP

     (по умолчанию 5)

-r n количество байтов за одну операцию считывания (read) для сервера "приема"

     (по умолчанию 1024)

-s   работает как сервер, а не как клиент

-u   использовать UDP вместо TCP

-v   подробный вывод

-w n количество байтов для каждой записи (write) клиента "рассылки"

     (по умолчанию 1024)

-x n время (в ms) для SO_RCVTIMEO (получение тайм-аута)

-y n время (в ms) для SO_SNDTIMEO (отправка тайм-аута)

-A   параметр SO_REUSEADDR

-B   параметр SO_BROADCAST

-D   параметр SO_DEBUG

-E   параметр IP_RECVDSTADDR

-F   порождение дочерних процессов (fork) после установления соединения

     (параллельный TCP-сервер)

-G a.b.c.d жесткая маршрутизация

-H n параметр IP_TOS (16=min del, 8=max thru, 4=max rel, 2=min cost)

-I   сигнал SIGIO

-J n параметр IP_TTL

-K   параметр SO_KEEPALIVE

-L n параметр SO_LINGER, n = linger time

-N   параметр TCP_NODELAY

-O n время (в мс) для ожидания после вызова listen, но перед первым приемом (accept)

-Р n время (в мс) перед первым считыванием или записью (рассылка/прием)

-Q n время (в мс) ожидания после получения FIN, но перед закрытием

-R n параметр SO_RCVBUF

-S n параметр SO_SNDBUF

-Т   параметр SO_REUSEPORT

-U n войти в срочный режим, прежде чем записать число n (только для отправителя)

-V   использовать writev() вместо write(): включает -k

-W   игнорировать ошибки записи для клиента приема

-X n параметр TCP_MAXSEG (устанавливает MSS)

-Y   параметр SO_DONTROUTE

-Z   MSG_PEEK

-2   параметр IP_ONESBCAST (255.255.255.255) для широковещательной передачи

 

В.4. Небольшие тестовые программы

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

 

В.5. Программа tcpdump

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

% tcpdump '(udp and port daytime) or icmp'

выводит только UDP-дейтаграммы с номером порта отправителя или получателя, равным 13 (сервер времени и даты), или ICMP-пакеты. Следующая команда:

% tcpdump 'tcp and port 80 and tcp[13:1] & 2 != 0'

выводит только TCP-сегменты с номером порта отправителя или получателя, равным 80 (сервер HTTP), у которых установлен флаг SYN. Флаг SYN имеет значение 2 в 13-м байте от начала TCP-заголовка. Следующая команда:

% tcpdump 'tcp and tcp[0:2] > 7000 and tcp[0:2] <= 7005'

выводит только те TCP-сегменты, у которых номер порта отправителя лежит в интервале от 7001 до 7005. Номер порта отправителя занимает 2 байта в самом начале TCP-заголовка (нулевое смещение).

В приложении А книги [111] более подробно описано действие данной программы.

ПРИМЕЧАНИЕ

Эта программа доступна по адресу http://www.tcpdump.org/ и работает под множеством реализаций Unix. Она написана Ван Якобсоном (Van Jacobson), Крэгом Лересом (Craig Leres) и Стивеном МакКаном (Steven McCanne) из LBL, и в настоящее время сопровождается командой tcpdump.org.

Некоторые поставщики предлагают свои программы, обладающие теми же возможностями. Например, в Solaris 2.x есть программа snoop. Но программа tcpdump функционирует под множеством версий Unix, а возможность использования одного и того же средства в неоднородном окружении является большим преимуществом.

 

В.6. Программа netstat

В тексте книги много раз использовалась программа netstat. Эта программа служит для следующих целей.

■ Она выводит статус точек доступа сети. Это было показано в разделе 5.6, когда мы прослеживали статус нашей точки доступа при запуске клиента и сервера.

■ Она показывает, к какой группе принадлежит каждый из интерфейсов узла. Обычно для этой цели используется флаг -ia, а в Solaris 2.x используется флаг -g.

■ С параметром -s эта программа сообщает статистику по каждому протоколу. Подобный пример был приведен в разделе 8.13, когда мы говорили о недостаточном управлении потоками в UDP.

■ При использовании параметра -r программа выводит таблицу маршрутизации, а с параметром -i — информацию об интерфейсе. Эта возможность была использована в разделе 1.9, когда с помощью программы netstat мы выясняли топологию сети.

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

 

В.7. Программа lsof

Название lsof происходит от «list open files» (перечислить открытые файлы). Как и tcpdump, эта программа является общедоступной и представляет собой удобное средство для отладки, которое было перенесено на множество версий Unix.

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

solaris % lsof -i TCP:daytime

COMMAND PID USER FD  TYPE DEVICE     SIZE/OFF INODE NAME

inetd   222 root 15u inet 0xf5a801f8 0t0      TCP   *:daytime

В выводе приводятся следующие данные: команда (данный сервис обеспечивается сервером inetd), идентификатор процесса, владелец процесса, дескриптор (15 и u означает, что он открыт на чтение и на запись), тип сокета, адрес протокола блока управления, размер смещения файла (не имеет значения для сокета), тип протокола и имя.

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

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

ПРИМЕЧАНИЕ

Программа находится по адресу ftp://vic.cc.purdue.edu/pub/tools/unix/lsof. Она написана Виком Абелем (Vic Abell).

Некоторые поставщики предлагают свои программы с похожими возможностями. Например, в BSD/OS предлагается программа fstat. Однако программа lsof работает под множеством версий Unix, а использование одного инструмента в неоднородном окружении вместо подбора различных средств для каждой среды является большим преимуществом.

 

Приложение Г

Различные исходные коды

 

Г.1. Заголовочный файл unp.h

Почти каждая программа в этой книге начинается с подключения заголовочного файла unp.h, показанного в листинге Г.1. Этот файл подключает все стандартные системные заголовочные файлы, необходимые для работы большинства программ, а также некоторые общие системные заголовочные файлы. В нем также определены такие константы, как MAXLINE, прототипы функций ANSI С для тех функций, которые мы определяем в тексте (например, readline), и все используемые нами функции-обёртки. Сами прототипы в приведенном ниже листинге мы не показываем.

Листинг Г.1. Заголовочный файл unp.h

//lib/unp.h

  1 /* Наш собственный заголовочный файл */

  2 #ifndef __unp_h

  3 #define __unp_h

  4 #include "../config.h" /* параметры конфигурации для данной ОС */

  5 /* "../config.h" генерируется сценарием configure */

  6 /* изменив список директив #include,

  7    нужно также изменить файл acsite.m4 */

  8 #include /* основные системные типы данных */

  9 #include /* основные определения сокетов */

 10 #include /* структура timeval{} для функции select() */

 11 #include /* структура timespec{} для функции pselect() */

 12 #include /* структура sockaddr_in{} и другие сетевые

                               определения */

 13 #include /* inet(3) функции */

 14 #include

 15 #include /* для неблокируемых сокетов */

 16 #include

 17 #include

 18 #include

 19 #include

 20 #include

 21 #include /* для констант S_xxx */

 22 #include /* для структуры iovec{} и ready/writev */

 23 #include

 24 #include

 25 #include /* для доменных сокетов Unix */

 26 #ifdef HAVE_SYS_SELECT_H

 27 #include /* для удобства */

 28 #endif

 29 #ifdef HAVE_SYS_SYSCTL_H

 30 #include

 31 #endif

 32 #ifdef HAVE_POLL_H

 33 #include /* для удобства */

 34 #endif

 35 #ifdef HAVE_SYS_EVENT_H

 36 #include /* для kqueue */

 37 #endif

 38 #ifdef HAVE_STRINGS_H

 39 #include /* для удобства */

 40 #endif

 41 /* Три заголовочных файла обычно нужны для вызова ioctl

 42    для сокета/файла: , ,

 43    */

 44 #ifdef HAVE_SYS_IOCTL_H

 45 #include

 46 #endif

 47 #ifdef HAVE_SYS_FILIO_H

 48 #include

 49 #endif

 50 #ifdef HAVE_SYS_SOCKIO_H

 51 #include

 52 #endif

 53 #ifdef HAVE_PTHREAD_H

 54 #include

 55 #endif

 56 #ifdef HAVE_NET_IF_DL_H

 57 #include

 58 #endif

 59 #ifdef HAVE_NETINET_SCTP_H

 60 #include

 61 #endif

 62 /* OSF/1 фактически запрещает recv() и send() в */

 63 #ifdef __osf__

 64 #undef recv

 65 #undef send

 66 #define recv(a,b,c,d) recvfrom(a,b,c,d,0,0)

 67 #define send(a,b,c,d) sendto(a,b,c,d,0,0)

 68 #endif

 69 #ifndef INADDR_NONE

 70 #define INADDR_NONE 0xffffffff /* должно было быть в */

 71 #endif

 72 #ifndef SHUT_RD     /* три новые константы Posix.1g */

 73 #define SHUT_RD   0 /* отключение чтения */

 74 #define SHUT_WR   1 /* отключение записи */

 75 #define SHUT_RDWR 2 /* отключение чтения и записи */

 76 #endif

 77 #ifndef INET_ADDRSTRLEN

 78 #define INET_ADDRSTRLEN 16 /* "ddd.ddd.ddd.ddd\0"

 79 1234567890123456 */

 80 #endif

 81 /* Нужно, даже если нет поддержки IPv6, чтобы мы всегда могли

 82    разместить в памяти буфер требуемого размера без директив #ifdef */

 83 #ifndef INET6_ADDRSTRLEN

 84 #define INET6_ADDRSTRLEN 46 /* максимальная длина строки адреса IPv6:

 85 "xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx" или

 86 "xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:ddd.ddd.ddd.ddd\0"

 87 1234567890123456789012345678901234567890123456 */

 88 #endif

 89 /* Определяем bzero() как макрос, если эта функция отсутствует в

       стандартной библиотеке С */

 90 #ifndef HAVE_BZERO

 91 #define bzero(ptr,n) memset(ptr, 0, n)

 92 #endif

 93 /* В более старых распознавателях отсутствует gethostbyname2() */

 94 #ifndef HAVE_GETHOSTBYNAME2

 95 #define gethostbyname2(host, family) gethostbyname((host))

 96 #endif

 97 /* Структура, возвращаемая функцией recvfrom_flags() */

 98 struct in_pktinfo {

 99  struct in_addr ipi_addr; /* IPv4-адрес получателя */

100  int    ipi_ifindex; /* полученный индекс интерфейса */

101 };

102 /* Нам нужны более новые макросы CMSG_LEN() и CMSG_SPACE(), но в

103    настоящее время их поддерживают далеко не все реализации. Им требуется

104    макрос ALIGN(), но это зависит от реализации */

105 #ifndef CMSG_LEN

106 #define CMSG_LEN(size) (sizeof(struct cmsghdr) + (size))

107 #endif

108 #ifndef CMSG_SPACE

109 #define CMSG_SPACE(size) (sizeof(struct cmsghdr) + (size))

110 #endif

111 /* POSIX требует макрос SUN_LEN(), но он определен

112 не во всех реализациях. Этот макрос 4.4BSD работает

123 независимо от того, имеется ли поле длины */

114 #ifndef SUN_LEN

115 #define SUN_LEN(su) \

116  (sizeof(*(su)) - sizeof((su)->sun_path) + strlen((su)->sun_path))

117 #endif

118 /* В POSIX "домен Unix" называется "локальным IPC".

119    Но пока не во всех системах определены AF_LOCAL и PF_LOCAL */

120 #ifndef AF_LOCAL

121 #define AF_LOCAL AF_UNIX

122 #endif

123 #ifndef PF_LOCAL

124 #define PF_LOCAL PF_UNIX

125 #endif

126 /* POSIX требует определения константы INFTIM в , но во многих

127    системах она по-прежнему определяется в . Чтобы

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

129    Это стандартное значение, но нет гарантии, что оно равно -1 */

130 #ifndef INFTIM

131 #define INFTIM (-1) /* бесконечный тайм-аут */

132 #ifdef HAVE_POLL_H

133 #define INFTIM_UNPH /* надо указать в unpxti.h, что эта константа

                           определена здесь */

134 #endif

135 #endif

136 /* Это значение можно было бы извлечь из SOMAXCONN в ,

137    но многие ядра по-прежнему определяют его как 5,

       хотя на самом деле поддерживается гораздо больше */

138 #define LISTENQ 1024 /* второй аргумент функции listen() */

139 /* Различные константы */

140 #define MAXLINE  4096 /* максимальная длина текстовой строки */

141 #define BUFFSIZE 8192 /* размер буфера для чтения и записи */

142 /* Определение номера порта, который может быть использован для

       взаимодействия клиент-сервер */

143 #define SERV_PORT      9877  /* клиенты и серверы TCP и UDP */

144 #define SERV_PORT_STR "9877" /* клиенты и серверы TCP и UDP */

145 #define UNIXSTR_PATH "/tmp/unix.str" /* потоковые клиенты и серверы

                                            домена Unix */

146 #define UNIXDG_PATH "/tmp/unix.dg" /* клиенты и серверы протокола

                                          дейтаграмм домена Unix */

147 /* Дальнейшие определения сокращают преобразования типов

       аргументов-указателей */

148 #define SA struct sockaddr

149 #define HAVE_STRUCT_SOCKADDR_STORAGE

150 #ifndef HAVE_STRUCT_SOCKADDR_STORAGE

151 /*

152  * RFC 3493: протокольно-независимая структура адреса сокета

153  */

154 #define __SS_MAXSIZE 128

155 #define __SS_ALIGNSIZE (sizeof(int64_t))

156 #ifndef HAVE_SOCKADDR_SA_LEN

157 #define __SS_PADS1SIZE (__SS_ALIGNSIZE - sizeof(u_char) -

sizeof(sa_family_t))

158 #else

159 #define _SS_PAD1SIZE (__SS_ALIGNSIZE - sizeof(sa_family_t))

160 #endif

161 #define __SS_PAD2SIZE (__SS_MAXSIZE — 2*__SS_ALIGNSIZE)

162 struct sockaddr_storage {

163 #ifdef HAVE_SOCKADDR_SA_LEN

164  u_char ss_len;

165 #endif

166  sa_family_t ss_family;

167  char        __ss_pad1[__SS_PAD1SIZE];

168  int64_t     ss_align;

169  char        __ss_pad2[_SS_PAD2SIZE];

170 };

171 #endif

172 #define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

173 /* заданные по умолчанию разрешения на доступ для новых файлов */

174 #define DIR_MODE (FILE_MODE | S_IXUSR | S_IXGRP | S_IXOTH)

175 /* разрешения по умолчанию на доступ к файлам для новых каталогов */

176 typedef void Sigfunc(int); /* для обработчиков сигналов */

177 #define min(a, b) ((а) < (b) ? (a) : (b))

178 #define max(a, b) ((a) > (b) ? (a) : (b))

179 #ifndef HAVE_ADDRINFO_STRUCT

180 #include "../lib/addrinfo.h"

181 #endif

182 #ifndef HAVE_IF_NAMEINDEX_STRUCT

183 struct if_nameindex {

184  unsigned int if_index; /* 1, 2, ... */

185  char *if_name; /* имя, заканчивающееся нулем: "le0", ... */

186 };

187 #endif

188 #ifndef HAVE_TIMESPEC_STRUCT

189 struct timespec {

190  time_t tv_sec; /* секунды */

191  long tv_nsec;  /* и наносекунды */

192 };

193 #endif

 

Г.2. Заголовочный файл config.h

Для обеспечения переносимости всего исходного кода, используемого в тексте книги, применялась утилита GNU autoconf. Ее можно загрузить по адресу http://ftp.gnu.org/gnu/autoconf. Эта программа генерирует сценарий интерпретатора с названием configure, который надо запустить после загрузки программного обеспечения в свою систему. Этот сценарий определяет, какие свойства обеспечивает ваша система Unix: имеется ли в структуре адреса сокета поле длины, поддерживается ли многоадресная передача, поддерживаются ли структуры адреса сокета канального уровня, и т.д. В результате получается файл с названием config.h. Этот файл — первый заголовочный файл, включенный в unp.h (см. предыдущий раздел). В листинге Г.2 показан заголовочный файл config.h для BSD/OS 3.0.

Строки, начинающиеся с #define, относятся к тем свойствам, которые обеспечены данной системой. Закомментированные строки и строки, начинающиеся с #undef, относятся к свойствам, данной системой не поддерживаемым.

Листинг Г.2. Заголовочный файл config.h для BSD/OS

i386-pc-bsdi3.0/config.h

 1 /* config.h. Автоматически генерируется сценарием configure. */

 2 /* Определяем константы, если имеется соответствующий заголовочный файл */

 3 #define CPU_VENDOR_OS "i386-pc-bsdi3.0"

 4 /* #undef HAVE_NETCONFIG_H */ /* */

 5 /* #undef HAVE_NETDIR_H */    /* */

 6 #define HAVE_PTHREAD_H 1      /* */

 7 #define HAVE_STRINGS_H 1      /* */

 8 /* #undef HAVE_XTI_INET_H */  /* */

 9 #define HAVE_SYS_FILIO_H 1    /* */

10 #define HAVE_SYS_IOCTL_H 1    /* */

11 #define HAVE_SYS_SELECT_H 1   /* */

12 #define HAVE_SYS_SOCKIO_H 1   /* */

13 #define HAVE_SYS_SYSCTL_H 1   /* */

14 #define HAVE_SYS_TIME_H 1     /* */

15 /* Определена, если можно подключить и */

16 #define TIME_WITH_SYS_TIME 1

17 /* Определены, если имеются соответствующие функции */

18 #define HAVE_BZERO 1

19 #define HAVE_GETHOSTBYNAME2 1

20 /* #undef HAVE_PSELECT */

21 #define HAVE_VSNPRINTF 1

22 /* Определены, если прототипы функций есть в заголовочном файле */

23 /* #undef HAVE_GETADDRINFO_PROTO */    /* */

24 /* #undef HAVE_GETNAMEINFO_PROTO */    /* */

25 #define HAVE_GETHOSTNAME_PROTO 1       /* */

26 #define HAVE_GETRUSAGE_PROTO 1         /* */

27 #define HAVE_HSTRERROR_PROTO 1         /* */

28 /* #undef HAVE_IF_NAMETOINDEX_PROTO */ /* */

29 #define HAVE_INET_ATON_PROTO 1         /* */

30 #define HAVE_INET_PTON_PROTO 1         /* */

31 /* #undef HAVE_ISFDTYPE_PROTO */       /* */

32 /* #undef HAVE_PSELECT_PROTO */        /* */

33 #define HAVE_SNPRINTF_PROTO 1          /* */

34 /* #undef HAVE_SOCKATMARK_PROTO */     /* */

35 /* Определены, если определены соответствующие структуры */

36 /* #undef HAVE_ADDRINFO_STRUCT */     /* */

37 /* #undef HAVE_IF_NAMEINDEX_STRUCT */ /* */

38 #define HAVE_SOCKADDR_DL_STRUCT 1     /* */

39 #define HAVE TIMESPEC STRUCT 1        /* */

40 /* Определены, если имеется указанное свойство */

41 #define HAVE_SOCKADDR_SA_LEN 1    /* в sockaddr{} есть поле sa_len */

42 #define HAVE_MSGHDR_MSG_CONTROL 1 /* в msghdr{} есть поле msg_control */

43 /* Имена устройств XTI для TCP и UDP */

44 /* #undef HAVE_DEV_TCP */               /* большинство здесь */

45 /* #undef HAVE_DEV_XTI_TCP */           /* для AIX */

46 /* #undef HAVE_DEV_STREAMS_XTISO_TCP */ /* для OSF 3.2 */

47 /* При необходимости определяем типы данных */

48 /* #undef int8_t */             /* */

49 /* #undef int16_t */            /* */

50 /* #undef int32_t */            /* */

51 #define uint8_t unsigned char   /* */

52 #define uint16_t unsigned short /* */

53 #define uint32_t unsigned int   /* */

54 /* #undef size_t */             /* */

55 /* #undef ssize_t */            /* */

56 /* socklen_t должен иметь тип uint32_t, но configure определяет его

57    как unsigned int. т. к. это значение используется в начале компиляции.

58    иногда до того, как в данной реализации определяется тип uint32_t */

59 #define socklen_t unsigned int  /* */

60 #define sa_family_t SA_FAMILY_T /* */

61 #define SA_FAMILY_T uint8_t

62 #define t_scalar_t int32_t /* */

63 #define t_uscalar_t uint32_t /* */

64 /* Определены, если система поддерживает указанное свойство */

65 #define IPV4 1       /* IPv4, V в верхнем регистре */

66 #define IPv4 1       /* IPv4, v в нижнем регистре, на всякий случай */

67 /* #undef IPV6 */    /* IPv6, V в верхнем регистре */

68 /* #undef IPv6 */    /* IPv6, v в нижнем регистре, на всякий случай */

69 #define UNIXDOMAIN 1 /* доменные сокеты Unix */

70 #define UNIXdomain 1 /* доменные сокеты Unix */

71 #define MCAST 1      /* поддержка многоадресной передачи */

 

Г.3. Стандартные функции обработки ошибок

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

if ( условие ошибки )

 err_sys( формат printf с любым количеством аргументов );

вместо

if ( условие ошибки ) {

 char buff[200];

 snprintf(buff, sizeof(buff), формат printf с любым количеством аргументов );

 perror(buff);

 exit(1);

}

Наши функции обработки ошибок используют следующую возможность ANSI С: список аргументов может иметь переменную длину. Более подробную информацию об этом вы найдете в разделе 7.3 книги [68].

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

Таблица Г.1. Стандартные функции обработки ошибок

Функция strerror (errno ?) Завершение ? Уровень syslog
err_dump Да abort(); LOG_ERR
err_msg Нет return; LOG_INFO
err_quit Нет exit(1); LOG_ERR
err_ret Да return; LOG_INFO
err_sys Да exit(1); LOG_ERR

В листинге Г.3 показаны первые пять функций из табл. Г.1.

Листинг Г.3. Стандартные функции обработки ошибок

//lib/error.c

 1 #include "unp.h"

 2 #include /* заголовочный файл ANSI С */

 3 #include /* для syslog() */

 4 int daemon_proc; /* устанавливается в ненулевое значение с

                       помощью daemon_init() */

 5 static void err_doit(int, int, const char*, va_list);

 6 /* Нефатальная ошибка, связанная с системным вызовом.

 7    Выводим сообщение и возвращаем управление */

 8 void

 9 err_ret(const char *fmt , ...)

10 {

11  va_list ap;

12  va_start(ap, fmt);

13  err_doit(1, LOG_INFO, fmt, ap);

14  va_end(ap);

15  return;

16 }

17 /* Фатальная ошибка, связанная с системным вызовом.

18    Выводим сообщение и завершаем работу */

19 void

20 err_sys(const char *fmt)

21 {

22  va_list ap;

23  va_start(ap, fmt);

24  err_doit(1, LOG_ERR, fmt, ap);

25  va_end(ap);

26  exit(1);

27 }

28 /* Фатальная ошибка, связанная с системным вызовом.

29    Выводим сообщение, сохраняем дамп памяти процесса и заканчиваем работу */

30 void

31 err_dump(const char *fmt, ... )

32 {

33  va_list ар;

34  va_start(ap, fmt);

35  err_doit(1, LOG_ERR, fmt, ap);

36  va_end(ap);

37  abort(); /* сохраняем дамп памяти и заканчиваем работу */

38  exit(1);

39 }

40 /* Нефатальная ошибка, не относящаяся к системному вызову.

41    Выводим сообщение и возвращаем управление */

42 void

43 err_msg(const char *fmt , ...)

44 {

45  va_list ap;

46  va_start(ap, fmt);

47  err_doit(0, LOG_INFO, fmt, ap);

48  va_end(ap);

49  return;

50 }

51 /* Фатальная ошибка, не относящаяся к системному вызову.

52    Выводим сообщение и заканчиваем работу. */

53 void

54 err_quit(const char *fmt, ...)

55 {

56  va_list ap;

57  va_start(ap, fmt);

58  err_doit(0, LOG_ERR, fmt, ap);

59  va_end(ap);

60  exit(1);

61 }

62 /* Выводим сообщение и возвращаем управление.

63    Вызывающий процесс задает "errnoflag" и "level" */

64 static void

65 err_doit(int errnoflag, int level, const char *fmt, va_list ap)

66 {

67  int errno_save, n;

68  char buf[MAXLINE + 1];

69  errno_save = errno; /* значение может понадобиться вызвавшему

                           процессу */

70 #ifdef HAVE_VSNPRINTF

71  vsnprintf(buf, MAXLINE, fmt, ap); /* защищенный вариант */

72 #else

73  vsprintf(buf, fmt, ap); /* незащищенный вариант */

74 #endif

75  n = strlen(buf);

76  if (errnoflag)

77   snprintf(buf + n, MAXLINE - n, ": %s", strerror(errno_save));

78  strcat(buf, "\n");

79  if (daemon_proc) {

80   syslog(level, buf);

81  } else {

82   fflush(stdout); /* если stdout и stderr совпадают */

83   fputs(buf, stderr);

84   fflush(stderr);

85  }

86  return;

87 }

 

Приложение Д

Решения некоторых упражнений

 

Глава 1

1.3. В операционной системе Solaris получаем:

solaris % daytimetcpcli 127.0.0.1

socket error: Protocol not supported

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

solaris % grep 'Protocol not supported' /usr/include/sys/errno.h

#define EPROTONOSUPPORT 120 /* Protocol not supported */

Это значение errno возвращается функцией socket. Далее смотрим в руководство пользователя:

solaris % man socket

В большинстве руководств пользователя в конце под заголовком «Errors» приводится дополнительная, хотя и лаконичная информация об ошибках.

1.4. Заменяем первое описание на следующее:

int sockfd, n, counter = 0;

Добавляем оператор

counter++;

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

printf("counter = %d\n", counter);

На экран всегда выводится значение 1.

1.5. Объявим переменную i типа int и заменим вызов функции write на следующий:

for (i = 0; i < strlen(buff); i++)

 Write(connfd, &buff[i], 1);

Результат зависит от расположения клиентского узла и узла сервера. Если клиент и сервер находятся на одном узле, счетчик обычно равен 1. Это значит, что даже если сервер выполнит функцию write 26 раз, данные будут возвращены за одну операцию считывания (read). Но если клиент запущен в Solaris 2.5.1, а сервер в BSD/OS 3.0, счетчик обычно равен 2. Просмотрев пакеты Ethernet, мы увидим, что первый символ отправляется в первом пакете сам по себе, а следующий пакет содержит остальные 25 символов. (Обсуждение алгоритма Нагла в разделе 7.9 объясняет причину такого поведения.)

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

 

Глава 2

2.1 Зайдите на веб-страницу http://www.iana.org/numbers.htm и найдите журнал под названием «IP Version Number». Номер версии 0 зарезервирован, версии 1-3 не использовались, а версия 5 представляет собой потоковый протокол Интернета (Internet Stream Protocol).

2.2. Все RFC бесплатно доступны по электронной почте, через FTP или Web. Стартовая страница для поиска находится по адресу http://www.ietf.org. Одним из мест расположения RFC является каталог ftp://ftp.isi.edu/in-notes. Для начала следует получить файл с текущим каталогом RFC, обычно это файл rfc-index.txt. HTML-версия хранится в файле http://www.rfc-editor.org/rfc-index.html. Если с помощью какого-либо редактора осуществить поиск термина «stream» (поток) в указателе RFC, мы выясним, что RFC 1819 определяет версию 2 потокового протокола Интернета. Какую бы информацию, которая может содержаться в RFC, мы ни искали, для поиска следует использовать указатель (каталог) RFC.

2.3. В версии IPv4 при таком значении MSS генерируется 576-байтовая дейтаграмма (20 байт для заголовка IPv4 и 20 байт для заголовка TCP). Это минимальный размер буфера для сборки фрагментов в Ipv4.

2.4. В данном примере сервер (а не клиент) осуществляет активное закрытие.

2.5. Узел в сети Token Ring не может посылать пакет, содержащий больше, чем 1460 байт данных, поскольку полученное им значение MSS равно 1460. Узел в сети Ethernet может посылать пакет размером до 4096 байт данных, но не превышающий величину MTU исходящего интерфейса (Ethernet) во избежание фрагментации. Протокол TCP не может превысить величину MSS, объявленную другой стороной, но он всегда может посылать пакеты меньшего размера.

2.6. В разделе «Protocol Numbers» (номера протоколов) RFC «Assigned Numbers» («Присвоенные номера») указано значение 89 для протокола OSPF.

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

 

Глава 3

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

3.2. Указатель должен увеличиваться на количество считанных или записанных байтов, но в языке С нет возможности увеличивать указатели типа void (поскольку компилятору не известно, на какой тип данных указывает указатель).

 

Глава 4

4.1. Посмотрите на определение констант, начинающихся с INADDR_, кроме INADDR_ANY (состоит из нулевых битов) и INADDR_NONE (состоит из единичных битов). Например, адрес многоадресной передачи класса D INADDR_MAX_LOCAL_GROUP определяется как 0xe00000ff с комментарием «224.0.0.255», что явно указывает на порядок байтов узла.

4.2. Приведем новые строки, добавленные после вызова connect:

len = sizeof(cliaddr);

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

printf("local addr: %s\n",

Sock_ntop((SA*)&cliaddr, len));

Это требует описания переменной len как socklen_t, a cliaddr как структуры struct sockaddr_in. Обратите внимание, что аргумент типа «значение-результат» для функции getsockname(len) должен быть до вызова функции инициализирован размером переменной, на которую указывает второй аргумент. Наиболее частая ошибка программирования при использовании аргументов типа «значение-результат» заключается в том, что про эту инициализацию забывают.

4.3. Когда дочерний процесс вызывает функцию close, счетчик ссылок уменьшается с 2 до 1, так что клиенту не посылается сегмент FIN. Позже, когда родительский процесс вызывает функцию close, счетчик ссылок уменьшается до нуля, и тогда сегмент FIN посылается.

4.4. Функция accept возвращает значение EINVAL, так как первый аргумент не является прослушиваемым сокетом.

4.5. Вызов функции listen без вызова функции bind присваивает прослушиваемому сокету динамически назначаемый порт.

 

Глава 5

5.1. Длительность состояния TIME_WAIT должна находиться в интервале между 1 и 4 минутами, что дает величину MSL от 30 с до 2 мин.

5.2. Наши клиент-серверные программы не работают с двоичными файлами. Допустим, что первые 3 байта в файле являются двоичной единицей (1), двоичным нулем (0) и символом новой строки. При вызове функции fgets в листинге 5.4 либо считывается MAXLINE - 1 символов, либо считываются символы до символа новой строки или до конца файла. В данном примере функция считает три символа, а затем прервет строку нулевым байтом. Но вызов функции strlen в листинге 5.4 возвращает значение 1, так как она остановится на первом нулевом байте. Один байт посылается серверу, но сервер блокируется в своем вызове функции readline, ожидая символа новой строки. Клиент блокируется, ожидая ответа от сервера. Такое состояние называется зависанием, или взаимной блокировкой: оба процесса блокированы и при этом каждый ждет от другого некоторого действия, которое никогда не произойдет. Проблема заключается в том, что функция fgets обозначает нулевым байтом конец возвращаемых ею данных, поэтому данные, которые она считывает, не должны содержать нулевой байт.

5.3. Программа Telnet преобразует входные строки в NVT ASCII (см. раздел 26.4 книги [111]), что означает прерывание каждой строки 2-символьной последовательностью CR (carriage return — возврат каретки) и LF (linefeed — новая строка). Наш клиент добавляет только разделитель строк (newline), который в действительности является символом новой строки (linefeed, LF). Тем не менее можно использовать клиент Telnet для связи с нашим сервером, поскольку наш сервер отражает каждый символ, включая CR, предшествующий каждому разделителю строк.

5.4. Нет, последние два сегмента из последовательности завершения соединения не посылаются. Когда клиент посылает серверу данные после уничтожения дочернего процесса сервера (ввод строки another line, см. раздел 5.12), сервер TCP отвечает сегментом RST. Сегмент RST прекращает соединение, а также предотвращает переход в состояние TIME_WAIT на стороне сервера (конец соединения, осуществивший активное закрытие).

5.5. Ничего не меняется, потому что процесс, запущенный на узле сервера, создает прослушиваемый сокет и ждет прибытия запросов на соединение. На третьем шаге мы посылаем сегмент данных, предназначенный для установленного соединения TCP (состояние ESTABLISHED). Наш сервер с прослушиваемым сокетом не увидит этот сегмент данных, и TCP сервера по-прежнему будет посылать клиенту сегмент RST.

5.6. В листинге Д.1 приведена программа. Запуск этой программы в Soalris генерирует следующий вывод:

solaris % tsigpipe 192.168.1.10

SIGPIPE received

write error: Broken pipe

Начальный вызов функции sleep и переход в режим ожидания на 2 с нужен, чтобы сервер времени и даты отправил ответ и закрыл свой конец соединения. Первая функция write отправляет сегмент данных серверу, который отвечает сегментом RST (поскольку сервер времени и даты полностью закрыл свой сокет). Обратите внимание, что наш TCP позволяет писать в сокет, получивший сегмент FIN. Второй вызов функции sleep позволяет получить от сервера сегмент RST, а во втором вызове функции write генерируется сигнал SIGPIPE. Поскольку наш обработчик сигналов возвращает управление, функция write возвращает ошибку EPIPE.

Листинг Д.1. Генерация SIGPIPE

//tcpcliserv/tsigpipe.c

 1 #include "unp.h"

 2 void

 3 sig_pipe(int signo)

 4 {

 5  printf("SIGPIPE received\n");

 6  return;

 7 }

 8 int

 9 main(int argc, char **argv)

10 {

11  int sockfd;

12  struct sockaddr_in servaddr;

13  if (argc != 2)

14   err_quit("usage: tcpcli ");

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

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

17  servaddr.sin_family = AF_INET;

18  servaddr.sin_port = htons(13); /* сервер времени и даты */

19  Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

20  Signal(SIGPIPE, sig_pipe);

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

22  sleep(2);

23  Write(sockfd, "hello", 5);

24  sleep(2);

25  Write(sockfd, "world", 5);

26  exit(0);

27 }

5.7. В предположении, что узел сервера поддерживает модель системы с гибкой привязкой (см. раздел 8.8), все будет работать. Узел сервера примет IP-дейтаграмму (которая в данном случае содержит TCP-сегмент), прибывшую на самый левый канал, даже если IP-адрес получателя является адресом самого правого канала. Это можно проверить, если запустить наш сервер на узле linux (см. рис. 1.7), а затем запустить клиент на узле solaris, но на стороне клиента задать другой IP-адрес сервера (206.168.112.96). После установления соединения, запустив на стороне сервера программу netstat, мы увидим, что локальный IP-адрес является IP-адресом получателя из клиентского сегмента SYN, а не IP-адресом канала, на который прибыл сегмент SYN (как отмечалось в разделе 4.4).

5.8. Наш клиент был запущен в системе Intel с прямым порядком байтов, где 32-разрядное целое со значением 1 хранится так, как показано на рис. Д.1.

Рис. Д.1. Представление 32-разрядного целого числа 1 в формате прямого порядка байтов

Четыре байта посылаются на сокет в следующем порядке: A, A + 1, A + 2 и A + 3, и там хранятся в формате обратного порядка байтов, как показано на рис. Д.2.

Рис. Д.2. Представление 32-разрядного целого числа с рис. Д.1 в формате обратного порядка байтов

Значение 0x01000000 интерпретируется как 16 777 216. Аналогично, целое число 2, отправленное клиентом, интерпретируется сервером как 0x02000000, или 33 554 432. Сумма этих двух целых чисел равна 50 331 648, или 0x03000000. Когда это значение, записанное в обратном порядке байтов, отправляется клиенту, оно интерпретируется клиентом как целое число 3.

Но 32-разрядное целое число -22 представляется в системе с прямым порядком байтов так, как показано на рис. Д.3 (мы предполагаем, что используется поразрядное дополнение до двух для отрицательных чисел).

Рис. Д.3. Представление 32-разрядного целого числа -22 в формате прямого порядка байтов

В системе с обратным порядком байтов это значение интерпретируется как 0xeaffffff, или -352 521 537. Аналогично, представление числа -77 в прямом порядке байтов выглядит как 0xffffffb3, но в системах с обратным порядком оно представляется как 0xb3ffffff, или -1 275 068 417. Сложение, выполняемое сервером, приводит к результату 0x9efffffe, или -1 627 389 954. Полученное значение в обратном порядке байтов посылается через сокет клиенту, где в прямом порядке байтов оно интерпретируется как 0xfeffff9e, или -16 777 314 — это то значение, которое выводится в нашем примере.

5.9. Метод правильный (преобразование двоичных значений в сетевой порядок байтов), но нельзя использовать функции htonl и ntohl. Хотя символ l в названиях данных функций обозначает «long», эти функции работают с 32-разрядными целыми (раздел 3.4). В 64-разрядных системах long занимает 64 бита, и эти две функции работают некорректно. Для решения этой проблемы следует определить две новые функции hton64 и ntoh64, но они не будут работать в системах, представляющих значения типа long 32 битами.

5.10. В первом сценарии сервер будет навсегда блокирован при вызове функции readn в листинге 5.14, поскольку клиент посылает два 32-разрядных значения, а сервер ждет два 64-разрядных значения. В случае, если клиент и сервер поменяются узлами, клиент будет посылать два 64-разрядных значения, а сервер считает только первые 64 бита, интерпретируя их как два 32-разрядных значения. Второе 64-разрядное значение останется в приемном буфере сокета сервера. Сервер отправит обратно 32-разрядное значение, и клиент навсегда заблокируется в вызове функции readn в листинге 5.13, поскольку будет ждать для считывания 64-разрядное значение.

5.11. Функция IP-маршрутизации просматривает IP-адрес получателя (IP-адрес сервера) и пытается по таблице маршрутизации определить исходящий интерфейс и следующий маршрутизатор (см. главу 9 [111]). В качестве адреса отправителя используется первичный IP-адрес исходящего интерфейса, если сокет еще не связан с локальным IP-адресом.

 

Глава 6

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

6.2. Если функция select сообщает, что сокет готов к записи, причем буфер отправки сокета вмещает 8192 байта, а мы вызываем для этого блокируемого сокета функцию write с буфером размером 8193 байта, то функция write может заблокироваться, ожидая места для последнего байта. Операции считывания на блокируемом сокете будут возвращать сообщение о неполном считывании, если доступны какие-либо данные, но операции записи на блокируемом сокете заблокированы до принятия всех данных ядром. Поэтому, чтобы избежать блокирования при использовании функции select для проверки на возможность записи, следует переводить сокет в неблокируемый режим.

6.3. Если оба дескриптора готовы для чтения, выполняется только первый тест — тест сокета. Но это не нарушает работоспособность клиента, а только лишь уменьшает его эффективность. Поэтому если при завершении функции select оба дескриптора готовы для чтения, первое условие if оказывается истинным, в результате чего сначала вызывается функция readline для считывания из сокета, а затем функция fputs для записи в стандартный поток вывода. Следующее условие if пропускается (поскольку мы добавили else), но функция select вызывается снова, сразу находит стандартное устройство ввода, готовое к чтению, и завершается. Суть в том, что условие готовности стандартного потока ввода для чтения сбрасывается считыванием из сокета, а не возвратом функции select.

6.4. Воспользуйтесь функцией getrlimit для получения значений константы RLIMIT_NOFILE, а затем вызовите функцию setrlimit для установки текущего гибкого предела (rlim_cur) равным жесткому пределу (rlim_max). Например, в Solaris 2.5 гибкий предел равен 64, но любой процесс может увеличить это значение до используемого по умолчанию значения жесткого предела (1024).

6.5. Серверное приложение непрерывно посылает данные клиенту, клиент TCP подтверждает их прием и сбрасывает.

6.6. Функция shutdown с аргументами SHUT_WR и SHUT_RDWR всегда посылает сегмент FIN, в то время как функция close посылает сегмент FIN только если в момент вызова функции close счетчик ссылок дескриптора равен 1.

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

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

 

Глава 7

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

Листинг Д.2. Вывод размера приемного буфера сокета и MSS до и после установления соединения

//sockopt/rcvbuf.c

 1 #include "urp.h"

 2 #include /* для TCP_MAXSEG */

 3 int

 4 main(int argc, char **argv)

 5 {

 6  int sockfd, rcvbuf, mss;

 7  socklen_t len;

 8  struct sockaddr_in servaddr;

 9  if (argc != 2)

10   err_quit("usage: rcvbuf ");

11  sockfd = Socket(AF_INET, SOCK_STREAM, 0);

12  len = sizeof(rcvbuf);

13  Getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len);

14  len = sizeof(mss);

15  Getsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &mss, &len);

16  printf("defaults: SO_RCVBUF = %d. MSS = %d\n", rcvbuf, mss);

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

18  servaddr.sin_family = AF_INET;

19  servaddr.sin_port = htons(13); /* сервер времени и даты */

20  Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

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

22  len = sizeof(rcvbuf);

23  Getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len);

24  len = sizeof(mss);

25  Getsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &mss, &len);

26  printf("after connect: SO_RCVBUF = %d, MSS = %d\n", rcvbuf, mss);

27  exit(0);

28 }

He существует какого-то одного «правильного» вывода для данной программы. Результаты зависят от системы. Некоторые системы (в особенности Solaris 2.5.1 и более ранние версии) всегда возвращают нулевой размер буфера сокета, не давая нам возможности увидеть, что происходит с этим значением в процессе соединения.

До вызова функции connect выводится значение MSS по умолчанию (часто 536 или 512), а значение, выводимое после вызова функции connect, зависит от возможных параметров MSS, полученных от собеседника. Например, в локальной сети Ethernet после выполнения функции connect MSS может иметь значение 1460. Однако после соединения (connect) с сервером в удаленной сети значение MSS может быть равно значению по умолчанию, если только ваша система не поддерживает обнаружение транспортной MTU. Если это возможно, запустите во время работы вашей программы программу tcpdump или подобную ей (см. раздел В.5), чтобы увидеть фактическое значение параметра MSS в сегменте SYN, полученном от собеседника.

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

7.3. Разместите в памяти структуру linger по имени ling и проинициализируйте ее следующим образом:

str_cli(stdin, sockfd);

ling.l_onoff = 1;

ling.l_linger = 0;

Setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));

exit(0);

Это заставит TCP на стороне клиента прекратить работу путем отправки сегмента RST вместо нормального обмена четырьмя сегментами. Дочерний процесс сервера вызывает функцию readline, возвращает ошибку ECONNRESET и выводит следующее сообщение:

readline error: Connection reset by peer

Клиентский сокет не должен проходить через состояние ожидания TIME_WAIT, даже если клиент выполняет активное закрытие.

7.4. Первый клиент вызывает функции setsockopt, bind и connect. Но если второй клиент вызовет функцию bind между вызовами функций bind и connect первого клиента, возвращается ошибка EADDRINUSE. Но как только первый клиент установит соединение с собеседником, вызов функции bind второго клиента будет работать, поскольку сокет первого клиента уже присоединен. В случае возвращения ошибки EADDRINUSE второму клиенту следует вызывать bind несколько раз, а не останавливаться при появлении первой ошибки — это единственный способ справиться с данной ситуацией.

7.5. Запускаем программу на узле без поддержки многоадресной передачи (MacOS X 10.2.6).

macosx % sock -s 9999 &   запускаем первый сервер с универсальным адресом

[1] 29697

macosx % sock -s 172.24.37.78 9999   пробуем второй сервер, но без -А

can't bind local address: Address already in use

macosx % sock -s -A 172.24.37.78 9999 &   пробуем опять с -A: работает

[2] 29699

macosx % sock -s -A 127.0.0.1 9999 &   третий сервер с -A; работает

[3] 29700

macosx % netstat -na | grep 9999

tcp4 0 0 127.0.0.1.9999     *.* LISTEN

tcp4 0 0 206.62.226.37.9999 *.* LISTEN

tcp4 0 0 *.9999             *.* LISTEN

7.6. Теперь попробуем проделать то же на узле с поддержкой многоадресной передачи, но без поддержки параметра SO_REUSEADDR (Solaris 9).

solaris % sock -s -u 8888 & запускаем первый

[1] 24051

solaris % sock -s -u 8888

can't bind local address: Address already in use

solaris % sock -s -u -A 8888 &   снова пробуем запустить второй с -A:

                               работает

solaris % netstat -na | grep 8888   мы видим дублированное связывание

*.8888 Idle

* 8888 Idle

В этой системе задавать параметр SO_REUSEADDR было необходимо только для второго связывания. Наконец, запускаем сценарий в MacOS X 10.2.6, где поддерживается как многоадресная передача, так и параметр SO_REUSEPORT. Сначала пробуем использовать SO_REUSEADDR для обоих серверов, но это не работает.

macosx % sock -u -s -A 7777 &

[1] 17610

macosx % sock -u -s -A 7777

can't bind local address: Address already in use

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

macosx % sock -u -s 8888 &

[1] 17612

macosx % sock -u -s -T 8888

can't bind local address: Address already in use

Наконец, задаем параметр SO_REUSEPORT для обоих серверов, и этот вариант работает.

macosx % sock -u -s -Т 9999 &

[1] 17614

macosx % sock -u -s -T 9999 &

[2] 17615

macosx % netstat -na | grep 9999

udp4 0 0 *.9999 *.*

udp4 0 0 *.9999 *.*

7.7. Этот параметр (-d) не делает ничего, поскольку программа ping использует ICMP-сокет, а параметр сокета SO_DEBUG влияет только на TCP-сокеты. Описание параметра сокета SO_DEBUG всегда было довольно расплывчатым, наподобие «этот параметр допускает отладку на соответствующем уровне протокола», и единственный уровень протокола, где реализуется данный параметр — это TCP.

7.8. Временная диаграмма приведена на рис. Д.4.

Рис. Д.4. Взаимодействие алгоритма Нагла с задержанными сегментами ACK

7.9. Установка параметра сокета TCP_NODELAY приводит к немедленной отправке данных из второй функции write, даже если имеется еще один небольшой пакет, ожидающий отправки. Это показано на рис. Д.5. Полное время в данном примере превышает 150 мс.

Рис Д.5. Предотвращение алгоритма Нагла путем установки параметра TCP_NODELAY

7.10. Как показано на рис. Д.6, преимущество данного решения состоит в уменьшении числа пакетов.

Рис. Д.6. Использование функции writev вместо параметра сокета TCP_NODELAY

7.11. В разделе 4.2.3.2 говорится: «задержка ДОЛЖНА быть меньше 0,5 с, а в потоке полноразмерных сегментов СЛЕДУЕТ использовать сегмент ACK по крайней мере для каждого второго сегмента». Беркли-реализации задерживают сегмент ACK не более, чем на 200 мс [128, с. 821].

7.12. Родительский процесс сервера в листинге 5.1 большую часть времени блокирован в вызове функции accept, а дочерний процесс в листинге 5.2 большую часть времени блокирован в вызове функции read, который содержится в функции readline. Проверка работоспособности с помощью параметра SO_KEEPALIVE не влияет на прослушиваемый сокет, поэтому в случае, если клиентский узел выйдет из строя, родительский процесс не пострадает. Функция read дочернего процесса возвратит ошибку ETIMEDOUT примерно через 2 ч после последнего обмена данными через соединение.

7.13. Клиент, приведенный в листинге 5.4, большую часть времени блокирован вызовом функции fgets, который, в свою очередь, блокирован операцией чтения из стандартной библиотеки ввода-вывода на стандартном устройстве ввода. Когда примерно через 2 ч после последнего обмена данными через соединение истечет время таймера проверки работоспособности и проверочные сообщения не выявят работоспособности сервера, ошибка сокета, ожидающая обработки, примет значение ETIMEDOUT. Но клиент блокирован вызовом функции fgets, поэтому он не увидит этой ошибки, пока не осуществит чтение или запись на сокете. Это одна из причин, по которой в главе 6 листинг 5.4 был изменен таким образом, чтобы использовать функцию select.

7.14. Этот клиент большую часть времени блокирован вызовом функции select, которая сообщит, что сокет готов для чтения, как только ожидающая обработки ошибка будет установлена в ETIMEDOUT (как показано в предыдущем решении).

7.15. Происходит обмен только двумя сегментами, а не четырьмя. Вероятность того, что таймеры двух систем будут строго синхронизированы, очень мала, следовательно, на одном конце соединения таймер проверки работоспособности сработает немного раньше, чем на другом. Первый из сработавших таймеров посылает проверочное сообщение, заставляя другой конец послать в ответ сегмент ACK. Но получение проверочного сообщения приводит к тому, что таймеру проверки работоспособности с более медленными часами будет присвоено новое значение — он сдвинется на 2 ч вперед.

7.16 Изначально в API сокетов не было функции listen. Вместо этого четвертый аргумент функции socket содержал параметр сокета, а параметр SO_ACCEPTCONN использовался для задания прослушиваемого сокета. Когда добавилась функция listen, флаг остался, но теперь его может устанавливать только ядро [128, с. 456].

 

Глава 8

8.1. Да. Функция read возвращает 4096 байт данных, а функция recvfrom возвращает 2048 байт (первую из двух дейтаграмм). Функция recvfrom на сокете дейтаграмм никогда не возвращает больше одной дейтаграммы, независимо от того, сколько приложение запрашивает.

8.2. Если протокол использует структуры адреса сокета переменной длины, clilen может быть слишком длинным. В главе 15 будет показано, что это не вызывает проблем со структурами адреса доменного сокета Unix, но корректным решением будет использовать для функции sendto фактическую длину, возвращаемую функцией recvfrom.

8.4. Запуск программы ping с такими параметрами позволяет просмотреть ICMP-сообщения, получаемые узлом, на котором она запущена. Мы используем уменьшенное количество отправляемых пакетов вместо обычного значения 1 пакет в секунду, только чтобы уменьшить объем выводимой на экран информации. Если запустить наш UDP-клиент на узле solaris, указав IP-адрес сервера 192.168.42.1, а затем запустить программу ping, получим следующий вывод:

aix % ping -v -I 60 127.0.0.1

PING 127.0.0.1: {127.0.0.1}: 56 data bytes

64 bytes from 127 0.0.1: icmp_seq=0 ttl=255 time=0 ms

36 bytes from 192.168.42.1: Destination Port Unreachable

Vr HL TOS  Len   ID Fig  Off TTL Pro cks  Src Dst Data

4   5  00 0022 0007 0   0000  1e  11 c770 192 168 42.2 192.168.42.1

UDP: from port 40645. to port 9877 (decimal)

ПРИМЕЧАНИЕ

He все версии ping выводят сообщения об ICMP-ошибках, даже если задан параметр -v.

8.5. Прослушиваемый сокет может иметь приемный буфер определенного размера, но прослушиваемым TCP-сокетом данные никогда не принимаются. Большинство реализаций не выделяют заранее память под буферы отправки и приема. Размеры буферов сокета, определяемые параметрами SO_SNDBUF и SO_RCVBUF, являются предельными значениями для соответствующего сокета.

8.6. Запустим программу sock с параметром -u (использовать UDP) и параметром -l (определяет локальный адрес и порт) на многоинтерфейсном узле freebsd.

freebsd % sock -u -l 12.106.32.254.4444 192.168.42.2 8888

hello

Локальный IP-адрес подключен к Интернету (см. рис. 1.7), но чтобы достичь получателя, дейтаграмма должна выйти через другой интерфейс. Наблюдая за сетью с помощью программы tcpdump, мы увидим, что IP-адрес отправителя, связанный с клиентом, не является адресом исходящего интерфейса.

14:28:29.614846 12.106.32.254.444 > 192.168.42.2.8888. udp 6

14:28:29.615255 192.168.42.2 > 12 106.32.254: icmp: 192.168 42.2

udp port 8888 unreachable

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

8.8. Наибольший размер IPv4-дейтаграммы составляет 65 535 байт и ограничивается 16-разрядным полем полной длины, показанным на рис. А.1. IP-заголовок требует 20 байт, UDP-заголовок — 8 байт, и для пользовательских данных остается не более 65 507 байт. В IPv6 (без поддержки джумбограмм) размер IP-заголовка составляет 40 байт, и под пользовательские данные отводится 65 487 байт.

В листинге Д.3 приведена новая версия dg_cli. Если забыть установить размер буфера отправки, Беркли-ядра возвратят из функции sendto ошибку EMSGSIZE, поскольку размер буфера отправки сокета обычно меньше, чем максимально возможный размер UDP-дейтаграммы (чтобы убедиться в этом, выполните упражнение 7.1).

Листинг Д.3. Запись дейтаграммы UDP/IPv4 максимального размера

//udpcliserv/dgclibig.c

 1 #include "unp.h"

 2 #undef MAXLINE

 3 #define MAXLINE 65507

 4 void

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

 6 {

 7  int size;

 8  char sendline[MAXLINE], recvline[MAXLINE + 1];

 9  ssize_t n;

10  size = 70000;

11  Setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));

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

13  Sendto(sockfd, sendline, MAXLINE, 0, pservaddr, servlen);

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

15  printf("received %d bytes\n", n);

16 }

Но если установить размеры буферов сокета клиента, как показано в листинге Д.3, и запустить программу, сервер ничего не возвратит. С помощью программы tcpdump можно убедиться, что клиентская дейтаграмма отправляется серверу, но если в сервер поместить функцию printf, вызов функции recvfrom не возвратит дейтаграмму. Проблема заключается в том, что приемный буфер UDP-сокета сервера меньше, чем посланная нами дейтаграмма, поэтому дейтаграмма отбрасывается и не доставляется на сокет. В системах BSD/OS это можно проверить, запустив программу netstat -s и проверив счетчик, указывающий количество дейтаграмм, отброшенных из-за переполнения буферов сокета (dropped due to full socket buffers), до и после получения нашей длинной дейтаграммы. Решением является модификация сервера путем задания размеров буферов приема и отправки сокета.

В большинстве сетей дейтаграмма длиной 65 535 байт фрагментируется. Как отмечалось в разделе 2.9, IP-уровнем должен поддерживаться размер буфера для сборки фрагментов, равный всего лишь 576 байт. Поэтому некоторые узлы не получат дейтаграмму максимального размера, посылаемую в данном упражнении. Кроме того, во многих Беркли-реализациях, включая 4.4BSD-Lite2, имеется ошибка, связанная со знаковыми типами данных, которая не позволяет UDP принимать дейтаграммы больше, чем 32 767 байт (см. строка 95, с. 770 [128]).

 

Глава 9

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

9.2. На стороне сервера выполняется автоматическое закрытие после закрытия ассоциации клиентом. SCTP не поддерживает состояние неполного закрытия, поэтому когда клиент вызывает close, все подготовленные сервером данные сбрасываются и ассоциация закрывается.

9.3. Сокет типа «один-к-одному» требует вызова connect, поэтому когда собеседнику отсылается сегмент COOKIE, никаких данных в буфере отправки быть еще не может. Сокет типа «один-ко-многим» допускает отправку данных с одновременной установкой соединения. Поэтому сегмент COOKIE в этом случае может быть совмещен с сегментом DATA.

9.4. Собеседник, с которым устанавливается ассоциация, может прислать данные только в том случае, если у него будет готов сегмент DATA до того, как соединение будет установлено, то есть если на обеих сторонах используются сокеты типа «один-ко-многим» и каждая сторона выполняет операцию send с неявной установкой соединения. Такой процесс установки ассоциации называется коллизией пакетов INIT и подробно описывается в главе 4 [117].

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

 

Глава 10

10.1 Если функция sctp_sendmsg возвращает ошибку, сообщение не будет отправлено, а приложение вызовет функцию sctp_recvmsg и заблокируется в ней навсегда, ожидая ответного сообщения, которое никогда не придет.

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

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

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

10.3. Чтобы каждая порция данных была помещена в свой пакет, мы установили размер сообщения 800 байт. Более правильным решением будет получение значения параметра сокета SCTP_MAXSEG для определения размера данных, помещающихся в один пакет.

10.4. Алгоритм Нагла (управляемый параметром сокета SCTP_NODELAY, см. раздел 7.10) вызывает проблемы только при передаче данных небольших объемов. Если данные передаются порциями такого размера, что SCTP вынужден передавать их немедленно, никакого замедления быть не может. Установка небольшого размера out_sz исказит результаты, потому что в некоторых случаях передача будет задерживаться до получения выборочных уведомлений от собеседника. Поэтому при передаче данных небольшого размера алгоритм Нагла следует отключать.

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

Сокет типа «один-ко-многим» позволяет устанавливать ассоциации неявно. Для изменения параметров ассоциации необходимо вызвать sendmsg со вспомогательными данными. Фактически при этом обязательно использовать неявное установление ассоциации.

 

Глава 11

11.1. В листинге Д.4 приведена программа, вызывающая функцию gethostbyaddr.

Листинг Д.4. Изменение листинга 11.1 для вызова функции gethostbyaddr

//names/hostent2.c

 1 #include "unp.h"

 2 int

 3 main(int argc, char **argv)

 4 {

 5  char *ptr, **pptr;

 6  char str[INET6_ADDRSTRLEN];

 7  struct hostent *hptr;

 8  while (--argc > 0) {

 9   ptr = *++argv;

10   if ( (hptr = gethostbyname(ptr)) == NULL) {

11    err_msg("gethostbyname error for host: %s: %s",

12     ptr, hstrerror(h_errno));

13    continue;

14   }

15   printf("official hostname: %s\n", hptr->h_name);

16   for (pptr = hptr->h_aliases; *pptr != NULL; pptr++)

17    printf(" alias: %s\n", *pptr);

18   switch (hptr->h_addrtype) {

19   case AF_INET:

20 #ifdef AF_INET6

21   case AF_INET6:

22 #endif

23    pptr = hptr->h_addr_list;

24    for (; *pptr != NULL; pptr++) {

25     printf("\taddress: %s\n",

26      Inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));

27     if ((hptr = gethostbyaddr(*pptr, hptr->h_length,

28      ptr->h_addrtype)) == NULL)

29      printf("\t(gethostbyaddr failed)\n");

30     else if (hptr->h_name != NULL)

31      printf("\tname = %s\n", hptr->h_name);

32     else

33      printf("\t(no hostname returned by gethostbyaddr)\n");

34    }

35    break;

36   default:

37    err_ret("unknown address type");

38    break;

39   }

40  }

41  exit(0);

42 }

Эта программа корректно работает на узле с единственным IP-адресом. Если запустить программу из листинга 11.1 на узле с четырьмя IP-адресами, то получим:

freebsd % hostent cnn.com

official hostname: cnn.com

address: 64.236.16.20

address: 64.236.16.52

address: 64.236.16.84

address: 64.236.16.116

address: 64 236.24.4

address: 64.236.24.12

address: 64.236.24.20

address: 64.236.24.28

Но если запустить программу из листинга Д.4 на том же узле, в выводе будет только первый IP-адрес:

freebsd % hostent2 cnn.com

official hostname: cnn.com

address: 64.236.24.4

name = www1.cnn.com

Проблема заключается в том, что две функции, gethostbyname и gethostbyaddr, совместно используют одну и ту же структуру hostent, как было показано в разделе 11.18. Когда наша новая программа вызывает функцию gethostbyaddr, она повторно использует данную структуру вместе с областью памяти, на которую структура указывает (массив указателей h_addr_list), стирая три оставшиеся IP-адреса, возвращаемые функцией gethostbyname.

11.2. Если ваша система не поддерживает повторно входимую версию функции gethostbyaddr (см. раздел 11.19), то прежде чем вызывать функцию gethostbyaddr, вам следует создать копию массива указателей, возвращаемых функцией gethostbyname, и данных, на которые указывает этот массив.

11.3. Сервер chargen отправляет клиенту данные до тех пор, пока клиент не закрывает соединение (то есть пока вы не завершите выполнение клиента).

11.4. Эта возможность поддерживается некоторыми распознавателями, но переносимая программа не может использовать ее, потому что POSIX никак ее не оговаривает. В листинге Д.5 приведена измененная версия. Порядок тестирования строки с именем узла имеет значение. Сначала мы вызываем функцию inet_pton, поскольку она обеспечивает быстрый тест «внутри памяти» (in-memory) для проверки, является ли строка допустимым IP-адресом в точечно-десятичной записи. Только если тест заканчивается неудачно, мы запускаем функцию gethostbyname, которая обычно требует некоторых сетевых ресурсов и времени.

Если строка является допустимым IP-адресом в точечно-десятичной записи, мы создаем свой массив указателей (addrs) на один IP-адрес, оставив без изменений цикл, использующий pptr.

Поскольку адрес уже был переведен в двоичное представление в структуре адреса сокета, мы заменяем вызов функции memcpy в листинге 11.2 на вызов функции memmove, так как при вводе IP-адреса в точечно-десятичной записи исходное и конечное поля в данном вызове одинаковые.

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

//names/daytimetcpcli2.c

 1 #include "unp.h"

 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd, n;

 6  char recvline[MAXLINE + 1];

 7  struct sockaddr_in servaddr;

 8  struct in_addr **pptr, *addrs[2];

 9  struct hostent *hp;

10  struct servent *sp;

11  if (argc != 3)

12   err_quit("usage: daytimetcpcli2 ");

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

14  servaddr.sin_family = AF_INET;

15  if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) == 1) {

16   addrs[0] = &servaddr.sin_addr;

17   addrs[1] = NULL;

18   pptr = &addrs[0];

19  } else if ((hp = gethostbyname(argv[1])) != NULL) {

20   pptr = (struct in_addr**)hp->h_addr_list;

21  } else

22   err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno));

23  if ((n = atoi(argv[2])) > 0)

24   servaddr.sin_port = htons(n);

25  else if ((sp = getservbyname(argv[2], "tcp")) != NULL)

26   servaddr.sin_port = sp->s_port;

27  else

28   err_quit("getservbyname error for %s", argv[2]);

29  for (; *pptr != NULL; pptr++) {

30   sockfd = Socket(AF_INET, SOCK_STREAM, 0);

31   memmove(&servaddr.sin_addr, *pptr, sizeof(struct in_addr));

32   printf("trying %s\n",

33    Sock_ntop((SA*)&servaddr, sizeof(servaddr)));

34   if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) == 0)

35    break; /* успех */

36   err_ret("connect error");

37   close(sockfd);

38  }

39  if (*pptr == NULL)

40   err_quit("unable to connect");

41  while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {

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

43   Fputs(recvline, stdout);

44  }

45  exit(0);

46 }

11.5. Программа приведена в листинге Д.6.

Листинг Д.6. Модификация листинга 11.2 для работы с IPv4 и IPv6

//names/daytimetcpcli3.c

 1 #include "unp.h"

 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd, n;

 6  char recvline[MAXLINE + 1];

 7  struct sockaddr_in servaddr;

 8  struct sockaddr_in6 servaddr6;

 9  struct sockaddr *sa;

10  socklen_t sal en;

11  struct in_addr **pptr;

12  struct hostent *hp;

13  struct servent *sp;

14  if (argc != 3)

15   err_quit("usage: daytimetcpcli3 ");

16  if ((hp = gethostbyname(argv[1])) == NULL)

17   err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno));

18  if ((sp = getservbyname(argv[2], "tcp")) == NULL)

19   err_quit("getservbyname error for %s", argv[2]);

20  pptr = (struct in_addr**)hp->h_addr_list;

21  for (; *pptr != NULL; pptr++) {

22   sockfd = Socket(hp->h_addrtype, SOCK_STREAM, 0);

23   if (hp->h_addrtype == AF_INET) {

24    sa = (SA*)&servaddr;

25    salen = sizeof(servaddr);

26   } else if (hp->h_addrtype == AF_INET6) {

27    sa = (SA*)&servaddr6;

28    salen = sizeof(servaddr6);

29   } else

30    err_quit("unknown addrtype %d", hp->h_addrtype);

31   bzero(sa, salen);

32   sa->sa_family = hp->h_addrtype;

33   sock_set_port(sa, salen, sp->s_port);

34   sock_set_addr(sa, salen, *pptr);

35   printf("trying %s\n", Sock_ntop(sa, salen));

36   if (connect(sockfd, sa, salen) == 0)

37    break; /* успех */

38   err_ret("connect error");

39   close(sockfd);

40  }

41  if (*pptr == NULL)

42   err_quit("unable to connect");

43  while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {

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

45   Fputs(recvline, stdout);

46  }

47  exit(0);

48 }

Используем значение h_addrtype, возвращаемое функцией gethostbyname, для определения типа адреса. Также используем функции sock_set_port и sock_set_addr (см. раздел 3.8), чтобы установить два соответствующих поля в структуре адреса сокета.

Эта программа работает, однако имеется два ограничения. Во-первых, мы должны обрабатывать все различия, следя за h_addrtype и задавая соответствующим образом sa или salen. Более удачным решением было бы иметь библиотечную функцию, которая не только просматривает имя узла и имя службы, но и заполняет всю структуру адреса сокета (например, getaddrinfo, см. раздел 11.6). Во-вторых, эта программа компилируется только на узлах с поддержкой IPv6. Чтобы ее можно было откомпилировать на узле, поддерживающем только IPv4, следует добавить в код огромное количество директив #ifdef, что, несомненно, усложнит программу.

11.7. Разместите в памяти большой буфер (превышающий по размеру любую структуру адреса сокета) и вызовите функцию getsockname. Третий аргумент является аргументом типа «значение-результат», возвращающим фактический размер адресов протоколов. К сожалению, это допускают только структуры адреса сокета с фиксированной длиной (IPv4 и IPv6). Нет гарантии, что этот буфер будет работать с протоколами, которые могут вернуть структуру адреса сокета переменной длины (доменные сокеты Unix, см. главу 15).

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

char host[NI_MAXHOST], serv[NI_MAXSERV];

После того как функция accept возвращает управление, вызываем вместо функции sock_ntop функцию getnameinfo:

if (getnameinfo(cliaddr, len, host, NI_MAXHOST, serv, NI_MAXSERV,

 NI_NUMERICHOST | NI_NUMERICSERV) == 0)

 printf("connection from %s.%s\n", host, serv);

Поскольку мы имеем дело с сервером, определяем флаги NI_NUMERICHOST и NI_NUMERICSERV, чтобы избежать поиска в DNS и /etc/services.

11.9. Первая проблема состоит в том, что второй сервер не может связаться (bind) с тем же портом, что и первый сервер, поскольку не установлен параметр сокета SO_REUSEADDR. Простейший способ справиться с такой ситуацией — создать копию функции udp_server, переименовать ее в udp_server_reuseaddr, сделать так, чтобы она установила параметр сокета, и вызывать ее в сервере.

11.10. Когда клиент выводит Trying 206.62.226.35..., функция gethostname возвращает IP-адрес. Пауза перед этим выводом означает, что распознаватель ищет имя узла. Вывод Connected to bsdi.unpbook.com. значит, что функция connect возвратила управление. Пауза между этими двумя выводами говорит о том, что функция connect пытается установить соединение.

 

Глава 12

12.1. Далее приведен сокращенный листинг. Обратите внимание, что клиент FTP в системе freebsd всегда пытается использовать команду EPRT (независимо от версии IP), но если это не срабатывает, то он пробует команду PORT.

freebsd % ftp aix-4

Connected to aix-4.unpbook.com.

220 aix FTP server ...

...

230 Guest login ok. access restrictions apply.

ftp> debug

Debugging on (debug=1).

ftp> passive

Passive mode: off; fallback to active mode= off

ftp> dir

---> EPRT |1|192 168.42.1|50484|

500 'EPRT |1|192.168.42.1|50484|' command not understood.

disabling epsv4 for this connection

---> PORT 192.168.42.1.197.52

200 PORT command successful.

---> LIST

150 Opening ASCII mode data connection for /bin/ls

...

freebsd % ftp ftp.kame.net

Trying 2001.200:0:4819:203:47ff:fea5:3085...

Connected to orange.kame.net.

220 orange.kame.net FTP server ...

...

230 Guest login ok. access restrictions apply.

ftp> debug

Debugging on (debug=1).

ftp> passive

Passive mode: off; fallback to active mode: off.

ftp> dir

---> EPRT |2|3ffe:b80:3:9ad1::2|50480|

200 EPRT command successful

---> LIST

150 Opening ASCII mode data connection for '/bin/ls'.

 

Глава 13

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

13.2. TCP-версии серверов echo, discard и chargen запускаются как дочерние процессы, после того как демон inetd вызовет функцию fork, поскольку эти три сервера работают, пока клиент не прервет соединение. Два других TCP-сервера, time и daytime, не требуют использования функции fork, поскольку эти службы легко реализовать (получить текущую дату, преобразовать ее, записать и закрыть соединение). Эти два сервера обрабатываются непосредственно демоном inetd. Все пять UDP-служб обрабатываются без использования функции fork, поскольку каждая из них генерирует единственную дейтаграмму в ответ на клиентскую дейтаграмму, которая запускает эту службу. Эти пять служб обрабатываются напрямую демоном inetd.

13.3. Это известная атака типа «отказ в обслуживании» [18]. Первая дейтаграмма с порта 7 заставляет сервер chargen отправить дейтаграмму обратно на порт 7. На эту дейтаграмму приходит эхо-ответ, и серверу chargen посылается другая дейтаграмма. Происходит зацикливание. Одним из решений, реализованным в системе BSD/OS, является игнорирование дейтаграмм, направленных любому внутреннему серверу, если номер порта отправителя пришедшей дейтаграммы принадлежит одному из внутренних серверов. Другим решением может быть запрещение этих внутренних служб — либо с помощью демона inetd на каждом узле, либо на маршрутизаторе, связывающем внутреннюю сеть организации с Интернетом.

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

Причина, по которой демон inetd не делает этого для UDP-сокета, состоит в том, что чтение дейтаграмм (recvfrom) осуществляется с помощью функции exec сервером, а не самим демоном inetd.

Демон inetd может считать дейтаграмму с флагом MSG_PEEK (см. раздел 14.7), только чтобы получить IP-адрес и номер порта клиента, но оставляет саму дейтаграмму для чтения серверу.

 

Глава 14

14.1. Если не установлен обработчик, первый вызов функции signal будет возвращать значение SIG_DFL, а вызов функции signal для восстановления обработчика просто вернет его в исходное состояние.

14.3. Приведем цикл for:

for (;;) {

 if ((n = Recv(sockfd, recvline, MAXLINE, MSG_PEEK)) == 0)

  break; /* сервер закрыл соединение */

 Ioctl(sockfd, FIONREAD, &npend);

 printf("%d bytes from PEEK, %d bytes pending\n", n, npend);

 n = Read(sockfd, recvline, MAXLINE);

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

 Fputs(recvline, stdout);

}

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

exit(main(argc, argv));

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

 

Глава 15

15.1. Функция unlink удаляет имя файла из файловой системы, и когда клиент позже вызовет функцию connect, она не выполнится. Это не влияет на прослушиваемый сокет сервера, но клиенты не смогут выполнить функции connect после вызова функции unlink.

15.2. Клиент не сможет соединиться с сервером с помощью функции connect, даже если полное имя существует, поскольку для успешного соединения с помощью функции connect доменный сокет Unix должен быть открыт и связан с этим полным именем (см. раздел 15.4).

15.3. При выводе адреса протокола клиента путем вызова функции sock_ntop мы получим сообщение datagram from (no pathname bound) (дейтаграмма от (имя не задано)), поскольку по умолчанию с сокетом клиента не связывается никакое имя.

Одним из решений является проверить доменный сокет Unix в функциях udp_client и udp_connect и связать с сокетом при помощи функции bind временное полное имя. Это приведет к зависимости от протокола в библиотечной функции, но не в нашем приложении.

15.4. Даже если мы заставим сервер вернуть в функции write 1 байт на его 26- байтовый ответ, использование функции sleep на стороне клиента гарантирует, что все 26 сегментов будут получены до вызова функции read, в результате чего функция read вернет полный ответ. Это еще одно подтверждение того, что TCP является потоком байтов с отсутствием границ записей.

Чтобы использовать доменные протоколы Unix, запускаем клиент и сервер с двумя аргументами командной строки /lосаl (или /unix) и /tmp/daytime (или любое другое временное имя, которое вы хотите использовать). Ничего не изменится: 26 байт будут возвращаться функцией read каждый раз, когда будет запускаться клиент.

Поскольку для каждой функции send сервер определяет флаг MSG_EOR, каждый байт рассматривается как логическая запись, и функция read при каждом вызове возвращает 1 байт. Причина в том, что Беркли-реализации поддерживают флаг MSG_EOR по умолчанию. Однако этот факт не документирован и не может использоваться в серийном коде. В данном примере мы используем эту особенность, чтобы показать разницу между потоком байтов и ориентированным на записи протоколом. С точки зрения реализации, каждая операция вывода идет в mbuf (буфер памяти) и флаг MSG_EOR сохраняется ядром вместе с mbuf, когда mbuf переходит из отправляющего сокета в приемный буфер принимающего сокета. Когда вызывается функция read, флаг MSG_EOR все еще присоединен к каждому mbuf, так что основная подпрограмма ядра read (поддерживающая флаг MSG_EOR, поскольку некоторые протоколы используют этот флаг) сама возвращает каждый байт. Если бы вместо read мы использовали recvmsg, флаг MSG_EOR возвращался бы в поле msg_flags каждый раз, когда recvmsg возвращала бы 1 байт. Такой подход в TCP не срабатывает, поскольку отправляющий TCP не анализирует флаг MSG_EOR в отсылаемом mbuf и в любом случае у нас нет возможности передать этот флаг принимающему TCP в TCP-заголовке. (Выражаем благодарность Мату Томасу (Matt Thomas) за то, что он указал нам это недокументированное «средство».)

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

Листинг Д.7. Определение фактического количества собранных в очередь соединений для различных значений аргумента backlog

//debug//backlog.c

 1 #include "unp.h"

 2 #define PORT 9999

 3 #define ADDR "127 0.0.1"

 4 #define MAXBACKLOG 100

 5 /* глобальные переменные */

 6 struct sockaddr_in serv;

 7 pid_t pid; /* дочерний процесс */

 8 int pipefd[2];

 9 #define pfd pipefd[1] /* сокет родительского процесса */

10 #define cfd pipefd[0] /* сокет дочернего процесса */

11 /* прототипы функций */

12 void do_parent(void);

13 void do_child(void);

14 int

15 main(int argc, char **argv)

16 {

17  if (argc != 1)

18   err_quit("usage: backlog");

19  Socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd);

20  bzero(&serv, sizeof(serv));

21  serv.sin_family = AF_INET;

22  serv.sin_port = htons(PORT);

23  Inet_pton(AF_INET, ADDR, &serv.sin_addr);

24  if ((pid = Fork()) == 0)

25   do_child();

26  else

27  do_parent();

28  exit(0);

29 }

30 void

31 parent_alrm(int signo)

32 {

33  return; /* прерывание блокированной функции connect() */

34 }

35 void

36 do_parent(void)

27 {

38  int backlog, j, k, junk, fd[MAXBACKLOG + 1];

39  Close(cfd);

40  Signal(SIGALRM, parent_alrm);

41  for (backlog = 0; backlog <= 14; backlogs) {

42   printf("backlog = %d. ", backlog);

43   Write(pfd, &backlog. sizeof(int)); /* сообщение значения дочернему процессу */

44   Read(pfd, &junk, sizeof(int)); /* ожидание дочернего процесса */

45   for (j = 1; j <= MAXBACKLOG; j++) {

46    fd[j] = Socket(AF_INET, SOCK_STREAM, 0);

47    alarm(2);

48    if (connect(fd[j], (SA*)&serv, sizeof(serv)) < 0) {

49     if (errno != EINTR)

50      err_sys("connect error, j = %d", j);

51     printf("timeout, %d connections completed\n", j - 1);

52     for (k = 1; k <= j; k++)

53      Close(fd[k]);

54     break; /* следующее значение backlog */

55    }

56    alarm(0);

57   }

58   if (j > MAXBACKLOG)

59    printf("Id connections?\n", MAXBACKLOG);

60  }

61  backlog = -1; /* сообщаем дочернему процессу, что все сделано */

62  Write(pfd, &backlog, sizeof(int));

63 }

64 void

65 do_child(void)

66 {

67  int listenfd, backlog, junk;

68  const int on = 1;

69  Close(pfd);

70  Read(cfd, &backlog, sizeof(int)); /* ожидание родительского процесса */

71  while (backlog >= 0) {

72   listenfd = Socket(AF_NET, SOCK_STREAM, 0);

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

74   Bind(listenfd, (SA*)&serv, sizeof(serv));

75   Listen(listenfd, backlog); /* начало прослушивания */

76   Write(cfd, &junk, sizeof(int)); /* сообщение родительскому процессу */

77   Read(cfd, &backlog, sizeof(int)); /* ожидание родительского процесса */

78   Close(listenfd); /* также закрывает все соединения в очереди */

79  }

80 }

 

Глава 16

16.1. Дескриптор используется совместно родительским и дочерним процессами, поэтому его счетчик ссылок равен 2. Если родительский процесс вызывает функцию close, счетчик ссылок уменьшается с 2 до 1, и пока он больше нуля, сегмент FIN не посылается. Еще одна цель вызова функции shutdown — послать сегмент FIN, даже если дескриптор больше нуля.

16.2. Родительский процесс продолжит запись в сокет, получивший сегмент FIN, а первый сегмент, посланный серверу, вызовет получение сегмента RST в ответ. После этого функция write пошлет родительскому процессу сигнал SIGPIPE, как показано в разделе 5.12.

16.3. Когда дочерний процесс вызывает функцию getppid для отправки сигнала SIGTERM, возвращаемый идентификатор процесса будет равен 1. Это указывает на процесс init, наследующий все продолжающие работать дочерние процессы, родительские процессы которых завершились. Дочерний процесс будет пытаться послать сигнал процессу init, не имея необходимых прав доступа. Но если не исключается, что данный клиент будет запущен с правами привилегированного пользователя, позволяющими посылать сигналы процессу init, то возвращенное функцией getppid значение должно быть проверено перед отправкой сигнала.

16.4. Если удалить эти две строки, вызывается функция select. Но функция select немедленно завершится, поскольку соединение установлено и сокет открыт для записи. Эта проверка и оператор goto предотвращают ненужный вызов функции select.

16.5. Это может случиться, если сервер отправляет данные сразу, как только завершается его функция accept, и если узел клиента занят, когда приходит второй пакет трехэтапного рукопожатия для завершения соединения со стороны клиента (см. рис. 2.5). SMTP-серверы, например, немедленно отсылают клиенту сообщение по новому соединению, прежде чем произвести из него считывание.

 

Глава 17

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

 

Глава 18

18.1. Элемент sdl_nlen будет равен 5, а элемент sdl_alen будет равен 8. Для этого требуется 21 байт, поэтому размер округляется до 24 байт [128, с. 89] в предположении, что используется 32-разрядная архитектура.

18.2. На этот сокет никогда не посылается ответ от ядра. Данный параметр сокета (SO_USELOOPBACK) определяет, посылает ли ядро ответ отправляющему процессу, как показано на с. 649-650 [128]. По умолчанию этот параметр включен, поскольку большинство процессов ожидают ответа. Но отключение данного параметра препятствует отправке ответов отправителю.

 

Глава 20

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

20.2. Когда в FreeBSD обработчик сигналов записывает байт в канал, а затем завершается, функция select возвращает ошибку EINTR. Она вызывается заново и при завершении сообщает о возможности чтения из канала.

 

Глава 21

21.1.Если запустить программу, то она не выведет ничего. Для предотвращения получения многоадресных дейтаграмм сервером, не ожидающим их, ядро не доставляет дейтаграммы на сокет, не выполнявший никаких многоадресных операций (в частности, не присоединявшийся к группам). Происходит следующее. В адресе получателя UDP-дейтаграммы стоит 224.0.0.1 — это группа всех узлов, в которой должны состоять узлы, поддерживающие многоадресную передачу. UDP-дейтаграмма посылается как многоадресный кадр Ethernet, и все узлы с поддержкой многоадресной передачи должны получить ее, поскольку все они входят в указанную группу. Все отвечающие узлы передают полученную UDP-дейтаграмму серверу времени и даты (обычно он является частью демона inetd), даже если этот сокет не находится в группе. Однако ядро сбрасывает полученную дейтаграмму, потому что процесс, связанный с портом сервера времени и даты, не установил параметры многоадресной передачи.

21.2. В листинге Д.8 показаны простые изменения функции main для связывания (bind) с адресом многоадресной передачи и портом 0.

Листинг Д.8. Функция main UDP-клиента, осуществляющая связывание с адресом многоадресной передачи

//mcast/udpcli06.c

 1 #include "unp.h"

 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;

 6  socklen_t salen;

 7  struct sockaddr *cli, *serv;

 8  if (argc != 2)

 9   err_quit("usage: udpcli06 ");

10  sockfd = Udp_client(argv[1], "daytime", (void**)&serv, &salen);

11  cli = Malloc(salen);

12  memcpy(cli, serv, salen); /* копируем структуру адреса сокета */

13  sock_set_port(cli, salen, 0); /* и устанавливаем порт в 0 */

14  Bind(sockfd, cli, salen);

15  dg_cli(stdin, sockfd, serv, salen);

16  exit(0);

17 }

К сожалению, все три системы, на которых проводилась проверка — FreeBSD 4.8, MacOS X и Linux 2.4.7, — позволяют использовать функцию bind, а затем посылают UDP-дейтаграммы с IP-адресом многоадресной передачи отправителя.

21.3. Если мы запустим программу ping для группы узлов 224.0.0.1 на нашем узле aix, получим следующий вывод:

solaris % ping 224.0.0.1

PING 224.0.0.1: 56 data bytes

64 bytes from 192.168.42.2: icmp_seq=0 ttl=255 time=0 ms

64 bytes from 192.168.42.1: icmp_seq=0 ttl=64 time=1 ms (DUP!)

^C

----224.0.0.1 PING Statistics----

1 packets transmitted. 1 packets received. +1 duplicates. 0% packet loss

round-trip min/avg/max = 0/0/0 ms

Ответили оба узла в правой сети Ethernet на рис. 1.7.

ПРИМЕЧАНИЕ

Для предотвращения определенных типов атак некоторые системы не отвечают на широковещательные и многоадресные ICMP-запросы. Чтобы получить ответ от freebsd, нам пришлось специально настроить эту систему:

freebsd % sysctl net.inet.icmp.bmcastecho=1

21.5. Величина 1 073 741 824 преобразуется в значение с плавающей точкой и делится на 4 294 967 296, что дает значение 0,250. В результате умножения на 1 000 000 получаем значение 250 000 в микросекундах, а это одна четверть секунды. Наибольшая дробная часть получается при делении 4 294 967 295 на 429 4967 296 и составляет 0,99 999 999 976 716 935 634. Умножая это число на 1 000 000 и отбрасывая дробную часть, получаем 999 999 — наибольшее значение количества микросекунд.

 

Глава 22

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

22.2. Да, если ответ содержит 0 байт пользовательских данных (например, структура hdr).

22.3. Поскольку функция select не изменяет структуру timeval, которая определяет ее ограничение по времени, нам следует заметить время отправки первого пакета (оно возвращается в миллисекундах функцией rtt_ts). Если функция select сообщает, что сокет готов к чтению, заметьте текущее время, а если функция recvmsg вызывается повторно, вычислите новый тайм-аут для функции select.

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

22.5. Вызов функции getaddrinfо без аргумента имени узла и без флага AI_PASSIVE заставляет эту функцию считать, что используется локальный адрес 0::1 (для IPv6) или 127.0.0.1 (для IPv4). Напомним, что структура адреса сокета IPv6 возвращается функцией getaddrinfo перед структурой адреса сокета IPv4 при условии, что поддерживается протокол IPv6. Если узел поддерживает оба протокола, вызов функции socket в udp_client закончится успешно при указании семейства протоколов AF_INET6.

В листинге Д.9 приведена не зависящая от протокола версия программы.

Листинг Д.9. Не зависящая от протокола версия программы из раздела 22.6

//advio/udpserv04.c

 1 #include "unpifi.h"

 2 void mydg_echo(int, SA*, socklen_t);

 3 int

 4 main(int argc, char **argv)

 5 {

 6  int sockfd, family, port;

 7  const int on = 1;

 8  pid_t pid;

 9  socklen_t salen;

10  struct sockaddr *sa, *wild;

11  struct ifi_info *ifi, *ifihead;

12  if (argc == 2)

13   sockfd = Udp_client(NULL, argv[1], (void**)&sa, &salen);

14  else if (argc == 3)

15   sockfd = Udp_client(argv[1], argv[2], (void**)&sa, &salen);

16  else

17   err_quit("usage; udpserv04 [ ] ");

18  family = sa->sa_family;

19  port = sock_get_port(sa, salen);

20  Close(sockfd); /* хотим узнать семейство, порт salen */

21  for (ifihead = ifi = Get_ifi_info(family, 1),

22   ifi ! = NULL; ifi = ifi->ifi_next) {

23   /* связывание с многоадресными адресами */

24   sockfd = Socket(family, SOCK_DGRAM, 0);

25   Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

26   sock_set_port(ifi->ifi_addr, salen, port);

27   Bind(sockfd, ifi->ifi_addr, salen);

28   printf("bound %s\n", Sock_ntop(ifi->ifi_addr, salen));

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

30    mydg_echo(sockfd, ifi->ifi_addr, salen);

31    exit(0); /* никогда не выполняется */

32   }

33   if (ifi->ifi_flags & IFF_BROADCAST) {

34    /* попытка связывания с широковещательным адресом */

35    sockfd = Socket(family, SOCK_DGRAM, 0);

36    Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

37    sock_set_port(ifi->ifi_brdaddr, salen, port);

38    if (bind(sockfd, ifi->ifi_brdaddr, salen) < 0) {

39     if (errno == EADDRINUSE) {

40      printf("EADDRINUSE: %s\n",

41       Sock_ntop(ifi->ifi_brdaddr, salen));

42      Close(sockfd);

43      continue;

44     } else

45      err_sys("bind error for %s",

46     Sock_ntop(ifi->ifi_brdaddr, salen));

47    }

48    printf ("bound %s\n", Sock_ntop(ifi->ifi_brdaddr, salen));

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

50     mydg_echo(sockfd, ifi->ifi_brdaddr, salen);

51     exit(0); /* никогда не выполняется */

52    }

53   }

54  }

55  /* связывание с универсальным адресом */

56  sockfd = Socket(family, SOCK_DGRAM, 0);

57  Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

58  wild = Malloc(salen);

59  memcpy(wild, sa, salen); /* копирует семейство и порт */

60  sock_set_wild(wild, salen);

61  Bind(sockfd, wild, salen);

62  printf("bound %s\n", Sock_ntop(wild, salen));

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

64   mydg_echo(sockfd, wild, salen);

65   exit(0); /* никогда не выполняется */

66  }

67  exit(0);

68 }

69 void

70 mydg_echo(int sockfd, SA *myaddr, socklen_t salen)

71 {

72  int n;

73  char mesg[MAXLINE];

74  socklen_t len;

75  struct sockaddr *cli;

76  cli = Malloc(salen);

77  for (;;) {

78   len = salen;

79   n = Recvfrom(sockfd, mesg, MAXLINE, 0, cli, &len);

80   printf("child %d, datagram from %s",

81    getpid(), Sock_ntop(cli, len));

82   printf(", to %s\n", Sock_ntop(myaddr, salen));

83   Sendto(sockfd, mesg, n, 0, cli, len),

84  }

85 }

 

Глава 24

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

24.2. В листинге Д.10 приведена версия программы с использованием функции poll.

Листинг Д.10. Версия программы из листинга 24.4, использующая функцию poll вместо функции select

//oob/tcprecv03p.c

 1 #include "unp.h"

 2 int

 3 main(int argc, char **argv)

 4 {

 5  int listenfd, connfd, n, justreadoob = 0;

 6  char buff[100];

 7  struct pollfd pollfd[1];

 8  if (argc == 2)

 9   listenfd = Tcp_listen(NULL, argv[1], NULL);

10  else if (argc == 3)

11   listenfd = Tcp_listen(argv[1], argv[2], NULL);

12  else

13   err_quit("usage: tcprecv03p [ ] ");

14  connfd = Accept(listenfd, NULL, NULL);

15  pollfd[0].fd = connfd;

16  pollfd[0].events = POLLRDNORM;

17  for (;;) {

18   if (justreadoob == 0)

19    pollfd[0].events |= POLLRDBAND;

20   Poll(pollfd, 1, INFTIM);

21   if (pollfd[0].revents & POLLRDBAND) {

22    n = Recv(connfd, buff, sizeof(buff) - 1, MSG_OOB);

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

24    printf("read %d OOB byte: %s\n", n, buff);

25    justreadoob = 1;

26    pollfd[0].events &= ~POLLRDBAND; /* отключение бита */

27   }

28   if (pollfd[0].revents & POLLRDNORM) {

29    if ((n = Read(connfd, buff, sizeof(buff) - 1)) == 0) {

30     printf("received EOF\n");

31     exit(0);

32    }

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

34    printf("read %d bytes %s\n", n, buff);

35    justreadoob = 0;

36   }

37  }

38 }

 

Глава 25

25.1. Нет, такая модификация приведет к ошибке. Проблема состоит в том, что nqueue уменьшается до того, как завершается обработка элемента массива dg[iget], что позволяет обработчику сигналов считывать новую дейтаграмму в данный элемент массива.

 

Глава 26

26.1. В примере с функцией fork будет использоваться 101 дескриптор, один прослушиваемый сокет и 100 присоединенных сокетов. Но каждый из 101 процесса (один родительский и 100 дочерних) имеет только один открытый дескриптор (игнорируем все остальные, такие как стандартный поток ввода, если сервер не является демоном). В случае сервера с потоками используется 101 дескриптор для одного процесса. Каждым потоком (включая основной) обрабатывается один дескриптор.

26.2. Обмена двумя последними сегментами завершения TCP-соединения (сегмент FIN сервера и сегмент ACK клиента в ответ на сегмент FIN сервера) не произойдет. Это переведет клиентский конец соединения в состояние FIN_WAIT_2 (см. рис. 2.4). Беркли-реализации прервут работу клиентского конца, если он остался в этом состоянии, по тайм-ауту через 11 минут [128, с. 825–827]. У сервера же в конце концов закончатся дескрипторы.

26.3. Это сообщение будет выводиться основным программным потоком в том случае, когда он считывает из сокета признак конца файла и при этом другой поток продолжает работать. Простейший способ выполнить это — объявить другую внешнюю переменную по имени done, инициализируемую нулем. Прежде чем функция copyto программного потока вернет управление, она установит эту переменную в 1. Основной программный поток проверит эту переменную, и если она равна нулю, выведет сообщение об ошибке. Поскольку значение переменной устанавливает только один программный поток, нет необходимости в синхронизации.

 

Глава 27

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

27.2. Мы бы поместили EOL (нулевой байт) в конец буфера.

27.3. Поскольку программа ping создает символьный (неструктурированный) сокет (см. главу 28), она получает полный IP-заголовок, включая все IP-параметры, для каждой дейтаграммы, которую она считывает с помощью функции recvfrom.

27.4. Потому что сервер rlogind запускается демоном inetd (см. раздел 13.5).

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

Ошибка оказалась безвредной, поскольку, как отмечалось, для отключения параметра сокета IP_OPTIONS можно либо задать пустой указатель в качестве четвертого аргумента, либо установить нулевое значение в пятом аргументе (длине) [128, с. 269].

 

Глава 28

28.1. Недоступными являются поле номера версии и поле следующего заголовка в IPv6. Поле полезной длины доступно либо как аргумент одной из функций вывода, либо как возвращаемое значений одной из функций ввода, но если требуется параметр увеличенного поля данных (jumbo payload option), сам параметр приложению недоступен. Заголовок фрагментации также недоступен приложению.

28.2. В конце концов приемный буфер клиентского сокета заполнится, и при этом функция демона write будет заблокирована. Мы не хотим, чтобы это произошло, поскольку демон тогда перестанет обрабатывать данные на всех своих сокетах. Простейшим решением является следующее: демон должен сделать свой конец соединения домена Unix с клиентом неблокируемым. Для этого демон должен вызывать функцию write вместо функции-обертки Write и игнорировать ошибку EWOULDBLOCK.

28.3. По умолчанию Беркли-ядра допускают широковещательную передачу через символьный сокет [128, с. 1057]. Поэтому параметр сокета SO_BROADCAST необходимо определять только для UDP-сокетов.

28.4. Наша программа не проверяет адреса многоадресной передачи и не устанавливает параметр сокета IP_MULTICAST_IF. Следовательно, ядро выбирает исходящий интерфейс, вероятно, просматривая таблицу маршрутизации для 224.0.0.1. Мы также не устанавливаем значение поля IP_MULTICAST_TTL, поэтому по умолчанию оно равно 1, и это правильное значение.

 

Глава 29

29.1. Этот флаг означает, что буфер перехода устанавливается функцией sigsetjmp (см. листинг 29.6). Хотя этот флаг может казаться лишним, существует вероятность, что сигнал может быть доставлен после того, как устанавливается обработчик ошибок, но перед тем как вызывается функция sigsetjmp. Даже если программа не вызывает генерацию сигнала, сигнал всё равно может быть сгенерирован другим путем (например, как в случае с командой kill).

 

Глава 30

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

30.2. Для передачи дескриптора действительно можно вместо потокового сокета использовать сокет дейтаграмм. В случае сокета дейтаграмм родительский процесс не получает признака конца файла на своем конце канала, когда дочерний процесс прерывается преждевременно, но для этих целей родительский процесс может использовать сигнал SIGCHLD. Следует иметь в виду, что эта ситуация отличается от случая с применением нашего демона icmpd (см. раздел 28.7): тогда между клиентом и сервером не было иерархических отношений (родительский процесс — дочерний процесс), поэтому использование признака конца файла было единственным способом для сервера обнаружить исчезновение клиента.

 

Глава 31

31.1. Здесь предполагается, что по умолчанию для протокола осуществляется нормальное завершение при закрытии потока, и для TCP это правильно.