| « Поставить закладку » « Сделать стартовой » | |||
|
|||
|
Глава 2 Основы
Совет 1. Различайте протоколы, требующие и не требующие установления логического соединения Один из фундаментальных вопросов сетевого программирования - это различие между протоколами, требующими установления логического соединения (connection-oriented protocols), и протоколами, не требующими этого (connectionless protocols). Хотя ничего сложного в таком делении нет, но начинающие их часто путают. Частично проблема кроется в выборе слов. Очевидно, что два компьютера должны быть как-то «соединены», если необходимо наладить обмен данными между ними. Тогда что означает «отсутствие логического соединения»? О наличии и отсутствии логического соединения говорят применительно к протоколам. Иными словами, речь идет о способе передачи данных по физическому носителю, а не о самом физическом носителе. Протоколы, требующие и не требующие логического соединения, могут одновременно разделять общий физический носитель; на практике обычно так и бывает. Но если это деление не имеет ничего общего с физическим носителем, по которому передаются данные, то что же лежит в его основе? Главное различие в том, что в протоколах, не требующих соединения, каждый пакет передается независимо от остальных. Тогда как протоколы, устанавливающие соединение, поддерживают информацию о состоянии, которая позволяет следить за последовательностью пакетов. При работе с протоколом, не требующим соединения, каждый пакет, именуемый датаграммой, адресуется и посылается приложением индивидуально (совет 30). С точки зрения протокола каждая датаграмма - это независимая единица, не имеющая ничего общего с другими датаграммами, которыми обмениваются приложения. Примечание Это не означает, что датаграммы независимы с точки зрения приложения. Если приложение реализует нечто более сложное, чем простой протокол запрос-ответ (клиент посылает серверу одиночный запрос и ожидает одиночного ответа на него), то, скорее всего, придется отслеживать состояние. Но суть в том, что приложение, а не протокол, отвечает за поддержание информации о состоянии. Пример сервера, который не требует установления соединения, но следит за последовательностью датаграмм, приведен в листинге 3.6. Обычно это означает, что клиент и сервер не ведут сложного диалога, - клиент посылает запрос, а сервер отвечает на него. Если позже клиент посылает новый запрос, то с точки зрения протокола это новая транзакция, не связанная с предыдущей. Кроме того, протокол не обязательно надежен, то есть сеть предпримет все возможное для доставки каждой датаграммы, но нет гарантий, что ни одна не будет потеряна, задержана или доставлена не в том порядке. С другой стороны, протоколы, требующие установления соединения, самостоятельно отслеживают состояние пакетов, поэтому они используются в приложениях, ведущих развитый диалог. Сохраняемая информация о состоянии позволяет протоколу обеспечить надежную доставку. Например, отправитель запоминает, когда и какие данные послал, но они еще не подтверждены. Если подтверждение не приходит в течение определенного времени, отправитель повторяет передачу. Получатель запоминает, какие данные уже принял, и отбрасывает пакеты-дубликаты. Если пакет поступает не в порядке очередности, то получатель может «придержать» его, пока не придут логически предшествующие пакеты. У типичного протокола, требующего наличия соединения, есть три фазы. Сначала устанавливается соединение между двумя приложениями. Затем происходит обмен данными. И, наконец, когда оба приложения завершили обмен данными, соединение разрывается. Обычно такой протокол сравнивают с телефонным разговором, а протокол, не требующий соединения, - с отправкой письма. Каждое письмо запечатывается в отдельный конверт, на котором пишется адрес. При этом все письма оказываются самостоятельными сущностями. Каждое письмо обрабатывается на почте независимо от других посланий двух данных корреспондентов. Почта не отслеживает историю переписки, то есть состояние последовательности писем. Кроме того, не гарантируется, что письма не затеряются, не задержатся и будут доставлены в правильном порядке. Это соответствует отправке датаграммы протоколом, не требующим установления соединения. Примечание Хаверлок [Haverlock 2000]отмечает, что более правильная аналогия - не письмо, а почтовая открытка, так как письмо с неправилънъш адресом возвращается отправителю, а почтовая открытка - никогда (как и в типичном протоколе, не требующем наличия соединения). А теперь посмотрим, что происходит, когда вы не посылаете письмо другу, а звоните по телефону. Для начала набираете его номер. Друг отвечает. Некоторое время вы разговариваете, потом прощаетесь и вешаете трубки. Так же обстоит дело и в протоколе, требующем соединения. В ходе процедуры установления соединения одна из сторон связывается с другой, стороны обмениваются «приветствиями» (на этом этапе они «договариваются» о тех параметрах и соглашениях, которым будут следовать далее), и соединение вступает в фазу обмена данными. Во время телефонного разговора звонящий знает своего собеседника. И перед каждой фразой не нужно снова набирать номер телефона - соединение установлено. Аналогично в фазе передачи данных протокола, требующего наличия соединения, не надо передавать свой адрес или адрес другой стороны. Эти адреса - часть информации о состоянии, хранящейся вместе с логическим соединением. Остается только посылать данные, не заботясь ни об адресации, ни о других деталях, связанных с протоколом. Как и в разговоре по телефону, каждая сторона, заканчивая передачу данных, информирует об этом собеседника. Когда обе стороны договорились о завершении, они выполняют строго определенную процедуру разрыва соединения. Примечание Хотя указанная аналогия полезна, но она все же не точна. В телефонной сети устанавливается физическое соединение. А приводимое «соединение»- целиком умозрительно, оно состоит лишь из хранящейся на обоих концах информации о состоянии. Чтобы должным образом понять это, подумайте, что произойдет, если хост на одном конце соединения аварийно остановится и начнет перезагружаться. Соединение все еще есть? По отношению к перезагрузившемуся хосту - конечно, нет. Все соединения установлены в его «прошлой жизни». Но для его бывшего «собеседника» соединение по-прежнему существует, так как у него все еще хранится информация о состоянии, и не произошло ничего такого, что сделало бы ее недействительной. В связи с многочисленными недостатками протоколов, не требующих соединения, возникает закономерный вопрос: зачем вообще нужен такой вид протоколов? Позже вы узнаете, что часто встречаются ситуации, когда для создания приложения использование именно такого протокола оправдано. Например, протокол без соединения может легко поддерживать связь одного хоста со многими и наоборот. Между тем протоколы, устанавливающие соединение, должны обычно организовать по одному соединению между каждой парой хостов. Важно то, что протоколы, не требующие наличия соединения, - это фундамент, на котором строятся более сложные протоколы. Рассмотрим набор протоколов TCP/IP. В совете 14 говорится, что TCP/IP - это четырехуровневый стек протоколов (рис. 2.1). Внизу стека находится интерфейсный уровень, который связан непосредственно с аппаратурой. Наверху располагаются такие приложения, как telnet, ftp и другие стандартные и пользовательские программы. Как видно из рис. 2.1, TCP и UDP построены поверх IP. Следовательно, IP - это фундамент, на котором возведено все здание TCP/IP. Но IP предоставляет лишь ненадежный сервис, не требующий установления соединения. Этот протокол принимает пакеты с вышерасположенных уровней, обертывает их в IP-пакет и направляет подходящему аппаратному интерфейсу для отправки в сеть. Послав пакет, IP, как и все протоколы, не устанавливающие соединения, не сохраняет информацию о нем.
Рис. 2.1 Упрощенное представление стека протоколов TCP/IP В этой простоте и заключается главное достоинство протокола IP. Поскольку IP не делает никаких предположений о физической среде передачи данных, он может работать с любым носителем, способным передавать пакеты. Так, IP работает на простых последовательных линиях связи, в локальных сетях на базе технологий Ethernet и Token Ring, в глобальных сетях на основе протоколов Х.25 и ATM (Asynchronous Transfer Mode - асинхронный режим передачи), в беспроводных сетях CDPD (Cellular Digital Packet Data - сотовая система передачи пакетов цифровых данных) и во многих других средах. Хотя эти технологии принципиально различны, с точки зрения IP они не отличаются друг от друга, поскольку способны передавать пакеты. Отсюда следует важнейший вывод: раз IP может работать в любой сети с коммутацией пакетов, то это относится и ко всему набору протоколов TCP/IP. А теперь посмотрим, как протокол TCP пользуется этим простым сервисом, чтобы организовать надежный сервис с поддержкой логических соединений. Поскольку TCP-пакеты (они называются сегментами) посылаются в составе 1Р-да-таграмм, у TCP нет информации, дойдут ли они до адреса, не говоря о возможности искажения данных или о доставке в правильном порядке. Чтобы обеспечить надежность, TCP добавляет к базовому IP-сервису три параметра. Во-первых, в ТСР-сегмент включена контрольная сумма содержащихся в нем данных. Это позволяет в пункте назначения убедиться, что переданные данные не повреждены сетью во время транспортировки. Во-вторых, TCP присваивает каждому байту порядковый номер, так что даже если данные прибывают в пункт назначения не в том порядке, в котором были отправлены, то получатель сможет собрать из них исходное сообщение. Примечание Разумеется, TCP не передает порядковый номер вместе с каждым байтом. Просто в заголовке каждого TCP-сегмента хранится порядковый номер первого байта. Тогда порядковые номера остальных байтов можно вычислить. В-третьих, в TCP имеется механизм подтверждения и повторной передачи, который гарантирует, что каждый сегмент когда-то будет доставлен. Из трех упомянутых выше добавлений механизм подтверждения/повторной передачи самый сложный, поэтому рассмотрим подробнее его работу. Примечание Здесь опускаются некоторые детали. Это обсуждение поверхностно затрагивает многие тонкости протокола TCP и их применение для обеспечения надежного и отказоустойчивого транспортного механизма. Более доступное и подробное изложение вы можете найти в RFC 793 [Postel 1981b] и RFC 1122 [Braden 1989], в книге [Stevens 1994]. В RFC 813 [Clark 1982] обсуждается механизм окон и подтверждений TCP. На каждом конце TCP-соединения поддерживается окно приема, представляющее собой диапазон порядковых номеров байтов, который получатель готов принять от отправителя. Наименьшее значение, соответствующее левому краю окна, - это порядковый номер следующего ожидаемого байта. Наибольшее значение, соответствующее правому краю окна, - это порядковый номер последнего байта, для которого у TCP есть место в буфере. Использование окна приема (вместо посылки только номера следующего ожидаемого байта) повышает надежность протокола за счет предоставления средств управления потоком. Механизм управления потоком предотвращает переполнение буфера TCP. Когда прибывает TCP-сегмент, все байты, порядковые номера которых оказываются вне окна приема, отбрасываются. Это касается как ранее принятых данных (с порядковым номерами левее окна приема), так и данных, для которых нет места в буфере (с порядковым номерами правее окна приема). Если первый допустимый байт в сегменте не является следующим ожидаемым, значит, сегмент прибыл не по порядку. В большинстве реализаций TCP такой сегмент помещается в очередь и находится в ней, пока не придут пропущенные данные. Если же номер первого допустимого байта совпадает со следующим ожидаемым, то данные становятся доступными для приложения, а порядковый номер следующего ожидаемого байта увеличивается на число байтов в сегменте. В этом случае считается, что окно сдвигается вправо на число принятых байтов. Наконец, TCP посылает отправителю подтверждение.(сегмент АСК), содержащее порядковый номер следующего ожидаемого байта. Например, на рис. 2.2а окно приема обведено пунктиром. Вы видите, что порядковый номер следующего ожидаемого байта равен 4, и TCP готов принять 9 байт (с 4 по 12). На рис. 2.26 показано окно приема после поступления байтов с номерами 4-7. Окно сдвинулось вправо на четыре номера, а в сегменте АСК, который пошлет TCP, номер следующего ожидаемого байта будет равен 8.
Рис. 2.2. Окно приема TCP Теперь рассмотрим эту же ситуацию с точки зрения протокола TCP на посылающем конце. Помимо окна приема, TCP поддерживает также окно передачи, разделенное на две части. В одной из них расположены байты, которые уже отосланы, но еще не подтверждены, а в другой - байты, которые еще не отправлены. Предполагается, что на байты 1-3 уже пришло подтверждение, поэтому на рис. 2.3а изображено окно передачи, соответствующее окну приема на рис. 2.2а. На рис. 2.36 вы видите окно передачи после пересылки байтов 4-7, но до прихода подтверждения. TCP еще может послать байты 8-12, не дожидаясь подтверждения от получателя. После отправки байтов 4-7 TCP начинает отсчет тайм-аута ретрансмиссии (retransmission timeout - КТО). Если до срабатывания таймера не пришло подтверждение на все четыре байта, TCP считает, что они потерялись, и посылает их повторно. Примечание Поскольку в многих реализациях не происходит отслеживания того, какие байты были посланы в конкретном сегменте, может случиться, что повторно переданный сегмент содержит больше байтов, чем первоначальный. Например, если байты 8 и 9 были посланы до срабатывания RТО-таймера, то такие реализации повторно передадут байты с 4 по 9. Обратите внимание, что срабатывание RTO-таймера не означает, что исходные данные не дошли до получателя. Например, может потеряться АСК-сегмент с подтверждением или исходный сегмент задержаться в сети на время, большее чем тайм-аут ретрансмиссии. Но ничего страшного в этом нет, так как если первоначально отправленные данные все-таки прибудут, то повторно переданные окажутся вне окна приема TCP и будут отброшены. После получения подтверждения на байты 4-7 передающий TCP «забывает» про них и сдвигает окно передачи вправо, как показано на рис. 2.3в.
Рис. 2.3. Окно передачи TCP TCP обеспечивает прикладного программиста надежным протоколом, требующим установления логических соединений. О таком протоколе рассказывается в совете 9. С другой стороны, UDP предоставляет программисту ненадежный сервис, не требующий соединения. Фактически UDP добавляет лишь два параметра к протоколу IP, поверх которого он построен. Во-первых, необязательную контрольную сумму для обнаружения искаженных данных. Хотя у самого протокола IP тоже есть контрольная сумма, но вычисляется она только для заголовка IP-пакета, поэтому TCP и UDP также включают контрольные суммы для защиты собственных заголовков и данных. Во-вторых, UDP добавляет к IP понятие порта. Для отправки IP-датаграммы конкретному хосту используются IP-адреса, то есть адреса, которые обычно приводятся в стандартной десятичной нотации Internet (совет 2). Но по прибытии на хост назначения датаграмму еще необходимо доставить нужному приложению. Например, один UDP-пакет может быть предназначен для сервиса эхо-контроля, а другой - для сервиса «время дня». Порты как раз и дают способ направления данных нужному приложению (этот процесс называют демультиплексированием). С каждым TCP и UDP-сокетом ассоциирован номер порта. Приложение может явно указать этот номер путем обращения к системному вызову bind или поручить операционной системе выбор порта. Когда пакет прибывает, ядро «ищет» в списке сокетов тот, который ассоциирован с протоколом, парой адресов и парой номеров портов, указанных в пакете. Если сокет найден, то данные обрабатываются соответствующим протоколом (в примерах TCP или UDP) и передаются тем приложениям, которые этот сокет открыли. Примечание Если сокет открыт несколькими процессами или потоками . (thread), то данные может считывать только один из них, остальным они будут недоступны. Возвращаясь к аналогии с телефонными переговорами и письмами, можно сказать, что сетевой адрес в TCP-соединении подобен номеру телефона офисной АТС, а номер порта - это добавочный номер конкретного телефона в офисе. Точно так же UDP-адрес можно представить как адрес многоквартирного дома, а номер порта - как отдельный почтовый ящик в его подъезде. Резюме В этом разделе обсуждены различия между протоколами, которые требуют и не требуют установления логического соединения. Вы узнали, что ненадежные протоколы, в которых происходит обмен датаграммами без установления соединения, - это фундамент, на котором строятся надежные протоколы на базе соединений. Попутно было кратко изложено, как надежный протокол TCP строится на основе ненадежного протокола IP. Также отмечалось, что понятие «соединение» в TCP носит умозрительный характер. Оно состоит из хранящейся информации о состоянии на обоих концах; никакого «физического» соединения, как при телефонном разговоре, не существует. Совет 2. Выясните, что такое подсети и CIDR Длина IP-адреса (в версии IPv4) составляет 32 бита. Адреса принято записывать в десятичной нотации - каждый из четырех байт представляется одним десятичным числом, которые отделяются друг от друга точками. Так, адрес 0x11345678 записывается в виде 17.52.86.120. При записи адресов нужно учитывать, что в некоторых реализациях TCP/IP принято стандартное для языка С соглашение о том, что числа, начинающиеся с нуля, записываются в восьмеричной системе. В таком случае 17.52.86.120 - это не то же самое, что 017.52.86.120. В первом примере адрес сети равен 17, а во втором - 15. Классы адресов По традиции все IP-адреса подразделены на пять классов, показанных на рис. 2.4. Адреса класса D используются для группового вещания, а класс Е зарезервирован для будущих расширений. Остальные классы - А, В и С - предназначены для адресации отдельных сетей и хостов.
Рис. 2.4. Классы IP-адресов Класс адреса определяется числом начальных единичных битов. У адресов класса А вообще нет бита 1 в начале, у адресов класса В - один такой бит, у адресов класса С - два и т.д. Идентификация класса адреса чрезвычайно важна, поскольку от этого зависит интерпретация остальных битов адреса. Остальные биты любого адреса классов А, В и С разделены на две группы. Первая часть любого адреса представляет собой идентификатор сети, вторая -идентификатор хоста внутри этой сети. Прмечание Биты идентификации класса также считаются частью идентификатора сети. Так, 130.50.10.200 - это адрес класса В, в котором идентификатор сети равен 0x8232. Смысл разбивки адресного пространства на классы в том, чтобы обеспечить необходимую гибкость, не теряя адресов. Например, класс А позволяет адресовать сети с огромным (16777214) количеством хостов. Примечание Существует 224, или 16777216 возможных идентификаторов хостов, но адрес 0 и адрес, состоящий из одних единиц, имеют специальный смысл. Адрес из одних единиц - это широковещательный адрес. IP-датаграммы, посланные по этому адресу, доставляются всем хостам в сети. Адрес 0 означает «этот хост» и используется хостом как адрес источника, которому в ходе процедуры начальной загрузки необходимо определить свой истинный сетевой адрес. Поэтому число хостов в сети всегда равно 2" - 2, где п - число бит в части адреса, относящейся к хосту. Поскольку в адресах класса А под идентификатор сети отводятся 7 бит, то всего существует 128 сетей класса А. Примечание Как и в случае идентификаторов хостов, два из этих адресов зарезервированы. Адрес 0 означает «эта сеть» и, аналогично хосту 0, используется для определения адреса сети в ходе начальной загрузки. Адрес 127 — это адрес «собственной» сети хоста. Датаграммы, адресованные сети 127, не должны покидать хост-отправитель. Часто этот адрес называют «возвратным» (loop-back) адресом, поскольку отправленные по нему датаграммы «возвращаются» на тот же самый хост. На другом полюсе располагаются сети класса С. Их очень много, но в каждой может быть не более 254 хостов. Таким образом, адреса класса А предназначены для немногих гигантских сетей с миллионами хостов, тогда как адреса класса С -для миллионов сетей с небольшим количеством хостов. В табл. 2.1 показано, сколько сетей и хостов может существовать в каждом классе, а также диапазоны допустимых адресов. Будем считать, что сеть 127 принадлежит классу А, хотя на самом деле она, конечно, недоступна для адресации. Таблица 2. 1. Число сетей, хостов и диапазоны адресов для классов А, В и С
Первоначально проектировщики набора протоколов TCP/IP полагали, что сети будут исчисляться сотнями, а хосты - тысячами. Примечание В действительности, как отмечается в работе [Huitema 1995], в исходном проекте фигурировали только адреса, которые теперь относятся к классу А. Подразделение на три класса было сделано позже, чтобы иметь более 256 сетей. Появление дешевых, повсеместно применяемых персональных компьютеров привело к значительному росту числа сетей и хостов. Нынешний размер Internet намного превосходит ожидания его проектировщиков. Такой рост выявил некоторые недостатки классов адресов. Прежде всего, число хостов в классах А и В слишком велико. Вспомним, что идентификатор сети, как предполагалось, относится к физической сети, например локальной. Но никто не станет строить физическую сеть из 65000 хостов, не говоря уже о 16000000. Вместо этого большие сети разбиваются на сегменты, взаимосвязанные маршрутизаторами. В качестве простого примера рассмотрим два сегмента сети, изображенной на рис. 2.5.
Рис. 2.5. Сеть из двух сегментов Если хосту HI нужно обратиться к хосту Н2, то он получает физический адрес, соответствующий IP-адресу Н2 (используя для этого метод, свойственный данной реализации физической сети), и помещает датаграмму «на провод». А если хосту HI необходимо обратиться к хосту НЗ? Напрямую послать датаграмму невозможно, даже если известен физический адрес получателя, поскольку HI и НЗ находятся в разных сетях. Поэтому HI должен отправить датаграмму через маршрутизатор R1. Если у двух сегментов разные идентификаторы сетей, то HI по своей маршрутной таблице определяет, что пакеты, адресованные сегменту 2, обрабатываются маршрутизатором R1, и отправляет ему датаграмму в предположении, что тот переправит ее хосту НЗ. Итак, можно назначить двум сегментам различные идентификаторы сети. Но есть и другие решения в рамках системы адресных классов. Во-первых, маршрутная таблица хоста HI может содержать по одному элементу для каждого хоста в сегменте 2, который определит следующего получателя на пути к этому хосту -R1. Такая же таблица должна размещаться на каждом хосте в сегменте 1. Аналогичные таблицы, описывающие достижимость хостов из сегмента 1, следует поместить на каждом хосте из сегмента 2. Очевидно, такое решение плохо масштабируется при значительном количестве хостов. Кроме того, маршрутные таблицы придется вести вручную, что очень скоро станет непосильной задачей для администратора. Поэтому на практике такое решение почти никогда не применяется. Во-вторых, можно реализовать ARP-прокси (proxy ARP) таким образом, чтобы R1 казался для хостов из сегмента 1 одновременно НЗ, Н4 и Н5, а для хостов из сегмента 2 - HI, H2 и R2. Примечание Агента ARP в англоязычной литературе еще называют promiscuous ARP (пропускающий ARP) или ARP hack (трюк ARP). Это решение годится только в случае, когда в физической сети используется протокол ARP (Address Resolution Protocol - протокол разрешения адресов) для отображения IP-адресов на физические адреса. В соответствии с ARP хост, которому нужно получить физический адрес, согласующийся с некоторым IP-адресом, должен послать широковещательное сообщение с просьбой хосту, обладающему данным IP-адресом, выслать свой физический адрес. ARP-запрос получают все хосты в сети, но отвечает только тот, IP-адрес которого совпадает с запрошенным. Если применяется агент ARP, то в случае, когда хосту HI необходимо послать IP-датаграмму НЗ, физический адрес которого неизвестен, он посылает ARP-запрос физического адреса НЗ. Но НЗ этот запрос не получит, поскольку находится в другой сети. Поэтому на запрос отвечает его агент - R1, сообщая свой собственный адрес. Когда R1 получает датаграмму, адресованную НЗ, он переправляет ее конечному адресату. Все происходит так, будто НЗ и HI находятся в одной сети. Как уже отмечалось, агент ARP может работать только в сетях, которые используют протокол ARP и к тому же имеют сравнительно простую топологию. Подумайте, что случится при наличии нескольких маршрутизаторов, соединяющих сегменты 1 и 2. Из вышесказанного следует, что общий способ организовать сети с несколькими сегментами - это назначить каждому сегменту свой идентификатор сети. Но у этого решения есть недостатки. Во-первых, при этом возможна потеря многих адресов в каждой сети. Так, если у любого сегмента сети имеется свой адрес класса В, то большая часть IP-адресов просто не будет использоваться. Во-вторых, маршрутная таблица любого узла, который направляет датаграммы напрямую в комбинированную сеть, должна содержать по одной записи для каждого сегмента. В указанном примере это не так страшно. Но вообразите сеть из нескольких сотен сегментов, а таких сетей может быть много. Понятно, что размеры маршрутных таблиц станут громадными. Примечание Эта проблема более серьезна, чем может показаться на первый взгляд. Объем памяти маршрутизаторов обычно ограничен, и нередко маршрутные таблицы размещаются в памяти специального назначения на сетевых картах. Реальные примеры отказа маршрутизаторов из-за роста маршрутных таблиц рассматриваются в работе [Huitema 1995]. Обратите внимание, что эти проблемы не возникают при наличии хотя бы одного идентификатора сети. IP-адреса не остаются неиспользованными, поскольку при потребности в новых хостах можно всегда добавить новый сегмент. С другой стороны, так как имеется лишь один идентификатор сети, в любой маршрутной таблице необходима всего одна запись для отправки датаграмм любому хосту в этой сети. Подсети Мне хотелось найти решение, сочетающее два достоинства: во-первых, небольшие маршрутные таблицы и эффективное использование адресного пространства, обеспечиваемые единым идентификатором сети, во-вторых, простота маршрутизации, характерная для сетей, имеющих сегменты с разными идентификаторами сети. Желательно, чтобы внешние хосты «видели» только одну сеть, а внутренние - несколько сетей, по одной для каждого сегмента. Это достигается с помощью механизма подсетей. Идея очень проста. Поскольку внешние хосты для принятия решения о выборе маршрута используют только идентификатор сети, администратор может распределять идентификаторы хостов по своему усмотрению. Таким образом, идентификатор хоста - это закрытая структура, не имеющая вне данной сети интерпретации. Разделение на подсети осуществляется по следующему принципу. Одна часть идентификатора хоста служит для определения сегмента (то есть подсети), в состав которого входит хост, а другая - для идентификации конкретного хоста. Рассмотрим, например, сеть класса В с адресом 190.50.0.0. Можно считать, что третий байт адреса - это идентификатор подсети, а четвертый байт - номер хоста в этой подсети. На рис. 2.6а приведена структура адреса с точки зрения внешнего компьютера. Идентификатор хоста - это поле с заранее неизвестной структурой. На рис. 2.66 показано, как эта структура выглядит изнутри сети. Вы видите, что она состоит из идентификатора подсети и номера хоста.
Рис. 2.6 Два взгляда на адрес сети класса В с подсетями В приведенном примере взят адрес класса В, и поле номера хоста выделено по границе байта. Но это необязательно. На подсети можно разбивать сети классов А, В и С и часто не по границе байта. С каждой подсетью ассоциируется маска подсети, которой определяется, какая часть адреса отведена под идентификаторы сети и подсети, а какая - под номер хоста. Так, маска подсети для примера, показанного на рис. 2.66, будет OxffffffOO. В основном маска записывается в десятичной нотации (255.255.255.0), но если разбивка проходит не по границе байта, то удобнее первая форма. Примечание Обратите внимание, что, хотя говорится о маске подсети, фактически она выделяет части, относящиеся как к сети, так и к подсети, то есть все, кроме номера хоста. Предположим, что для идентификатора подсети отведено 10 бит, а для номера хоста - 6 бит. Тогда маска подсети будет 255.255.255.192 (OxffffffcO). Как следует из рис. 2.7, в результате наложения этой маски на адрес 190.50.7.75 получается номер сети/подсети, равный 190.70.7.64. Для проверки убедитесь, что адрес 190.50.7.75 принадлежит хосту 11 в подсети 29 сети 190.50.0.0. Важно не забывать, что эта интерпретация имеет смысл только внутри сети. Для внешнего мира адрес интерпретируется как хост 1867 в сети 190.50.0.0. Теперь следует выяснить, как маршрутизаторы на рис. 2.5 могут воспользоваться структурой идентификатора хоста для рассылки датаграмм внутри сети. Предположим, что есть сеть класса В с адресом 190.5.0.0 и маска подсети равна 255.255.255.0. Такая структура показана на рис. 2.66.
Рис. 2.7. Наложение маски подсети с помощью операции AND для выделения сетевой части IP-адреса На рис. 2.8 первому сегменту назначен идентификатор подсети 1, а второму - идентификатор подсети 2. Рядом с сетевым интерфейсом каждого хоста указан его IP-адрес. Обратите внимание, что третий байт каждого адреса - это номер подсети, которой принадлежит интерфейс. Однако внешнему компьютеру эта интерпретация неизвестна.
Рис. 2.8. Сеть с подсетями Возвращаясь к вышесказанному, следует выяснить, что происходит, когда хосту HI нужно обратиться к хосту НЗ. HI берет адрес НЗ (190.50.2.1) и накладывает на него маску подсети (255.255.255.0), получая в результате 190.5.2.0. Поскольку HI находится в подсети 190.5.1.0, то НЗ напрямую недоступен, поэтому он сверяется со своей маршрутной таблицей и обнаруживает, что следующий адрес на пути к НЗ - это R1. Примечание Во многих реализациях эти два шага объединены за счет помещения в маршрутную таблицу обеих подсетей. При поиске маршрута IP выявляет одно из двух: либо целевая сеть доступна непосредственно, либо датаграмму надо отослать промежуточному маршрутизатору. Затем HI отображает IP-адрес R1 на его физический адрес (например, с помощью протокола ARP) и посылает R1 датаграмму. R1 ищет адрес назначения в своей маршрутной таблице, пользуясь той же маской подсети, и определяет местонахождение НЗ в подсети, соединенной с его интерфейсом 190.50.2.4. После чего R1 доставляет датаграмму хосту НЗ, получив предварительно его физический адрес по IP-адресу, - для этого достаточно передать датаграмму сетевому интерфейсу 190.50.2.4. А теперь предположим, что HI необходимо отправить датаграмму Н2. При наложении маски подсети на адрес Н2 (190.5.1.2) получается 190.50.1.0, то есть та же подсеть, в которой находится сам хост HI. Поэтому HI нужно только получить физический адрес Н2 и отправить ему датаграмму напрямую. Далее разберемся, что происходит, когда хосту Е из внешней сети нужно отправить датаграмму НЗ. Поскольку 190.50.2.1 - адрес класса В, то маршрутизатору на границе сети хоста Е известно, что НЗ находится в сети 190.50.0.0. Так как шлюзом в эту сеть является R2, рано или поздно датаграмма от хоста Е дойдет до этого маршрутизатора. С этого момента все совершается так же, как при отправке датаграммы хостом HI: R2 накладывает маску, выделяет адрес подсети 190.50.2.0, определяет R1 в качестве следующего узла на пути к НЗ и посылает R1 датаграмму, которую тот переправляет НЗ. Заметьте, что хосту Е неизвестна внутренняя топология сети 190.50.0.0. Он просто посылает датаграмму шлюзу R2. Только R2 и другие хосты внутри сети определяют существование подсетей и маршруты доступа к ним. Важный момент, который нужно помнить, - маска подсети ассоциируется с сетевым интерфейсом и, следовательно, с записью в маршрутной таблице. Это означает, что разные подсети в принципе могут иметь разные маски. Предположим, что адрес класса В 190.50.0.0 принадлежит университетской сети, а каждому факультету выделена подсеть с маской 255.255.255.0 (на рис. 2.8 показана только часть всей сети). Администратор факультета информатики, которому назначена подсеть 5, решает выделить один сегмент сети Ethernet компьютерному классу, а другой - всем остальным факультетским компьютерам. Он мог бы потребовать у администрации университета еще один номер подсети, но в компьютерном классе всего несколько машин, так что нет смысла выделять ему адресное пространство, эквивалентное целой подсети класса С. Вместо этого он предпочел разбить свою подсеть на два сегмента, то есть создать подсеть внутри подсети. Для этого он увеличивает длину поля подсети до 10 бит и использует маску 255.255.255.192. В результате структура адреса выглядит, как показано на рис. 2.9. Старшие 8 бит идентификатора подсети всегда равны 0000 0101 (5), поскольку основная сеть адресует всю подсеть как подсеть 5. Биты X и Y определяют, какой Ethernet-сегмент внутри подсети 190.50.5.0 адресуется. Из рис. 2.10 видно, что если XY = 10, то адресуется подсеть в компьютерном классе, а если XY = 01 - оставшаяся часть сети. Частично топология подсети 190.50.5.0 изображена на рис. 2.10.
Рис. 2.9 Структура адреса для подсети 190.50.5.0 В верхнем сегменте (подсеть 190.50.1.0) на рис. 2.10 расположен маршрутизатор R2, обеспечивающий выход во внешний мир, такой же, как на рис. 2.8. Подсеть 190.50.2.0 здесь не показана. Средний сегмент (подсеть 190.50.5.128) - это локальная сеть Ethernet в компьютерном классе. Нижний сегмент (подсеть 190.50.5.64) - это сеть Ethernet, объединяющая остальные факультетские компьютеры. Для упрощения номер хоста каждой машины один и тот же для всех ее сетевых интерфейсов и совпадает с числом внутри прямоугольника, представляющего хост или маршрутизатор.
Рис. 2.10. Подсеть внутри подсети Маска подсети для интерфейсов, подсоединенных к подсетям 190.50.5.64 и 190.50.5.128, равна 255.255.255.192, а к подсети 190.50.1.0 - 255.255.255.0. Эта ситуация в точности аналогична предыдущей, которая рассматривалась для рис. 2.8. Так же, как хостам вне сети 190.50.0.0 неизвестно то, что третий байт адреса определяет подсеть, так и хосты в сети 190.50.0.0, но вне подсети 190.50.5.0, не могут определить, что первые два бита четвертого байта задают подсеть подсети 190.50.5.0. Теперь кратко остановимся на широковещательных адресах. При использовании подсетей существует четыре типа таких адресов для вещания: ограниченный, на сеть, на подсеть и на все подсети. Ограниченное вещание Адрес для ограниченного вещания - 255.255.255.255. Вещание называется ограниченным, поскольку датаграммы, посланные на этот адрес, не уходят дальше маршрутизатора. Они ограничены локальным кабелем. Такое широковещание применяется, главным образом, во время начальной загрузки, если хосту неизвестен свой IP-адрес или маска своей подсети. Процесс передачи широковещательной датаграммы хостом, имеющим несколько сетевых интерфейсов, зависит от реализации. Во многих реализациях датаграмма отправляется только по одному интерфейсу. Чтобы приложение отправило широковещательную датаграмму по нескольким интерфейсам, ему необходимо узнать у операционной системы, какие интерфейсы сконфигурированы для поддержки широковещания. Вещание на сеть В адресе для вещания на сеть идентификатор сети определяет адрес этой сети, а идентификатор хоста состоит из одних единиц. Например, для вещания на сеть 190.50.0.0 используется адрес 190.50.255.255. Датаграммы, посылаемые на такой адрес, доставляются всем хостам указанной сети. Требования к машрутизаторам (RFC 1812) [Baker 1995] предусматривают по умолчанию пропуск маршрутизатором сообщений, вещаемых на сеть, но эту возможность можно отключить. Во избежание атак типа «отказ от обслуживания» (denial of service), которые используют возможности, предоставляемые направленным широковещанием, во многих маршрутизаторах пропуск таких датаграмм, скорее всего, будет заблокирован. Вещание на подсеть В адресе для вещания на все подсети идентификаторы сети и подсети определяют соответствующие адреса, а идентификатор хоста состоит из одних единиц. Не зная маски подсети, невозможно определить, является ли данный адрес адресом для вещания на подсеть. Например, адрес 190.50.1.255 можно трактовать как адрес для вещания на подсеть только при условии, если маршрутизатор имеет информацию, что маска подсети равна 255.255.255.0. Если же известно, что маска подсети равна 255.255.0.0, то это адрес не считается широковещательным. При использовании бесклассовой междоменной маршрутизации (CIDR), которая будет рассмотрена ниже, широковещательный адрес этого типа такой же, как и адрес вещания на сеть; RFC 1812 предлагает трактовать их одинаково. Вещание на все подсети В адресе для вещания на все подсети задан идентификатор сети, а адреса подсети и хоста состоят из одних единиц. Как и при вещании на подсеть, для опознания такого адреса необходимо знать маску подсети. К сожалению, применение адреса для вещания на все подсети сопряжено с некоторыми проблемами, поэтому этот режим не внедрен. При использовании CIDR этот вид широковещания не нужен и, по RFC 1812, «отправлен на свалку истории». Ни один из описанных широковещательных адресов нельзя использовать в качестве адреса источника IP-датаграммы. И, наконец, следует отметить, что в некоторых ранних реализациях TCP/IP, например в системе 4.2BSD, для выделения широковещательного адреса в поле идентификатора хоста ставились не единицы, а нули. Бесклассовая междоменная маршрутизация - CIDR Теперь вам известно, как организация подсетей решает одну из проблем, связанных с классами адресов: переполнение маршрутных таблиц. Хотя и в меньшей степени, подсети все же позволяют справиться и с проблемой истощения IP-адресов за счет лучшего использования пула идентификаторов хостов в пределах одной сети. Еще одна серьезная проблема - это недостаток сетей класса В. Как показано на рис. 2.5, существует менее 17000 таких сетей. Поскольку большинство средних и крупных организаций нуждается в количестве IP-адресов, превышающем возможности сети класса С, им выделяется идентификатор сети класса В. В условиях дефицита сетей класса В организациям приходилось выделять блоки адресов сетей класса С, но при этом вновь возникает проблема, которую пытались решить с помощью подсетей, - растут маршрутные таблицы. Бесклассовая междоменная маршрутизация (CIDR) решает эту проблему, вывернув принцип организации подсетей «наизнанку». Вместо увеличения CIDR уменьшает длину идентификатора сети в IP-адресе. Предположим, некоторой организации нужно 1000 IP-адресов. Ей выделяют четыре соседних идентификатора сетей класса С с общим префиксом от 200.10.4.0 до 200.10.7.0. Первые 22 бита этих идентификаторов одинаковы и представляют номер агрегированной сети, в данном случае 200.10.4.0. Как и для подсетей, для идентификации сетевой части IP-адреса используется маска сети. В приведенном здесь примере она равна 255.255.252.0 (OxfffffcOO). Но в отличие от подсетей эта маска сети не расширяет сетевую часть адреса, а укорачивает ее. Поэтому CIDR называют также суперсетями. Кроме того, маска сети в отличие от маски подсети экспортируется во внешний мир. Она становится частью любой записи маршрутной таблицы, ссылающейся на данную сеть. Допустим, внешнему маршрутизатору R надо переправить датаграмму по адресу 200.10.5.33, который принадлежит одному из хостов в агрегированной сети. Он просматривает записи в своей маршрутной таблице, в каждой из которых хранится маска сети, и сравнивает замаскированную часть адреса 200.10.5.33 с хранящимся в записи значением. Если в таблице есть запись для сети, то в ней будет храниться адрес 200.10.4.0 и маска сети 255.255.252.0. Когда выполняется операция побитового AND между адресом 200.10.5.33 и этой маской, получается значение 200.10.4.0. Это значение совпадает с хранящимся в записи номером подсети, так что маршрутизатору известно, что именно по этому адресу следует переправить датаграмму. Если возникает неоднозначность, то берется самое длинное соответствие. Например, в маршрутной таблице может быть также запись с адресом 200.10.0.0 и маской сети 255.255.0.0. Эта запись также соответствует адресу 200.10.5.33, но поскольку для нее совпадают только 16 бит, а не 22, как в первом случае, то предпочтение отдается первой записи. Примечание Может случиться так, что Internet сервис-провайдер (ISP) «владеет» всеми IP-адресами с префиксом 200.10. В соответствии со второй из рассмотренных выше записей маршрутизатор отправил бы этому провайдеру все датаграммы, адрес назначения которых начинается с 200.10. Тогда провайдер смог бы указать более точный маршрут, чтобы избежать лишних звеньев в маршруте или по какой-то иной причине. В действительности механизм CIDR более общий. Он называется «бесклассовым», так как понятие «класса» в нем полностью отсутствует. Таким образом, каждая запись в маршрутной таблице содержит маску сети, определяющую сетевую часть IP-адреса. Если принять, что адрес принадлежит некоторому классу, то эта маска может укоротить или удлинить сетевую часть адреса. Но поскольку в CIDR понятия «класса» нет, то можно считать, что сетевая маска выделяет сетевую часть адреса без изменения ее длины. В действительности, маска - это всего лишь число, называемое префиксом, которое определяет число бит в сетевой части адреса. Например, для вышеупомянутой агрегированной сети префикс равен 22, и адрес этой сети следовало бы записать как 200.10.4.0/22, где /22 обозначает префикс. С этой точки зрения адресацию на основе классов можно считать частным случаем CIDR, когда имеется всего четыре (или пять) возможных префиксов, закодированных в старших битах адреса. Гибкость, с которой CIDR позволяет задавать размер адреса сети, позволяет эффективно распределять IP-адреса блоками, размер которых оптимально соответствует потребностям сети. Вы уже видели, как можно использовать CIDR для агрегирования нескольких сетей класса С в одну большую сеть. А для организации маленькой сети из нескольких хостов можно выделить лишь часть адресов сети класса С. Например, сервис-провайдер выделяет небольшой компании с единственной ЛВС адрес сети 200.50.17.128/26. В такой сети может существовать до 62 хостов (26-2). В RFC 1518 [Rekhter и Li 1993] при обсуждении вопроса об агрегировании адресов и его влиянии на размер маршрутных таблиц рекомендуется выделять префиксы IP-адресов (то есть сетевые части адреса) иерархически. Примечание Иерархическое агрегирование адресов можно сравнить с иерархической файловой системой вроде тех, что используются в UNIX и Windows. Так же, как каталог верхнего уровня содержит информацию о своих подкаталогах, но не имеет сведений о находящихся в них файлах, доменам маршрутизации верхнего уровня известно лишь о промежуточных доменах, а не о конкретных сетях внутри них. Предположим, что региональный провайдер обеспечивает весь трафик для префикса 200/8, а к нему подключены три локальных провайдера с префиксами 200.1/16,200.2/16 и 200.3/16. У каждого провайдера есть несколько клиентов, которым выделены части располагаемого адресного пространства (200.1.5/24 и т.д.). Маршрутизаторы, внешние по отношению к региональному провайдеру, должны хранить в своих таблицах только одну запись - 200/8. Этого достаточно для достижения любого хоста в данном диапазоне адресов. Решения о выборе маршрута можно принимать, даже не зная о разбиении адресного пространства 200/8. Маршрутизатор регионального провайдера должен хранить в своей таблице только три записи: по одной для каждого локального провайдера. На самом нижнем уровне локальный провайдер хранит записи для каждого своего клиента. Этот простой пример позволяет видеть суть агрегирования. Почитать RFC 1518 очень полезно, поскольку в этом документе демонстрируются преимущества использования CIDR. В RFC 1519 [Fuller et al. 1993] описаны CIDR и ее логическое обоснование, а также приведены подробный анализ затрат, связанных с CIDR, и некоторые изменения, которые придется внести в протоколы междоменной маршрутизации. Текущее состояние организации подсетей и CIDR Подсети в том виде, в каком они описаны в RFC 950 [Mogul and Postel 1985], -это часть Стандартного протокола (Std. 5). Это означает, что каждый хост, на котором установлен стек TCP/IP, обязан поддерживать подсети. CIDR (RFC 1517 [Hinden 1993], RFC 1518, RFC 1519) - часть предложений к стандартному протоколу, и потому не является обязательной. Тем не менее CIDR применяется в Internet почти повсеместно, и все новые адреса выделяются этим способом. Группа по перспективным разработкам в Internet (IESG - Internet Engineering Steering Group) выбрала CIDR как промежуточное временное решение проблемы роста маршрутных таблиц. В перспективе обе проблемы - исчерпания адресов и роста маршрутных таблиц - предполагается решать с помощью версии 6 протокола IP. IPv6 имеет большее адресное пространство (128 бит) и изначально поддерживает иерархию. Такое адресное пространство (включая 64 бита для идентификатора интерфейса) гарантирует, что вскоре IP-адресов будет достаточно. Иерархия 1Р'6-адресов позволяет держать размер маршрутных таблиц в разумных пределах. Резюме В этом разделе рассмотрены подсети и бесклассовая междоменная маршрутизация (CIDR). Вы узнали, как они применяются для решения двух проблем, свойственных адресации на основе классов. Подсети позволяют предотвратить рост маршрутных таблиц, обеспечивая в то же время гибкую адресацию. CIDR служит для эффективного выделения IP-адресов и способствует их иерархическому назначению. Совет 3. Разберитесь, что такое частные адреса и NAT Раньше, когда доступ в Internet еще не был повсеместно распространен, организации выбирали произвольный блок IP-адресов для своих сетей. Считалось, что сеть не подключена и «никогда не будет подключена» к внешним сетям, поэтому выбор IP-адресов не имеет значения. Но жизнь не стоит на месте, и в настоящее время очень мало сетей, которые не имеют выхода в Internet. Теперь необязательно выбирать для частной сети произвольный блок IP-адресов. В RFC 1918 [Rekhter, Moskowitz et al. 1996] специфицированы три блока адресов, которые не будут выделяться:
Если использовать для своей сети один из этих блоков, то любой хост сможет обратиться к другому хосту в этой же сети, не опасаясь конфликта с глобально выделенным IP-адресом. Разумеется, пока сеть не имеет выхода во внешние сети, выбор адресов не имеет значения. Но почему бы сразу не воспользоваться одним из блоков частных адресов и не застраховаться тем самым от неприятностей, которые могут произойти, когда внешний выход все-таки появится? Что случится, когда сеть получит внешний выход? Как хост с частным IP-адресом сможет общаться с другим хостом в Internet или другой внешней сети? Самый распространенный ответ - нужно воспользоваться преобразованием сетевых адресов (Network Address Translation - NAT). Есть несколько типов устройств, поддерживающих NAT. Среди них маршрутизаторы, межсетевые экраны (firewalls) и автономные устройства с поддержкой NAT. Принцип работы NAT заключается в преобразовании между частными сетевыми адресами и одним или несколькими глобально выделенными IP-адресами. Большинство устройств с поддержкой NAT можно сконфигурировать в трех режимах:
На рис. 2.11 представлена небольшая сеть с тремя хостами, для которой используется блок адресов 10/8. Имеется также маршрутизатор, помеченый: NАТ, у которого есть адрес в частной сети и адрес в Internet.
Рис. 2.11 Частная сеть с маршрутизатором, который поддерживает NAT Поскольку показан только один глобальный адрес, ассоциированный с NAT, предположим, что маршрутизатор сконфигурирован с возможностью использования метода PAT. Статический режим и режим выбора из пула аналогичны методу PAT, но проще его, поскольку не нужно преобразовывать еще и номера портов. Допустим, что хосту Н2 надо отправить SYN-сегмент TCP по адресу 204.71.200.69 -на один из Web-серверов www.yahoo.com. - чтобы открыть соединение. На рис. 2.12а видно, что у сегмента, покидающего Н2, адрес получателя равен 204.71.200.69.80, а адрес отправителя - 10.0.0.2.9600. Примечание Здесь использована стандартная нотация, согласно которой адрес, записанный в форме A.B.C.D.P означает IP-адрес A.B.C.D и порт Р. В этом нет ничего особенного, за исключением того, что адрес отправителя принадлежит частной сети. Когда этот сегмент доходит до маршрутизатора, NAT должен заменить адрес отправителя на 205.184.151.171, чтобы Web-сервер на сайте Yahoo знал, куда посылать сегмент SYN/ACK и последующие. Поскольку во всех пакетах, исходящих от других хостов в частной сети, адрес отправителя также будет заменен на 205.184.151.171, NAT необходимо изменить еще и номер порта на некоторое уникальное значение, чтобы потом определять, какому хосту следует переправлять входящие пакеты. Исходящий порт 9600 преобразуется в 5555. Таким образом, у сегмента, доставленного на сайт Yahoo, адрес получателя будет 204.71.200.69.80, а адрес отправителя - 205.184.151.171.5555.
Рис. 2.12. Преобразование адресов портов Из рис. 2.126 видно также, что в дошедшем до маршрутизатора ответе Yahoo адрес получателя равен 205.184.151.171.5555. NAT ищет этот номер порта в своей внутренней таблице и обнаруживает, что порт 5555 соответствует адресу 10.0.0.1.9600, так что после получения от маршрутизатора этого пакета в хосте Н2 появится информация, что адрес отправителя равен 204.71.200.69.80, а адрес получателя-10.0.0.1.9600. Описанный здесь метод PAT выглядит довольно примитивно, но есть много усложняющих его деталей. Например, при изменении адреса отправителя или номера исходящего порта меняются как контрольная сумма заголовка 1Р-датаграммы, так и контрольная сумма TCP-сегмента, поэтому их необходимо скорректировать. В качестве другого примера возможных осложнений рассмотрим протокол передачи файлов FTP (File Transfer Protocol) [Reynolds and Postel 1985]. Когда FTP-клиенту нужно отправить файл или принять его от FTP-сервера, серверу посылается команда PORT с указанием адреса и номера порта, по которому будет ожидаться соединение (для передачи данных) от сервера. При этом NAT нужно распознать TCP-сегмент, содержащий команду PORT протокола FTP, и подменить в ней адрес и порт. В команде PORT адрес и номер порта представлены в виде ASCII-строк, поэтому при их подмене может измениться размер сегмента. А это, в свою очередь, повлечет изменение порядковых номеров байтов. Так что NAT должен за этим следить, чтобы вовремя скорректировать порядковые номера в сегменте подтверждения АСК, а также в последующих сегментах с того же хоста. Несмотря на все эти сложности, NAT работает неплохо и широко распространен. В частности, PAT - это естественный способ подключения небольших сетей к Internet в ситуации, когда имеется только одна точка выхода. Резюме В этом разделе показано, как схема NAT позволяет использовать один из блоков частных сетевых адресов для внутренних хостов, сохраняя при этом возможность выхода в Internet. Метод PAT, в частности, особенно полезен для небольших сетей, у которых есть только один глобально выделенный IP-адрес. К сожалению, поскольку PAT изменяет номер порта в исходящих пакетах, он может оказаться несовместимым с нестандартными протоколами, которые передают информацию о номерах портов в теле сообщения. Совет 4. Разрабатывайте и применяйте каркасы приложений Большинство приложений TCP/IP попадают в одну из четырех категорий: D TCP-сервер; Q TCP-клиент; а UDP-сервер; а UDP-клиент. В приложениях одной категории обычно встречается почти одинаковый «стартовый» код, который инициализирует все, что связано с сетью. Например, TCP-сервер должен поместить в поля структуры sockaddr_in адрес и порт получателя, получить от системы сокет типа SOCK_STREAM, привязать к нему выбранный адрес и номер порта, установить опцию сокета SO_REUSEADDR (совет 23), вызвать listen, а затем быть готовым к приему соединения (или нескольких соединений) с помощью системного вызова accept. На каждом из этих этапов следует проверять код возврата. А часть программы, занимающаяся преобразованием адресов, должна иметь дело как с числовыми, так и с символическими адресами и номерами портов. Таким образом, в любом TCP-сервере есть порядка 100 почти одинаковых строк кода для выполнения всех перечисленных выше задач. Один из способов решения этой проблемы - поместить стартовый код в одну или несколько библиотечных функций, которые приложение может вызвать. Эта стратегия использована в книге. Но иногда приложению нужна слегка видоизмененная последовательность инициализации. В таком случае придется либо написать ее с нуля, либо извлечь нужный фрагмент кода из библиотеки и подправить его. Чтобы справиться и с такими ситуациями, можно построить каркас приложения, в котором уже есть весь необходимый код. Затем скопировать этот каркас, внести необходимые изменения, после чего заняться логикой самого приложения. Не имея каркаса, легко поддаться искушению и срезать некоторые углы, например, жестко «зашить» в приложение адреса (совет 29) или сделать еще что-то сомнительное. Разработав каркас, вы сможете убрать все типичные функции в библиотеку, а каркас оставить только для необычных задач. Чтобы сделать программы переносимыми, следует определить несколько макросов, в которых скрыть различия между API систем UNIX и Windows. Например, в UNIX системный вызов для закрытия сокета называется close, а в Windows -closesocket. Версии этих макросов для UNIX показаны в листинге 2.1. Версии для Windows аналогичны, приведены в приложении 2. Доступ к этим макросам из каркасов осуществляется путем включения файла skel. h. Листинг 2.1. Заголовочный файл skel.h skel.h 1 ftifndef _SKEL_H_ 2 ttdefine _SKEL_H_ 3 /* версия для UNIX */ 4 #define INIT() ( program_name = 5 strrchrf argv[ 0 ], '/' ) ) ? 6 program_name++ : 7 ( program_name = argv[ 0 ] ) 8 ttdefine EXIT(s) exit( s ) 9 tdefine CLOSE(s) if ( close( s ) ) error( 1, errno, 10 "ошибка close " ) 11 #define set_errno(e) errno = ( e ) 12 ttdefine isvalidsock(s) ( ( s ) >= 0 ) 13 typedef int SOCKET; 14 tendif /* _SKEL_H_ */ skel.h Каркас TCP-сервера Начнем с каркаса TCP-сервера. Затем можно приступить к созданию библиотеки, поместив в нее фрагменты кода из каркаса. В листинге 2.2 показана функция main. Листинг 2.2. Функция main из каркаса tcpserver.skel tcpserver.skel 1 ttinclude <stdio.h> 2 #include <stdlib.h> 3 ttinclude <unistd.h> 4 tinclude <stdarg.h> 5 #include <string.h> 6 Mnclude <errno.h> 7 ttinclude <netdb.h> 8 tinclude <fcntl.h> 9 #include <sys/time.h> 10 ttinclude <sys/socket.h> 11 #include <netinet/in.h> 12 Mnclude <arpa/inet.h> 13 #include "skel.h" 14 char *program_name; 15 int main( int argc, char **argv ) 16 { 17 struct sockaddr_in local; 18 struct sockaddr_in peer; 19 char *hname; 20 char *sname; 21 int peerlen; 22 SOCKET si; 23 SOCKET s; 24 const int on = 1; 25 INITO; 26 if ( argc == 2 ) 27 { 28 hname = NULL; 29 sname = argv[ 1 ]; 30 } 31 else 32 { 33 hname = argvf 1 ]; 34 sname = argv[ 2 ]; 35 } 36 set_address( hname, sname, &local, "tcp" ); 37 s = socket( AF_INET, SOCK_STREAM, 0 ); 38 if ( !isvalidsock( s ) ) 39 error( 1, errno, "ошибка вызова socket" ); 40 if ( setsockopt( s, SOL_SOCKET, SO_REUSEADDR, &on, 41 sizeof( on ) ) ) 42 error( 1, errno, "ошибка вызова setsockopt" ); 43 if ( bind( s, ( struct sockaddr * ) &local, 44 sizeof ( local } ) ) 45 error( 1, errno, "ошибка вызова bind" ); 46 if ( listen( s, NLISTEN ) ) 47 error( 1, errno, "ошибка вызова listen" ); 48 do 49 { 50 peerlen = sizeof( peer ) ; 51 si = accept( s, ( struct sockaddr * )&peer, &peerlen ); 52 if ( !isvalidsock( si ) ) 53 error( 1, errno, "ошибка вызова accept" ); 54 server( si, &peer ); 55 CLOSE( si ); 56 } while ( 1 }; 57 EXIT( 0 ); 58 } tcpserver.skel Включаемые файлы и глобальные переменные 1-14 Включаем заголовочные файлы, содержащие объявления используемых стандартных функций. 25 Макрос INIT выполняет стандартную инициализацию, в частности, установку глобальной переменной program_name для функции error и вызов функции WSAStartup при работе на платформе Windows. Функция main 26-35 Предполагается, что при вызове сервера ему будут переданы адрес и номер порта или только номер порта. Если адрес не указан, то привязываем к сокету псевдоадрес INADDR_ANY, разрешающий прием соединений по любому сетевому интерфейсу. В настоящем приложении в командной строке могут, конечно, быть и другие аргументы, обрабатывать их надо именно в этом месте. 36 Функция set_address записывает в поля переменной local типа sockaddr_in указанные адрес и номер порта. Функция set_address показана в листинге 2.3. 37-45 Получаем сокет, устанавливаем в нем опцию SO_REUSEADDR (совет 23) и привязываем к нему хранящиеся в переменной local адрес и номер порта. 46-47 Вызываем listen, чтобы сообщить ядру о готовности принимать соединения от клиентов. 48-56 Принимаем соединения и для каждого из них вызываем функцию server. Она может самостоятельно обслужить соединение или создать для этого новый процесс. В любом случае после возврата из функции server соединение закрывается. Странная, на первый взгляд, конструкция do-while позволяет легко изменить код сервера так, чтобы он завершался после обслуживания первого соединения. Для этого достаточно вместо while ( 1 ); написать while ( 0 ); Далее обратимся к функции set_address. Она будет использована во всех каркасах. Это естественная кандидатура на помещение в библиотеку стандартных функций. Листинг 2.3. Функция set_address tcpserver.skel 1 static void set_address( char *hname, char *sname, 2 struct sockaddr_in *sap, char *protocol ) 3 { 4 struct servent *sp; 5 struct hostent *hp; 6 char *endptr; 7 short port; 8 bzero( sap, sizeoff *sap ) ); 9 sap->sin_family = AF_INET; 10 if ( hname != NULL ) 11 ( 12 if ( !inet_aton( hname, &sap->sin_addr ) ) 13 { 14 hp = gethostbyname( hname ); 15 if ( hp == NULL ) 16 errorf 1, 0, "неизвестный хост: %sn", hname ); 17 sap->sin_addr = *( struct in_addr * )hp->h_addr; 18 } 19 } 20 else 21 sap->sin_addr.s_addr = htonl( INADDR_ANY ); 22 port = strtol( sname, &endptr, 0 ); 23 if ( *endptr == ' ' ) 24 sap->sin_port = htons( port }; 25 else 26 { 27 sp = getservbyname( sname, protocol ); 28 if ( sp == NULL ) 29 error( 1, 0, "неизвестный сервис: %sn", sname ); 30 sap->sin_port = sp->s_port; 31 } 32 } tcpserver.skel sef_acfc/ress 8-9 Обнулив структуру sockaddr_in, записываем в поле адресного семейства AF_INET. 10-19 Если hname не NULL, то предполагаем, что это числовой адрес в стандартной десятичной нотации. Преобразовываем его с помощью функции inet_aton, если inet_aton возвращает код ошибки, - пытаемся преобразовать hname в адрес с помощью gethostbyname. Если и это не получается, то печатаем диагностическое сообщение и завершаем программу. 20-21 Если вызывающая программа не указала ни имени, ни адреса хоста, устанавливаем адрес INADDR_ANY. 22-24 Преобразовываем sname в целое число. Если это удалось, то записываем номер порта в сетевом порядке (совет 28). 27-30 В противном случае предполагаем, что это символическое название сервиса и вызываем getservbyname для получения соответствующего номера порта. Если сервис неизвестен, печатаем диагностическое сообщение и завершаем программу. Заметьте, что getservbyname уже возвращает номер порта в сетевом порядке. Поскольку иногда приходится вызывать функцию set_address напрямую, здесь приводится ее прототип: #include "etcp.h" void set_address( char *host, char *port, i struct sockaddr_in *sap, char *protocol ); Последняя функция - error - показана в листинге 2.4. Это стандартная диагностическая процедура. #include "etcp.h" void error( int status, int err, char *format, ...); . Если status не равно 0, то error завершает программу после печати диагностического сообщения; в противном случае она возвращает управление. Если err не равно 0, то считается, что это значение системной переменной errno. При этом в конец сообщения дописывается соответствующая этому значению строка и числовое значение кода ошибки. Далее в примерах постоянно используется функция error, поэтому добавим ее в библиотеку. Листинг 2.4. Функция error tcpserver.skel 1 void error( int status, int err, char *fmt, ... ) 2 { 3 va_list ap,- 4 va_start ( ар, fmt ) ; 5 fprintff stderr, "%s: ", program_name ); 6 vfprintf( stderr, fmt, ap ); 7 va_end( ap ); 8 if ( err ) 9 fprintf( stderr, ": %s (%d)n", strerror( err ), err ); 10 if ( status ) 11 EXIT( status ); 12 } tcpserver.skel В каркас включена также заглушка для функции server: static void server(SOCKET s, struct sockaddr_in *peerp ) { } Каркас можно превратить в простое приложение, добавив код внутрь этой заглушки. Например, если скопировать файл tcpserver. skel в hello, с и заменить заглушку кодом static void server(SOCKET s, struct sockaddr_in *peerp ) { send( s, "hello, worldn", 13, 0) ; } то получим сетевую версию известной программы на языке С. Если откомпилировать и запустить эту программу, а затем подсоединиться к ней с помощью программы telnet, то получится вполне ожидаемый результат: bsd: $ hello 9000 [1] 1163 bsd: $ telnet localhost 9000 Trying 127 .0.0.1... Connected to localhost Escape character '*]'. hello, world Connection closed by foreign host. Поскольку каркас tcpserver. skel описывает типичную для TCP-сервера ситуацию, поместим большую часть кода main в библиотечную функцию tcp_server, показанную в листинге 2.5. Ее прототип выглядит следующим образом: #include "etcp.h" SOCKET tcp_server( char *host, char *port ); Возвращаемое значение: сокет в режиме прослушивания (в случае ошибки завершает программу). Параметр host указывает на строку, которая содержит либо имя, либо IP-адрес хоста, а параметр port - на строку с символическим именем сервиса или номером порта, записанным в виде ASCII-строки. Далее будем пользоваться функцией tcp_server, если не возникнет необходимость модифицировать каркас кода. Листинг 2.5. Функция tcp_server tcp_server.с 1 SOCKET tcp_server( char *hname, char *sname ) 2 { 3 struct sockaddr_in local; 4 SOCKET s; 5 const int on = 1; 6 set_address( hname, sname, &local, "tcp" ); 7 s = socket( AF_INET, SOCK_STREAM, 0 ); 8 if ( !isvalidsockf s ) ) 9 error( 1, errno, "ошибка вызова socket" ); 10 if ( setsockopt( s, SOL_SOCKET, SO_REUSEADDR, 11 ( char * )&on, sizeoff on ) ) ) 12 error( 1, errno, "ошибка вызова setsockopt" ); 13 if ( bind( s, ( struct sockaddr * ) &local, 14 sizeoff local ) ) ) 15 error( 1, errno, "ошибка вызова bind" ); 16 if ( listen( s, NLISTEN ) ) 17 error( 1, errno, "ошибка вызова listen" ); 18 return s; 19 } tcp_server.с Каркас TCP-клиента Рассмотрим каркас приложения TCP-клиента (листинг 2.6). Если не считать функции main и замены заглушки server заглушкой client, TO код такой же, как для каркаса TCP-сервера. Листинг 2.6. Функция main из каркаса tcpclient.skel tcpclient.skel 1 int main( int argc, char **argv ) 2 { 3 struct sockaddr_in peer; 4 SOCKET s; 5 INIT(); 6 set_address( argv[ 1 ], argv[ 2 ], &peer, "tcp" ); 7 s = socket( AF_INET, SOCK_STREAM, 0 ); 8 if ( !isvalidsock( s ) ) 9 error( 1, errno, "ошибка вызова socket" ); 10 if ( connect( s, ( struct sockaddr * )&peer, 11 sizeof( peer ) ) } 12 error( 1, errno, "ошибка вызова connect" ); 13 client) s, &peer ); 14 EXIT( 0 ) ; 15 } tcpclient.skel tcp_dient.skel 6-9 Как и в случае tcpserver. skel, записываем в поля структуры sockaddr_in указанные адрес и номер порта, после чего получаем сокет. 10-11 Вызываем connect для установления соединения с сервером. 13 После успешного возврата из connect вызываем заглушку client, передавая ей соединенный сокет и структуру с адресом сервера. Протестировать клиент можно, скопировав каркас в файл helloc. с и дописав в заглушку следующий код:
Этот клиент читает из сокета данные и выводит их на стандартный вывод до тех пор, пока сервер не пошлет конец файла (EOF). Подсоединившись к серверу hello, получаете:
Поместим фрагменты кода tcpclient. skel в библиотеку, так же, как поступили с каркасом tcpclient. skel. Новая функция- tcp_client, приведенная в листинге 2.7, имеет следующий прототип:
Возвращаемое значение: соединенный сокет (в случае ошибки завершает npограмму Как и в случае tcp_server, параметр host содержит либо имя, либо IP-адрес хоста, а параметр port - символическое имя сервиса или номер порта в виде ASCII-строки. Листинг 2.7. Функция tcp_client Ccp_client.с 1 SOCKET tcp_client{ char *hname, char *sname ) 2 { 3 struct sockaddr_in peer; 4 SOCKET s; 5 set_address( hname, sname, &peer, "tcp" ); 6 s = socket( AF_INET, SOCK_STREAM, 0 ); 7 if ( !isvalidsock( s ) } 8 error( 1, errno, "ошибка вызова socket" ) ; 9 if ( connect( s, ( struct sockaddr * )&peer, 10 sizeof ( peer ) ) ) 11 error( 1, errno, "ошибка вызова connect" ); 12 return s; 13 } tcp_client.с Каркас UDP-сервера Каркас UDP-сервера в основном похож на каркас TCP-сервера. Его отличительная особенность - не нужно устанавливать опцию сокета SO_REUSEADDR и обращаться к системным вызовам accept и listen, поскольку UDL - это протокол, не требующий логического соединения (совет 1). Функция main из каркаса приведена в листинге 2.8. Листинг 2.8. Функция main из каркаса udpserver.skel udpserver.skel 1 int main( int argc, char **argv ) 2 { 3 struct sockaddr_in local; 4 char *hname; 5 char *sname; 6 SOCKET s; 7 INIT(); 8 if ( argc == 2 ) 9 { 10 hname = NULL; 11 sname = argv[ 1 ]; 12 } 13 else 14 { 15 hname = argv[ 1 ]; 16 sname = argv[ 2 ]; 17 } 18 set_address( hname, sname, &local, "udp" }; 19 s = socket( AF_INET, SOCK_DGRAM, 0 ); 20 if ( !isvalidsock( s ) ) 21 error( 1, errno, "ошибка вызова socket" ); 22 if ( bind( s, ( struct sockaddr * ) &local, 23 sizeof( local ) ) ) 24 error( 1, errno, "ошибка вызова bind" ); 25 serverf s, &local ); 26 EXIT( 0 ) ; 27 } udpserver. skel udpserver.skel 18 Вызываем функцию set_address для записи в поля переменной local типа sockaddr_in адреса и номера порта, по которому сервер будет принимать датаграммы. Обратите внимание, что вместо "tcp" задается третьим параметром " udp". 19-24 Получаем сокет типа SOCK_DGRAM и привязываем к нему адрес и номер порта, хранящиеся в переменной local. 25 Вызываем заглушку server, которая будет ожидать входящие датаграммы. Чтобы получить UDP-версию программы |