WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 |   ...   | 3 | 4 || 6 | 7 |   ...   | 8 |

«Создание сетевых приложений в среде Linux Руководство разработчика Шон Уолтон Москва • Санкт Петербург • Киев 2001 ББК 32.973.26 018.2.75 УДК 681.3.07 Издательский дом "Вильяме" По общим вопросам ...»

-- [ Страница 5 ] --

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

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

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

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

Третье, и последнее, правило — многослойность. Это не просто разбиение дан ных на модули. Имеется в виду многоуровневый подход ко всей технологии. В Глава 11. Экономия времени за счет объектов www.books-shop.com качестве примера можно привести модель OSI. На каждом ее уровне свои законы и интерфейсы.

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

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

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

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

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

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

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

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

Никакой связи с внешними программами они не имеют.

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

238 Часть III. Объектно ориентированные сокеты www.books-shop.com Наследование поведения Допустим, нужно написать модуль, который делает то же самое, что и другой модуль, но несколько функций в нем отличаются. Добиться этого можно с по мощью механизма наследования. Старая поговорка гласит: "Обращайтесь с ре бенком как со взрослым, и он будет вести себя правильно".

У объекта есть ряд атрибутов и методов. Можно создать производный от него объект, который унаследует все свойства предка, изменив некоторые из них и до бавив ряд своих. На рис. 11.1 изображена иерархия объекта Device (устройство), у которого есть ряд потомков.

Рис. 11.1. В иерархию наследования объекта Device (устройство) входят два абстрактных объекта — BlockDevice (блок ориентированное устройство) и CharDevice (байт ориентированное устройство) — и три обычных: Network (сеть), Disk (диск) и SerialPort (последовательный порт) Глядя на иерархию, можно сказать, что "последовательный порт (SerialPort) является байт ориентированным (CharDevice) устройством (Device)". В объекте Device определены пять интерфейсных (абстрактных) функций: initialize(), open(), read(), write() и close(). В объекте CharDevice к ним добавляется функция IOctl(). Наконец, в объекте SerialPort все эти функции реализованы так, как это требуется в случае последовательного порта.

Глава 11. Экономия времени за счет объектов www.books-shop.com Абстракция данных Третья базовая концепция, абстракция, схожа с описанной ранее моделью аб страктного программирования, но немного ее расширяет. В абстрактном про граммировании разработчик сосредоточивает свои усилия на создании алгоритма функции, игнорируя тип данных, с которыми она имеет дело. Таким способом пишутся базовые вычислительные структуры, в частности стеки и словари.

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

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

Благодаря абстракции можно вызвать функцию объекта, не зная, как именно она реализована. Например, у объекта Device есть два потомка: Disk и Network. В следующем фрагменте программы создается переменная dev абстрактного типа Device, а затем в нее помещается ссылка на объект Disk:

/* Создаем объект Disk и записываем ссылку на него */ /* в переменную dev абстрактного типа Device */ /***************************************************/ Device *dev = new Disk();

dev >Initialize();

Хотя в объекте Device метод Initialize() не реализован, а лишь объявлен, компилятор понимает, что данный метод относится к объекту Disk. В то же время метод Address() нельзя вызвать по отношению к объекту Device, так как он объ явлен ниже в иерархии. Чтобы получить к нему доступ, необходимо немного мо дифицировать код:

BlockDevice *dev = new Disk();

dev >Address();

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

240 Часть III. Объектно ориентированные сокеты www.books-shop.com Полиморфизм методов Четвертая, и последняя, из базовых концепций ООП — полиморфизм. Его суть заключается в том, что родственные методы, реализованные на разных уровнях иерархии, могут иметь одинаковые имена. Это упрощает их запоминание.

Принадлежность метода к тому или иному объекту определяется на основании его аргументов. Например, вместо двух методов — PlayVideo() и PlayMIDI() — можно создать один метод Р1ау(), которому в первом случае передается видео клип, а во втором случае — MIDI файл.

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

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

Классы и объекты Программисты часто путаются в терминах "класс" и "объект". Класс — это описание категории родственных объектов, а объект — это конкретный экземп ляр класса. Класс можно сравнить с чертежом, а объект — с домом, построенным на основании этого чертежа.

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

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

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

Глава 11. Экономия времени за счет объектов piracy@books-shop.com Свойства Опубликованные атрибуты класса называются свойствами. Обычно они дос тупны только для чтения или же представлены в виде связки функций Getxxx() и Setxxx(), позволяющих получать и задавать их значения.

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

Подклассы наследуют свойства своих родительских классов.

Методы Методы — это действия, которые выполняет объект. Благодаря полиморфизму дочерние классы могут переопределять методы, унаследованные от своих роди тельских классов. Обычно при этом вызывается исходный метод, чтобы роди тельский класс мог проинициализировать свои закрытые атрибуты.

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

Права доступа Права доступа определяют, кто может обращаться к той или иной части клас са. В Java и C++ определены три уровня доступа: private, protected и public.

• private. Доступ к членам класса разрешен только из его собственных методов.

• protected. Доступ к членам класса разрешен из его методов, а также из методов его подклассов.

• public. Доступ к членам класса разрешен отовсюду.

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

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

• Отношение "является". Наследование: один класс наследует свойства и методы другого.

• Отношение "содержит". Включение: один класс является членом дру гого.

• Отношение "использует". Использование: один класс объявлен дружест венным другому или же вызывает его открытые методы.

242 Часть III. Объектно ориентированные сокеты www.books-shop.com Между двумя объектами должно существовать только одно отношение. На пример, объект А не может быть потомком объекта Б и в то же время содержать его в качестве встроенного объекта. Подобная двойственность отношений свиде тельствует о неправильно выполненном анализе.

Расширение объектов Когда есть хороший объект, всегда хочется его расширить или еще улучшить.

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

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

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

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

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

Потоковая передача данных Представьте, что объект пакует сам себя и записывает на диск либо посылает куда то по сети. По достижении места назначения или же когда программа за гружает объект, он автоматически распаковывает себя. Подобная методика ис пользуется при обеспечении постоянства объектов и в распределенном програм мировании. В частности, она реализована в Java. Элементы потоковой обработки можно применять и в C++, но самоидентификация объектов (называемая ин троспективным анализом в Java) здесь невозможна.

Глава 11. Экономия времени за счет объектов www.books-shop.com Перегрузка Перегрузка операторов (поддерживаемая в C++) расширяет концепцию поли морфизма, Некоторые программисты ошибочно полагают, что это тождественные понятия, но на самом деле перегрузка операторов является объектным расшире нием C++. В Java, например, она не поддерживается, хотя этот язык считается объектно ориентированным.

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

• Расширение, а не переопределение. Нельзя создавать операторы, чьи опе ранды относятся к тому же типу, что и раньше;

например, нельзя поме нять смысл операции (int)+(int).

• Не все операторы доступны. Переопределять можно практически все операторы, за исключением нескольких (например, условный оператор ?:).

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

Нельзя также изменить приоритет оператора и его ассоциативность (порядок вычисления операндов).

Интерфейсы В C++ существует одна проблема. Если класс А использует класс Б, структура последнего должна быть непосредственно известна программисту или же одним из предков класса Б должен быть класс, являющийся потомком для А. Когда класс Б поддерживает несколько интерфейсов, возникают неприятности с множе ственным наследованием.

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

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

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

244 Часть III. Объектно ориентированные сокеты www.books-shop.com Особые случаи Объектная технология сталкивается с теми же проблемами, что и другие тех нологии программирования. Не все задачи можно сформулировать в терминах ООП. Ниже мы рассмотрим особые виды классов и попытаемся разобраться, как с ними работать.

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

Запись, или структура, — это неупорядоченный набор данных. Единственные методы, присутствующие в нем, — это функции вида Get() и Set(). Объекты по добного рода встречаются редко. Необходимость их существования проверяется следующими вопросами.

• Существует ли тесная связь между полями записи? Например, может ока заться, что при модификации одного поля должно измениться другое.

Следует выявить связанные таким способом поля и запрограммировать их изменения в функциях Set().

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

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

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

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

• Существует ли тесная взаимосвязь между классами? Когда один класс активно вызывает методы другого класса, то, возможно, некоторые из них просто принадлежат неправильному классу.

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

Глава 11. Экономия времени за счет объектов www.books-shop.com Языковая поддержка Поддержка объектов внедрена во многие современные языки программиро вания. Существует даже объектная версия Cobol. Классическими объектно ориентированными языками являются SmallTalk и Eiffel. Объектно ориенти рованными можно считать также языки Java и C++, знакомству с которыми будут посвящены две последующие главы. В то же время поддержка объектов во всех этих языках реализована настолько по разному, что необходимо провести их классификацию.

Классификация объектно ориентированных языков В действительности лишь некоторые языки являются истинно объектно ориентированными. Среди распространенных языков к таковым можно отнести, пожалуй, лишь SmallTalk. Любые действия, выполняемые в этом языке, рассмат риваются как действия над объектами.

По отношению к таким языкам, как C++ и Java, можно сказать, что они пред назначены для работы с объектами. В них поддерживаются практически все кон цепции ООП, и на этих языках пишутся очень эффективные объектно ориентированные программы. Но ничто не мешает вам, к примеру, в среде C++ написать и скомпилировать обычную С программу. В ней даже можно создать квазиобъекты с помощью типа данных struct. Даже Java программа может пред ставлять собой одну большую функцию main(). Особенностью таких языков явля ется то, что они не заставляют программиста придерживаться всех принципов объектной технологии.

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

Работа с объектами в процедурных языках Объектная технология — замечательная вещь, когда программа пишется на объектно ориентированном языке (таком как C++ или Java). Но это не всегда возможно. Что делать в подобном случае?

Объектная технология создавалась на базе более ранних технологий (абстрактное и модульное программирование), поэтому многие элементы ООП можно реализовать и в обычных процедурных языках. Но для этого требуется оп ределенная дисциплина программирования.

• Инкапсуляция. Необходимо представить все интерфейсы модуля в виде процедур или функций. Им следует присваивать имена вида <имя_модуля>_<имя_функции>().

246 Часть III. Объектно ориентированные сокеты www.books-shop.com • Абстракция. Если язык поддерживает операцию приведения типа, то аб стракцию можно обеспечить, записывая в одно из полей структуры идентификатор требуемого типа данных.

• Классы и объекты. Можно создать их прообразы, если в языке поддер живаются записи или структуры.

• Атрибуты. Это просто поля записи.

• Свойства. Для каждого опубликованного свойства необходимо создать связку функций Get( )/Set().

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

• Отношения. Отношения включения и использования доступны в любом языке.

• Постоянство. Можно самостоятельно отслеживать параметры програм мы и загружать их при запуске.

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

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

Резюме: объектно ориентированное мышление Объектная технология — это основа хороших программных разработок. Пра вильно применяя принципы ООП (абстракция, полиморфизм, наследование и инкапсуляция), можно создавать эффективные программные модули, которые бу дут многократно использоваться другими программистами. Кроме того, их легче отлаживать и расширять, чем обычные программы. Наконец, при объектно ориентированном подходе программист больше занят анализом предметной об ласти, чем собственно программированием.

Глава 11. Экономия времени за счет объектов www.books-shop.com Глава Сетевое программирование в Java В этой главе...

Работа с сокетами Ввод вывод в Java Конфигурирование сокетов Многозадачные программы Существующие ограничения Резюме www.books-shop.com До сего момента вопросы сетевого программирования и, в частности, работы с сокетами рассматривались применительно к языку С. Его достоинства очевидны для системного программиста, но в результате получаются программы, которые не всегда переносимы и не всегда допускают многократное использование.

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

(Компания Sun Microsystems оставляет за собой право объявлять некоторые ин терфейсы, методы и классы устаревшими и не рекомендуемыми для дальней шего использования.) Концепция переносимости на уровне кода в действительности не нова. Прин цип "скомпилировал однажды — запускай везде" легко реализовать, имея соот ветствующие средства. Java программа компилируется в байт код, который вы полняется в рамках виртуальной машины. Виртуальная машина Java (JVM — Java Virtual Machine) интерпретирует каждую команду последовательно, подобно мик ропроцессору. Конечно, скорость интерпретации байт кода не сравнится со ско ростью выполнения машинных кодов (создаваемых компилятором языка С), но, поскольку современные процессоры обладают очень высоким быстродействием, потеря производительности не столь заметна.

Java — простой и в то же время интересный язык. Обладая навыками про граммирования в C/C++ и разбираясь в особенностях объектной технологии, можно быстро изучить его. В этом языке имеется очень мощный, исчерпываю щий набор стандартных библиотек классов, в котором не так то легко ориенти роваться. Поэтому не помешает всегда держать под рукой интерактивную доку ментацию по JDK (Java Development Kit — комплект средств разработки в среде Java) и несколько хороших справочников.

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

Прежде всего мы рассмотрим, какие классы существуют в Java для работы с сокетами, какие имеются средства ввода вывода, как конфигурировать сокеты и работать с потоками. / Работа с сокетами Многие программисты считают, что основные преимущества Java — независи мость от интерфейса пользователя и встроенные сетевые возможности. Предпоч тительным сетевым протоколом в Java является TCP. С ним легче работать, чем с дейтаграммами (протокол UDP), кроме того, это наиболее надежный протокол. В Java можно также посылать дейтаграммы, но напрямую подобная возможность не поддерживается базовыми библиотечными классами ввода вывода.

Глава 12. Сетевое программирование в Java www.books-shop.com Программирование клиентов и серверов Каналы потоковой передачи (TCP соединения) лучше всего соответствуют возможностям Java. Java пытается скрывать детали сетевого взаимодействия и уп рощает сетевые интерфейсы. Многие операции, связанные с поддержкой прото кола TCP, перенесены в библиотечные классы ввода вывода. В результате в соз дании сокетов принимает участие лишь несколько объектов и методов.

TCP клиенты Java Вот как, например, создается клиентский сокет:

Socket s = new Socket(String Hostname, int PortNum);

Socket s = new Socket(InetAddress Addr, int PortNum);

Самый распространенный вариант таков:

Socket s = new Socket("localhost", 9999);

Для подключения к серверу больше ничего не требуется. Когда виртуальная машина (ВМ) создает объект класса Socket, она назначает ему локальный номер порта, выполняет преобразование данных в сетевой порядок следования байтов и подключает сокет к серверу. Если требуется дополнительно указать локальный сетевой интерфейс и порт, то это делается так:

Socket s = new Socket(String Hostname, int PortNum, InetAddress localAddr, int localPort);

Socket s = new Socket(InetAddress Addr, int PortNum, InetAddress localAddr, int localPort);

Класс InetAddress преобразует имя узла или IP адрес в двоичный адрес. Чаще всего с объектом этого класса не работают напрямую, так как проще сразу вы звать конструктор Socket (), которому передается имя узла.

Поддержка стандартов IPv4/IPv В настоящее время Java поддерживает стандарт IPv4. Согласно проекту Merlin (информацию можно получить на Web узле java.sun.com), поддержка стандарта IPv6, появится, когда она бу дет внедрена в операционные системы. Классы наподобие InetAddress, осуществляющие пре образование имен, должны легко адаптироваться к новым протоколам. Но очевидно, что некото рые функции, например InetAddress.getHostAddress(), придется заменить при переходе на новый, расширенный формат адресов.

Прием/отправка сообщений После создания объекта класса Socket программа еще не может посылать или принимать через него сообщения. Необходимо предварительно связать с ним входной (класс InputStream) и выходной (класс OutputStream) потоки:

InputStream i = s.getlnputstream();

OutputStream о = s.getOutputStream();

Чтение и запись данных осуществляются блоками:

250 Часть III. Объектно ориентированные сокеты www.books-shop.com byte[] buffer = new byte[1024];

int bytes_read = i.read(buffer);

// чтение блока данных из сокета o.write(buffer);

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

if ( i.available() > 100 ) // чтение не производится, если в буфере меньше 100 байтов bytes = i.read(buffer);

После завершения работы можно закрыть сокет (а также все каналы ввода вывода) с помощью одного единственного метода Socket. close ():

// очистка s.close();

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

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

В листинге 12.1 показано, как создать простейший эхо клиент.

Листинг 12.1. Пример простейшего эхо клиента в Java //**************************************************************** // Простейший эхо клиент (из файла SimpleEchoClient.Java) //**************************************************************** Socket s = new Socket("127.0.0.1, 9999");

// создаем сокет InputStream i = s.getInputstream();

// создаем входной поток OutputStream о = s.getOutputStream();

// создаём выходной поток String str;

do { byte[] line = new byte[100];

System.in.read(line);

// читаем строку с консоли о.write(line);

// посылаем сообщение i.read(line);

// принимаем его обратно str = new String(line);

// преобразуем его в строку System.out.println(str.trim());

// отображаем сообщение } while ( !str.trim().equals("bye") );

s.close();

// закрываем соединение В этом примере продемонстрирован простой цикл чтения и отправки сообще ний. Он еще не доведен до конца, так как при попытке компиляции будет выда но предупреждение о том, что не перехватываются некоторые исключения. Пере Глава 12. Сетевое программирование в Java piracy@books-shop.com хват исключений — это важная часть любых сетевых операций. Поэтому к пока занному тексту нужно добавить следующий программный код:

try { // <— Здесь должен размещаться исходный текст } catch (Exception err) { System.err.println(err);

} Блоки try...catch делают пример завершенным. Полученный текст можно вставить непосредственно в метод main() основного класса программы.

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

ServerSocket s = new ServerSocket(int PortNum);

ServerSocket s = new ServerSocket(int PortNum, int Backlog);

ServerSocket s new ServerSocket(int PortNum, int Backlog, InetAddress BindAddr);

Параметры Backlog и BindAddr заменяют собой вызовы С функций listen() (создание очереди ожидания) и bind() (привязка к конкретному сетевому интер фейсу). Если вы помните, в языке С текст серверной программы занимал 7— строк. В Java то же самое можно сделать с помощью двух строк:

ServerSocket s = new ServerSocket(9999);

Socket с = s.accept();

Назначение объекта ServerSocket состоит лишь в организации очереди ожида ния. Когда поступает запрос от клиента, сервер с помощью метода ServerSocket.accept() создает новый объект класса Socket, через который проис ходит непосредственное взаимодействие с клиентом.

В листинге 12.2 показано, как создать простейший эхо сервер.

Листинг 12.2. Пример простейшего эхо сервера в Java // Простейший эхо сервер (из файла SimpleEchoServer.Java) //**************************************************************** try.

{ ServerSocket s = new ServerSocket("9999");

// создаем сервер while (true) { Socket с = s.accept();

// ожидаем поступления запросов InputStream i = c.getlnputstream();

// входной поток OutputStream о =c.getOutputStream();

// выходной поток do 252 Часть Ш. Объектно ориентированные сокеты www.books-shop.com byte[] line = new byte[100];

// создаем временный буфер i.read(line);

// принимаем сообщение от клиента о.write(line);

// посылаем его обратно } while ( !str.trim().equals("bye") );

c.close();

// закрываем соединение } } catch (Exception err) { System.err.println(err);

} Передача UDP сообщений Иногда возникает необходимость передавать сообщения в виде дейтаграмм, т.е. цо протоколу UDP. В Java есть ряд классов, которые позволяют работать с UDP сокетами. Основной из них — это класс DatagramSocket.

UDP сокет создается очень просто:

DatagramSocket s = new DatagramSocket();

При необходимости можно указать локальный порт и сетевой интерфейс:

DatagramSocket s = new DatagramSocket(int localPort);

DatagramSocket s = new DatagramSocket(int localPort, InetAddress localAddr);

Сразу после своего создания UDP сокет готов к приему и передаче сообщений.

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

DatagramPacket d = new DatagramPacket(byte[] buf, int len);

DatagramPacket d = new DatagramPacket(byte[] buf, int len, InetAddress Addr, int port);

Первый вариант конструктора предназначен для создания объекта, который принимает сообщение. С помощью второго конструктора создается отправляемый пакет. В нем дополнительно указываются адрес и порт назначения. Параметр buf ссылается на предварительно созданный массив байтов, а параметр len определя ет длину массива или максимальную длину принимаемого пакета.

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

В листинге 12.3 показан текст программы отправителя.

Листинг 12.3. Создание UDP сокета и отправка дейтаграммы // Простейший отправитель дейтаграмм // (из файла SimplePeerSource.java) Глава 12. Сетевое программирование в Java www.books-shop.com DatagramSocket s = new DatagramSocket();

// создаем сокет byte[] line = new byte[100J;

System.out.print("Enter text to send: ");

int len = System.in.read(line);

InetAddress dest = // выполняем преобразование адреса InetAddress.getByName("127.0.0.1");

DatagramPacket pkt = // создаем дейтаграмму new DatagramPacket(line, len, dest, 9998);

s.send(pkt);

// отправляем сообщение s.close();

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

Текст программы получателя представлен в листинге 12.4.

Листинг 12.4. Прием дейтаграммы и ее отображение // Простейший получатель дейтаграмм // (из файла SimplePeerDestination.Java) DatagramSocket s = new DatagramSocket(9998);

// создаем сокет byte[] line = new byte[100];

DatagramPacket pkt = // создаем буфер для поступающего сообщения new DatagramPacket(line, line.length);

s.receive(pkt);

// принимаем сообщение String msg = new String(pkt.getData());

// извлекаем данные System.out.print("Got message: " + msg);

s.close();

// закрываем соединение Групповая передача дейтаграмм Протокол UDP позволяет отправить одно сообщение нескольким адресатам.

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

Чтобы принять участие в групповом вещании, программа подключается к оп ределенному IP адресу, зарезервированному для групповой рассылки. Все про граммы, входящие в группу, будут получать сообщения, посылаемые по этому ад ресу. Групповой сокет представляется в Java объектом класса MulticastSocket, у которого есть два конструктора:

MulticastSocket ms = new MulticastSocket();

MulticastSocket ms = new MuiticastSocket(int localPort);

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

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

254 Часть III. Объектно ориентированные сокеты www.books-shop.com Когда групповой сокет создан, он ведет себя так же, как и обычный UDP сокет (объект класса DatagramSocket). Через него можно непосредственно отправ лять и получать сообщения. Чтобы перейти в режим группового вещания, нужно присоединиться к группе. В стандарте IPv4 выделен следующий диапазон адресов для группового вещания: 224.0.0.0 239.255.255.255.

MulticastSocket ms = new MulticastSocket(16900);

ms.joinGroup(InetAddress.getByName ("224.0.0.1"));

ms.joinGroup(InetAddress.getByName("228.58.120.11"));

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

Объектно ориентированное программирование и планирование на будущее Стандарт IPv6 поддерживает групповую передачу UDP сообщений и планирует реализовать мно гоадресную доставку TCP пакетов. Tew самым проблема'ненадежности протокола UDP будет решена. Но дело в том, что класс MulticastSocket порожден от класса DatagramSocket, по этому не может быть адаптирован к грядущим изменениям, Это хороший пример того, к чему приводит отсутствие планирования. Создавая иерархию объектов, лучше оставить место для по следующих изменений или расширений, чем потом переделывать всю иерархию.

В листинге 12.5 показано, как создать и сконфигурировать групповой сокет.

Листинг 12.5. Создание группового сокета, привязка его к порту 16900, присоединение к группе и ожидание сообщений //**************************************************************** // Простейший получатель групповых сообщений // (из файла SimpleMulticastDestination.Java) //**************************************************************** MulticastSocket s = new MulticastSocket{16900);

/•/ Создаем сокет ms.joinGroup(InetAddress.getByName("224.0.0.1"));

// присоединение к группе String msg;

do { byte[] line = new byte[100];

DatagramPacket pkt = new DatagramPacket(line, line.length);

ms.receive(pkt);

msg = new String(pkt.getData());

System.out.println("From "+pkt.getAddress()+'4"tmsg.trim());

} while ( !msg.trim().equalsf"close") );

ms.close();

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

Глава 12. Сетевое программирование в Java www.books-shop.com В вод вывод в Java До сего момента в примерах использовались очень простые интерфейсы вво да вывода. Сила сокетов в Java заключается еще и в том, что их можно связывать с самыми разными потоками ввода вывода. В Java имеется целый ряд классов, обеспечивающих различные формы чтения и записи данных.

Классификация классов ввода вывода В Java имеется шесть основных типов информационных потоков. Все связан ные с ними классы служат определенным целям и порождаются от базовых клас сов Reader, Writer, InputStream и OutputStream.

• Память. Ввод вывод, основанный на буферах памяти. Обращения к ре альным аппаратным устройствам не происходит. Массивы, расположен ные в ОЗУ, служат виртуальными накопителями данных. К данному ти пу потоков относятся такие классы, как ByteArraylnputStream, ByteArrayOutputStream, CharArrayReader, CharArrayWriter, StringReader и StringWriter.

• Файл. Ввод вывод средствами файловой системы. Сюда относятся клас сы FilelnputStream, FileOutputStream, FileReader и FileWriter.

• Фильтр. Ввод вывод, связанный с трансляцией или интерпретацией символьных потоков. Например, из входного потока могут выделяться записи, ограниченные символами новой строки, символами табуляции или запятыми. В эту группу входят классы FilterReader, FilterWriter, PrintWriter и PrintStream (устарел).

• Объект. Прием и передача целых объектов. Это одна из наиболее впе чатляющих возможностей Java. Достаточно присвоить классу метку Serializable и можно передавать и принимать экземпляры его объектов.

Данная возможность реализуется с помощью классов ObjectlnputStream и ObjectOutputStream.

• Канал. Ввод вывод, напоминающий механизм межзадачного взаимодей ствия в языке С. В программе создается канал, который связывается с другим каналом, после чего два программных потока могут обменивать ся сообщениями. В эту группу входят классы PipedlnputStream, PipedOutputStream, PipedReader и PipedWriter.

• Поток. Общие средства буферизованного потокового ввода вывода.

Именно они используются сокетами. Сюда входят абстрактные классы InputStream и OutputStream. Если нужно передавать данные через сокет в каком то более конкретном виде, необходимо выполнить преобразова ние потока в другую форму. Базовые функции преобразования реализу ются классами InputStreamReader и OutputStreamWriter.

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

256 Часть III. Объектно ориентированные сокеты www.books-shop.com Преобразование потоков Класс Socket может напрямую работать лишь с двумя классами ввода вывода:

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

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

Работу со строками удобно вести с помощью класса BufferedReader. Соответст вующее преобразование нужно выполнить через класс InputStreamReader, служа щий посредником при переходе от классов семейства InputStream к классам се мейства Reader:

Socket s = new Socket(host, port);

InputStream is = s.getInputstream();

InputStreamReader isr = new InputStreamReader(is);

BufferedReader br = new BufferedReader(isr);

String 1 = br.readLine();

Строки 2—5 можно записать короче:

BufferedReader br = new BufferedReader(new InputStreamReader( s.getInputstream()));

Передача строк осуществляется немного проще:

String msg = new String( "Welcome to my Java website");

SocketServer ss = new SocketServer(9999);

Socket s = ss.accept();

PrintWriter pw = new PrintWriter(s.getOutputStream(), true);

pw.println(msg);

s.close();

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

Если обмен данными осуществляется между двумя Java программами, можно воспользоваться классами ObjectlnputStream и ObjectOutputStream для прие ма/передачи объектов, реализующих интерфейс Serializable:

String msg = new String("Test");

Socket s = new Socket(hostname, port);

ObjectOutputStream oos = new ObjectOutputStream( s.getOutputStream());

oos.writeObject(msg);

Принимающая сторона получает сообщение в виде объекта и преобразует его к требуемому типу:

Глава 12. Сетевое программирование в Java www.books-shop.com Socket s = ss.accept();

ObjectInputStream ois = new ObjectInputStreatn(s.getInputStream());

String newMsg = (String) ois.readObject();

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

Конфигурирование сокетов В главе 9, "Повышение производительности", рассказывалось о том, как рабо тать с многочисленными параметрами сокетов. Аналогичные функции имеются и в Java. Следует, однако, учитывать, что если операционная система не поддержи вает тот или иной параметр, то и Java его не предоставляет.

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

getSoTimeout() setSoTimeout(int timeout) С помощью этих методов можно получить или задать значение параметра SO_TIMEOUT, определяющего период ожидания данных. Этот параметр давно уста рел, поэтому в Linux вместо него применяются функции fcntl(), poll() и select().

Следующие два метода позволяют определить или задать размер (в байтах) внутреннего выходного буфера. Соответствующее значение хранится в параметре SO_SNDBUF.

getSendBufferSize() setSendBufferSize(int size) А эти два метода связаны со входным буфером (параметр SO_RCVBUF):

getReceiveBufferSize() setReceiveBufferSize(int size) Следующие методы позволяют соответственно определить и задать, должен ли сокет продолжать обработку буферизованных данных после своего закрытия и в течение какого времени (параметр SO_LINGER):

getSoLinger() setSoLinger(boolean on, int linger) Первый из показанных ниже методов определяет, используется ли алгоритм Нейгла, а второй метод включает или отключает его (параметр TCP_NODELAY):

getTcpNoDelay() setTcpNoDelay(boolean on) 258 Часть III. Объектно ориентированные сокеты www.books-shop.com Конфигурирование групповых сокетов Перечисленные здесь методы применимы к объекту MulticastSocket. Следую щие два метода позволяют соответственно узнать и задать предельно допустимое число переходов, совершаемых пакетом (параметр IP_MULTICAST_TTL;

странно, но Java не позволяет задавать значение TTL для других типов сокетов):

getTimeToLive() setTimeToLive(int ttl) Показанные ниже методы позволяют соответственно определить и задать ос новной сетевой интерфейс для группового вещания (параметр IP_MULTICAST_IF):

getInterface() setInterfасе(InetAddress inf) Многозадачные программы Некоторые методики программирования в Java существенно упрощены. Наря ду со встроенными средствами сетевого программирования и классами ввода вывода в Java имеются встроенные средства многопотокового программирования.

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

Создание потокового класса Чтобы создать потоковый класс, необходимо либо сделать его потомком клас са Thread, либо реализовать в нем интерфейс Runnable. В обоих случаях требуется определить в классе метод run( ), включающий код потока:

public class TestThread extends Thread { public void run ( ) { /*** здесь находится код потока ***/ Когда метод run() завершается, поток прекращает свою работу.

Если потоковый класс порождается от класса Thread, у него появляется метод start(), предназначенный для запуска потока. Ниже показано, как запустить по ток:

public static void main (String[] args) { start();

// < запуск потока и вызов метода run() /*** во время работы потока можно выполнять другие действия ***/ Глава 12. Сетевое программирование в Java www.books-shop.com Если нужно создать несколько одновременно выполняющихся потоков, вызо вите метод start () требуемое число раз. Но, как уже говорилось, все потоки по лучают доступ к одним и тем же данным. Чтобы заставить поток выполняться в своем адресном пространстве, создайте для него отдельный объект:

Thread t = new TestThread();

t.start();

Корректная работа с потоками Java потоки могут легко захватить ресурсы центрального процессора (это особенно справедливо в случае Windows, чем Linux/UNIX). He забудьте прочитать главу 7, "Распределение нагрузки:

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

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

Добавление потоков к классу Не всегда можно объявить класс производным от класса Thread. Обычно это имеет место, когда класс уже является потомком класса Frame. В Java запрещено множественное наследование, поэтому сделать класс производным от двух клас сов невозможно. В таком случае необходимо объявить, что класс реализует ин терфейс Runnable. Результат будет тем же, а программа претерпит лишь незначи тельные изменения.

public class TestThread extends Frame implements Runnable { public void run() { /*** код потока ***/ } public static void someMethodf) { Thread t = new Thread (this);

t.start();

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

260 Часть III. Объектно ориентированные сокеты www.books-shop.com Синхронизация методов Основное преимущество потоков заключается в возможности совместного дос тупа к ресурсам. Это вызывает проблемы взаимоблокировок, которые были опи саны в главе 7, "Распределение нагрузки: многозадачность". В Java эта проблема решается с помощью синхронизированных методов.

Синхронизированный метод является аналогом семафора в библиотеке Pthreads. Он объявляется следующим образом:

public synchronized changeSomething() { /*** совместный доступ к ресурсу ***/ } Ключевое слово synchronized объявляет метод критической секцией. Таким образом, если метод вызван в одном потоке, другой поток, обращающийся к ме тоду, вынужден будет ждать.

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

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

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

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

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

public synchronized changeSomething() { while ( buffer_not_full) // достаточно ли данных в буфере?

{ try { wait() } // нет, перемещаемся в конец очереди catch (Exception err) { System.err.println(err);

} } /* Отправляем сообщение */ notifyAll();

// уведомляем ожидающие потоки } Метод wait() помещает текущий поток назад в очередь ожидания. При этом планировщик "будит" следующий поток. Если он обнаруживает, что данные го товы, он выходит из цикла и отправляет сообщение. Метод notifyAll() сообщает всем ожидающим потокам, что ресурс свободен, Глава 12. Сетевое программирование в Java piracy@books-shop.com Существующие ограничения В Java имеются очень удобные средства сетевого программирования, позво ляющие быстро создавать полнофункциональные программы. В этом языке мож но легко создавать сокеты, отправлять сообщения и обрабатывать исключитель ные ситуации. Тем не менее в нем есть ряд ограничений.

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

• Поддержка только стандарта IPv4. В настоящий момент Java поддержи вает только сети TCP/IPv4. На момент написания книги на Web узле java.sun.com была информация о проекте Merlin, в рамках которого в язык планировалось внедрить поддержку протокола IPX и стандарта IPv6. Но очевидно, что некоторые из существующих методов придется переписать.

• Отсутствие низкоуровневых сокетов. В Java не поддерживаются неструк турированные IР сокеты.

• Неполный набор параметров сокетов. Не все параметры сокетов поддер живаются.

• Отсутствие эквивалента системного вызова fork (). Из программы можно запустить только внешний модуль, но нельзя создать новый про цесс. Можно эмулировать процесс, создав новый потоковый объект, но при этом нет гарантии целостности ресурсов, так как у потоков общее адресное пространство.

• Отсутствие широковещания. В Java не поддерживается широковещание.

Возможно, это связано с тем, что данный режим применяется все реже.

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

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

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

Благодаря средствам работы с потоками в Java становится проще проектировать серверы. Потоковый класс создается двумя способами: либо путем наследования класса Thread, либо путем реализации интерфейса Runnable. В обоих случаях метод start() запускает новый поток, вызывая написанный пользователем метод run().

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

262 Часть III. Объектно ориентированные сокеты www.books-shop.com Глава Программирование сокетов в C++ В этой главе...

Зачем программировать сокеты в C++? Создание библиотеки сокетов Тестирование библиотеки сокетов Существующие ограничения Резюме: библиотека сокетов упрощает программирование www.books-shop.com В предыдущей главе рассматривалось программирование сокетов в Java. Java — очень мощный язык программирования, обладающий множеством преимуществ, среди которых следует отметить независимость от платформы и оперативную компиляцию. Но не всем нравится Java, в основное из за отсутствия стабильно сти и недостаточной производительности, поэтому многие программисты предпо читают использовать C++.

Создание сетевой оболочки (или библиотеки классов) требует знания практи чески всех технологий, рассматриваемых в книге. Фактически в этой главе будут затронуты некоторые технологии, подробно описываемые лишь в части IV, "Сложные сетевые методики".

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

Зачем программировать сокеты в C++?

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

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

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

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

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

264 Часть III. Объектно ориентированные сокеты www.books-shop.com Конечно, программист захочет узнать, как автор библиотеки классов реализо вал некоторые элементы, но степень информации, которую можно ему предоста вить, не должна быть слишком высокой. Инкапсуляция — один из инструментов объектно ориентированного программирования — является средством защиты как автора библиотеки, так и конечного программиста. Скрыв некоторые детали реализации, можно быть уверенным в том, что программист не изменит их впо следствии. Это также позволит в случае необходимости адаптироваться к систем ным изменениям, не затронув внешние интерфейсы.

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

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

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

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

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

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

Но сегодня это уже не проблема. Современные компоновщики отбрасывают код, ненужный в программе.

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

Глава 13. Программирование сокетов в C++ www.books-shop.com Определение общих характеристик К библиотеке должны предъявляться некие общие требования. Они задают направления, в которых следует вести работу. Часто полученный список требова ний недостаточно детализирован, поэтому приготовьтесь задавать вопросы. Биб лиотека должна поддерживать все основные возможности сокетов, описанные в данной книге.

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

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

Такое взаимодействие осуществляется по протоколу UDP.

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

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

Отправка простейших сообщений Второе требование не менее важно: библиотека должна позволять принимать и отправлять сообщения. У сообщения есть адрес, канал, по которому оно переда ется, и непосредственно тело.

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

Можно сделать так, чтобы сообщение доставлялось автономно. Например, в главе 11, "Экономия времени за счет объектов", упоминалась потоковая передача данных, подразумевающая их автоматическую упаковку и распаковку. В Java это сделать легко: достаточно объявить, что класс реализует интерфейс serializable.

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

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

266 Часть III. Объектно ориентированные сокеты www.books-shop.com Обработка исключений Третье требование заключается в необходимости контроля над различными исключениями и ошибками, возникающими при работе в сети. В сетевом про граммировании ошибки могут произойти в любое время: при создании и конфи гурировании сокета, приеме и передаче сообщений и т.д.

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

В C++ обработка исключений осуществляется с помощью конструкции try/catch, аналогичной той, что применяется в Java. Перехватывать нужно все ис ключения, иначе программа может завершиться аварийно. Непредвиденные ис ключения можно перехватывать на самом верхнем уровне, т.е. в функции main(), и возвращать какое нибудь осмысленное сообщение, чтобы упростить отладку программы.

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

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

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

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

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

Глава 13. Программирование сокетов в C++ www.books-shop.com Рис. 13.1. Все классы исключений порождаются от класса Exception, который сам по себе обрабатывает только ошиб ки работы со строками Сообщения: упаковка и доставка данных Для отправки и приема сообщений через подсистему ввода вывода необходи мы блоки данных и буферы. Виртуальный класс Message содержит базовые методы для упаковки (Wrap()) и распаковки (Unwrap()) своих собственных объектов. Это напоминает механизм сериализации в Java.

Другой класс, TextMessage, является примером того, как следует использовать эти методы. Например, метод Wrap() выделяет непрерывный блок памяти и копи рует в него текст (ответственность за освобождение блока ложится на того, кто вызывает этот метод). Сообщение в объекте TextMessage является строкой пере менной длины.

Метод Unwrap() выполняет противоположное Действие. Он принимает блок данных и восстанавливает его внутреннюю структуру. Иногда сообщение оказы вается неполным, например, размер сообщения в объекте TextMessage может пре вышать 64 Кбайт (максимальный размер буфера), поэтому метод Unwrap() должен в случае необходимости запрашивать недостающие данные. Метод возвращает значение True, когда процесс восстановления завершен.

Адресация: идентификация источника и приемника Каждое сообщение имеет адрес отправителя и получателя. Знать эти адреса необходимо для того, чтобы обеспечить правильную доставку сообщения. Этой цели служит класс HostAddress. Используя структуру struct sockaddr, он управляет IP адресами сообщений.

Все адреса, с которыми ведется работа в библиотеке сокетов, должны быть объектами класса HostAddress. Может поддерживаться несколько типов адресов.

268 Часть III. Объектно ориентированные сокеты www.books-shop.com Со кеты: создание, конфигурирование и подключение Последний компонент инкапсулирует все основные функции. В его основе лежит класс Socket, который управляет всеми соединениями и пользуется услуга ми других компонентов. Конкретные типы соединений реализуются в дочерних классах: SocketServer, SocketClient, Datagram, Broadcast и MessageGroup (групповая доставка). В них инкапсулированы все детали конфигурирования соответствую щих протоколов.

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

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

Отношения На рис. 13.2 изображены базовые компоненты библиотеки и отношения между ними. Как можно было предположить, класс Socket связан со всеми остальными компонентами отношениями "использует" или "генерирует".

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

В настоящий момент нас интересует класс Socket. На рис. 13.3 изображена схема отношений между его дочерними классами. Классы Broadcast и MessageGroup работают с UDP сокетами и имеют схожие функции, поэтому явля ются потомками одного класса — Datagram. Классы SocketClient и SocketServer работают с ТСР сокетами, поэтому в них инкапсулированы другие функции.

Глава 13. Программирование сокетов в C++ www.books-shop.com Рис. 13.3. Классы Datagram, MessageGroup и Broadcast связаны друг с другом, тогда как классы Socketclient и SocketServer обособлены Абстрактные элементы Класс Socket, не представленный на рис. 13.3, должен быть суперклассом, т.е.

предком всех остальных классов в своей иерархии. Его назначение заключается в обеспечении дочерних классов стандартным набором атрибутов и методов. В него входят методы Get()/Set() для всех атрибутов, а также методы Send(), Receive() и Close(). В то же время объекты этого класса не должны создаваться напрямую, так как с ним не связан конкретный протокол.

Добавление поддержки неструктурированных сокетов Можно изменить существующую иерархию таким образом, чтобы класс Socket отвечал за соз дание неструктурированных сокетов. Однако это может привести к возникновению некоторых трудностей. Дело в том, что функции неструктурированных сокетов ближе к UDP, чем к TCP, по этому в классах Socketclient и SocketServer их придется отключать. Это не самый удачный подход. Можно пойти другим путем — создать дополнительный класс, расположенный в иерар хии между классами socket и Datagram. В любом случае структура компонента усложнится.

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

Классы SocketServer и Socketclient обладают целым рядом общих характери стик, которые не могут быть инкапсулированы в классе Socket. Чтобы разрешить эту проблему, можно создать промежуточный класс SocketStream, который реали зует основные свойства ТСР сокета. На рис. 13.4 изображена полная иерархия класса Socket.

Рис. 13.4. В полную иерархию классов входят два виртуальных класса:

Socket и SocketStream 270 Часть III. Объектно ориентированные сокеты www.books-shop.com Определение задач каждого класса Мы достигли этапа, на котором необходимо ответить на вопрос: "Для каких целей можно использовать нашу библиотеку?" В отличие от традиционной мето дики нисходящего программирования, которая ориентирована на создание про ектов, а не библиотек, нельзя заранее предсказать, что конкретно может понадо биться пользователю.

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

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

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

Все атрибуты в основном связаны с параметрами сокетов. О них рассказыва лось в главе 9, "Повышение производительности", а полный их список приведен в приложении А, "Информационные таблицы". Как правило, параметры сокетов не являются переменными членами класса. Они обрабатываются с помощью ме тодов семейств Get() и Set(), в которых вызываются функции getsockopt() и setsockopt().

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

Один из атрибутов — параметр IP_TOS — обрабатывается особым образом. С ним связаны сразу четыре метода класса Datagram: MinimizeDelay(), MaximizeThroughput(), MaximizeReliability() и MinimizeCost(). Все они вызывают одну и ту же функцию setsockopt(), но с разными флагами: IPTOS_LOWDELAY (минимальная задержка), IPTOS_THROUGHPUT (максимальная пропускная способность), IPTOS_RELIABILITY (максимальная надежность) и IPTOS_LOWCOST (минимальная стоимость) соответствен но. В результате программисту не приходится делать это самостоятельно.

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

Глава 13. Программирование сокетов в C++ piracy@books-shop.com Puc. 13.5. Иерархия класса Socket с указанием методов всех классов Методы: что необходимо делать У большинства классов в нашей библиотеке есть не только методы, связанные с атрибутами, но и специализированные методы, реализующие особенности того или иного протокола. В основном все они вызывают свои аналоги из библиотеки Socket API (табл. 13.1).

272 Часть III. Объектно ориентированные сокеты www.books-shop.com Таблица 13.1. Специальные методы, реализованные в классах компонента Socket Класс Метод Описание Socket Bind() Вызывает функцию bind() Send() Вызывает функию send() либо sendto() Receive() Вызывает функцию recv() либо recvfrom() CloseInput() Закрывает входной канал с помощью функции shutdown() CloseOutput() Закрывает выходной канал с помощью функции shutdown() SocketServer Accept() Вызывает функцию accept() SocketClient Connect() Вызывает функцию connect() MessageGroup Connect() Вызывает функцию connect() Join() Подключает сокет к группе многоадресной доставки сообщений Drop() Удаляет сокет из группы многоадресной доставки сообщений У класса Broadcast нет специальных методов и атрибутов. Все свои методы он наследует от родительских классов, но используется особым образом.

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

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

//******************************************************* //*** Конструктор класса Socket, вызываемый по умолчанию.

//*** Принимает параметры Network (PF_INET) и Protocol //*** (SOCK_STREAM), создавая сокет и помещая его //*** дескриптор в атрибут SD.

Socket::Socket(ENetwork Network, EProtocol Protocol) {.

SD = socket(Network, Protocol, 0 ) ;

if ( SD < 0 ) throw NetException("Could not create socket");

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

Глава 13. Программирование сокетов в C++ www.books-shop.com Не все операции доступны в конструкторе. В частности, в нем нельзя вызы вать методы создаваемого им объекта, если только они не бьши объявлены стати ческими (спецификатор static). Пока конструктор не завершился, компилятор считает, что контекст класса еще не полностью сформирован, поэтому запрещает вызывать обычные его методы.

Статические методы класса не зависят от контекста, и их можно вызывать всегда. Для этого не требуется даже, чтобы существовал объект класса: при вызо ве статического метода вместо имени объекта можно указывать имя класса, после которого следует оператор ::. По сути, это служебные методы класса. Они редко используются, но в определенных ситуациях оказываются полезными. Некоторые методы, например обработчики сигналов, не могут выполняться в контексте объ ектов, поэтому они должны быть статическими.

Деструкторы: что необходимо очищать Подобно тому как конструкторы инициализируют объект и подготавливают его к использованию, деструкторы выполняют обратные действия, удаляя объект из памяти. В отличие от Java, где имеется встроенный механизм уборки мусора, в C++ требуется удалять объекты вручную.

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

Одно из наиболее часто выполняемых действий в деструкторе — закрытие файлов. Ниже приведено описание деструктора класса Socket:

//*************************************** //*** деструктор класса Socket Socket::~Socket(void) { if ( close(SD) != 0 ) throw FileException("Can't close socket");

} Все деструкторы следует объявлять виртуальными (спецификатор virtual), чтобы при удалении объекта компилятор мог вызвать все родительские деструк торы в иерархии. Когда деструктор не является виртуальным, это свидетельствует о том, что деструкторы родительских классов не должны вызываться. Если дест руктор суперкласса (начального в иерархии) объявлен виртуальным, то деструк торы всех дочерних классов также будут виртуальными. Но для ясности лучше все же явно указывать спецификатор virtual.

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

274 Часть III. Объектно ориентированные сокеты www.books-shop.com Эхо клиент и эхо сервер Первый пример (наиболее часто упоминаемый в книге) — это связка эхо клиента и сервера. С их помощью можно легко проверить, правильно ли устанав ливается соединение. Остальное уже будет проще проверять.

Листинг 13.1. Эхо клиент (echo client.cpp) // Основное тело клиента HostAddress addr(strings[l]);

// формат <адрес:порт> try { SocketClient client(addr);

// создаем сокет и подключаемся TextMessage msg(1024);

//резервируем буфер для сообщения do // повторяем цикл до тех пор, пока не будет получено "bye" { char line[100];

client.Receive(msg);

// принимаем сообщение printf("msg=%s", msg.GetBuffer());

// отображаем его fgets(line, sizeof(line), stdin);

// запрашиваем строку у пользователя msg = line;

// помещаем ее в буфер client.Send(msg);

// и отправляем на сервер } while ( strcmp(msg.GetBuffer(), "bye\n") != О );

} catch (Exceptions err) { err.PrintException();

} Во всех программах используется одинаковый формат обработчиков try/catch.

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

//******************************************** // Перехват всех непредусмотренных исключений catch(...) { fprintf(stderr, "Unknown exception!\n");

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

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

Глава 13. Программирование сокетов в C++ www.books-shop.com Листинг 13.2. Эхо сервер (echo server.cpp) // Основное тело сервера try { SocketServer server(port);

// создаем и конфигурируем сокет do // цикл выполняется бесконечно server.Accept(Echoer);

// принимаем запрос от клиента while (1);

} Как видите, тело сервера очень простое. Класс SocketServer самостоятельно выполняет все действия по инициализации и конфигурированию сокета. Осталь ные операции реализованы в функции Echoer() (листинг 13.3).

Листинг 13.3. Обработка сообщений в эхо сервере (echo server.cpp, часть 2) // Процедура обслуживания клиентских запросов try { TextMessage msg(1024);

// формируем буфер сообщений client.Send(welcome);

// посылаем приветственное сообщение do { client.Receive(msg);

// принимаем сообщение client.Send(msg);

// и возвращаем его обратно } while ( msg.GetSize() > 0 && strcmp(msg.GetBuffer(), "bye\n") != О ) _} Опять таки, алгоритм достаточно прост: сервер принимает сообщение и воз вращает его обратно, пока клиент не пришлет строку "bye". Обратите внимание на то, что в этом фрагменте есть свой блок try/catch. Это очень хорошая идея, так как возникающие здесь ошибки связаны непосредственно с соединением и не должны влиять на работу основного тела сервера. Если не указать этот блок, то в случае отключения клиента сервер прекратит работу, так как в блоке try/catch функции main() будет перехвачено исключение.

Размещение обработчиков исключений Тщательно выбирайте места размещения обработчиков. Не обязательно учитывать все возмож ные типы исключений. Как правило, отдельный блок try/catch требуется там, где осуществля ется прием и передача сообщений, а все остальные исключения можно обрабатывать на самом верхнем уровне программы.

276 Часть III. Объектно ориентированные сокеты www.books-shop.com Многозадачная одноранговая передача сооб щений В настоящий момент в библиотеке не реализован многозадачный режим, но это несложно сделать. Ниже приведен классический пример UDP модуля, кото рый в процессе отправки данных может принимать любое число сообщений (листинг 13.4).

Листинг 13.4. Одноранговая передача сообщений (реег.срр) // Модуль отправки дейтаграмм try { HostAddress addr(strings[l]);

// определяем собственный адрес Socket *channel = new Datagram(addr);

// создаем сокет if ( !fork() ) //"порождаем новый процесс receiver(channel);

// вызываем принимающую сторону channel >CloseInput();

// закрываем входной канал HostAddress peer(strings[2]);

// определяем адрес получателя TextMessage msg(1024);

// резервируем буфер для сообщения do { char line[IOO];

fgets(line, sizeof(line), stdin);

// запрашиваем строку у пользователя msg = line;

// помещаем ее в буфер channel >Send(peer, msg);

//и отправляем получателю } while ( !done );

delete channel;

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

• Придерживайтесь правил — учтите все замечания, которые приводились в главе 7, "Распределение нагрузки: многозадачность". В частности, не забывайте получить от потомка сигнал завершения. Кроме того, нужно помнить, что потоки совместно пользуются каналами ввода вывода. Ес ли канал закрывается на одном конце, он будет автоматически закрыт во всех потоках.

• Совместное использование памяти — это почти не решаемая проблема.

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

• Ограничения C++ реализовывать многозадачный режим в C++ опас но. Очень многое делается "за кулисами", поэтому нужно внимательно следить за тем, какой поток какими данными владеет. Если объект соз дается в стеке, а потом запускается новый поток, последствия могут быть непредсказуемыми.

Глава 13. Программирование сокетов в C++ www.books-shop.com Существующие ограничения Описываемая библиотека классов является примером (хоть и незавершенным) того, как можно программировать сокеты в C++. Она должна подсказать читате лям, как следует писать свои собственные приложения.

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

Передача сообщений неизвестно го/неопределенного типа Библиотека позволяет порождать новые классы сообщений от класса Message.

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

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

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

В первую очередь при определении класса сокета необходимо решить, что бу дет создаваться внутри сокета: процессы или потоки? И является ли новое зада ние отдельным объектом? В этом случае придется порождать новый класс сразу от двух классов: Socket и Process/Thread.

Резюме: библиотека сокетов упрощает программирование Применяя объектно ориентированный язык, такой как C++, можно сущест венно упростить программирование сокетов. Библиотека сокетов координирует взаимодействие компонентов, которые представляют собой наборы сетевых клас сов. Каждый класс решает свою часть общей задачи.

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

Компонент Exception отделен от остальных, чтобы уменьшить риск возникнове ния внутренних ошибок. Компоненты HostAddress и Message являются служебны ми и применяются в компненте Socket.

278 Часть III. Объектно ориентированные сокеты www.books-shop.com Глава Ограничения объектно ориентированного программирования В этой главе...

Правильное использование объектов Объекты не решают всех проблем Проблема чрезмерной сложности Проблема управления проектами Резюме: зыбучие пески ООП www.books-shop.com Объектная технология не решает всех проблем. У нее есть своя область при менения. Не следует пытаться использовать ее всегда и везде, так как во многих случаях лучше подходят другие технологии программирования.

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

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

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

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

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

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

Каждая функция в данном случае представляет собой пару "глагол — объект", на пример: "Напечатать отчет". Субъектом действия подразумевается программа.

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

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

Сценарий №9: "Отправка сообщения пользователю сети" [Пользователь вводит сообщение и щелкает на кнопке Send] Устанавливаем соединение с удаленным узлом Вызываем абонента [Абонент подтверждает наличие связи] Отправляем сообщение Выдаем подтверждение пользователю [Пользователь получает код завершения] 280 Часть III. Объектно ориентированные сокеты www.books-shop.com Случай №9.1: "Отправка сообщения: в приеме сообщения отказано" [Пользователь вводит сообщение и щелкает на кнопке Send] Устанавливаем соединение с удаленным узлом Вызываем абонента [Абонент отказывается принять сообщение] Сообщаем пользователю об отказе [Пользователь получает код завершения] Случай №9.2: "Отправка длинного сообщения пользователю сети" [Пользователь вводит сообщение и щелкает на кнопке Send] Устанавливаем соединение с удаленным узлом Вызываем абонента [Абонент подтверждает наличие связи] Отправляем сообщение по частям Выдаем подтверждение пользователю [Пользователь получает код завершения] Последний этап — на основании сценариев графически изобразить ход работы программы. Важно, чтобы каждый сценарий был учтен и было показано, как входные данные изменяются на пути к пункту своего назначения.

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

Имя объекта задает его текущее поведение и возможную эволюцию. Хорошее имя всегда является существительным. Например, лучше назвать объект Socket, чем NetIO, поскольку работа в сети подразумевает не только ввод вывод. Следует избегать употребления глаголов и отглагольных существительных, так как в этом случае осуществляется привязка к тому, что объект делает сейчас, и ограничива ется его использование в будущем.

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

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

На этапе анализа предметную область нужно рассматривать на макроуровне.

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

Глава 14. Ограничения объектно ориентированного... piracy@books-shop.com Системные ограничения Иногда в список системных требований входят конкретные аппаратные или программные ограниче ния. Если их немного и они связаны с финансовым/обеспечением проекта, их можно учесть сразу.

Однако в большинстве случаев их можно проигнорировать вплоть до этапа проектирования.

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

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

Избыточное наследование На этапе проектирования классы, определенные на этапе анализа, наполняют ся деталями. Здесь необходимо быть очень внимательным, так как велико иску шение связать между собой все классы отношениями наследования. Основной довод здесь таков: "Мы провели тщательный и полноценный анализ. Давайте те перь так же тщательно выполним проектирование". Тем не менее подобная до тошность редко необходима.

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

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

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

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

282 Часть III. Объектно ориентированные сокеты www.books-shop.com • Соответствует ли основное назначение класса поставленной задаче?

• Достаточно ли переписать всего один или два метода?

• Правильную ли роль играют родительские классы?

• Правильно ли обрабатываются существующие данные?

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

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

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

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

• Не передавайте данные по значению или через указатель — вместо этого везде, где возможно, применяйте ссылки (s). В противном случае могут возникнуть потерянные указатели или же произойдет снижение произ водительности из за постоянного вызова конструкторов и деструкторов.

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

• Выбирайте оператор, соответствующий смыслу операции, — например, запись <строка>+<строка> понятна, а запись *<стек> не обязательно означает выражение <стек> >Рор{).

• При необходимости делайте перегруженными операторы new и delete — некоторые авторы рекомендуют делать это всегда, хотя это спорный во прос.

• Никогда не перегружайте непонятные операторы — перегруженные опе раторы вызова функции ("()") и доступа к полям структур ("." и " >") редко будут использоваться правильно.

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

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

Глава 14. Ограничения объектно ориентированного... www.books-shop.com Объекты не решают всех проблем Некоторые люди заявляют, что ООП — единственно верная технология про граммирования. Конечно, с помощью объектов можно сделать многое, но опре деленные задачи все же проще решать другими средствами. Нельзя ограничивать себя каким то одним инструментом.

И снова об избыточном наследовании Важное место в ООП занимает концепция наследования, которая подразуме вает повторное использование существующих классов. К сожалению, некоторые программисты считают это панацеей от всех бед, поэтому применяют наследова ние везде, где только возможно. Они пытаются объединить все классы в одну ие рархию, даже в тех случаях, когда это вовсе не требуется. (Обратите внимание на то, что подобные суждения неприменимы в отношении Java, где все классы по рождаются от класса Object.) Недоступный код ООП позволяет эффективно решать многие традиционные задачи программи рования. Поскольку эти задачи постоянны и неизменны, возможность повтор ного использования кода очень важна. Как сказал кто то, не исключено, что большинство программ уже было кем то написано. Тем не менее не стоит забы вать, что не все подвластно объектам. Есть области, в которых проявляются не достатки ООП.

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

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

Мутации объектов Еще один недостаток заключается в мутации объектов, когда один объект мо жет быть преобразован в другой без операции приведения типа. Даже в объектно ориентированных языках программирования со строгой типизацией, таких как C++ и Java, эта операция потенциально опасна. Мутация — это форма преобра зования, при которой объект переходит из одного состояния в другое по опреде ленным правилам. Сначала создается общий объект, который по мере анализа 284 Часть III. Объектно ориентированные сокеты www.books-shop.com постепенно раскрывает свои свойства, в результате чего может даже получиться совершенно новый, ранее неизвестный в программе класс. Мутация широко применяется при работе с объектными потоками, где имеется большой двоичный объект (BLOB — binary large object), который нужно "расшифровать". Например, можно создать конструктор трансляции, который в зависимости от внутренней структуры полученного аргумента создает объект того или иного класса.

Мутация выполняется при соблюдении следующих условий.

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

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

• Модуль преобразования должен уметь создавать экземпляры абстрактного класса — это может вызывать споры, но в процессе анализа объекта, как правило, приходится начинать с абстрактного класса.

• В результате всегда должен получаться экземпляр конкретного класса — причина этого понятна: не должен существовать объект неполного класса.

• Модуль преобразования должен иметь доступ к закрытым членам класса — это, казалось бы, противоречит принципу инкапсуляции, но для того чтобы выяснить природу двоичного объекта, необходимо иметь доступ к внутренним частям конечного класса.

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

Мутировавшие объекты — это отличное средство работы с классами, о кото рых на этапе создания программы еще не было известно.

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

Игнорирование устоявшихся интерфейсов За время существования объектно ориентированного программирования про фессионалы усвоили ряд уроков. Основной из них — важность устоявшихся ин терфейсов. Интерфейс класса определяет его долговечность. Но у программистов редко хватает времени на разработку наилучшего интерфейса.

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

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

Глава 14. Ограничения объектно ориентированного... www.books-shop.com Множественное наследование Сложность возникает вследствие увлечения наследованием. В C++ можно создавать классы, являющиеся потомками сразу нескольких классов. Управлять такой иерархией достаточно сложно. В главе 11, "Экономия времени за счет объ ектов", вводилось понятие связности модулей. Когда применяется множествен ное наследование, возникает чрезмерная связность между классами. В случае из менения родительских классов разработчику часто приходится проверять интер фейс дочернего класса, что неудобно.

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

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

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

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

В зависимости от сложности классов их размер может на 20—50% превышать ;

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

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

Можно избежать разрастания, придерживаясь следующих правил.

• Старайтесь не перегружать операторы без особой необходимости.

• Не пытайтесь втиснуть все классы в рамки единой иерархии наследова ния.

• Не применяйте виртуальное наследование.

• Поменьше используйте виртуальные методы.

• Старайтесь избегать множественного наследования.

286 Часть III. Объектно ориентированные сокеты www.books-shop.com • Не злоупотребляйте inline функциями. (Некоторые специалисты утвер ждают, что следует вообще избегать макроподстановки функций, оста вив это на усмотрение компилятора.) Проблема управления проектами Объектный проект выдвигает новый и непривычный набор проблем для руко водителя, которому приходится согласовывать усилия большого числа людей и проверять, в правильном ли направлении движется работа. Руководитель объект ного проекта должен быть универсалом. Умение правильно распределять обязан ности, координировать, организовывать и даже быть дипломатом — все это чрез вычайно важно для успешной реализации проекта. Нужно ведь не только уло житься в заданные сроки и не превысить бюджет. Если команда распадается после завершения проекта, или документация оказывается неудовлетворительной, или клиент остается неудовлетворенным, проект можно считать неудачным.

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

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

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

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

• Спонсор проекта — выделяет деньги на его реализацию и определяет ос новные направления проекта.

• Представитель заказчика — взаимодействует с заказчиком и согласовы вает с ним промежуточные итоги. Он должен быть знаком с образом мышления заказчика и представлять, как будет применяться разрабаты ваемая система. Он определяет исходные требования, предъявляемые к продукту, и оказывает консультации по ходу выполнения проекта.

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

• Технические специалисты — занимаются анализом в процессе проектиро вания. Они связывают программные модели с объектами реального мира и регулярно согласовывают промежуточные итоги с бизнес аналитиками.

• Специалист, отвечающий за контроль качества, — собирает информацию о проекте и руководит процессом тестирования. После того как бизнес аналитики утвердят исходные требования к проекту, специалист по кон тролю начнет разрабатывать тесты, на основании которых будет осуществ Глава 14. Ограничения объектно ориентированного... www.books-shop.com ляться прием проекта. Когда процедура проектирования будет завершена, этот человек приступит к выполнению граничных и рабочих тестов.

• Программисты — осуществляют проектирование системы и программи руют ее на выбранном языке.

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

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

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

Как свидетельствует мировой опыт, лишь 20% времени проекта уходит непо средственно на программирование. Остальные 80% поровну распределяются меж ду этапами анализа/проектирования и тестирования. В американской программ ной индустрии показатели обычно такие: 33% — анализ и проектирование, 34% — программирование и 33% — тестирование. Итого программирование за нимает в лучшем случае 34% времени. Так почему бы не тратить спокойно время на анализ исходных требований и проектирование?

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

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

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

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

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

Среди всех участников проекта специалисту по контролю качества выпадает са мая трудная миссия.

Объектная технология не помогает в данном вопросе, а лишь усложняет все в два три раза. Раньше достаточно было тестирования по методу прозрачного и черного ящиков (подробно они описаны ниже). С появлением модулей возникла потребность в промежуточном тестировании. А в объектном проекте нужно тес 288 Часть III. Объектно ориентированные сокеты www.books-shop.com тировать наследование, полиморфизм, интерфейсы классов и т.д. Это заставило многих изменить существующие подходы к тестированию.

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

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

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

Обычно это вполне может сделать программист или разработчик.

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

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

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

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

Глава 14. Ограничения объектно ориентированного... www.books-shop.com www.books-shop.com Часть Сложные сетевые методики IV В этой части...

Глава 15. Удаленные вызовы процедур (RPC) Глава 16. Безопасность сетевых приложений Глава 17. Широковещательная, групповая и магист ральная передача сообщений Глава 18. Неструктурированные сокеты Глава 19. IPv6: следующее поколение протокола IP piracy@books-shop.com Глава Удаленные вызовы процедур (RPC) В этой главе...

Возвращаясь к модели OSI Сравнение методик сетевого и процедурного программирования Связующие программные средства Создание RFC компонентов с помощью утилиты rpcgen Учет состояния сеанса в открытых соединениях Резюме: создание набора RPC компонентов www.books-shop.com Если смотреть с точки зрения прикладного программиста, то знать все детали сетевого программирования достаточно затруднительно. Иногда хочется просто сосредоточиться на разработке конкретного программного алгоритма, предоста вив детали сетевого взаимодействия соответствующим библиотекам и функциям.

И здесь на арену выходит технология RPC (Remote Procedure Calls — удаленные вызовы процедур).

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

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

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

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

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

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

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

Часть функциональных возможностей можно покрыть своими собственными интерфейсами. Это позволит другим программам использовать готовые функции, как часто происходит в различных сетевых моделях, а сами интерфейсы можно сделать прозрачными по отношению к сети. Программы, работающие с "прозрачными" протоколами, избавляют пользователя (или программиста) от не Глава 15. Удаленные вызовы процедур (RPC) www.books-shop.com обходимости самостоятельно взаимодействовать с сетью. Идея заключается в том, чтобы сделать сетевое соединение по возможности автоматизированным.

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

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

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

Границы языков Первая проблема, с которой можно столкнуться, — это сам язык сетевого взаимодействия. Например, в языке С можно делать много такого, что не разре шается сетевыми протоколами. В первую очередь нужно помнить о том, что пе редача данных в сети осуществляется в виде значений этих данных. Указатели не допустимы по очевидной причине: адрес памяти на одном компьютере не равен адресу на другом. Это противоречит некоторым распространенным принципам программирования. Никому не нравится передавать структуру или массив по зна чению, так как копии данных "съедают" память и ресурсы процессора.

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

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

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

В результате возникает проблема возвращения данных обратно из процедуры или функции. Можно передавать все данные по значению, но иногда без указате лей не обойтись. Ниже показан прототип функции getphoto(), реализующей запрос к серверу на получение фотографии. Типы данных image_t и host_t являются поль 294 Часть IV. Сложные сетевые методики www.books-shop.com зовательскими. Без применения указателя невозможно было бы обнаружить ошиб ки, так как ни структура, ни массив не могут принимать значение NULL.

/*******************************************************/ /*** Пример, передачи аргумента по значению запрос? ***/ /*** на получение фотографий ***/ /**************************************************/ image_t *getphoto(host_t host);

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

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

Как правило, соответствующие указания даются только в документации. Рас смотрим следующий пример:

/********************************************************/ /*** Пример сетевой функции ***/ /* ***ЗАМЕЧАНИЯ*** */ /* getuserinfo() — получение информации о пользователе */ /* от сервера */ /* user — (входной) идентификатор пользователя */ /* host — (входной) внешнее имя сервера */ /*' data (выходной) результаты запроса */ /* ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ: успех йЯи неудача */ /* (проверьте переменную errno) */ int getuserinfо(char* user, char* host, userinfo_t *data);

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

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

Глава 15. Удаленные вызовы процедур (RPC) www.books-shop.com Сетевой вызов в своей простейшей форме не хранит информацию о состоя нии, он лишь требует от сервера выполнить четко определенное действие. До в случае организации сеанса необходимо осуществить ряд подготовительных про цедур как на стороне клиента, так и на стороне сервера.

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

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

Pages:     | 1 |   ...   | 3 | 4 || 6 | 7 |   ...   | 8 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.