Курс лекций "Разработка распределенных приложений" Аннотация Главная цель курса - научить студентов разрабатывать распределенные приложения и дать подробный обзор существующих технологий. Курс лекций знакомит студентов с современными технологиями построения клиент-серверных приложений в двухзвенных и трехзвенных архитектурах. В курсе освещается применение программной модели COM+, вопросы взаимодействия служб COM и .NET Framework, взаимодействие приложений посредством .NET Remoting, создание и использование служб Windows, создание и развертывание Web-сервисов. Рассматриваются возможности, предоставляемые библиотеками классов среды проектирования Visual Studio .NET, по созданию Web-приложений. Дается краткий обзор технологии ASP.NET, используемой для создания распределенных приложений, изучаются вопросы создания пользовательских элементов управления ASP.NET и публикации данных на ASP-страницах, затрагиваются вопросы создания мобильных приложений ASP.NET.
Тема 1. Модель компонентных объектов – COM+ ТРЕБОВАНИЯ К COM КОМПОНЕНТАМ COM - это метод разработки программных компонентов - небольших двоичных исполняемых файлов, которые предоставляют необходимые сервисы приложениям, операционным системам и другим интерфейсам.
Разработка распределенных приложений
2
COM - это спецификация. Она указывает как создавать динамически взаимозаменяемые компоненты. На основе COM построены технологии Active X,DirectX, OLE (2 версия). OLE 1 была реализована с использованием DDE (динамический обмен данными). DDE в свою очередь построен на основе архитектуры передачи сообщений Windows. Технология Com позволяет выполнить разделение монолитного приложения на отдельные компоненты, что делает приложение § более динамичным; § облегчает обновление частей приложения; § позволяет собирать новые приложения из имеющихся частей библиотеки компонентов; § легче выполнить адаптацию приложения к уонкретным требованиям; § позволяет заменять компоненты во время работы приложения; § упрощает процесс разработки распределенных приложений. Компоненты COM должны: § подключаться динамически; § инкапсулировать (скрывать) детали своей реализации. Клиент - это программа или компонент, использующий другой компонент. Клиент подсоединяется к компоненту через интерфейс. Если компонент изменяется без изменения интерфейса, то изменения в клиенте не требуются. Изоляция клиента (предоставляемого ему интерфейса) от реализации накладывает на компоненты следeдующие ограничения: 1. Компонент должен скрывать используемый язык программирования. 2. Компонент должен распространятся в двоичной форме. 3. Новые версии компонента должны работать как с новыми, так и со старыми клиентами. 4. Компоненты должны быть (прозрачно) перемещаемы по сети. Удаленный компонент для клиента рассматривается также, как и локальный (иначе это бы вызывало перекомпиляцию клиента при перемещении компонента).
Разработка распределенных приложений
3
5. Компонент должен одинаково выполняться : - внутри одного процесса - в разных процессах - на разных машинах.
БИБЛИОТЕКА COM В состав COM входит библиотека API. Она предоставляет сервисы управления компонентами. Эти функции можно использовать как из библиотеки, так и реализовать самостоятельно. Для подключения COM-библиотеки следует вызвать метод CoInitialize или OleInitialize. Освобождение библиотеки выполняется вызовом CoUninitialize. Для каждого процесса библиотеку следует инициализировать только один раз. По общему соглашению COM инициализируется в EXE, а не в DLL. Для того чтобы использовать возможности не только COM, но и OLE следует для инициализации библиотеки использовать функции (это требует больше ресурсов): OleInitialize и OleUninitialize соответственно.
ИНТЕРФЕЙСЫ ОПРЕДЕЛЕНИЕ ИНТЕРФЕЙСА Интерфейс DLL - это набор функций, экспортируемых ею. Инерфейс класса С++ - это набор членов данного класса. Интерфейс COM - это не только набор функций, но и определенная структура в памяти, содержащая массив указателей на функции, где каждый элемент массива содержит адрес функции, реализуемой компонентом. В языке прогаммирования С++ интерфейс реализуется с помощью абстрактных базовых классов. Каждый компонент может поддерживать множество интерфейсов. И, следовательно, для реализации компонента с несколькими интерфейсами используется множественное наследование абстрактных базовых классов.
Разработка распределенных приложений
4
РЕАЛИЗАЦИЯ ИНТЕРФЕЙСА CОМ интерфейсы в С++ формируются на основе чисто абстрактныхе базовых классов (такие классы содержат только чисто виртуальные функции (virtual fx() = 0 ;)).Чисто виртуальные функции только объявляются, а реализуются в производных классах. Будем называть наследование от чисто абстрактного базового класса наследованием интерфейса. При использовании чисто абстрактных базовых классов в памяти создается определенная структура - таблица виртуальных функций (vtbl). Microsoft Win32 SDK содержит заголовочный файл OBJBASE.H, в котором определен интерфейс: #define interface struct В среде проектирования Borland Developer Studio в языке Object Pascal (Delphi for Windows32) interface является ключевым словом и сразу реализуется как COM-интерфейс. Так как в разных языках программирования передача параметров выполняется по-разному, то необходимо соглашение о передаче параметров. Технология COM использует соглашение о вызове функций _stdcall : функция выбирает параметры из стека перед возвратом в вызывающую процедуру. В С++ - стек очищает вызывающая процедура (обязательно и для переменного числа параметров). Интерфейс - это набор функций. Компонент - набор интерфейсов. Система набор компонентов.
Разработка распределенных приложений
5
На следующей схеме приведены два компонента, каждый из которых реализует интерфейс IX1 и IX2. Компонент 1
Компонент 2 Fx1 Fx2
o-
o-
Fx1 Fx2 o--
IX1
IX1
Fxn
Fxn
Fx1 Fx2
Fx1 Fx2 o--
IX2 Fxn
IX2 Fxn
После того, как интерфейс опубликован, его нельзя изменять. Публикация - это описание интерфейса и кодов ответа успешного выполнения. ТАБЛИЦА ВИРТУАЛЬНЫХ ФУНКЦИЙ Чисто абстрактный базовый класс определяет нужную для СОМ структуру блока памяти. На следующей схеме представлена структура адресов функций, формируемая как таблица виртуальных функций. Интерфейс IX Табл. вирт. функций pIX Указатель vtbl ® &Fx1 ® ® &Fx2 ® &Fx3 ® &Fx4 ®
Разработка распределенных приложений
6
Таблица виртуальных функций - это массив указателей на реализации виртуальных функций для интерфейса. Для приведенной выше схемы объявление интерфейса выглядит следующим образом: interface IX { virtual void __stdcall Fx1() = 0; virtual void __stdcall Fx2() = 0; virtual void __stdcall Fx3() = 0; virtual void __stdcall Fx4() = 0; }; Реализация этого интерфейса может быть записана как: class CA : public IX { public: // Реализация интерфейса IX virtual void __stdcall Fx1() {count << "CA::Fx1"<<endl ;} virtual void __stdcall Fx2() {count << "CA::Fx2"<<endl ;} ... // Конструктор CA (double d) : m_Fx2(d*d), m_Fx3(d*d*d) { } // Данные экземпляра double m_Fx2; double m_Fx3; }; Классы С++ позволяют работать с данными напрямую, а интерфейсы - нет. Доступ к данным COM-сервера должен осуществляться только через соответствующие методы интерфейса. Следующая схема демонстрирует, что только функции, объявленные в интерфейса, могут быть доступны посредством указателя на интерфейс. Интерфейс IX Клиент CA pA
Табл. вирт. функций ®
Указатель vtbl &m_Fx2
® ®
&Fx1 &Fx2
® ®
Fx1 Fx2
Разработка распределенных приложений &m_Fx3
®
7 &Fx3 &Fx4
® ®
Fx3 Fx4
ОБЪЯВЛЕНИЕ COM ИНТЕРФЕЙСА Для вызова метода COM-компонента клиент запрашивает у компонента интерфейс. Требование обязательного запроса интерфейса делает систему прозрачной для изменения версий. IUnknown – это специальный интерфейс (определен в заголовочном файле UNKNWN.H Win32 SDK), который наследуют все COM-интерфейсы. Этот интерфейс расположен в верхней части таблицы виртуальных функций и имеет следующее формальное описание. interface IUnknown { virtual HRESULT __stdcall QueryInterface ( const IID& iid, void** ppv)=0; virtual ULONG __stdcall AddRef() =0; virtual ULONG __stdcall Release() = 0; } pIX
®
Указатель vtbl
® ® ®
QueryInterface QueryInterface Release &Fx
® ® ® ®
QueryInterface QueryInterface Release Fx
Таким образом, чтобы интерфейс был COM интерфейсом, - он должен наследоваться от интерфейса IUnknown. Для получение указателя на IUnknown можно использовать функцию CoCreateInstance из COM-библиотеки. Спецификация СОМ определяет следующие соглашения по реализации метода QueryInterface: § клиент всегда получает один и тот же IUnknown;
Разработка распределенных приложений
8
§
клиент сможет получить интерфейс снова, если смог получить его раньше; § клиент всегда может снова получить интерфейс, который у него уже есть; § клиент всегда может вернуться туда, откуда начал (т.е., если IX получается через IY, то это возможно повторят многократно); § если клиент каким либо способом получил доступ к некоторому интерфейсу, то сможет получить к немуе доступ и другим способом (при наличии других интерфейсов). Интерфейсы, поддерживаемые компонентом - это те интерфейсы, указатели на которые возвращает метод QueryInterface. В модели DCOM может использоваться QueryMultiInterface - для запроса нескольких интерфейсов за один вызов. КОНТРОЛЬ ССЫЛОК НА КОМПОНЕНТ Клиент знает возможности, предоставляемые компонентом, только через интерфейсы. Поэтому он не может напрямую управлять временем жизни компонента как, т.к.: § в разных местах кода клиента могут быть вставлены вызовы этого компонента через различные интерфейсы и клиент может окончить использование одного интерфейса раньше другого; § сложно определить момент удаления компонента из памяти, т.к. не ясно указывают ли два указателя на интерфейсы одного и того же компонента (поэтому надо будет запрашивать IUnknown через оба интерфейса и сравнивать результаты); § возможность загрузить компонент и не выгружать до конца выполнения программы не обеспечивает эффективного выполнения; § удобным выходом является - сообщать компоненту о начале использования каждого его интерфейса и о завершении использования, а компонент сам должен отслеживать число ссылок. Функции AddRef и Release описываются в компоненте и реализуют технику управления памятью, называемую подсчет ссылок (reference couting). Это позволяет компонентам самим себя удалять - когда значение счетчика доходит до нуля. Существует три основных правила:
Разработка распределенных приложений
9
1. Функции, вызывающие интерфейсы, должны вызывать и AddRef для соответствующего указателя. (Это также относится и к QueryInterface и функции CreateInstance. Поэтому в клиенте не следует вызывать AddRef после получения указателя на интерфейс.) 2. При завершении работы с интерфейсом следует вызывать Release. 3. При создании новой ссылки на интерфейс (присвоении значения одного указателя другому) всегда следует вызывать AddRef. Компонент может поддерживать: § отдельные счетчики по каждому интерфейсу и, следовательно, клиент должен вызывать AddRef и Release именно для конкретного указателя на интерфейс (pIUnknown->Release() может не сработать); § общий счетчик на все интерфейсы. КОДЫ ОТВЕТА Все доступные коды ответа содержатся в файле Win32 WINERROR.H. ( #define E_NOINTERFACE 0x80004002L ) Код возврата - это32 битовое значение: 31р - Признак критичности | 30-16рр - Средство | 15-0 Код возврата В файле Win32 WINERROR.H предусмотрен набор кодов ответа, включая следующие значения кодов ответа: S_OK – функция выполнена успешно NOERROR S_FALSE E_UNEXPECTED неожиданная ошибка E_NOTIMPL не реализовано E_NOINTERFACE – интерфейс не найден E_OUTOFMEMORY – нехватает памяти. E_FAIL ошибка по неуказанной причине Все идентификаторы средств кроме FACILITY_ITF задют СОМ универсальные коды ответа. Эти коды всегда и везде одни и те же.
Разработка распределенных приложений
10
Код ответа имеет тип HRESULT. Методы СОМ-компонентов должны возвращать код ответа типа HRESULT. Макросы SUCCEEDED и FAILED позволяют обрабатывать несколько кодов ответа. GUID Любой компонент и интерфейс регистрируется в реестре Windows, по своему уникальному идентификатору – GUID. GUID это 128 битовое уникальное значение (48 - уникально для компьютера и 60 - для времени) (16 байтов). GUID может формироваться вызовом GuidGen.exe. На следующем рисунке приведен диалог, используемый для формирования уникального идентификатора GUID.
Для сравнения GUID в файле OBJBASE.H определены методы IsEqullGUID, IsEqualIID и IsEqualCLSID. Идентификатор класса (GUID) имеет тип CLSID.
Разработка распределенных приложений
11
Любой компонент и все его интерфейсы регистрируются в реестре Windows. Реестр Windows состоит из разделов. Раздел содержит § подразделы § набор именованных параметров § один параметр по умолчанию. Ветвь дерева HKEY_CLASSES_ROOT содержит разделы: § CLSID - 16 байтовые идентификаторы класса компонента и дружественное имя компонента (в параметре по умолчанию) § InProcServer32 - местоположение компонента § ProgID - текстовый идентификатор компонента (программный идентификатор) § VersionIndependedProgID - текстовый идентификатор компонента без номера версии § Текстовый идентификатор компонента (с подразделами CLSID и CurVer); § CATID - идентификатор категории компонентов; § IID - информация об интерфейсах (используется для доступа к интерфейсу через границы процессов); § TypeLib - библиотека типа, содержащая информацию о параметрах функций-членов интерфейсов. (Связывание с именем файла, в котором хранится библиотека типа); § APPID - связывание идентификатора приложения APPID с именем удаленного сервера. Реестр можно редактировать в редакторе реестра REGEDIT.EXE. Для того чтобы зарегистрировать в реестре компонент, расположенный в DLL, используются функции DllRegisterServer и DllUnregisterServer. Также зарегистрировать компонент можно вызовом REGSRV32.EXE. Для того, чтобы выполнить регистрацию программно вызовом DllRegisterServer, следует загрузить DLL, вызвав метод LoadLibrary и GetProcAddress. Реализация DllRegisterServer - это простой код обновления реестра. Для регистрации компонента или удаления его из реестра могут использоваться
Разработка распределенных приложений
12
следующие функции: RegOpenKeyEx, RegCreateKeyEx, RegSetValueEx, RegEnumKeyEx, RegDeleteKey, RegCloseKey. КАТЕГОРИИ КОМПОНЕНТОВ Для того чтобы до создания компонента определять, поддерживает ли он нужный интерфейс, следует использовать категории компонентов. Категория компонентов - это набор интерфейсов, которым присвоен CLSID, называемый в данном случае CATID. Все входящие в категорию интерфейсы обязаны быть реализованы компонентом. Компонент должен реализовать все интерфейсы категории Компонент может входить в несколько категорий и поддерживать интерфейсы, не входящие в категорию. Компонент регистрирует себя сам в некоторой категории. Этим компонент гарантирует, что он поддерживает все входящие в категорию интерфейсы. УПРАВЛЕНИЕ ПАМЯТЬЮ Для управления памятью используется специальный менеджер, который позволяет: § компоненту передать клиенту блок памяти; § клиенту освободить этот блок памяти. Управление памятью можно осуществлять различными способами: § 1. § Менеджер используется через интерфейс, называемый IMalloc. § Этот интерфейс возвращается функцией CoGetMalloc. § Для выделения и освобождения блока памяти используются функции: IMalloc::Alloc и IMalloc::Free. § 2. § Использовать функции CoTaskMemAlloc и CoTaskMemFree. Например: void* CoTaskMemAlloc (ULONG cb); // размер выделяемого блока в байтах void CoTaskMemFree (void* pv); // указатель на освобождаемый блок памяти В реестре содержатся строковые представления CLSID.
Разработка распределенных приложений
13
Библиотека COM содержит следующие функции для преобразования значений: StringFromCLSID Получает значение CLSID в виде строки StringFromIID Получает значение IID в виде строки StringFromGUID2 Конвертирует GUID в строку символов Unicode (двухбайтовые символы) wchar_t szCLSID[58]; int r= StringFromGUID2(CLSID_Component1,szCLSID,58); далее для преобразования в обычную строку можно использовать функцию wcstombs(CLSID_S,szCLSID,58), где char CLSID_S [58]; CLSIDFromString Получает значение CLSID IIDFromString Получает значение IID Для получения и освобождения строки можно использовать следующий код: wchar_t* string; // Получить строку из CLSID :: StringFromCLSID(CLSID_Component1, &string); // Использовать строку, а затем освободить ::CoTaskMemFree(string); ФАБРИКИ КЛАССА Библиотека СОМ для создания других компонентов предоставляет функцию CoCreateInstance (файл OLE32.DLL - для динамической компоновки или OLE32.LIB - для статической компоновки). Процесс создания компонента имеет следующее формальное описание: CoInitialize(NULL); //Инициализация библиотеки HRESULT __stdcall CoCreateInstance( const CLSID& clsid, // CLSID компонента IUnkown* pIUnknownOuter, // Внешний компонент DWORD dwCont, // Внешний контекст const IID& iid, // Запрашиваемый интерфейс
Разработка распределенных приложений
14
void** ppv); // Указатель на запрашиваемый интерфейс CoUninitialize(); // Отключить библиотеку Контекст класса определяется следующими константами (или их совокупностью): § CLSCTX_INPROC_SERVER - Клиент использует только те компоненты, которые исполняются в одном с ним процессе. § CLSCTX_INPROC_HANDLER - Используются обработчики в процессе - это компонент внутри процесса, который реализует часть компонента, а другая часть реализуется компонентом вне процесса: локальным или удаленным сервером. § CLSCTX_LOCAL_SERVER Используемые компоненты выполняются в другом процессе (но на той же машине) - это EXE файлы. § CLSCTX_REMOTE_SERVER - допускаются компоненты, выполняющиеся на другой машине (требуется DCOM). Преимущества и недостатки различных контекстов: скорость и совместная память. Файл OBJBASE.H содержит константы, позволяющие объединять контексты: § CLSCTX_INPROC § CLSCTX_ALL § CLSCTX_ SERVER Фабрика классов (ФК) - это компонент, предоставляющий сервисы для создания других компонентов. Но только соответствующие конкретному CLSID в созданной фабрике класса. ФК инкапсулирует создание компонента. ФК создается тем же разработчиком, что и сам компонент. И, как правило, содержится в той же DLL, что и компонент. На самом деле часто CoCreateInstance создает компонент - фабрику класса, а затем ФК порождает нужный компонент. COM библиотека для работы с ФК предоставляет интерфейс IClassFactory. IClassFactory – это стандартный интерфейс создания компонентов.
Разработка распределенных приложений Для его применения следует: 1. Создать компонент - фабрику класса (CoGetClassObject) и получить указатель IClassFacory HRESULT hr= CoGetClassObject(clsid, dwClsContex, NULL, IID_IClassFactory, (void)** &pIFactory); 2. Используя IClassFacory создать сам компонент hr= pIFactory->CreateInctance(pIUnknownOuter, iid,ppv); 3. Освободить фабрику класса pIFactory->Release(); Метод CoCreateInstance по CLSID возвращает указатель на интерфейс компонента, а метод CoGetClassObject по CLSID возвращает указатель на интерфейс ФК для данного CLSID. Метод CoGetClassObject имеет следующее формальное описание: HRESULT __stdcall CoGetClassObject( const CLSID& clsid, DWORD dwCont, // Внешний контекст COSERVERINFO* pServerInfo // Зарезервировано для DCOM const IID& iid, void** ppv); Интерфейс IClassFacory имеет следующее формальное описание: interfase IClassFactory: IUnknown { HRESULT __stdcall CreateInstance( IUnkown* pIUnknownOuter, // Внешний компонент const IID& iid, void** ppv); HRESULT __stdcall LockServer(BOOL block); }
15
Разработка распределенных приложений
16
Microsoft также объявила интерфейс IClassFactory2 для создания компонентов с поддержкой лицензирования или разрешения на создание. Клиент обязан передать фабрике класса посредством IClassFactory2 конкретный ключ или лицензию, для того чтобы ФК смогла создавать компоненты. Функция DllGetClassObject определяет точку входа DLL и имеет следующее формальное описание: STDAPI DllGetClassObject ( const CLSID& clsid, const IID& iid, void** ppv); Эта функция вызывается из CoGetClassObject. Рассмотрим более подробно процесс создания компонента посредством ФК: 1. Клиент вызывает CoCreateInstanse, реализованную в СОМ-библиотеке; 2. CoCreateInstanse реализована с помощью CoGetClassObject; 3. CoGetClassObject отыскивает компонент в реестре; 4. Если найден, то CoGetClassObject загружает DLL (являющуюся сервером компонента); 5. CoGetClassObject вызывает DllGetClassObject (реализованную DLLсервером); 6. DllGetClassObject создает фабрику класса оператором new; 7. DllGetClassObject дополнительно запрашивает у фабрики класса интерфейс IClassFactory; 8. Этот интерфейс IClassFactory возвращается для CoCreateInstanse; 9. CoCreateInstanse вызывает функцию CreateInstanse этого интерфейса IClassFactory; 10.CreateInstanse создает компонент (используя new) и запрашивает интерфейс IX.
Разработка распределенных приложений
17
На следующей схеме приведен способ применения одной фабрики класса , используемой для создания нескольких компонентов: Клиент DLL Вызывает à DllGetClassObject CLSID_1 | CoCreateInstance &CreateFunction_1 â CLSID_2 | &CreateFunction_2 Фабрика класса
CLSID_n | &CreateFunction_n
Компонент 1
Компонент n |
CreateFunction_1
CreateFunction_1
При использовании одной фабрики класса для создания нескольких компонентов можно использовать следующий алгоритм: 1. Для каждого компонента есть своя функции CreateFunction (создает компонент с помощью new и возвращает указатель IUnknown); 2. Из указателей на эти функции строится таблица, индексируемая CLSID каждого компонента; 3. Функция DllGetClassObject - отыскивает в таблице указатель нужной функции - создает фабрику класса и передает ей этот указатель; 4. ФК вместо оператора new вызывает соответствующую функцию создания компонента. Можно использовать один код ФК для создания нескольких компонентов, но один экземпляр ФК всегда создает только компоненты одного CLSID.
СЕРВЕРЫ ВНЕ ПРОЦЕССА Для того, чтобы с интерфейсами можно было работать через границы процессов необходимо следующее:
Разработка распределенных приложений §
18
Процесс должен иметь возможность вызывать функцию в другом процессе; § Процесс должен иметь возможность передавать другому процессу данные; § Клиент не должен беспокоиться о том, является ли компонент сервером внутри или вне процесса. Существуют различные методы межпроцессорной коммуникации: § DDE; § именованные каналы; § разделяемая память; § локальный вызов процедуры (LPC) (используется в СОМ). LPC это средство для связи между разными процессами на одной и той же машине, построенное на основе удаленного вызова процедуры (RPC). Стандарт RPC определен OSF (Open Software Foundation) в спецификации DCE (Distributed Computing Environment) RPC. RPC обеспечивает коммуникацию между процессами на разных машинах с помощью разнообразных сетевых протоколов. DCOM использует RPC для связи по сети. LPC - это механизм, в котором вызовы процедур реализуются операционной системой (которая знает соответствие физических адресов и логических адресов памяти процессов). Маршалинг - процесс передачи (копирования) параметров из адресного пространства клиента в адресное пространство компонента (ЕХЕ-модуля). Демаршалинг – процесс передачи параметров компоненту. Пр передачи параметров: § для процессов расположенных на одной машине достаточно простого копирования данных; § для процессов расположенных на разных машинах дополнительно следует преобразовать данные в стандартный формат (учитывая межмашинные различия). Механизм LPC способен скопировать данные, но ему недостаточно информации из заголовочного файла. Например, указатели на структуры следует обрабатывать иначе, чем целые числа.
Разработка распределенных приложений
19
Маршалинг указателя - это копирование в другой процесс структуры, на которую он указывает. Если указатель - это указатель на интерфейс, то область памяти на которую он указывает копироваться не должна. Маршалинг компонента реализуется интерфейсом IMarshal. При создании компонента COM запрашивает у компонента этот интерфейс и вызывает функции-члены этого интерфейса для маршалинга и демаршалинга параметров до и после вызова функций. Библиотека СOM реализует стандартную версию интерфейса IMarshal.
DCOM СОЗДАНИЕ ЛОКАЛЬНОГО ИЛИ УДАЛЕННОГО ОБЪЕКТА Технология DCOM (Microsoft Distributed Component Object) позволяет взаимодействовать компонентам, расположенным на разных компьютерах. Взаимодействие между клиентом и компонентом, расположенных не разных компьютерах представлено на следующей схеме.
Разработка распределенных приложений
20
В COM класс указывается своим уникальным идентификатором GUID (128 битовое значение). В следующей таблице приведены функции из COMбиблиотеки, которые можно использовать для создания объекта – COMкомпонента. Создает неинициализированный экземпляр CoCreateInstance(Ex) (
…) объекта класса и возвращает указатель на интерфейс CoGetInstanceFromFile
Создает новый экземпляр объекта и инициализирует его из файла
CoGetInstanceFromStorage
Создает новый экземпляр объекта и инициализирует его из памяти
CoGetClassObject (…)
Возвращает указатель на интерфейс для объекта "фабрика класса", который может быть использован для создания одного или нескольких неинициализированных экземпляров объекта класса
CoGetClassObjectFromURL Возвращает указатель на интерфейс объекта
Разработка распределенных приложений
21
фабрика класса для заданного класса. Если специфицированного класса нет, то функция будет выбирать подходящий класс для Multipurpose Internet Mail Extension (MIME) типа COM библиотека просматривает DLL и EXE в системном регистре и создает объект. Для DCOM, механизм создания объекта в COM библиотеке расширен разрешением создавать объект на другой машине. Для создания удаленного объекта COM библиотеки необходимо знать имя сервера, на котором зарегистрирован компонент. COM библиотека вызывает SCM-менеджер (service control manager) на клиентской машине соединяемой с SCMменеджером на серверной машине, и требует создания данного объекта.. DCOM обеспечивает два основных механизма, которые позволяют клиентам указывать имя удаленного сервера при создании объекта: 1. Фиксация конфигурации в системном реестре или в DCOM Class Store. 2. Задание явного параметра в функциях CoCreateInstanceEx, CoGetInstanceFromFile, CoGetInstanceFromStorage, or CoGetClassObject Для определения удаленного компьютера, на котором должен быть запущен компонент следует использовать диалог Службы компонентов (DCOMCNFG.EXE). На следующем рисунке отображен процесс определения свойства компонента "Запустить приложение на указанном компьютере".
Разработка распределенных приложений
Для создания удаленного компонента можно использовать функцию CoCreateInstanceEx, которая имеет следующее формальное описание: HRESULT CoCreateInstanceEx( REFCLSID rclsid, IUnknown* punkOuter, DWORD dwClsCtx, COSERVERINFO* pServerInfo,
22
Разработка распределенных приложений ULONG cmq, MULTI_QI* pResults
// Структура MULTI_QI имеет три члена: //идентификатор интерфейса – pIID, // указатель на возвращаемый интерфейс –
pItf // и значение, возвращаемое при вызове //метода QueryInterface - hr ); Структура MULTI_QI определяется как: typedef struct tagMULTI_QI { const IID *pIID; IUnknown *pItf; HRESULT hr; } MULTI_QI; Например: MULTI_QI mqi[]={
23
{IID_I1, NULL , hr1}, {IID_I2, NULL, hr2} }; // Выполним соединение с сервером "MyeServer.ru" COSERVERINFO info = {0, L"MyServer.ru", NULL, 0}; // Создание объекта и запрос двух интерфейсов HRESULT hr=CoCreateInstanceEx( CLSID_My1, // Запрос экземпляра класса CLSID_My1. NULL, // Без агрегирования CLSCTX_SERVER, //Подходит любой найденный сервер. &info, // Содержит имя удаленного сервера sizeof(mqi)/sizeof(mqi[0]), // Количество запрашиваемых интерфейсов (2) &mqi); // Структура определяющая IID и указатели на интерфейс if (SUCCEEDED(hr))
Разработка распределенных приложений { if (SUCCEEDED(mqi[0].hr)) { I1* p1=mqi[0].pItf; // Получаем первый указатель на интерфейс hr=p1->Fx1(); // Используем указатель на интерфейс p1->Release(); // Удаляем указатель на интерфейс } if (SUCCEEDED(mqi[1].hr)) { LPWSTR pParam=NULL; I2* p2=mqi[1].pItf; // Получаем второй указатель на интерфейс hr=p2->Fy1(&pParam); // Используем указатель на интерфейс p2->Release(); // Удаляем указатель на интерфейс } }
ВКЛЮЧЕНИЕ И АГРЕГИРОВАНИЕ ВНУТРЕННИХ КОМПОНЕНТОВ Включение и агрегирование компонентов позволяет настраивать и подстраивать уже существующие компоненты. При включении: § внешний компонент содержит указатели на интерфейсы внутреннего компонента; § внешний компонент является клиентом внутреннего компонента используя интерфейсы последнего он реализует свои собственные интерфейсы; § внешний компонент может заново реализовать интерфейс, поддерживаемый внутренним компонентом, передавая последнему вызовы этого интерфейса; § внешний компонент может расширить этот интерфейс, добавляя свой код перед вызовом внутреннего компонента и после него. На следующей схеме показано включение внутреннего компонента, предоставляющее доступ к его интерфейсу. Внешний компонент
24
Разработка распределенных приложений
25
Интерфейс IX Интерфейс IY Внутренний компонент Интерфейс IZ
Агрегирование - это особый вид включения. При агрегировании: § внешний компонент: - не реализует заново интерфейс внутреннего компонента, - не передает внутреннему компоненту вызовы этого интерфейса явно; § внешний компонент передает указатель на интерфейс внутреннего компонента непосредственно клиенту; § клиент далее напрямую вызывает методы интерфейса внутреннего компонента. Это освобождает внешний компонент от повторной реализации функций интерфейса и передачи вызовов внутреннему компоненту. Недостаток агрегирования в том, что нельзя специализировать функции интерфейса. Клиент должен думать, что работает с одним компонентом. Это реализуется через QueryInterface. Реализация включения выполняется только для внешнего компонента остальные ничего не знают. Для реализации включения можно выполнить следующее: 1. Создать внутренний компонент в коде внешнего, используя CoCreateInstance - инициализировать внешний компонент iY* m_pIY; // Указатель на интерфейс включаемого компонента HRESULT __stdcall CFactory::CreateInstance(IUnknown* pOuter,
Разработка распределенных приложений
26
const IID& iid, void** ppv) { if (pOuter != NULL ) return CLASS_E_NOAGRERATION; ClassA* pA=new ClassA; HRESULT hr=pA-> CoCreateIstance( CLSID_Comp2, // для CLSID внутреннего компонента NULL, CLSCTX_INPROC_SERVER, IID_IY, (void**)&m_pIY)
2. 3. 4.
5.
hr=pA->QueryInterface(iid,ppv); pA->Release; return hr; } Сохранить в переменной указатель на интерфейс IY внутреннего компонента Когда клиент запрашивает у внешнего компонента интерфейс IY, то тот возвращает свой интерфейс Когда клиент вызывает метод интерфейса IY, то внешний компонент передает вызов внутреннему компоненту virtual void Fy1() {m_pIY->Fy1();} Когда внешний компонент завершает работу, то его деструктор вызывает m_pIY->Release() для внутреннего компонента.
Расширение интерфейса позволяет добавить к интерфейсу свой код, но это очень трудоемко для "больших" интерфейсов. Например: // Интерфейс - реализован в компоненте MyClassA interface ICA : IUnknown { void F1(); void F2(); void F3(); } ; // Интерфейс Interface ICB : ICA { void Fb1(); void Fb2(); void Fb3(); } ;
Разработка распределенных приложений
27
Внешний компонент может включить MyClassA и использовать его интерфейс ICA для реализации членов ICA, наследующих этот интерфейс void MyClassB::F1() { m_pICA->F1();} Агрегирование можно рассматривать как динамический вид наследования. Внешний компонент не может напрямую передавать указатель ни интерфейс внутреннего компонента. Решением может служить то, что компонент, который будет агрегироваться, может иметь два интерфейса IUnknown: обычный и используемый для передачи вызовов внешнему компоненту. Так, для реализации агрегирования внутренний компонент реализует два интерфейса IUnknown § делегирующий - передает вызовы функций членов IUnknown либо внешнему IUnknown либо неделегирующему IUnknown. § неделегирующий - обычный IUnknown. Объявление внешнего компонента, который реализует интерфейс IX, а IY внутреннего компонента предоставляет через агрегирование: Основные действия внешнего компонента происходят внутри его функции QueryInterface, которая возвращает указатель на интерфейс внутреннего объекта class CA: public IX { public // IUnknown virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv); virtual ULONG __stdcall AddRef(); virtual ULONG __stdcall Release(); // Интерфейс IX virtual void __stdcall Fx() {cout<< " "<<endl;} // Конструктор и деструктор CA();
Разработка распределенных приложений
28
~CA(); // Инициализация - создание включаемого компонента HRESULT Init(); // Произвольная функция, создающая внутренний компонент private: // Счетчик ссылок long m_cRef; // Указатель на IUnknown внутреннего компонента IUnknown* m_pInnerUnknown // Функция QueryInterface внешнего компонента возвращает указатель на // интерфейс внутреннего объекта HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv) { // ... if (iid == IID_IX) {*ppv = (IX*)this; } else if (iid == IID_IY) {return m_pInnerUnknown-> QueryInterface(iid,ppv) ;} Интерфейсы внутреннего компонента должны использовать интерфейс IUnknown, реализованный внешним компонентом. Интерфейс IUnknown внутреннего компонента должен быть скрыт от клиента. Для этого: § внутренний компонент должен знать, что он агрегируется и иметь указатель на внешний IUnknown; § передавать внутреннему компоненту вызовы своего IUnknown внешнему компоненту. Внешний IUnknown: HRESULT __stdcall CoCreateInstance( const CLSID& clsid, IUnkown* pIUnknownOuter, // Внешний компонент
Разработка распределенных приложений
29
// передает указатель внутреннему компоненту DWORD dwClsContext, // Контекст сервера const IID& iid, void** ppv); HRESULT __stdcall CreateInstance( . // IClassFactoty::CreateInstance IUnkown* pIUnknownOuter, // Внешний компонент const IID& iid, void** ppv); Внешний компонент передает указатель на свой интерфейс IUnknown внутреннему компоненту. Если этот указатель не равен NULL, то компонент агрегируется. (Внутренний компонент определяет агрегируется ли он по этому указателю). Если внутренний компонент агрегируется, то делегирующий IUnknown передает вызовы внешнему IUnknown, реализованному внешним компонентом. Клиенты агрегата (внешний+внутрренний) вызывают делегирующий IUnknown, а внешний компонент работает с внутренним через неделегируемый интерфейс. При этом: § Неделегирующий IUnknown передает вызовы обычным образом; § Делегирующий IUnknown внутреннего компонента передает вызовы: o внешнему IUnknown, o неделегирующему IUnknown. Это проиллюстрировано на следующей схеме: Внешний компонент QueryInterface AddRef Release Fx
®
Реализация внешнего IUnknown
Разработка распределенных приложений
30
Код управления внутренним компонентом Внутренний компонент QueryInterface AddRef Release Fx
®
(если есть агрегирование) Реализация делегирующего IUnknown ¯(нет агрегирования) Реализация неделегирующего IUnknown
Реализация неделегирующего IUnknown: § Внутренний компонент использует реализацию IUnknown внешнего компонента; § Внешний компонент вызывает неделегирующий IUnknown для управления временем жихни внутреннего компонента. Т.к. имя интерфейса не имеет значение, а важна только структура памяти, то назовем неделегирующий IUnknown - INondelUnknown. Объявим struct INondelUnknown { virtual HRESLT __stdcall NondelQueryInterface(...)=0; virtual ULONG __stdcall NondelAddRef()=0; virtual ULONG __stdcall NondelReleace()=0; } Изменения будут только в реализации новой NondelQueryInterface: HRESLT __stdcall CB::NondelQueryInterface (const IID& iid, void** ppv) { if (iid == IID_IUnknown) {*ppv = (INondelUnknown *)this; } // Приведение типа // гарантирует, что будет вызван неделегирующий IUnknown
Разработка распределенных приложений
31
else if (iid == IID_IY) {*ppv = (IY*)this; } // ... Неделигирующий IUnknown ( INondelUnknown) всегда возвращает указатель на себя. Указатель на неделегирующий. IUnknown передается только внешнему компоненту. При реализации делегирующего IUnknown вызовы передаются либо: § внешнему IUnknown (если компонент агрегируется); § неделегирующему IUnknown. Например: // m_pOuterUnknown - указатель на внешний интерфейс class CB: public IY, public INondelUnknown { public: // Делегирующий IUnknown . virtual HRESLT __stdcall QueryInterface(...) { return m_pOuterUnknown->QueryInterface();} // Если есть делегирование { return m_pOuterUnknown-> QueryInterface(iid,ppv);} virtual ULONG __stdcall AddRef() { return m_pOuterUnknown-> AddRef(); } virtual ULONG __stdcall Releace() { return m_pOuterUnknown-> Release();} } // Нелегирующий IUnknown . virtual HRESLT __stdcall NondelQueryInterface(...); virtual ULONG __stdcall NondelAddRef() ; virtual ULONG __stdcall NondelReleace() ; // Интерфейс IY virtual void __stdcall Fy() { ... ;}
Разработка распределенных приложений
32
// Конструктори деструктор CB (IUnknown* m_pOuterUnknown); // Указатель на внешний интерфейс, // используемый при создании компонента ~CB(); private: long m_cRef; IUnknown* m_pOuterUnknown Обобщим этапы реализации компонента, использующего агрегирование: 1. Реализуем внутренний компонент с двумя интерфейсами IUnknown Для делегирующего интерфейся выполняем переадресацию на внешний интерфейс (m_pOuterUnknown) (во внешнем компоненте) В клиенте создаем внутренний компонент, передавая ему одновременно указатель на внешний IUnknown. Используем для этого функцию CoCreateInstance HRESULT CA::Init() { IUnknow* pOuterUnknown = this; HRESULT hr= CoCreateInstance(CLSID_Comp2, pOuterUnknown, CLSCTX_INPROC_SERVER, IDD_IUnknown, (void**)& m_pInnerUnknown); // Когда компонент // создается как внутр. и агрег-ый он // может возвращать только IUnknown ... return S_OK; } 2. Реализация IClassFactory::CreateInstance внешнего компонента вызывает метод, создающий внутренний компонент. 3. Фабрика класса внутреннего компонента изменена: - IClassFactory::CreateInstance использует INondelUnknown вместо IUnknown - функция вызывает не QueryInterface, а NondelQueryInterface - ФК возвращает указатель на неделегирующий QueryInterface HRESULT hr=pB= NondelQueryInterface(iid,ppv);
Разработка распределенных приложений
33
pB->NondelRelease(); return hr; 4. Указатель на внешний IUnknown передается конструктору внутреннего компонента. Конструктор инициализирует m_pOuterUnknown. Затем эта переменная используется делегирующим IUnknown для передачи вызовов (либо неделегирующему IUnknown либо внешнему IUnknown). Если компонент не агрегируется, то конструктор помещает в m_pOuterUnknown указатель на неделегрующий IUnknown. (т.е. равно или (IUnknown*)this или параметру). 5. Т.к. внешний компонент при агрегировании может запрашивать только интерфейс IUnknown, то для запроса интерфейса внутреннего компонента используется QueryInterface.
ДИСПЕТЧЕРСКИЕ ИНТЕРФЕЙСЫ И АВТОМАТИЗАЦИЯ Автоматизация это другой способ управления компонентом. Автоматизация - надстройка над COM. Сервер Автоматизации - это компонент СОМ, который реализует интерфейс IDispatch. Контроллер Автоматизации - это клиент СОМ, взаимодействующий с сервером через интерфейс IDispatch. (Для вызовы функций сервера использует функции члены интерфейса - неявный вызов). Первоначально Автоматизация разрабатывалась для Visual Basic. Почти любой сервис, представимый через интерфейсы COM может быть представлен и через IDispatch. ИНТЕРФЕЙС IDISPATCH IDispatch предоставляет доступ ко всем сервисам через один единственный интерфейс. IDispatch обеспечивает вызов функции по трем параметрам: ProgID компонента, имени функции и ее аргументов. Интерфейс IDispatch имеет следующее формальное описание: interface IDispatch : IUnknown //из файла OAidl.idl {
Разработка распределенных приложений
34
HRESULT GetTypeInfoCount([out] UINT* pctinfo) ; HRESULT GetTypeInfo ([in] UINT iTInfo, [in] LCID lsid;; [out] ITypeInfo** ppTInfo) ; HRESULT GetIDsOfName ([in] REFIID riid, // Принимает имя функции и // возвращает ее [in, size_is(cNames)] LPOLESTR *rgszNames, // Диспетчерский // идентификатор [in] UINT cNames, [in] LCID lcid, [out, size_is(cNames)] DISPID *rgDispId) ; // DISPID это длинное // целое // LONG и не уникально //У каждой реализации IDispatch имеется свой собственный IID (иногда называется DIID). HRESULT Invoke ([in] DISPID dispIdMember, автоматизации [in] REFIID riid, вызываемой
// Контроллер // передает DISPID // функции в Invoke
[in] LCID lsid, [in] WORD wFlags, [in, out] DISPPARAMS* pDispParams, [out] VARIANT* pVarResult, [out] EXEPINFO* pExcepinfo, [out] UINT* puArgErr );
Разработка распределенных приложений
35
DISPID используется функцией-членом Invoke как индекс в массиве указателей на функции. Однако, сервер Автоматизации не обязан реализовывать Invoke именно таким образом. Он может использовать обычный оператор switch. IDispatch::Invoke реализует набор функций, доступ к которым осуществляется по индексу. Набор функций, реализованных с помощью IDispatch::Invoke называется диспетчерским интерфейсом (disp-интерфейсом) Реализация IDispatch::Invoke определяет набор функций, посредством которых взаимодействую сервер и контроллер автоматизации. Пример возможной реализации DISP-интерфейса: Интерфейс IDispatch IDispatch* -> pVtbll ->
&QueryInterface
pIDispatch
&AddRef &Release &GetTypeInfoCount &GetTypeInfo &GetDsOfNames
DISP-интерфейс DISPID Массив имен à 1 F1 ф-ия 2 F2 GetDsOfNames 3 F3
&Invoke ф-ия Invoke æ
DISPID Массив указателей на функции 1 &F1 2 &F2 3 &F3
Разработка распределенных приложений
36
ДУАЛЬНЫЕ ИНТЕРФЕЙСЫ Если интерфейс COM, реализующий IDispatch::Invoke наследует не IUnknown, а IDispatch, то такой интерфейс называется дуальным интерфейсом. Дуальный интерфейс - это disp-интерфейс все члены которого, доступные посредством вызова метода Invoke, доступны и напрямую через таблицу виртуальных функций vtbl. Следующая схема иллюстрирует различные способы применения функции Invoke (при наследовании от IUnknown и при наследовании от IDispatch). Интерфейс IDispatch DISPинтерфейс IDispatch* -> pIDispatch
pVtbll ->
&QueryInterface
DISPID Массив имен
&AddRef &Release &GetTypeInfoCount &GetTypeInfo &GetDsOfNames &Invoke
1 2 3
ф-ия GetDsOfNames
ф-ия Invoke ->
Реализация IDispatch::Invoke с помощью интерфейса СОМ. Интерфейс F3 наследует интерфейсу IDispatch
F1 F2 F3
Интерфейс F3 (СОМ) pVt &F1 bl -> &F2 &F3
DISP-интерфейс
Разработка распределенных приложений
IDispatch*-> pVtbll -> pIDispatch
37
&QueryInterface &AddRef &Release &GetTypeInfoCount &GetTypeInfo &GetDsOfNames &Invoke &F1 &F2 &F3
ф-ия GetDsOfNames
DISPID Массив имен 1 F1 2 F2 3 F3
ф-ия Invoke
-> å
Реализация IDispatch::Invoke с помощью дуального интерфейса. Вызовы через дуальные интерфейсы быстрее выполняются и их легче реализовывать. Основное ограничение - набор типов параметров.
БИБЛИОТЕКИ ТИПА Библиотека типа - независимый от языка эквивалент заголовочных файлов. Библиотека типа СОМ предоставляет информацию типа о компонентах, интерфейсах, методах, свойствах, аргументах и структурах. Библиотека типа - это откомпилированная версия файла IDL, к которой возможен доступ из программы. Это двоичный файл. Библиотека Автоматизации предоставляет стандартные компоненты для создания и чтения таких двоичных файлов. Для генерации библиотеки типа можно использовать MIDL. Создание библиотеки типа
Разработка распределенных приложений
38
CreateTypeLib создает библиотеку типа и возвращает интерфейс ICreateTypeLib. Этот интерфейс можно использовать для занесения в библиотеку. типа различной информации. Использование библиотек типа Библиотеку типа можно либо включить в DLL или EXE, либо добавлять как ресурс. Библиотека COM предоставляет следующие функции для загрузки бибилиотеки типа: LoadRegTypeLib из реестра LoadTypeLib с диска по имени файла LoadTypeLibFromResource из ресурсов EXE или DLL. Для регистрации библиотеки типа (если вызов по имени) предназначается функция: RegisterTypeLib. При загрузке возвращается указатель на интерфейс ITypeLib (используется для доступа к библиотеке типа). Получение информации об интерфейсе или компоненте выполняется посредством функции: ITypeLib::GetTypeInfo - передаются CLSID или IID - возвращает указатель на ITypeInfo для запрошенного элемента Интерфейс ITypeInfo может реализовать IDispatch автоматически. Информация о библиотеке типа находится в разделе HKEY_CLASSES_ROOT TypeLib. Каждая библиотека типа имеет свой GUID. Все, что следует в фигурных скобках после ключевого слова library включается в библиотеку типа. Оператор coclass определяет компонент. Практически библиотека типа может быть сформирована из IDL файла. В IDL файле описываются все интерфейсы, реализуемые компонентом. В среде проектирования IDL файл можно создать, выполнив команду меню File | New | File, и выбрав на панели Templates элемент Mdi File (.idl). На
Разработка распределенных приложений следующем рисунке приведен диалог New File, позволяющий создать IDL файл.
Следующий рисунок иллюстрирует объявление интерфейса в IDL файле.
39
Разработка распределенных приложений
Для определенного выше интерфейса возможна следующая реализация: // Интерфейс IX
40
Разработка распределенных приложений
41
HRESULT __stdcall CA::FxStrIn(wchar_t* szIn) { ostrstream sout ; sout << "Метод FxStrIn получил следующую строку: " << szIn << ends ; return S_OK ; } HRESULT __stdcall CA::FxStrOut(wchar_t** pszOut) { const wchar_t wsz[] = L"[Строка от FxStrOut]" ; const int iLength = (wcslen(wsz)+1)*sizeof(wchar_t) ; // Выделение памяти под строку wchar_t* pBufString = static_cast<wchar_t*>(::CoTaskMemAlloc(iLength)) ; if (pBufString == NULL) { return E_OUTOFMEMORY ; } // Копируем строку в буфер wcscpy(pBuf, wsz) ; *pszOut = pBufString ; return S_OK ; }
ПОТОКИ СОМ Многопоточность используется для создания приложений с малым временем отклика. Например, в WEB: 1-й поток перекачивает страницу, 2-й отображает, 3-й реализует интерфейс пользователя. СОМ использует потоки Win32. Для создания и синхронизации потоков используется API Win32. В приложении Win32 имеются потоки двух типов: потоки пользовательского интерфейса (user-interface thread) и рабочие потоки (worker thread). С потоками пользовательского интерфейса связано одно или несколько окон. Эти потоки имеют циклы выборки сообщений (что обеспечивает работу окна и реакцию на действия пользователей). Рабочие потоки не связаны с окном, и как правило, не имеют циклов выборки сообщений. В каждом процессе может быть несколько и тех и других потоков. Для потока пользовательского интерфейса для каждого окна существует оконная процедура.
Разработка распределенных приложений
42
Оконная процедура данного окна вызывается только потоком, который владеет данным окном (т.е. создавшим это окно). Следовательно, оконная процедура всегда выполняется в одном и том же потоке, невзирая на то, какой именно поток послал сообщение этой процедуре на обработку. Поэтому, все сообщения уже и так синхронизированы (окно будет получать сообщения упорядоченно). В СОМ эти же типы потоков называются: · разделенный поток (apartment thread) вместо потока пользовательского интерфейса · свободный поток (free thread) вместо рабочий поток. Подразделение это разделенный поток плюс цикл выборки сообщений. На следующей схеме приведен пример работы подразделения. Цикл выборки
CoInitialize
Клиент
Оконная процедура
Цикл выборки
Компонент (внутри процесса) Компонент
CoUninitialize ê Поток управления
ê
У компонентов внутри процесса нет своих циклов выборки (используют клиентский цикл выборки сообщений). Для компонентов вне процесса: § есть цикл выборки § маршалинг вызовов между процессами
Разработка распределенных приложений
43
На следующих схемахе приведен пример работы потоков, расположенных в разных процессах. Сервер компонента вне процесса CoInitialize
CoInitialize Компонент Для вызова внутри процесса маршалинг не выполняется
Клиент Для вызова между процессами необходим маршалинг Цикл выборки
CoUninitialize
Цикл выборки компонента вне процесса
Компонент Цикл выборки
CoUninitialize
ê
ê Компонент внутри процесса, расположенный в другом подразделении:
CoInitialize Клиент
CoInitialize Компонент Для вызова внутри процесса маршалинг
Разработка распределенных приложений
44 не выполняется
Для вызова между подразделениями необходим маршалинг Цикл выборки
Цикл выборки сообщений используется процедурой потока
CoUninitialize
Компонент Цикл выборки
CoUninitialize Границы подразделений
ê ê Между процессом и подразделением есть общие черты: § и у процесса и у подразделения есть свой цикл выборки сообщений; § маршалинг вызовов функций внутри однопоточного процесса и внутри подразделения не нужен; § имеет место естественная синхронизация, т.к. один поток; § и поток и подразделение должны инициализировать библиотеку СОМ. Разделенный поток - это единственный поток внутри подразделения. Разделенный поток владеет созданным им окном и оконная процедура вызывается только им. Разделенный поток владеет созданным им компонентом. Компонент внутри подразделения будет вызываться только "своим" разделенным потоком. Потокобезопасность обеспечена всегда. Для компонентов, связанных свободными потоками синхронизация не выполняется: если компонент создан свободным потоком он может вызываться любым потоком и в любой момент времени. Модель свободных потоков переносит заботу о синхронизации с СОМ на компонент.
Разработка распределенных приложений
45
Компонент, созданный свободным потоком называется компонентом свободных потоков. Все потоки имеют к такому компоненту свободный доступ. Такому компоненту не нужен цикл выборки сообщений. Для правильного маршалинга и синхронизации вызовов компонента СОМ надо знать потоком какого типа он исполняется. Для разделенных потоков обычно СОМ выполняет необходимые маршалинг и синхронизацию. Для свободных потоков маршалинг может быть не нужен, а выполнение синхронизации должен обеспечивать на компонент. Существуют следующие общие правила по синхронизации потоков и применения маршалинга: § вызовы внутри одного потока синхронизируются самим потоком; § вызовы посредством свободного потока не могут быть синхронизированы самим потоком; § вызовы посредством разделенного потока синхронизируются; § вызовы между разными процессами всегда выполняются с применением маршалинга; § вызовы внутри одного потока не используют маршалинг; § при вызове компонента в разделенном потоке применяется с маршалинг; § при вызове компонента в свободном потоке маршалинг применяется не всегда. Возможны следующие типы вызовов: § Вызовы внутри одного потока. И клиент и компонент выполняется в одном потоке и следовательно не нужен маршалинг и вызовы синхронизируются. § Разделенный-разделенный Клиент, выполняется в разделенном потоке и вызывает компонент, выполняющийся в другом разделенном потоке. 1. Синхронизацию вызова выполняет СОМ
Разработка распределенных приложений
46
2. СОМ также выполняет маршалингш интерфейсов (даже если потоки в одном процессе). 3. Вызов компонента в разделенном потоке аналогичен вызову компонента вручную. § Свободный-свободный Клиент в свободном потоке вызывает компонент свободных потоков и следовательно: 1. Синхронизацию вызова СОМ не выполняет 2. Вызов будет выполнять поток клиента. 3. Компонент должен сам синхронизировать доступ к себе. 4. Если компонент и клиент внутри одного процесса, то маршалинг не выполняется. § Свободный-разделенный Клиент в свободном потоке вызывает компонент в подразделении и следовательно: 1. Синхронизацию вызова выполняет СОМ 2. Компонент будет вызван потоком подразделения. 3. Маршалинг интерфейса необходим (без разницы в одном или в разных процессах оба потока). В большинстве случаев маршалинг выполнит СОМ, но не всегда. § Разделенный-свободный Клиент в разделенном потоке вызывает компонент в свободном потоке и следовательно: 1. Синхронизацию вызова СОМ не выполняет 2. Синхронизацию должен вып. компонент свободного потока. 3. Маршалинг интерфейса выполняется, но если оба потока принадлежат одному процессу, то СОМ может оптимизировать маршалинг (передав указатели клиенту непосредственно).
COM+ Технология DCOM позволила разворачивать компонентные приложения в сети, но также имела ряд ограничений:
Разработка распределенных приложений § § § §
47
различные приложения клиенты не могли совместно использовать код, реализующий бизнес-логику и работу с базами данных (это также ограничивает многократное использование кода); изменения бизнес логики влекут за собой и изменения кода клиента; каждое приложение клиент должно было иметь свое соединение с базой данных; каждый клиентский компьютер должен иметь драйвер для подключения к БД.
Службы СОМ+ - это прикладные службы, используемые для создания и развертывания в сети бизнес-компонентов. В СОМ+ входят ряд служб, включая следующие: § служба транзакций – позволяющая компонентам автоматически участвовать в транзакциях. Создаваемый компонентом COM+ объект активируется вызовом метода BeginTransaction и деактивируется методом CommitTransaction или AbortTransaction (при откате транзакции вызывается метод RollbackTransaction); § служба сообщений Queued Components – основана на модели MSMQ, обеспечивающей асинхронный обмен сообщениямипри помощи именованных очередей: вызовы ставятся в очередь к компоненту и выполняются, когда компонент становится доступным (решает проблему временного отключения от сети). Утилита Component Services позволяет настраивать очереди сообщений для компонентов; § служба безопасности – позволяющая управлять доступом с использованием ролей не только декларативно (автоматически), но и программно; § служба пуда объектов, используемая для создания и управления готовыми объектами; § служба слабосвязанных событий, которая используется для предоставления компонентам (издателям) возможности отправлять уведомления в хранилища событий, которое затем просматривается клиентами (подписчиками) для нахождения нужной информации; § служба активации по запросу (Just In-time Activation), которая позволяет работать с компонентом, имеющим состояния: активен, не активен, не существует.
Разработка распределенных приложений
48
Тема 2. Компоненты COM И .NET СБОРКИ Сборки представляют собой файлы, которые состоят из нескольких РЕфайлов. Сборка включает в себя: декларацию, один или несколько модулей, и возможно один или несколько ресурсов. Модуль имеет расширение .netmodule и представляет собой DLL. Декларацию сборки можно хранить: § в однофайловой сборке (для простого приложения или DLL); § для многофайловых сборок - как самостоятельная часть сборки или в составе одного из модулей сборки. В декларации содержится (частично информация указывается в свойствах проекта, диалоге Assembly Information): § имя сборки; § информация о версии; § совместно используемое имя и подписанный хэш сборки; § список всех файлов сборки; § список ссылок на внешние сборки, встречающихся в декларации; § список всех типов, используемых в сборке, и указание модуля, содержащего данный тип; § список запрещенных прав доступа к сборке; § атрибуты, введенные пользователем; § доп. информация о приложении (название, авторские права, фирма и т.п.). Развертываемые сборки могут быть: § закрытыми (по умолчанию); § совместно используемыми( shared assembly). В памяти сборки хранятся в формате РЕ-файлов, как и обычные EXE-файлы и DLL-модули. Формат РЕ-файлов является основным форматом хранения выполнимых файлов в Windows. Формат PE-файлов также может служить
Разработка распределенных приложений
49
для представления объектных модулей, компилируемых далее в одну выполняемую программу. При выполнении программы данные из PE-файла размещаются в памяти компьютера. Управление памятью в Windows выполняет менеджер виртуальной памяти. Вся оперативная память подразделяется на блоки – физические страницы, размером в 4096 байт. При недостатке оперативной памяти менеджер виртуальной памяти использует файл подкачки (размещаемый на жестком диске), перемещая туда временно неиспользуемые блоки оперативной памяти. Каждый процесс в Windows запускается в своем виртуальном адресном пространстве. Под каждый процесс выделяется 4 Гб памяти: из них 2 Гб выделяются непосредственно процессу, а 2 Гб используются операционной системой. При выполнении файла загрузчик операционной системы выполняет отображение отдельных частей PE-файла на адресное пространство процесса. При этом он использует таблицу релокаций PE-файла для корректировки абсолютных адресов в исполняемом коде и передает управление на точку входа в выполняемую программу. На следующей схеме представлено примерное размещение отдельных частей в PE-файле и в памяти процесса. PE-файл сборки
Память процесса Секция N
Секция экспорта Секция импорта Секция 3 Секция таблицы релокаций Секция N
Секция 2
... Секция 3 Секция 2 Секция 1
Секция 1
Разработка распределенных приложений Таблица секций Дополнительный файла
заголовок
PE-
50 Таблица секций Дополнительный файла
заголовок
Заголовок PE-файла
Заголовок PE-файла
Заголовок MS DOS
Заголовок MS DOS
PE-
Секции могут содержать код, данные или служебную информацию. Количество секций указывается в заголовке PE-файла. При отображении секций из PE-файла в память процесса секции выравниваются по блокам физической памяти. Поэтому в оперативной памяти данные занимают больше места, чем в PE-файле. Для доступа к данным вычисляется – RVAадрес (Relative Virtual Address), определяющий относительный виртуальный адрес элемента в памяти процесса. PE-файл может содержать различное количество секций, но всегда содержит хотя бы одну секцию с кодом. Секция определяется именем и атрибутами. В зависимости от средства, создавшего PE-файл названия секций могут различаться. Visual Studio при создании выполнимых файлов именует секции кода как .text, константы -как .rdata, таблицы импорта – как .idata, таблицу экспорта как .edata, и таблицу релокаций – как .reloc. Для сборок .NET в секции .idata всегда описывается импортируемая функция из библиотеки mscoree.dll. Эта функция определяет точку входа, на которую передается управление при запуске сборки. Для выполнимых EXEфайлов указывается функция _CorExeMain, а для DLL-библиотек – функция _CorDllMain. Именно функция _CorExeMain выполняет запуск CLR. Далее CLR производит JIT-компиляцию программы и выполняет ее. Выполняемый файл всегда загружается по некоторому указанному базовому адресу, находящемуся в заголовке PE-файла. DLL-библиотеки грузятся в адресное пространство выполняемого файла, поэтому указанный в них базовый адрес может быть уже использован, и загрузчик размещает DLLбиблиотеку по другому адресу. При этом требуется скорректировать абсолютные адреса, имеющиеся в файлеDLL-библиотеки. Все эти абсолютные адреса с их RVA-адресами содержатся в PE-файле в таблице релокаций.
Разработка распределенных приложений
51
СОЗДАНИЕ СБОРОК Для построения сборки программы на C# можно выполнить в Visual Studio команду меню Project | Properties | Application| Output type (выбрать из Windows Application, Console Application, Class Library). Для получения информации о сборках и управления сборками библиотека Framework предоставляет класс Assembly из пространства имен System.Reflection. Этот класс содержит ряд методов, включая следующие: § GetAssembly - получает текущую загруженную сборку, в которой определен указываемый класс. Например: // Сборки в C# Assembly Assembly1; Int32 Int1 = new Int32(); // Создаем объект Type Type1; Type1 = Int1.GetType(); // Создаем объект типа Type для объекта Int1 Assembly1 = Assembly.GetAssembly(Int1.GetType()); // Используя свойство CodeBase запрашиваем размещение сборки Console.WriteLine("CodeBase=" + Assembly1.CodeBase); // Этот же код на C++ можно записать следующим образом: Assembly^ Assembly1; Int32 Int1(0); Type^ Type1; Type1 = Int1.GetType(); Assembly1 = Assembly::GetAssembly( Int1.GetType() ); Console::WriteLine( "CodeBase= {0}", Assembly1->CodeBase ); § GetCallingAssembly - возвращает объект Assembly для метода, из которого был вызван текущий выполняемый метод. Например: Assembly Assembly1; Int32 Int1 = new Int32(); Type Type1; Type1 = Int1.GetType(); Assembly1 = Assembly.GetAssembly(Int1.GetType()); // Отображаем имя сборки (свойство FullName), вызвавшей данный метод
Разработка распределенных приложений
§
52
Console.WriteLine("GetCallingAssembly=" + Assembly.GetCallingAssembly().FullName); GetExportedTypes – возвращает в объекте типа Type[] список экспортируемых типов, объявленных в сборке и доступных вне сборки. Например: using System; using System.Collections.Generic; using System.Text; using System.Reflection; namespace Console3 { using System; using System.Reflection; public class Program { public static void Main() { foreach (Type t in Assembly.GetExecutingAssembly().GetExportedTypes()) { Console.WriteLine(t); } }} public class PublicClass { public class PublicNestedClass {} protected class ProtectedNestedClass {} internal class FriendNestedClass {} private class PrivateNestedClass {} } internal class FriendClass { public class PublicNestedClass {} protected class ProtectedNestedClass {} internal class FriendNestedClass {} private class PrivateNestedClass {} }}
Разработка распределенных приложений
53
// На следующем рисунке приведен результат выполнения этого примера
§ § § § § §
GetLoadedModules – возвращает список (Module[]) всех загруженных модулей, являющихся растью данной сборки; GetModules – возвращает список (Module[]) всех модулей, являющихся растью данной сборки; GetName – возвращает имя данной сборки Load – загружает сборку с указанным именем сборки; LoadFile – загружает сборку из указанного файла; LoadFrom – загружает сборку по имени или из файла. Например: Assembly Assembly1; Assembly1 = Assembly.LoadFrom("c:\\Work.Assembly.dll"); // Получение ссылки на метод загруженной сборки MethodInfo Method = Assembly1.GetTypes()[0].GetMethod("M1"); // Запрос информации о параметрах метода ParameterInfo[] Params = Method.GetParameters(); // Отображение информации о параметрах метода foreach (ParameterInfo Param in Params) { Console.WriteLine("Param=" + Param.Name.ToString()); // Param = sMyParam1 Console.WriteLine(" Type=" + Param.ParameterType.ToString());// Type = System.String Console.WriteLine(" Position=" + Param.Position.ToString()); // Position = 0 Console.WriteLine(" Optional=" + Param.IsOptional.ToString()); // Optional=False
Разработка распределенных приложений
54
} Класс Assembly позволяет получать не просто информацию о всех методах сборки, но и получать информацию о методах с указанными модификаторами доступа, с определением статичесого метода или метода экземпляра класса. Например: // При вызове программы параметр указывает имя файла сборки: // ClassLoad MyAssembly using System; using System.Reflection; using System.Security.Permissions; public class ClassLoad { [PermissionSetAttribute(SecurityAction.Demand, Name="FullTrust")] public static void Main(string[] args) { Assembly a = Assembly.Load(args[0]); // Загрузка сборки Type[] mytypes = a.GetTypes(); BindingFlags flags = (BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.DeclaredOnly); foreach(Type t in mytypes) { MethodInfo[] mi = t.GetMethods(flags); Object obj = Activator.CreateInstance(t); foreach(MethodInfo m in mi) {
Разработка распределенных приложений
55
Console.WriteLine(m); } } } } // Компиляция следующей сборки: csc /t:library MyAssembly.cs // выполнит создание файла MyAssembly.dll. using System; public class MyAssembly { public void Method1() { Console.WriteLine("Это Method1"); } public void Method2() { Console.WriteLine("Это Method2"); } public void Method3() { Console.WriteLine("Это Method3"); } } Следующий пример иллюстрирует создание и загрузку сборок. Например: // M1Server.cs // csc /t:library M1Server.cs - компиляция public class M1Server { } Ссылка из клиента на созданную DLL: // M1Client.cs // csc M1Client.cs /r:M1Server.dll using System; using System.Diagnostics; using System.Reflection; class M1ClientApp { public static void Main() { Assembly DLLAssembly = Assembly.GetAssembly(typeof(M1Server)); // Запрос типа
Разработка распределенных приложений
56
Console.WriteLine("M1Server.dll сборка"); Console.WriteLine("\t" + DLLAssembly); Process p = Process.GetCurrentProcess(); // Запрос текущего // процесса string AssemblyName = p.ProcessName + ".exe"; Assembly NewAssembly = Assembly.LoadFrom(AssemblyName); Console.WriteLine("M1Client.exe сборка"); Console.WriteLine("\t" + NewAssembly); } } // M1Client.cs // csc M1Client.cs /r:M1Server.dll /r:System.Diagnostics.dll using System; using System.Diagnostics; using System.Reflection; class M1ClientApp { public static void Main() { Assembly DLLAssembly = Assembly.GetAssembly(typeof(M1Server)); Console.WriteLine(DLLAssembly); Process p = Process.GetCurrentProcess(); string AssemblyName = p.ProcessName + ".exe"; Assembly NewAssembly = Assembly.LoadFrom(AssemblyName); Console.WriteLine(NewAssembly); } }
Взаимодействие COM И .NET СОМ – это компоненты неуправляемого кода, а компоненты .NET – это компоненты управляемого кода, выполняемые в среде CLR.
Разработка распределенных приложений
57
Рroxy – это код, позволяющий получать команды от компонента, изменять их и передавать другому компоненту. Proxy может использоваться для вызова неуправляемого кода из управляемого кода .NET. Этот код называется RCW (Runtime-Callable Wrappe) – оболочка, выполняющая обращения в период выполнения. Следующая схема показывает взаимодействие двух компонентов (включая .NET программу, называемую NetUI.exe).
Для каждого экземпляра COM-объекта используется один объект RWC.
Разработка распределенных приложений
58
На следующем рисунке изображена схема взаимодействия компонентов.
Неуправляемый код
СОМ-объект
o o o
IUnkniwn IDispatch IMyInterface
Код, управляемый CLR
Оболочка RCW
Приложение, управляемое .NET
Оболочка метаданных/ информация о типе Библиотека типов СОМ
Утилита tlbimp.exe (Type Library Importer) позволяет просматривать библиотеку типа компонента и формировать не ее основе соответствующие метаданные для CLR среды .NET. Например: tlbimp имя_файла.tbl /out:имя_файла.dll Дизассемблер ILDASM (аналогично OLEVIEW для СОМ) позволяет просматривать метаданные и IL-код.
Разработка распределенных приложений
59
Раннее связывание с СОМ-компонентами Для каждого COM-объекта создается один RCW, который: § устанавливает взаимнооднозначное соответствие между методами и полями классов метаданных и методами и свойствами интерфейсов, реализованных СОМ-объектов; § управляет счетчиком ссылок на СОМ-объект; § выполняет обработку ошибок (преобразуя ошибку в объект класса COMException из пространства имен System.Runtime.InteropServices. Для реализации раннего связывания следует добавить пространство имен, в котором определены метаданные, сформированные утилитой tlbimp (по умолчанию это будет имя_файлаLib), а далее создать экземпляр класса и можно вызывать любые его общедоступные методы. Например: using System; using System.Diagnostics; using System.Reflection; class MyAttr : System.Attribute{} enum MyEnum{} class MyBaseClass{} class MyoDerivedClass : MyBaseClass{} class MyStruct{} class GetTypesApp { protected static string GetAssemblyName(string[] args) {string assemblyName; if (0 == args.Length) { Process p = Process.GetCurrentProcess(); assemblyName = p.ProcessName + ".exe"; } else assemblyName = args[0]; return assemblyName; } public static void Main(string[] args)
Разработка распределенных приложений {
60
string assemblyName = GetAssemblyName(args); Console.WriteLine("Loading info for " + assemblyName); Assembly a = Assembly.LoadFrom(assemblyName); Type[] types = a.GetTypes(); foreach(Type t in types) { Console.WriteLine("\nИнф. для: " + t.FullName); Console.WriteLine("\tБазовый класс = " + t.BaseType.FullName); } }}
Создание и регистрация обслуживаемых компонентов Обслуживаемые компоненты - компоненты использующие сервисы. .NET позволяет создавать обслуживаемые компоненты, использующие сервисы СОМ+, и использующие общий контекст с приложениями СОМ+. Для работы со службами СОМ+ используется пространство имен System.EnterpriseServices, а обслуживаемые компоненты наследуются от классов System.EnterpriseServices.ServicedComponent. Обслуживаемые компоненты могут вызывать службы СОМ+ посредством атрибутов из пространства имен System.EnterpriseServices. Атрибуты позволяют: § задать: область действия атрибута (применим ли он к классу, методу или всей сборке); § значение параметра, которое будет присвоено полю, если определение атрибута в коде отсутствует; § значение атрибута по умолчании. Для конфигурирования обслуживаемых компонентов .NET предоставляет ряд атрибутов, включая следующие атрибуты: § ApplicationAccessControlAttribute – определяет параметры безопасности (для сборки);
Разработка распределенных приложений §
§ § § § §
61
ApplicationActivationAttribute – определяет где будет выполняться обслуживаемый компонент (в системном процессе или в процессе его создающем); если значение не указано, то используется – Library$; ApplicationIDAttribute – определяет GUID приложения, содержащего обслуживаемые компоненты ApplicationNameAttribute – определяет имя COM+ компонента, реализующего обслуживающие компоненты; TransactionsAttribute – определяет тип транзакции; SecureMetodAttribute – используется для реализации безопасного вызова методов класса или сборки; ComponentAccessControlAttribute – используется для проверки удостоверений безопасности вызываемых методов класса.
Созданный обслуживаемый компонент необходимо добавить в приложение СОМ+. Для этого следует: § назначить сборке строгое имя (используя атрибут AssemblyKeyFileAttributeb). Создать файл открытого ключа можно утилитой sn.exe, а установить такую сборку в глобальный кеш сборок – gacutil.exe (/i – установить, /u – удалить) [assembly: AssemblyKeyFile("имя_файла_с_ключами.dat"); § зарегистрировать сборку в реестре Window – при этом обслуживаемые компоненты добавляются к приложению СОМ+ и конфигурируются. Тип активизации определяет, где будет создан экземпляр обслуживаемого компонента (при указании Server – зависимые сборки должны быть в глобальном кеше). Приложение COM определяется атрибутами ApplicationNane и ApplicationID; § зарегистрировать и установить определение библиотек типов в приложении СОМ+: - для ручной регистрации – RerSvcv.exe - программная регистрация – класс RegistrationHelper.
Разработка распределенных приложений
62
ПРИМЕНЕНИЕ НЕУПРАВЛЯЕМОГО КОДА По умолчанию приложения на C# относятся к управляемому коду. Но при необходимости управляемый код может взаимодействовать с неуправляемым кодом. К неуправляемому коду, вызываемому из управляемых C# приложений, можно отнести функции DLL-библиотек и сервисы COM-компонентов. Приложение управляемого кода также может включать фрагменты небезопасного кода. Небезопасный код тоже относится к неуправляемому коду, так как выделение и освобождение памяти в нем не контролируется исполняющей средой .NET. НЕБЕЗОПАСНЫЙ КОД Фрагмент небезопасного кода в C# следует помечать ключевым словом unsafe. Например: int i1;unsafe {int *i2=&i1;} Ключевым словом unsafe требуется обязательно помечать любой фрагмент кода, который содержит указатель. Модификатор unsafe может быть указан для методов, свойств и конструкторов (за исключением статических конструкторов), а также для блоков кода. Например: using System; class Class1 { unsafe static void M1 (int* p) // Небезопасный код: указатель на int { *p *= *p; } // *p – доступ к значению unsafe public static void Main() // Небезопасный код: применен оператор получения адреса (&) { int i = 4; M1 (&i); Console.WriteLine (i); } } Чтобы использовать небезопасный код, следует установить опцию компилятора /unsafe. Для этого в среда прогаммирования Visual Studio .NET достаточно выбрать имя проекта в окне Solution Explorer и через контекстное меню вызвать диалог Property Pages а затем на странице
Разработка распределенных приложений
63
Configuration Properties | Build установить значение опции Allow unsafe code blocks равным True. Указатели можно использовать только с размерными типами, массивами и строками. При задании указателя на массив первый элемент массива должен быть размерного типа. Выполняющая среда .NET для эффективного управления памятью при удалении одних объектов "перемещает" другие объекты, чтобы исключить фрагментацию памяти свободными блоками памяти. Таким образом, выполняющая среда .NET по умолчанию не гарантирует, что объект, на который получен указатель, всегда будет иметь один и тот же адрес. Для предотвращения перемещения объекта следует использовать оператор fixed, который имеет следующее формальное описание: fixed ( тип* имя_указателя = выражение ) выполняемый_оператор_или_блок В качестве типа можно указывать любой неуправляемый тип или void. Выражение является указателем на заданный тип. Фиксация объекта применяется только для указанного выполняемого оператора или блока. Доступ к фиксированной переменной не ограничен областью видимости небезопасного кода. Поэтому фиксированная переменная может указывать на значение, располагаемое в более широкой области видимости, чем данная область видимости небезопасного кода. При этом компилятор C# не выдает предупреждений о такой ситуации. Однако, компилятор C# не позволит установить указатель на управляемую переменную вне оператора fixed. Например: class Point { public int x, y; } class Class1 {[STAThread] static void Main(string[] args) { unsafe { Point pt = new Point(); // pt — это управляемая переменная fixed ( int* p = &pt.x ) { *p = 1 } // pt.x – указывает на значение // размерного типа
Разработка распределенных приложений } } } Одним оператором fixed можно фиксировать несколько указателей, но только в том случае, если они одного типа. Например: fixed (byte* pa1 = array1, pa2 = array2) {...} В том случае, если требуется зафиксировать указатели различных типов, можно использовать вложенные операторы fixed. Например: fixed (int* p1 = &p.x) fixed (double* p2 = &array1[5]) { } Указатели, которые инициализированы оператором fixed, не могут быть изменены. Если объект, на который устанавливается указатель, размещается в стеке (например, переменная типа int), то его местоположение фиксировать не требуется. Разместить блок памяти в стеке можно и явным образом, используя оператор stackalloc, который имеет следующее формальное описание: тип * имя_указателя = stackalloc тип [ выражение ]; В качестве типа может быть указан любой неуправляемый тип. Например: using System; class Class1 {public static unsafe void Main() { int* a1 = stackalloc int[100]; int* p = a1; // Указатель на первый элемент массива *p++ = *p++ = 1; for (int i=2; i<100; ++i, ++p) *p = p[-1] + p[-2]; for (int i=0; i<10; ++i) Console.WriteLine (a1[i]); } }
64
Разработка распределенных приложений
65
Время жизни указателя, инициализированного с применением stackalloc, ограничено временем выполнения метода, в котором этот указатель объявлен. Инициализировать указатели посредством stackalloc можно только для локальных переменных. DLL-БИБЛИОТЕКИ Для того, чтобы вызвать метод из DLL-библиотеки, его следует объявить с модификатором extern и атрибутом DllImport. Например: [DllImport("Имя_библиотеки.dll")] static extern int M1(int i1, string s1); Класс атрибута DllImportAttribute имеет следующее определение: namespace System.Runtime.InteropServices { [AttributeUsage(AttributeTargets.Method)] public class DllImportAttribute: System.Attribute { public DllImportAttribute(string dllName) {...} public CallingConvention CallingConvention; public CharSet CharSet; // Набор символов public string EntryPoint; // Имя метода public bool ExactSpelling; // Точное соответствие // написания имени метода public bool PreserveSig; // Определяет, будет ли предотвращено // изменение сигнатуры метода (по умолчанию установлено // значение true). При изменении сигнатуры возвращаемое // значение будет иметь тип HRESULT и будет // добавлен out-параметр retval public bool SetLastError; public string Value { get {...} } } }
Разработка распределенных приложений
66
Атрибут DllImport имеет один позиционный параметр, определяющий имя DLL-библиотеки, и пять именованных параметров. Параметр EntryPoint позволяет указать имя метод из DLL-библиотеки. При этом имя метода, помечаемого данным атрибутом, может отличаться от имени метода в DLLбиблиотеке. Например: [DllImport("myDll.dll", EntryPoint="M1")] static extern int New_name_of_M1(int i1, string s1); Именованный параметр CharSet определяет используемый в DLLбиблиотеке набор символов (ANSI или Unicode). По умолчанию используется значение CharSet.Auto. Например: [DllImport("myDll.dll", CharSet CharSet.Ansi)] static extern int M1(int i1, string s1); Для каждого типа параметра при вызове метода из DLL-библиотеки выполняющая среда .NET производит подразумеваемое по умолчанию преобразование типов (например, тип string в тип LPSTR (Win32). Для того, чтобы явным образом указать тип, используемый в методе DLL-библиотеки, следует применить к параметру атрибут MarshalAsAttribute. Например: [DllImport("myDll.dll", CharSet CharSet.Unicode)] static extern int M1(int i1, [MarshalAs(UnmanagedType.LPWStr)] string s1); Атрибут MarshalAsAttribute может быть прикреплен к полю, методу или параметру. Прикрепление данного атрибута к методу позволяет указать явное преобразование типа для возвращаемого значения.
Разработка распределенных приложений
67
Тема 3. Реализация распределенного доступа средствами NET. Remoting АРХИТЕКТУРА .NET. REMOTING Инфраструктура NET. Remoting - это набор служб, выполняющих: § активацию объектов § управление временем жизни объектов § обмен сообщениями между удаленными объектами через каналы (посредством объектов, выполняющих передачу сообщений). Любое сообщение, передаваемое через канал, кодируется и декодируется посредством внутренних объектов .NET., которые называются форматирующими объектами сериализации (serialization formatters). Эти объекты могут использовать различные форматы, такие как двоичный или SOAP-форомат. Любое сообщение может быть закодировано в двоичном или XML формате. Для взаимодействия объектов через границы Remoting надо иметь: § серверный объект; § клиентский объект – при создании экземпляра серверного объекта создается объект прокси со всеми ссылками на свойства и методы серверного объекта. § транспортный механизм, выполняющий передачу вызовов между объектами.
ВЗАИМОДЕЙСТВИЯ ОБЪЕКТОВ Объект может быть дистанцируемым (remootable), который может быть скопирован или передан по ссылке или по значению, и недистанцируемым (nonremootable), у которого нет методов, позволяющих средствам Remoting скопировать его в другой домен приложений. Дистанцируемые объекты могут быть:
Разработка распределенных приложений § §
68
передаваемы по ссылке (для них необходим прокси). Эти объекты наследуются от класса System.MarshalByRefObject. передаваемы по значению (должен быть реализован интерфейс ISerializable или объект должен быт помечен атрибутом SerializableAttribute).
СОЗДАНИЕ СЕРВЕРНОГО ОБЪЕКТА Создание серверного объекта может выполняться в следующих режимах: § в режиме Singleton – один на всех (тип WellKnownObjectMode. Singleton) § в режиме SingleCall – создание объекта для каждого вызова метода. Для удаленного объекта может применяться: § серверная активизация; § клиентская активизация. При серверной активизация объект на сервере создается в момент вызова метода. При клиентской активизации объект на сервере создается одновременно с созданием его экземпляра вызовом метода new. АРЕНДА Диспетчер аренды используется для поиска объектов, готовых к уничтожению. Диспетчер аренды размещается в домене серверного приложения. Объекту, передаваеммому из домена приложения по ссылке, назначается время аренды. Если срок аренды истек, то диспетчер проверяет список спонсоров данного объекта. Спонсор объекта может изменить аренду, увеличив время аренды. Для инициализации аренды объекта следует переопределить функцию InitializeLifetimeService класса MarshalByRefObject. После назначения аренды возможно изменять только значение свойства CurrentLeaseTime.
ПЕРЕДАЧА СООБЩЕНИЙ ЧЕРЕЗ КАНАЛЫ Удаленные объекты используют каналы для взаимодействия. Каналы позволяют приложениям из разных доменов приложений, из разных
Разработка распределенных приложений
69
процессов или компьютеров обмениваться сообщениями посредством различныз транспортных протоколов. Интерфейсы и классы, используемые для работы с каналами, содержатся в пространстве имен System.Runtime.Remoting.Channels. Любой канал реализует интерфейс IChannel со свойствами ChannelName и ChannelPriority. Каналы подразделяются на принимающие и отправляющие. Принимающие каналы реализуют интерфейс IChannelReceiver, а отправляющие – IChannelSender. Интерфейс IChannelReceiver определяет методы StartListening и StopListening. Интерфейс IChannelSender определяет метод CreateMessageSink. Интерфейс IChannelReceiver реализуется классами HttpServerChannel и TcpServerChannel. Перед использованием канала он должен быть зарегистрирован. Это реализуется методами класса ChannelServices.
Тема 4. Создание и управление службами Windows Службы Windows Службы Windows функционируют в виде фоновых процессов. Службы Windows не имеют пользовательского интерфейса. Службы Windows, как правило, создаются для управления и контроля ресурсов. Все службы можно просмотреть, выполнив Панель управления | Администрирование | Службы. Служба Windows регистрируется в реестре Windows как выполняемый объект. Локальное и удаленное управление службами осуществляет Диспетчер управления службами (SCM - Service Control Manager). Среда программирования Visual Studio .NET: § позволяет создавать приложения, управляющие службами через SCM;
Разработка распределенных приложений §
70
предоставляет классы библиотеки Framework, используемые для создания служб.
АРХИТЕКТУРА СЛУЖБ WINDOWS Архитектура служб Windows представляется как три компонента: § приложение, реализующее функциональность одной или нескольких служб (приложение-служба); § приложение, управляющее поведением службы (приложениеконтроллер службы); § Service Control Manager – утилита, используемая для управления зарегистрированными на компьютере службами. Список всех зарегистрированных служб расположен в реестре Windows в разделе: HKEY_LOCAL_MACHINE | SYSTEM | CurrentControlSet | Services. Служба может находиться в состоянии: работает, остановлена, приостановлена. Служба может выполняться в отдельном процессе (Win32OwnProcess) или в процессе совместно с другими службами (Win32ShareProcess). СОЗДАНИЕ СЛУЖБЫ Для создания служб, установки и управления службами предназначены классы из пространства имен System.ServiceProcess. Создать службу можно, используя методы класса ServiceBase. Регистрация служб в системном реестре выполняется методами классов ServiceInstaller (устанавливает класс службы) и ServiceProcessInstaller (создает процесс, в котором будет выполняться служба). Для запуска и остановки службы используются методы класса ServiceController. Классы пространства имен System.Diagnostics предназначены для отслеживания и отладки служб: § класс EventLog – позволяет службе заносить информацию об ошибках в журнал событий;
Разработка распределенных приложений §
класс PerfomanceCounter – позволяет отслеживать используемость ресурсов; § класс Trace – позволяет выполнять трассировку службы. § класс Debug - предназначается для отладки служб. Методы базового класса создаваемой службы System.ServiceProcess.ServiceBase: § OnStart – вызывается при запуске службы; § OnPause – вызывается при приостановке службы; § OnStop – вызывается при останове службы; § OnContinue – вызывается при возобновлении работы службы; § OnShutDown – вызывается при выключении ПК; § OnCustomCommand – вызывается для обработки пользовательских команд, информация о команде указывается параметрами; § OnPowerEvent – вызывается при получении сообщения от службы управления питанием. Переопределение методов класса System.ServiceProcess.ServiceBase позволяет определить поведение службы. Методы класса System.ServiceProcess.ServiceController, используемые для программного управления службой: § Close – отключает экземпляр класса ServiceController от службы и освобождает все ресурсы, занятые службой; § Continue – возобновляет работу службы; § ExecuteCommand – передает службе произвольную команду; § Pause – приостанавливает службу; § Refresh – обновляет значения всех свойств; § Start – запускает службу; § Stop – останавливает службу, а также все зависимые от нее другие службы. Для создания служб можно использовать шаблон Windows Service. Далее следует: 1.Установить следующие свойства службы: ServiceName – имя службы для SCM, Name – имя класса службы.
71
Разработка распределенных приложений
72
2.Дополнительно можно изменить следующие свойства службы: CanStop – можно ли останавливать службу после запуска, CanPauseAndContinue, CanShutdown, AutoLog – позволяет службе использовать системный журнал событий (при значении равном false используются собственные журналы событий). 3.Переопределить методы обработки событий: OnStart и OnStop. 4. ЗАПУСК СЛУЖБЫ При запуске службы SCM инициирует вызов метода OnStart в найденном системой exe-файле. Свойство StartType позволяет указать автоматический запуск службы при загрузке ПК. Перечисление ServiceStartMode предоставляет следующие значения: Automatic, Manual, Disabled. Событие OnStop передается службе в том случае, если значение свойства CanStop равно true. ЖУРНАЛ СОБЫТИЙ Доступ к журналу событий выполняется посредством компонента EventLog. По умолчанию разрешается записывать информацию в журналы: System, Security иApplication. Например: // DBWriter – имя службы и имя класса службы public class DBWriter : System.ServiceProcess.ServiceBase { // Журнал событий: private System.Diagnostics.EventLog eventLog1; // Счетчик производительности (Отображаются на вкладке Servers // в разделе PerformanceCounter private System.Diagnostics.PerformanceCounter perfCounter1; private System.ComponentModel.Container components = null; public DBWriter() { InitializeComponent(); } static void Main() { System.ServiceProcess.ServiceBase[] ServicesToRun; ServicesToRun = new System.ServiceProcess.ServiceBase[] { new DBWriter() };
Разработка распределенных приложений
73
System.ServiceProcess.ServiceBase.Run(ServicesToRun); // Запуск // сервиса } private void InitializeComponent() { this.eventLog1 = new System.Diagnostics.EventLog(); this.perfCounter1 = new System.Diagnostics.PerformanceCounter(); ((System.ComponentModel.ISupportInitialize)(this.eventLog1)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.perfCounter1)).BeginInit(); // eventLog1: this.eventLog1.Log = "Trans Log"; // Имя файла журнала событий this.eventLog1.Source = "Trans Service"; // perfCounter1: this.perfCounter1.CategoryName = "MyCounters"; this.perfCounter1.CounterName = "Ctr1"; this.perfCounter1.ReadOnly = false; // Увеличение значения счетчика с шагом 4: //perfCounter1.Increment(4); // Уменьшение счетчика с шагом 3: perfCounter1.Decrement(3); // Установка значения счетчика равным 10: // perfCounter1.RawValue=10; // Получение значения счетчика: int i1=perfCounter1.RawValue; // Сервис DBWriter: this.AutoLog = false; this.ServiceName = "DBWriter"; ((System.ComponentModel.ISupportInitialize)(this.eventLog1)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.performanceCounter1)).End Init(); } protected override void Dispose( bool disposing ) { if( disposing ) {if (components != null) { components.Dispose(); } } base.Dispose( disposing ); }
Разработка распределенных приложений protected override void OnStart(string[] args) { // Создаем и закрываем файл Log.tmp FileStream tfs = File.Create("C:\\Log.tmp"); tfs.Close(); // Если не существует, то создаем файл Customers.db if (!File.Exists("C:\\Customers.db")) {FileStream cfs= File.Create("C:\\Customers.db"); cfs.Close(); } // Записываем данные( entry) в log-файл событий eventLog1.WriteEntry("DBWriter сервис запущен в " + System.DateTime.Now.ToString()); } protected override void OnStop() { // Удаляем файл Log.tmp File.Delete("C:\\Log.tmp"); // Устанавливаем значение счетчика производительности равным 0 perfCounter1.RawValue = 0; // Записываем данные в log-файл eventLog1.WriteEntry("DBWriter сервис остановлен в " + System.DateTime.Now.ToString()); } protected override void OnCustomCommand(int command) { if (command == 201) Commit(); else if (command == 200) Rollback(); } private void Commit() {
74
Разработка распределенных приложений
75
// Создаем для чтения данных из Log.tmp поток StreamReader StreamReader sr = new StreamReader(new FileStream("C:\\Log.tmp", FileMode.Open)); // Созаем для записи данных в Customers.db поток StreamWriter StreamWriter sw = new StreamWriter(new FileStream("C:\\Customers.db", FileMode.Append, FileAccess.Write)); sw.WriteLine(sr.ReadToEnd()); sw.Flush(); //Закрываем файлы sr.Close(); sw.Close(); //Увеличиваем счетчик и записываем вход в EventLog perfCounter1.Increment(); truncateLogFile(); eventLog1.WriteEntry("DBWriter сервис завиксировал транзакцию в " + System.DateTime.Now.ToString()); } private void Rollback() { truncateLogFile(); eventLog1.WriteEntry("DBWriter сервис выполнил откат транзакции в" + System.DateTime.Now.ToString()); } private void truncateLogFile() { // Удаляем данные из файла Log.tmp FileStream fs =new FileStream("C:\\Log.tmp", FileMode.Truncate); fs.Flush(); fs.Close(); }
Разработка распределенных приложений
76
} } УСТАНОВКА СЛУЖБЫ Для установки службы используются установочные компоненты, реализуемые классами: § System.ServiceProcess.ServiceProcessInstaller – один экземпляр на процесс; § System.ServiceProcess.ServiceInstaller – один экземпляр на каждую службу процесса; § System.Diagnostics.EventLogInstaller – для настройки объектов журналов событий; § System.Diagnostics.PerformanceCounterInstaller – для настройки счетчиков производительности. Класс, используемый для установки службы должен наследоваться от System.Configuration.Install.Installer. Для настройки контекста безопасности используется свойство Account класса ServiceProcessInstaller, которое может принимать следующие значения: § LocalService – учетная запись, обладающая расширенным набором прав на локальном ПК и предоставляющая учетные данные компьютера другим пользователям; § LocalSystem – учетная запись пользователя с ограниченными правами на локальном компьютере, предоставляющая учетные данные анонимного пользователя удаленным серверам; § NetworkService – учетная запись пользователя с ограниченными правами на локальном компьютере, предоставляющая учетные данные удаленным серверам; § User – контекст пользователя, требующий при установке приложения службы ввода имени и пароля пользователя.
Разработка распределенных приложений
77
После добавления к приложению установочных компонентов и компиляции, полученный exe-файл следует установить, выполнив команду: Instalutil имя_службы.exe Для удаления службы следует выполнить: Instalutil /u имя_службы.exe Управление службами выполняется: § интерактивно посредством SCM; § программно при помощи класса ServiceController.
УПРАВЛЕНИЕ СЛУЖБАМИ Класс ServiceController позволяет: § просматривать список всех служб; § запускать и останавливать службы; § приостанавливать и возобновлять службы; § запрашивать и читать свойства служб; § задавать пользовательские команды для исполнения их службой. Компонент типа ServiceController можно добавить из окна Server Explorer, выбрав имя сервера, а затем имя службы которой следует управлять, и выполнив команду контекстного меню Add To Designer. Компонент также можно добавить из панели инструментов (страница Components). Имя службы указывается свойством ServiceName, а имя компьютера – MachineName. Динамические свойства службы можно настраивать программно. Для динамических свойств создается конфигурационный файл с парами "ключ=значение".
Разработка распределенных приложений
78
Тема 5. Создание и развертывание WEB-сервисов ИНФРАСТРУТУРА WEB-СЕРВИСОВ Web-сервисы позволяют программным компонентам реализовывать обмен сообщениями с использованием различных стандартных протоколов (HTTP, XML, XSD, SOAP, WSDL). Для обмена данными Web-сервисы XML применяют обмен сообщениями в формате XML. Это позволяет устанавливать взаимодействие компонентов, реализованных на разных языках программирования, и функционирующих на различных компьютерах. Для доступа к Web-сервису необходимо знать: § адрес Web-сервиса; § имя вызываемого метода. Клиент находит документы с описанием Web-сервиса на языке WSDL посредством механизма обнаружения сервиса. Клиент использует механизм каталогов для получения сведений об опубликованных Web-сервисах. Описание Web-сервиса представляет собой XML документ, содержащий формат сообщений, используемый для обмена данными.
Разработка распределенных приложений
79
На следующей схеме приведен механизм обнаружения и запроса Webсервиса с использованием компонентов инфраструктуры Web-сервисов ( каталог, - обнаружение, - описание, - обмен сообщениями с Webсервисом).
Клиент Web-сервиса XML
Webсервис XML
Microsoft описывает работу со своими сервисами и спецификацию UDDI (Universal Description, Discovery, and Integration) на странице http://uddi.microsoft.com/
Разработка распределенных приложений
СОЗДАНИЕ WEB-СЕРВИСА Для создание Web-сервиса в среде программирования Visual Studio .NET выполните команду меню New|Web Site и выбирете пункт ASP.NET Web Service. На следующем рисунке приведен диалог создания Web-сервиса.
В поле Location следует выбрать месторасположение создаваемого Webсервиса (File System, HTTP или FTP). В результате будет создан проект, содержащий файл, реализующий Webсервис и наследуемый от класса System.Web.Services.WebService , и ASP файл.
80
Разработка распределенных приложений
81
На следующем рисунке приведен автоматически сформированный код Webсервиса.
Атрибут [WebMethod] используется для объявления методов Web-сервиса. Этот атрибут может определять поведение методов Web-сервиса посредством следующихсвойств: § BufferResponse § CacheDuration § Description § EnabledSession § MessageName § TransactionOption. Следующий пример иллюстрирует код Web-сервиса, содержащего два метода: using System;
Разработка распределенных приложений
82
using System.Web; using System.Web.Services; using System.Web.Services.Protocols; [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class MyService : System.Web.Services.WebService { public MyService () { } [WebMethod] public string WebMetod1() { return "Web-сервис MyService"; } [WebMethod] public int WebMetod2(int i) { return i*i; } } Для запуска Web-сервиса из среды программирования Visual Studio .NET выполните команду меню Debug|Start Without Debugging. В результате будет отображено окно со списком методов запускаемого Webсервиса. Это окно приведено на следующем рисунке.
Разработка распределенных приложений
83
ПУБЛИКАЦИЯ WEB-СЕРВИСА Для публикации Web-сервиса достаточно выполнить команду меню Build|Publish Web Site и указать URL адрес размещения Web-сервиса. Для размещения Web-сервиса на локальном компьютере можно указать домашний каталог IIS (например, C:\Inetpub\wwwroot\MyServices). При этом dll-файлы автоматически будут скопированы в подкаталог /bin. ОБНАРУЖЕНИЕ WEB-СЕРВИСА Описание местоположения Web-сервиса может быто указано в DISCO файле. Клиент программно может обнаружить Web-сервис по DISCO-файлу. Для отображения информации из DISCO файла достаточно после URL Webсервиса указать ?DISCO. На следующем рисунке приведен код автоматически сформированного DISCO файла.
Элемент обнаружения указывается тегом . Используя команду меню WebSite|Add Web References можно добавить в проект файлы .disco, .discomap и .wsdl.
Разработка распределенных приложений
84
ДОСТУП К WEB-СЕРВИСУ Для создания ASP-приложения, использующего Web-сервис, сначала следует произвести обнаружение сервиса (В окне Solution Explorer выполнить команду контекстного меню Add Web References). На следующем рисунке приведен результат запроса Web-сервисов на локальном компьютере.
После добавления выбранного Web-сервиса в окне Solution Explorer появится секция App_WebReferences с файлами .disco, .discomap и .wsdl. WSDL файл содержит прокси класс для используемого Web-сервиса. В приложении клиенте следует создать объект данного прокси класса. При этим имя класса создаваемого Web-сервиса следует квалифицировать именем пристранства имен, в котором расположен данный класс. Например: protected void Button1_Click(object sender, EventArgs e) { WebService1.Service WS = new WebService1.Service(); } Вызов метода Web-сервиса выполняется через идентификатор объекта подключенногопрокса класса.
Разработка распределенных приложений
85
КОНФИГУРИРОВАНИЕ WEB-СЕРВИСА Параметры конфигурации Web-сервиса указываются в конфигурационном файле Web.config. Это XML документ, содержащий корневой тег и вложенные в него теги с атрибутами, устанавливающими параметры конфигурации. Например: <system.web>
Конфигурационный файл Web.config может содержать следующие элементы: § authentication – определяет правила аутентификации пользователей Web-сервиса; § authorization – определяет политику авторизации, позволяя или запрещая некоторым группам пользователей доступ к Web-сервису (например, - разрешен доступ всем пользователям); § compilation – определяет, будет ли в Web-сервис включена отладочная информация; § trace – разрешает выполнятьтрассировку Web-сервиса; § customErrors – позволяет управлять сообщениями об ошибках.
Разработка распределенных приложений
86
Тема 6. Серверные Web-приложения Общие принципы создания серверных приложений Данные, отображаемые WEB-броузером, представляют собой HTMLстраницу. Для соединения WEB-броузера и WEB-сервера используется протокол TCP/IP(Transmission Control Protocol/Internet Protocol). Протокол TCP/IP предназначен для установления соединения между двумя компьютерами в сети, обычно называемых клиентом и сервером. Протокол TCP/IP определяет IP-адрес и номер порта. IP–адрес задает имя компьютера в сети. IP-адрес указывается или как числовой идентификатор компьютера или при использовании сервера DNS как символьный псевдоним числового идентификатора. Локальный компьютер всегда адресуется как 127.0.0.1 или localhost. При работе в Интернет все используемые IP-адреса уникальны. Поэтому для задания своему ПК некоторого IP-адреса следует получить его у провайдера. При работе без Интернета, в локальной сети предприятия, можно самостоятельно установить различные IP-адреса для каждого ПК. Например: 192.168.0.2; 192.168.0.3; 192.168.0.4 и т.д. Номер порта – это значение, однозначно идентифицирующее некоторый логический порт приложения, через который можно получать и посылать данные. НТТР-запрос формируется в соответствии с протоколом HTTP (HyperText Transfer Protocol). В HTTP-запросе указывает GET или POST метод передачи данных. При вводе URL-адреса в поле адреса WEB-броузера или выполнении формы (пользователь щелкнул по кнопке типа SUBMIT) используется GET-метод. Если форма содержала данные, то они будут добавлены в конец строки с URL-адресом. Соответственно такой способ накладывает ограничение на размер передаваемых параметров. Если при выполнении формы атрибут METOD установлен равным POST, то используется POST-метод, при котором сначала на сервер посылается строка POST-запроса и HTTP-заголовки запроса, а затем пустая строка и строка, содержащая передаваемые параметры.
Разработка распределенных приложений
87
По HTTP-запросу WEB-броузер посылает на WEB-сервер информацию, содержащую URL-адрес документа, тип запроса и значения параметров. URL-адрес может указывать как простую HTML-страницу, так и приложение, выполняемое на WEB-сервере. Такое приложение иногда называется серверным приложением. К серверным приложениям относятся CGI-приложения и ISAPI-приложения, и ASP- приложения. В отличие от CGI-приложений, выполняемых в отдельном процессе, ISAPIприложения реализуются как DLL-библиотеки. Результатом выполнения CGI или ISAPI приложения чаще всего является динамически сформированная HTML-страница.
Серверные приложения Серверные приложения можно создавать на различных языках программирования, таких как C++ или С#. При формировании серверного приложения в Visual Studio .NET следует при создании нового проекта выбрать соответствующий шаблон, например MFC ISAPI Extension Dll. Библиотека MFC предоставляет ряд классов, поддерживающих работу с HTTP-запросами: § CHtmlStream; § CHttpServerContext; § CHttpServer; § CHttpFilterContext; § CHttpFilter; § CHttpArgList. ISAPI-приложение создается как DLL-библиотека. Класс, выполняющий обработку HTTP-запроса, формирует динамическую HTML-страницу. Этот класс наследуется от класса CHttpServer. Для того чтобы добавить функции, используемые для обработки запросов, следует: 1. Создать функцию для каждой команды. При вызове такой функции в качестве параметра ей передается объект типа CHttpServerContext.
Разработка распределенных приложений
88
2. Указать для каждой команды вход в таблице обработки команд. 3. При необходимости для реализации собственной обработки запроса надо переопределить метод CHttpExtensionProc. Для таблицы описания команд в MFC-библиотеку включены пять следующих макросов: § BEGIN_PARSE_MAP – определяет начало таблицы описания команд и указывает класс функций членов и базовый класс; § END_PARSE_MAP - определяет конец таблицы описания команд; § ON_PARSE_COMMAND – идентифицирует команду и указывает соответствующую ей функцию; § ON_PARSE_COMMAND_PARAMS – определяет список параметров обрабатываемой команды. Этот макрос должен следовать непосредственно за макросом ON_PARSE_COMMAND; § DEFAULT_PARSE_COMMAND – определяет команду, используемую в том случае, если нет явного указания выполняемой команды. Макрос ON_PARSE_COMMAND используется при определении команды для объекта класса CHttpServer (или наследуемого от него), поступающей от клиента, и имеет следующее описание: ON_PARSE_COMMAND(FnName, mapClass, Args) Параметры: FnName – имя функции члена класса, а также и имя команды. mapClass – имя класса указанной функции. Args – указывает тип списка параметров и может принимать следующие значения: ITS_EMPTY - параметров нет; ITS_PSTR ITS_RAW
ITS_I2 ITS_I4 ITS_R4
- указатель на строку; - данные предварительно необрабатываемые; Используется в том случае, если список параметров HTTPзапроса может иметь различное число параметров; - значение типа short; - значение типа long; - значение типа float;
Разработка распределенных приложений ITS_R8 ITS_I8 ITS_ARGLIST
- значение типа double; - значение типа 64-битовое integer; - указатель на объект типа CHttpArgList.
Для запросов типа http://LOCALSERVER/ISAPI_2.dll?Myfunc&s1=10&s2=35&c1=y можно использовать макрос с типом параметров ITS_ARGLIST: ON_PARSE_COMMAND(Myfunc, CMyHttpServer, ITS_ARGLIST). Далее для разбора такого списка параметром используется класс CHttpArgList. Этот класс представляет собой массив структур типа CHttpArg. Данные в этом случае доступны через объект CHttpArg. Поле CHttpArg::m_pstrValue содержит значение параметра, а поле CHttpArg::m_pstrArg – имя параметра. Например, для строки http://localserver/myh1.dll&Arg1=str 1&Arg2&Arg3=str 2 надо реализовать разбор параметров по следующей схеме: CHttpArgList | GetFirstArg() |----------------à | GetNextArg() | |
CHttpArg m_pstrArg="Arg1" m_pstrValue="str 1" m_pstrRaw="Arg1 = str 1"
| GetNextArg() | ---------------à | | |
CHttpArg m_pstrArg="Arg2" m_pstrValue="" m_pstrRaw="Arg2"
| GetNextArg() |---------------à
CHttpArg m_pstrArg="Arg3"
89
Разработка распределенных приложений
90
m_pstrValue="str 3" m_pstrRaw="Arg3 = str 3"
Для выполнения ISAPI-приложения разработанную DLL-библиотеку следует поместить в каталог WEB-сервера. В следующем листинге приведен код заголовочного файла и файла реализации класса ISAPI-приложения наследуемого от CHttpServer. В автоматически сформированный код ISAPI-приложения внесены изменения, добавляющие с создаваемую HTML-страницу две строки текста и форму, содержащую элемент управления. // Заголовочный файл MyISAPI_1.h #pragma once #include "resource.h" class CMyISAPI_1Extension : public CHttpServer { public: CMyISAPI_1Extension(); // Конструктор ~CMyISAPI_1Extension(); public: virtual BOOL GetExtensionVersion(HSE_VERSION_INFO* pVer); virtual BOOL TerminateExtension(DWORD dwFlags); void Default(CHttpServerContext* pCtxt); DECLARE_PARSE_MAP() }; // Файл реализации MyISAPI_1.cpp #include "stdafx.h" #include "MyISAPI_1.h" CWinApp theApp; // Объект приложение BEGIN_PARSE_MAP(CMyISAPI_1Extension, CHttpServer) // Таблица
Разработка распределенных приложений
91
// обработки команды // TODO: место для определения ON_PARSE_COMMAND() и // ON_PARSE_COMMAND_PARAMS() ON_PARSE_COMMAND(Default, CMyISAPI_1Extension, ITS_EMPTY) DEFAULT_PARSE_COMMAND(Default, CMyISAPI_1Extension) END_PARSE_MAP(CMyISAPI_1Extension) CMyISAPI_1Extension theExtension; // Только один объект //ISAPI-расширение класса, // наследуемого от CHttpServer CMyISAPI_1Extension::CMyISAPI_1Extension(){ } // Конструктор CMyISAPI_1Extension::~CMyISAPI_1Extension() { } BOOL CMyISAPI_1Extension::GetExtensionVersion(HSE_VERSION_INFO* pVer) { // Вызов метода базового класса CHttpServer::GetExtensionVersion(pVer); // Загрузка строки описания TCHAR sz[HSE_MAX_EXT_DLL_NAME_LEN+1]; ISAPIVERIFY(::LoadString(AfxGetResourceHandle(), // Макро - если 0, IDS_SERVER, sz, HSE_MAX_EXT_DLL_NAME_LEN)); // то завершение _tcscpy(pVer->lpszExtensionDesc, sz); return TRUE; } BOOL CMyISAPI_1Extension::TerminateExtension(DWORD dwFlags) { // Метод класса CHttpServer – позволяет выполнить завершение // потоков и работы ISAPI-расширения //TODO: Clean up any per-instance resources return TRUE; } // CMyISAPI_1Extension : методы обработчики // Код формируемой HTML-страницы записывается методом Default // в поток вывода
Разработка распределенных приложений
92
void CMyISAPI_1Extension::Default(CHttpServerContext* pCtxt) { StartContent(pCtxt); // Начало HTML-страницы WriteTitle(pCtxt); // Формирование значения тега TITLE // _T – для Unocode конвертируется в L *pCtxt << _T(" HTML-page from "); // Первая строка HTML-страницы *pCtxt << _T("ISAPI-application "); // Формирование строки HTML-документа для отображения формы *pCtxt << _T(" Механизм параллельного вызова используется и непосредственно самими элементами управления ASP.NET, такими как GridView и TreeView.
Разработка распределенных приложений
103
КОМПОНЕНТЫ WEB PARTS FRAMEWORK Технология Web Parts позволяет пользователям самим определять какой контент будет отображаться на их Web-страницах. Такие страницы с настраиваемой структурой составляются из компонентов Web Parts. В отличие от фрейма, Web-части являются серверными элементами управления. Web Parts Framework представляется следующими элементами управления: § WebPartManager – управляет элементами, находящимися на странице Web Parts; § WebPart – определяет контент, отображаемый пользователю; § WebPartZone – определяет контейнер для элементов управления Web Parts уровня страницы; § CatalogPart – определяет список доступных элементов Web Parts, которые можно динамически добавлять на Web-страницу; § CatalogZone – определяет контейнер уровня страницы для элементов управления CatalogPart; § ConnectionsZone – содержит элементы WebPartConnection и предоставляет пользовательский интерфейс для управления ими; § EditorPart – определяет базовые свойства для всех элементов управления, используемых для редактирования содержимого элементов Web Parts; § EditorZone - определяет контейнер уровня страницы для элементов управления EditorPart; § WebPartConnection – создает соединение между элементами двух Webчастей на странице; § AppearanceEditorPart, LayoutEditorPart, BehaviorEditorPart, PropertyGridEditorPart – элементы управления части-редактора, позволяющие персонализировать различные аспекты поведения элементов управления Web-частей на странице. Базовым классом Web-частей является класс WebPart. Наследуемый от него класс определяет контент, отображаемый на странице.
Разработка распределенных приложений
104
Класс EditorPart является базовым классом частей-редакторов. Он позволяет редактировать структуру и свойства элементов Web-частей. Класс CatalogPart является базовым классом частей-каталогов. Этот класс позволяет динамически менять список частей, отображаемых на странице, выбирая их из списка доступных частей. Web-страница может быть разделена на несколько зон. Каждая зона – это контейнер частей, коророму присущ некоторый пользовательский интерфейс и определенная функциональность. Одна зона может содержатьболее одной части. Для каждого класса частей предусмотрен свой класс зоны (контейнером для WebPart является класс WebPartZone). Основным управляющим классом на страницах, построенных по технологии Web Parts, является класс WebPartManager. Этот класс позволяет: § информировать зоны и части об зменении режима отображения; § переносить части из одной зоны в другую; § перемещать части в пределах своей зоны; § создавать соединения между элементами; § управлятьсобытиями; § использовать интерфейс для редактирования свойств и поведения элементов управления и т.д.
СОЗДАНИЕ WEB-ЧАСТЕЙ Перед использованием Web-частей следует создать ASP.NET приложение (New|Wev Site и выбрать Empty Web Site). Далее в приложение следует добавить ASP-страницу (выполнить в окне Solution Explorer команду контекстного меню Web New Item и выбрать Web Form). В автоматически сформированный код страницы после тега