3. Обмен данными через DDE3.1. Архитектура "клиент-сервер" 3.2. Инициализация и создание канала связи 3.3. Передача данных через канал DDEML 3.4. Приложение DDEMLSR 3.5. Приложение DDEMLCL Операционная система Windows предоставляет пользователю возможность запустить одновременно несколько приложений. Как правило, пользователь именно так и поступает. Однако сама по себе эта достаточно важная особенность Windows не имела бы столь большого значения, если бы не существовали удобные механизмы обмена данными между приложениями. В предыдущей главе мы рассмотрели возможность передачи данных из одного приложения в другое через универсальный буфер обмена Clipboard. При этом обмен данными носит эпизодический характер и находится под контролем пользователя. Пользователь выделяет фрагмент документа, копирует его в Clipboard и затем вставляет в другой или тот же самый документ. Однако существуют и другие способы передачи данных между приложениями. Предметом нашего изучения в этой главе будет механизм динамического обмена данными DDE (Dynamic Data Exchange), позволяющий создать постоянно действующие каналы между несколькими одновременно работающими приложениями Windows. Эти каналы могут создаваться автоматически при запуске приложения или при необходимости, а также по явному запросу пользователя. После того как каналы созданы, они будут работать без вмешательства пользователя. В операционной системе Windows версии 3.0 механизм DDE был реализован через передачу сообщений с помощью хорошо известной вам функции SendMessage с использованием глобальных блоков памяти, доступных всем приложениям. Фактически при создании DDE-приложений программист был вынужден вникать во все детали процесса создания канала связи, придерживаясь определенного в SDK протокола. Поэтому использованию динамического обмена данных DDE сопутствовали многочисленные затруднения. Начиная с версии 3.1 в составе Windows появилось расширение - управляющая библиотека динамического обмена данными DDEML, выполненная как обычная DLL-библиотека. При создании новых DDE-приложений в SDK рекомендуется пользоваться именно этой библиотекой, а не старым механизмом, основанном на передаче сообщений. Библиотека DDEML хороша сама по себе, так как она упрощает DDE-приложения и избавляет программиста от учета утомительных деталей протокола обмена сообщениями. Однако есть еще две причины, по которой следует пользоваться этой библиотекой. Во-первых, в новых версиях Windows, а также в операционной системе Windows NT динамический обмен данных организуется исключительно с помощью библиотеки DDEML. Поэтому если в будущем вы собираетесь переносить свое приложение в среду Windows NT или Windows-95, имеет смысл сразу ориентироваться на работу с DDEML. Во-вторых, вспомним о существовании такой операционной системы, как Windows for Workgroups. Сетевые возможности Windows for Workgroups базируются на сетевом динамическом обмене данными Network DDE (операционная система Windows NT также поддерживает Network DDE). Механизм Network DDE позволяет организовать каналы обмена данными между приложениями, которые запущены на разных рабочих станциях сети. Поэтому освоение "обычной" библиотеки DDEML можно считать первым шагом на пути к изучению сетевых возможностей различных версий Windows. Исходя из сказанного выше, мы считаем нецелесообразным изучение устаревшего протокола обмена сообщениями DDE. Вместо этого мы сразу займемся библиотекой DDEML, ставшей стандартным средством организации динамического обмена данными в операционной системе Windows. 3.1. Архитектура "клиент-сервер"Для тех из вас, кто никогда не имел дело с локальными сетями персональных компьютеров и распределенной обработкой данных, мы немного расскажем об архитектуре "клиент-_ -сервер", имеющей самое непосредственное отношение к DDE. Прежде всего, определимся с терминологией. Клиентом и сервером мы будем называть процессы (программы или приложения), работающие одновременно на одном или разных компьютерах (объединенных в сеть), и взаимодействующих между собой определенным образом. Клиент посылает серверу запрос на получение данных или выполнение какой-либо работы (рис. 3.1). Это может быть запрос к серверу базы данных, задание серверу резервного копирования дисков или что-то аналогичное.
Рис. 3.1. Взаимодействие между клиентом и сервером Сервер, получив от клиента запрос, выполняет соответствующие действия и посылает клиенту ответ. Например, сервер базы данных находит нужные записи и посылает их клиенту. Сервер резервного копирования вводит в действие механизм выгрузки данных на магнитную ленту или магнитооптическое дисковое устройство и сообщает клиенту о запуске процедуры. Процесс передачи запроса серверу мы будем называть транзакцией. Строго говоря, транзакция - это совокупность трех действий: посылка запроса, выполнение запроса, прием ответа. Транзакция называется завершенной, если выполнены все три действия. Если же на одном из трех этапов произошел сбой, транзакция останется незавершенной. После восстановления незавершенная транзакция откатывается, что необходимо в ряде случаев для сохранения целостности данных (разумеется, за правильное выполнение отката транзакции отвечает сервер). Например, если сервер базы данных получил от клиента запрос на обновление записей в нескольких файлах и перед аварийным отключением электропитания успел выполнить только часть работы, после восстановления ему необходимо восстановить состояние базы данных на момент начала незавершенной транзакции. Топология системы, имеющей архитектуру "клиент-сервер", может быть различной. На рис. 3.1 показана система, состоящая из одного клиента и одного сервера. Однако в системе может быть несколько клиентов, работающих с одним сервером (рис. 3.2), или несколько клиентов, работающих одновременно с несколькими серверами (рис. 3.3).
Рис. 3.2. Система с одним сервером и несколькими клиентами Обратите внимание на пунктирную линию, соединяющую серверы на рис. 3.3. Между серверами тоже может идти обмен транзакциями. Таким образом, любой процесс может выступать одновременно и клиентом, и сервером.
Рис. 3.3. Система с двумя серверами и несколькими клиентами Очень хорошо, скажете вы, но причем тут DDE? А вот причем. Приложения, использующие технологию динамического обмена данных DDE, выступают как клиенты или серверы (или одновременно как клиенты и серверы). При этом взаимодействие между ними - это ни что иное, как транзакции. Библиотека DDEML позволяет создавать системы, имеющие различные топологии. В среде Windows версии 3.1 в качестве клиентов и серверов могут выступать приложения, работающие на одном компьютере. В локальной сети, созданной на базе операционных систем Windows for Workgroups и Windows NT, клиентами и серверами могут быть приложения, работающие на разных компьютерах. В нашей книге мы не будем рассматривать сетевую передачу данных через Network DDE, так как этот материал заслуживает отдельного и более глубокого изучения. Из-за ограниченного объема книги мы подробно рассмотрим только простейший, но часто встречающийся случай - когда в системе есть только один клиент и один сервер. Более того, мы ограничимся одним вариантом взаимодействия клиента и сервера, при котором инициатором передачи данных является клиент. Полное описание библиотеки DDEML вы сможете найти в документации, которая поставляется вместе с SDK for Windows 3.1. 3.2. Инициализация и создание канала связиПредположим, что нам нужно создать канал связи между двумя одновременно работающими приложениями Windows. Опишем соответствующие процедуры при использовании библиотеки DDEML. Пусть одно из этих двух приложений будет сервером, а другое - клиентом. В процессе инициализации сервер должен выполнить такие действия: зарегистрировать себя в библиотеке DDEML; зарегистрировать предоставляемый сервис, которым сможет воспользоваться приложение-клиент Клиент должен сделать следующее: зарегистрировать себя в библиотеке DDEML; создать канал связи с сервером, указав необходимый сервис Рассмотрим эти действия подробнее. Регистрация в библиотеке DDEMLБиблиотека DDEML используется одновременно многими приложениями, однако, подобно всем DLL-библиотекам, находится в оперативной памяти в единственном экземпляре. Функция LibMain, выполняющая инициализацию DLL-библиотеки, вызывается только один раз при первой загрузке библиотеки в память (для Windows версии 3.1), поэтому LibMain не может использоваться для регистрации приложений. Если приложение собирается использовать DDEML, оно должно зарегистрировать себя в библиотеке DDEML, вызвав специально предназначенную для этого функцию с именем DdeInitialize. Прототип функции DdeInitialize определен в файле ddeml.h (который должен быть включен в исходный текст DDEML-приложения наряду с файлом windows.h): UINT WINAPI DdeInitialize( DWORD FAR* pidInst, // адрес идентификатора приложения PFNCALLBACK pfnCallback, // адрес функции обратного вызова DWORD afCmd, // флаги DWORD ulRes); // зарезервировано Функция DdeInitialize используется в процессе инициализации и серверов, и клиентов DDEML. Сама по себе она не создает никаких каналов передачи данных между приложениями, однако процедура регистрации приложения, выполняемая этой функцией, должна быть проведена до вызова любых других функций, имеющих отношение к DDEML. Займемся параметрами функции DdeInitialize. Параметр pidInst представляет собой указатель на двойное слово, в которое после регистрации будет записан идентификатор, присвоенный копии приложения библиотекой DDEML (одновременно могут работать несколько копий одного и того же DDEML-приложения). Иными словами, в процессе регистрации библиотека DDEML присваивает копии приложения некоторый идентификатор, под которым она его "знает". Вы должны указывать полученный от функции идентификатор при вызове всех остальных функций библиотеки DDEML. Перед вызовом функции DdeInitialize ваше приложение должно записать в двойное слово, адрес которого передается через первый параметр, нулевое значение. Заметим, что идентификатор копии приложения, присвоенный в процессе регистрации, и идентификатор копии приложения, полученный через параметр функции WinMain - разные по смыслу (и по значению) идентификаторы. Параметр pfnCallback представляет собой указатель на функцию обратного вызова, определенную приложением для обработки транзакций. Как сервер, так и клиент должны определить такую функцию. Функция обратного вызова вызывается системой DDEML и содержит в себе всю логику обработки транзакций, определенную вами при разработке приложения. Если приложение вызывает функцию DdeInitialize несколько раз для многократной регистрации, каждый раз следует указывать отдельную функцию обратного вызова. Многократная регистрация вполне допустима, так как каждый раз библиотека DDEML будет создавать для себя новый идентификатор приложения. Такая методика используется при создании DLL-библиотек, работающих с DDEML. Обычным приложениям достаточно зарегистрировать себя один раз и, соответственно, определить одну функцию обратного вызова. Через параметр afCmd передается двойное слово, каждый бит которого является флагом, определяющим режимы работы канала связи, а также влияющие на действия, выполняемые функцией DdeInitialize. Последний параметр с именем ulRes зарезервирован и должен иметь нулевое значение. Приведем фрагмент кода, выполняющего регистрацию сервера в библиотеке DDEML: idInst = 0L; lpDdeSrProc = MakeProcInstance((FARPROC)DDEServerCallback, hInst); if(DdeInitialize((LPDWORD)&idInst, (PFNCALLBACK)lpDdeSrProc, APPCLASS_STANDARD, 0L)) { return FALSE; } В этом фрагменте вначале создается переходник для функции обратного вызова, затем адрес этого переходника указывается во втором параметре функции DdeInitialize. В случае успеха функция DdeInitialize возвращает нулевое значение. Для проверки можно также использовать константу DMLERR_NO_ERROR, определенную в файле ddeml.h. Если произошла ошибка, возвращается ненулевой код ошибки. Соответствующие константы определены в файле ddeml.h и имеют префикс имени DMLERR. Немного о флагах, передаваемых через параметр afCmd. Символические константы с префиксом имени APPCLASS позволяют задать класс приложения с точки зрения использования DDEML. Класс APPCLASS_STANDARD предназначен для регистрации стандартного DDEML-приложения. Этот класс использован в приведенном выше фрагменте кода и в приложении DDEMLSR, исходные тексты которого вы увидите позже. Класс APPCLASS_MONITOR предназначен для отладчиков и других приложений, управляющих работой системы DDEML. В качестве примера можно привести приложение DDESpy. Это приложение поставляется в составе Microsoft SDK for Windows 3.1 и предназначено для отладки DDE-приложений (и, разумеется, DDEML-приложений). В конце данной главы мы научим вас использовать приложение DDESpy для отладки созданных вами DDE-приложений. Символические константы с префиксом имени APPCMD позволяют конкретизировать функции, выполняемые приложением, и экономить системные ресурсы. Если DDEML-приложение выполняет только функции клиента, следует указать флаг APPCMD_CLIENTONLY: if(DdeInitialize((LPDWORD)&idInst, (PFNCALLBACK)lpDdeClProc, APPCMD_CLIENTONLY, 0L)) { return NULL; } В простейших случаях можно ограничиться использованием класса APPCLASS_STANDARD при создании сервера DDEML и флага APPCMD_CLIENTONLY при создании клиента DDEML. Мы так и поступили в наших приложениях DDEMLSR и DDEMLCL, выполняющих, соответственно, функции сервера и клиента DDEML. Остальные флаги влияют на то, когда и зачем будет вызываться функция обратного вызова. Если приложение больше не собирается работать с библиотекой DDEML, оно должно вызвать функцию DdeUninitialize, передав ей в качестве единственного параметра идентификатор копии приложения, полученный от функции DdeInitialize: BOOL WINAPI DdeUninitialize(DWORD idInst); Регистрация сервисаСледующий этап в инициализации сервера DDEML заключается в регистрации предоставляемого им сервиса. Библиотека DDEML использует трехступенчатую схему адресации данных, передаваемых по каналу связи - сервис (service), раздел (topic) и элемент данных (data item). Приложение задает элементы адреса в виде текстовых строк размером не более 255 байт. Это ограничение возникло в результате использования для реализации DDEML атомов, которые представляют собой идентификаторы текстовых строк, хранящихся в специальной системной таблице (для Windows версии 3.1). Размер таких строк не должен превышать 255 байт. Сервер DDEML может предоставлять сервис одного или нескольких видов. Как правило, один сервер предоставляет только один сервис, причем текстовая строка, идентифицирующая сервис, часто совпадает с именем приложения. Но можно выбрать любую другую строку. Например, наше приложение DDEMLSR предоставляет сервис "BMPService". Как можно догадаться из названия, этот сервис связан с bmp-файлами (в действительности мы привели сильно упрощенную версию сервера bmp-файлов, в которой для сокращения объема листингов изъяты функции обслуживания bmp-файлов). Второй элемент адреса - раздел. В рамках одного сервиса можно определить несколько разделов. Когда клиент DDEML создает канал с сервером, он указывает сервис и раздел. Раздел объединяет группу элементов данных или выполняемых функций. В приложении DDEMLSR определен один раздел "BMPFile". Канал DDEML служит для передачи блоков данных. В рамках одного раздела сервер может обмениваться с клиентом разными блоками данных, каждый из которых идентифицируется при передаче именем элемента данных. В процессе создания канала связи не требуется указывать элементы данных. Для иллюстрации сказанного выше предположим, что мы создаем сервер BMPSERV.EXE, предназначенный для отображения битовых изображений DIB, причем путь к соответствующему bmp-файлу и управляющая информация должны передаваться серверу через канал связи DDE. При регистрации сервер BMPSERV.EXE регистрирует один сервис "BMPServer" и два раздела: "BMPFile" и "Control" (рис. 3.4).
Рис. 3.4. Сервис, разделы и элементы данных для сервера BMPSERV В разделе "BMPFile" определены элементы данных "Filename" (имя отображаемого bmp-файла) и "Title" (заголовок изображения или подпись под изображением). В разделе "Control" определен один элемент данных "Mode", определяющий режим отображения (размеры и расположение окна, органы управления для работы с изображением и т. п.). Клиент может создавать два канала с сервером. Первый канал можно обозначить как BMPServer/BMPFile, второй - BMPServer/Control. Через канал BMPServer/BMPFile передается путь к отображаемому файлу и заголовок изображения, а через канал BMPServer/Control клиент может управлять режимом отображения. Разумеется, предложенная схема не единственно возможная и даже не самая простая. В нашем случае можно было ограничиться одним каналом, передавая по нему либо путь к файлу, либо управляющую информацию, имеющую отношение к отображению содержимого файла. Регистрация сервиса выполняется сервером DDEML обычно сразу после вызова функции DdeInitialize и выполняется в два этапа. На первом этапе текстовая строка имени сервиса сохраняется в специальной системной таблице (таблице атомов), для чего вызывается функция DdeCreateStringHandle: HSZ WINAPI DdeCreateStringHandle( DWORD idInst, // идентификатор приложения LPCSTR psz, // адрес текстовой строки int iCodePage); // кодовая страница Через параметр idInst приложение должно передать идентификатор, полученный на этапе регистрации приложения в библиотеке DDEML функцией DdeInitialize. Параметр psz представляет собой указатель на текстовую строку, закрытую двоичным нулем. Размер этой строки не должен превышать 255 байт. В качестве значения для параметра iCodePage можно указать CP_WINANSI (эта константа равна нулю). Можно также использовать значение, полученное от функции GetKBCodePage. Функция GetKBCodePage не имеет параметров и возвращает номер текущей кодовой страницы. Идентификатор текстовой строки, возвращенный функцией DdeCreateStringHandle и соответствующий регистрируемому сервису, следует передать функции DdeNameService: HDDEDATA WINAPI DdeNameService( DWORD idInst, // идентификатор приложения HSZ hsz1, // идентификатор строки имени сервиса HSZ hsz2, // зарезервировано UINT afCmd); // флаги Через параметр idInst приложение должно передать идентификатор, полученный на этапе регистрации приложения в библиотеке DDEML функцией DdeInitialize. Параметр hsz1 предназначен для передачи имени сервиса через идентификатор текстовой строки, возвращенной функцией DdeCreateStringHandle. Параметр hsz2 зарезервирован, для него следует использовать нулевое значение. При регистрации сервиса через параметр afCmd следует передать значение DNS_REGISTER (регистрация сервиса). Сервер DDEML в процессе своей работы может динамически регистрировать и отменять виды предоставляемого сервиса. Для отмены сервиса через параметр afCmd передается значение DNS_UNREGISTER. Перед завершением работы сервер DDEML должен отменить весь зарегистрированный им ранее сервис, вызвав функцию DdeInitialize с параметром afCmd, имеющим значение DNS_UNREGISTER. Если регистрация сервиса выполнена успешно, функция DdeNameService возвращает ненулевое значение, а при ошибке - нулевое. Приведем фрагмент кода, выполняющего регистрацию сервиса "BMPServer": hszService = DdeCreateStringHandle(idInst, "BMPServer", CP_WINANSI); DdeNameService(idInst, hszService, (HSZ)NULL, DNS_REGISTER); Одновременно с регистрацией сервиса сервер обычно создает идентификаторы текстовых строк, содержащих имена используемых разделов и элементов данных. Для этого вызывается все та же функция DdeCreateStringHandle: hszTopic = DdeCreateStringHandle(idInst, szTopic, CP_WINANSI); hszItem = DdeCreateStringHandle(idInst, szItem, CP_WINANSI); Отметим, что регистрацию сервиса выполняет только сервер DDEML. Что же касается создания идентификаторов текстовых строк функцией DdeCreateStringHandle, то эта операция выполняется как сервером, так и клиентом. Полученные идентификаторы используются при создании канала и в процессе передачи данных. Зная идентификатор строки, приложение может получить строку, вызвав функцию DdeQueryString: DWORD WINAPI DdeQueryString( DWORD idInst, // идентификатор приложения HSZ hsz, // идентификатор строки LPSTR psz, // адрес буфера для записи строки DWORD cchMax, // размер буфера int iCodePage); // кодовая страница Назначение параметров понятно из комментариев в прототипе функции. Если идентификатор созданной текстовой строки используется в функции обратного вызова (которую мы рассмотрим чуть позже), за освобождение ресурсов, связанных с текстовой строкой, отвечает система DDEML. В противном случае приложение должно самостоятельно уничтожать созданные им идентификаторы, вызывая функцию DdeFreeStringHandle: BOOL WINAPI DdeFreeStringHandle( DWORD idInst, // идентификатор приложения HSZ hsz); // идентификатор уничтожаемой строки В случае успеха функция DdeFreeStringHandle возвращает ненулевое значение, при ошибке - нулевое. Функция обратного вызова DDEMLКогда сервер или клиент регистрирует себя в библиотеке DDEML при помощи функции DdeInitialize, он указывает адрес переходника, созданного для функции обратного вызова. Функция обратного вызова предназначена для обработки всех событий, возникающих в процессе создания каналов связи и передачи данных. В простейшем случае функция обратного вызова сервера DDEML может выглядеть следующим образом: HDDEDATA EXPENTRY _export DDEServerCallback( WORD wType, // код транзакции WORD wFmt, // формат данных HCONV hConv, // идентификатор канала HSZ hsz1, // первый идентификатор строки HSZ hsz2, // второй идентификатор строки HDDEDATA hData, // идентификатор глобальной области данных DWORD dwData1, // первое дополнительное двойное слово DWORD dwData2) // второе дополнительное двойное слово { switch(wType) { // Создание канала передачи данных case XTYP_CONNECT: { ........... return((HDDEDATA)TRUE); } // Запрос данных от сервера case XTYP_REQUEST: { ........... return(hData); } // Запрос на выполнение команды case XTYP_EXECUTE: ........... break; // Передача данных серверу case XTYP_POKE: { ........... return((HDDEDATA)DDE_FACK); } // Подтверждение создания канала case XTYP_CONNECT_CONFIRM: { ........... break; } // Завершение работы канала case XTYP_DISCONNECT: { ........... break; } // Ошибка case XTYP_ERROR: { ........... break; } } return((HDDEDATA)NULL); } Функция обратного вызова должна быть определена как экспортируемая, поэтому мы указали ключевое слово _export. Через первый параметр wType передается код транзакции. Подобно функции окна, которая обрабатывает сообщения, функция обратного вызова DDEML выполняет обработку транзакций. В зависимости от кода транзакции и результата обработки функция обратного вызова DDEML возвращает то или иное значение. Второй параметр задает код формата передаваемых данных. Для кодов формата используются те же значения, что и для форматов Clipboard, например, CF_TEXT. Через параметр hConv передается идентификатор канала передачи данных (который мы еще не научились создавать). Назначение остальных параметров функции обратного вызова зависит от кода транзакции. В приведенном выше фрагменте кода используются символические имена кодов транзакций, определенные в файле ddeml.h, и имеющие префикс имени XTYP. В дальнейшем мы подробно расскажем о некоторых транзакциях, использованных в наших приложениях. Функция обратного вызова для клиента DDEML выглядит точно также, отличаясь лишь составом обрабатываемых транзакций. Приведем для примера исходный текст такой функции из нашего приложения DDEMLCL: HDDEDATA EXPENTRY DDEClientCallback( WORD wType, WORD wFmt, HCONV hConv, HSZ hsz1, HSZ hsz2, HDDEDATA hData, DWORD dwData1, DWORD dwData2) { switch(wType) { case XTYP_DISCONNECT: return((HDDEDATA) NULL); case XTYP_ERROR: break; case XTYP_XACT_COMPLETE: break; } return((HDDEDATA)NULL); } При регистрации приложения в библиотеке DDEML функцией DdeInitialize можно указать флаги, запрещающие или разрешающие поступление транзакций некоторых типов в функцию обратного вызова. Запретив вызов функции обратного вызова для необрабатываемых транзакций, можно ускорить работу приложения. Создание и уничтожение каналаПоследнее, что нужно сделать перед началом передачи данных по каналу DDEML, - создать канал связи (conversation). Теперь мы к этому вполне готовы. Кто является инициатором создания канала? Канал связи между клиентом и сервером создается всегда по инициативе клиента. После регистрации в библиотеке DDEML клиент вызывает функцию DdeConnect, создающую канал связи: HCONV WINAPI DdeConnect( DWORD idInst, // идентификатор приложения HSZ hszService, // идентификатор строки сервиса HSZ hszTopic, // идентификатор строки раздела CONVCONTEXT FAR* pCC); // адрес данных контекста Через параметр idInst приложение должно передать идентификатор, полученный на этапе регистрации приложения в библиотеки DDEML функцией DdeInitialize. Параметры hszService и hszTopic предназначены для передачи идентификаторов строк, содержащих, соответственно, имена сервиса и раздела. Эти идентификаторы были получены нами ранее при помощи функции DdeCreateStringHandle. Последний параметр - указатель на структуру типа CONVCONTEXT. Эта структура используется для указания информации о национальном языке и кодовой странице, соответствующей передаваемым данным. В простейшем случае для данного параметра можно указать значение NULL, при этом будет использована кодовая страница CP_WINANSI (что приемлемо в подавляющем большинстве случаев). В нашем приложении DDEMLCL для создания канала с сервером используется следующий фрагмент кода: hConv = DdeConnect(idInst, hszService, hszTopic, (PCONVCONTEXT)NULL); Идентификатор канала, полученный от функции DdeConnect, следует сохранить для обеспечения возможности получения данных от сервера. Что же происходит, когда клиент создает канал, вызывая функцию DdeConnect? Прежде всего, библиотека DDEML посылает транзакцию с кодом XTYP_CONNECT всем активным серверам, которые зарегистрировали сервис, указанный во втором параметре функции DdeConnect. Для транзакции XTYP_CONNECT параметры функции обратного вызова принимают следующие значения:
Обработчик транзакции XTYP_CONNECT, расположенный в функции обратного вызова сервера, должен проверить сервис и раздел, идентификаторы которых переданы через параметры функции. Если сервер поддерживает этот сервис и раздел, можно создавать канал. В таком случае функция обратного вызова должна вернуть значение TRUE. Иначе следует вернуть FALSE. Приведенный ниже фрагмент кода, взятый из нашего приложения DDEMLSR, проверяет только сервис (так как в нашем приложении определен только один сервис и один раздел): case XTYP_CONNECT: { if((HSZ)hsz2==(HSZ)hszService) return((HDDEDATA)TRUE); else return((HDDEDATA)FALSE); } Заметим, что мы сравниваем не строки, содержащие имя сервиса, а идентификаторы, так как для одинаковых строк в данном случае будут созданы одинаковые идентификаторы. В случае успешного создания канала сервер получает от системы DDEML транзакцию с кодом XTYP_CONNECT_CONFIRM. При обработке этой транзакции сервер может сохранить идентификатор созданного канала (который передается функции обратного вызова через параметр hConv) для дальнейшего использования. Приведем назначение параметров функции обратного вызова для транзакции XTYP_CONNECT_CONFIRM:
Когда канал связи больше не нужен, клиент или сервер может уничтожить его, вызвав функцию DdeDisconnect: BOOL WINAPI DdeDisconnect(HCONV hConv); В качестве единственного параметра этой функции передается идентификатор уничтожаемого канала. В процессе удаления канала "партнер" приложения, выступившего инициатором удаления канала, получает транзакцию XTYP_DISCONNECT. Соответствующий обработчик может при необходимости выполнить действия по освобождению ресурсов, заказанных приложением для работы с данным каналом связи. Приложение DDEMLSR обрабатывает транзакцию XTYP_DISCONNECT следующим образом: case XTYP_DISCONNECT: { hConvApp = NULL; break; } Все, что делает этот фрагмент кода - это запись нулевого значения в идентификатор не существующего больше канала связи. 3.3. Передача данных через канал DDEMLИтак, мы создали канал связи между сервером и клиентом. И сервер, и клиент получили и сохранили идентификаторы созданного канала связи. Теперь все готово для того чтобы приступить к передаче данных. Передача и прием данных может выполняться в трех режимах: по явному запросу, через "теплый" канал, или через "горячий" канал. В первом случае клиент посылает серверу запрос, указав нужный элемент данных. Сервер, получив такой запрос, предоставляет клиенту нужные данные. "Теплый" и "горячий" каналы связи создаются в тех случаях, когда клиент должен постоянно следить за изменениями данных, хранящихся в памяти сервера. В "теплом" режиме при изменении данных сервер посылает клиенту соответствующее извещение. Получив такое извещение, клиент запрашивает у сервера новые данные. В "горячем" режиме при изменении данных сервер самостоятельно посылает клиенту данные без дополнительного запроса. Предметом нашего рассмотрения будет самый простой режим - по явному запросу клиента. Процесс передачи данных заключается в посылке транзакций. Отметим, что транзакции бывают синхронные и асинхронные. Клиент, пославший синхронную транзакцию, дожидается ее завершения в течение заданного интервала времени. Если по истечении этого интервала времени транзакция не завершилась, клиент получает код ошибки. После посылки асинхронной транзакции клиент не ждет завершения транзакции. Когда транзакция будет завершена, клиент получит от системы DDEML транзакцию XTYP_XACT_COMPLETE. В наших примерах мы будем работать с синхронными транзакциями. Запрос данных от сервераДля того чтобы получить данные от сервера, клиент должен послать серверу транзакцию XTYP_REQUEST. Задача посылки серверу транзакции решается с помощью функции DdeClientTransaction: HDDEDATA WINAPI DdeClientTransaction( void FAR* pData, // адрес данных, передаваемых серверу DWORD cbData, // размер передаваемых данных HCONV hConv, // идентификатор канала HSZ hszItem, // идентификатор элемента данных UINT uFmt, // формат данных UINT uType, // код транзакции DWORD dwTimeout, // продолжительность периода ожидания DWORD FAR* pdwResult); // указатель на двойное слово, // в которое будет записан результат выполнения транзакции Если приложение запрашивает данные у сервера, для первых двух параметров следует указать нулевые значения (так как клиент не передает данные серверу, а наоборот, запрашивает их). Через параметр hConv следует передать идентификатор созданного ранее канала связи. Так как по одному каналу связи можно передавать различные элементы данных, следует указать нужный элемент данных с помощью параметра hszItem. Формат данных передается через параметр uFmt. Здесь вы можете использовать один из идентификаторов формата Clipboard, такой как CF_TEXT или CF_BITMAP, в зависимости от того, что собой представляют передаваемые данные. Через параметр uType следует передать код транзакции, посылаемой серверу. Для запроса данных следует послать транзакцию XTYP_REQUEST. Параметр dwTimeout задает для синхронных транзакций время ожидания завершения транзакции (в миллисекундах). Вы можете указать для этого параметра значение TIMEOUT_ASYNC, в этом случае будет запущена асинхронная транзакция. Параметр pdwResult должен содержать указатель на двойное слово. В это слово будет записан код результата выполнения транзакции. Если проверка не используется, через этот параметр можно передать нулевое значение. Приведем фрагмент кода приложения DDEMLCL, в котором выполняется запрос данных от сервера: hData = DdeClientTransaction(NULL, 0, hConv, hszItem, CF_TEXT, XTYP_REQUEST, 1000, &dwResult); if(hData != NULL) { DdeGetData(hData, szBuf, nBufSize, 0L); return TRUE; } В случае успешного завершения транзакции XTYP_REQUEST функция DdeClientTransaction возвращает идентификатор области глобальной памяти, в которой расположены полученные данные. Приложение должно переписать эти данные в свой буфер, вызвав для этого функцию DdeGetData: DWORD WINAPI DdeGetData( HDDEDATA hData, // идентификатор области памяти void FAR* pDst, // адрес буфера DWORD cbMax, // размер буфера DWORD cbOff); // смещение начала данных Последний параметр указывает смещение внутри области глобальной памяти, начиная с которого выполняется копирование. В нашем примере смещение равно нулю. Теперь посмотрим на сервер. Получив от клиента транзакцию XTYP_REQUEST, он должен передать данные клиенту. Приведем назначение параметров функции обратного вызова (сторона сервера) для транзакции XTYP_REQUEST:
Возможный вариант обработчика транзакции XTYP_REQUEST показан в следующем фрагменте кода, взятом из нашего приложения DDEMLSR: case XTYP_REQUEST: { // Создаем идентификатор данных hData = DdeCreateDataHandle(idInst, szDDEServerVersion, lstrlen(szDDEServerVersion) + 1, 0L, hszItem, CF_TEXT, 0); // В случае успеха возвращаем созданный идентификатор if(hData != NULL) return(hData); else return(NULL); } Этот фрагмент передает клиенту текстовую строку szDDEServerVersion. Однако вначале сервер должен заказать блок глобальной памяти, записав туда передаваемые данные. Это необходимо сделать при помощи функции DdeCreateDataHandle: HDDEDATA WINAPI DdeCreateDataHandle( DWORD idInst, // идентификатор приложения void FAR* pSrc, // адрес буфера DWORD cb, // размер буфера DWORD cbOff, // смещение начала данных HSZ hszItem, // идентификатор элемента данных UINT wFmt, // формат данных UINT afCmd); // флаги Если с помощью функции DdeCreateDataHandle вы заказываете блок памяти, идентификатор которого будет возвращен функцией обратного вызова, последний параметр следует указать как NULL. В этом случае система DDEML освободит блок памяти самостоятельно, как только в нем отпадет потребность. Итак, при обработке транзакции XTYP_REQUEST функция обратного вызова сервера создала блок глобальной памяти и записала туда передаваемые данные. Затем она должна возвратить идентификатор созданного блока памяти или NULL, если блок памяти создать невозможно. Передача данных серверуРассмотрим обратную процедуру - передачу данных от клиента серверу. Процедура передачи данных состоит из двух шагов. Вначале надо создать блок глобальной памяти и записать в него передаваемые данные. Для этого следует воспользоваться только что рассмотренной нами функцией DdeCreateDataHandle: hData = DdeCreateDataHandle (idInst, szString, lstrlen(szString) + 1, 0L, hszItem, CF_TEXT, 0); Затем клиент должен передать серверу транзакцию XTYP_POKE, вызвав для этого функцию DdeClientTransaction: if(hData != NULL) hData = DdeClientTransaction((LPBYTE)hData, -1, hConv, hszItem, CF_TEXT, XTYP_POKE, 1000, &dwResult); В качестве первого параметра функции DdeClientTransaction передается идентификатор созданного блока памяти. Обратите внимание, что размер блока задан во втором параметре функции как -1. Так и должно быть, если через первый параметр передается не указатель на область памяти, а идентификатор блока памяти. Приведем описание параметров для транзакции XTYP_POKE:
Обработчик транзакции XTYP_POKE в приложении DDEMLSR выглядит следующим образом: case XTYP_POKE: { // Проверяем элемент данных if(hsz1 == hszTopic) { // Получаем данные DdeGetData(hData, (LPBYTE) szDDEData, 200L, 0L); // Отображаем принятые данные на экране if(szDDEData != NULL) { MessageBox(NULL, szDDEData, "DDEML Server", MB_OK | MB_SYSTEMMODAL | MB_ICONINFORMATION); // Признак успешного завершения транзакции return((HDDEDATA)DDE_FACK); } } else return((HDDEDATA)NULL); break; } Этот обработчик сначала проверяет идентификатор элемента данных, затем получает данные с помощью рассмотренной нами ранее функции DdeGetData. Полученные данные отображаются на экране при помощи функции MessageBox. Обратите внимание, что в качестве признака успешного завершения транзакции возвращается значение DDE_FACK, определенное в файле ddeml.h. Выполнение командыПомимо передачи данных возможно такое взаимодействие между клиентом и сервером, когда клиент передает серверу команды в виде текстовой строки, а сервер их исполняет. Вообще говоря, этот механизм можно реализовать с помощью рассмотренных нами ранее транзакций XTYP_POKE и XTYP_REQUEST, однако существует специально предназначенная для передачи команд транзакция XTYP_EXECUTE. Вот соответствующие параметры, передаваемые функции обратного вызова:
Процесс передачи команды очень напоминает процесс передачи данных серверу через транзакцию XTYP_POKE. Вначале необходимо при помощи функции DdeCreateDataHandle создать блок памяти, содержащей текстовую строку команды. Отличие заключается в том, что параметр hszItem должен быть указан как NULL: hData = DdeCreateDataHandle (idInst, szCmdString, lstrlen(szCmdString) + 1, 0L, NULL, wFmt, 0); Затем с помощью функции DdeClientTransaction серверу посылается транзакция XTYP_EXECUTE: if(hData != NULL) hData = DdeClientTransaction((LPBYTE)hData, -1, hConv, hszItem, wFmt, XTYP_EXECUTE, 1000, &dwResult); Для того чтобы получить доступ к командной строке, обработчик транзакции XTYP_EXECUTE, расположенный в функции обратного вызова сервера, должен использовать функцию DdeAccessData: BYTE FAR* WINAPI DdeAccessData( HDDEDATA hData, // идентификатор блока памяти DWORD FAR* pcbDataSize); // указатель на переменную, // в которую будет записан размер блока памяти Эта функция возвращает указатель на начало области памяти, содержащей команду. После успешного выполнения команды функция обратного вызова сервера должна вернуть значение DDE_FACK. Если команда не поддерживается, нужно вернуть значение DDE_FNOTPROCESSED. В том случае, когда сервер может выполнить команду позже (потому что занят, например, выполнением другой команды), функция обратного вызова должна вернуть значение DDE_FBUSY. Клиент может проверить результат выполнения передачи команды, если проанализирует содержимое двойного слова, на которое указывал параметр lpdwResult перед вызовом функции DdeClientTransaction. Например, если в этом слове установлен бит DDE_FBUSY, можно попробовать повторить посылку команды позже. 3.4. Приложение DDEMLSRТеперь, когда вы познакомились с основными возможностями библиотеки DDEML, самое время приступить к практике. Мы подготовили для вас два приложения - DDEMLSR и DDEMLCL, которые, как нетрудно догадаться, являются сервером и клиентом DDEML. Приложение DDEMLSR (рис. 3.5) регистрирует сервис "BMPServer". Клиент может установить канал связи с разделом "BMPFile" и работать с элементом данных "DDEData".
Рис. 3.5. Приложение DDEMLSR Функции, выполняемые сервером, предельно просты. После запуска и регистрации сервиса сервер находится в состоянии ожидания запросов от клиента. Предусмотрены два вида запросов - запрос данных от сервера (транзакция XTYP_REQUEST) и передача данных серверу (транзакция XTYP_POKE). Когда сервер получает запрос на передачу данных клиенту, он в ответ передает текстовую строку, в которой находится описание версии приложения DDEMLSR. Если клиент посылает серверу данные (в виде текстовой строки), сервер отображает данные на экране при помощи функции MessageBox (рис. 3.6).
Рис. 3.6. Сервер отображает текстовую строку, полученную от клиента по каналу DDE Функция WinMain и функция главного окна приложения определены в файле ddemlsr.cpp (листинг 3.1). Листинг 3.1. Файл ddeml/ddemlsr.cpp // ---------------------------------------- // Приложение DDEMLSR // Сервер DDEML // ---------------------------------------- #define STRICT #include <windows.h> #include <windowsx.h> #include <mem.h> #pragma hdrstop #include "ddemlsr.hpp" // Прототипы функций BOOL DDEServerOpen(HINSTANCE hInst, LPSTR szService, LPSTR szTopic, LPSTR szItem); void DDEServerClose(void); BOOL InitApp(HINSTANCE); LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM); // Имя класса окна char const szClassName[] = "DDEMLSERVER"; // Заголовок окна char const szWindowTitle[] = "DDEML Server"; HWND hwnd; HINSTANCE hInst; // ===================================== // Функция WinMain // ===================================== #pragma argsused int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) { MSG msg; // Можно запускать только одну копию приложения if(hPrevInstance) return FALSE; // Инициализируем приложение if(!InitApp(hInstance)) return FALSE; // Сохраняем идентификатор приложения hInst = hInstance; hwnd = CreateWindow( szClassName, // имя класса окна szWindowTitle, // заголовок окна WS_CAPTION | WS_BORDER | WS_MINIMIZEBOX | WS_OVERLAPPED | WS_SYSMENU, 0, 0, 200, 100, 0, 0, hInstance, NULL); // Если создать окно не удалось, завершаем приложение if(!hwnd) return FALSE; // Если главное окно сервера должно быть // невидимым, поставьте перед следующей // строкой символ комментария ShowWindow(hwnd, SW_SHOWNORMAL); while(GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ===================================== // Функция InitApp // Выполняет регистрацию класса окна // ===================================== BOOL InitApp(HINSTANCE hInstance) { ATOM aWndClass; // атом для кода возврата WNDCLASS wc; // структура для регистрации memset(&wc, 0, sizeof(wc)); wc.lpszMenuName = "APP_MENU"; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC) WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(hInstance, "APP_ICON"); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(LTGRAY_BRUSH); wc.lpszClassName = (LPSTR)szClassName; aWndClass = RegisterClass(&wc); return (aWndClass != 0); } // ===================================== // Функция WndProc // ===================================== LRESULT CALLBACK _export WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { HDC hdc; PAINTSTRUCT ps; switch (msg) { case WM_CREATE: { // Инициализируем DDEML DDEServerOpen(hInst, (LPSTR)"BMPServer", (LPSTR)"BMPFile", (LPSTR)"DDEData"); return 0; } // Обработка сообщений от меню case WM_COMMAND: { switch (wParam) { case CM_HELPABOUT: { MessageBox(hwnd, "DDEML Server\nVersion 1.0\n" "(C) Frolov A.V., 1995", "About DDEML Server", MB_OK | MB_ICONINFORMATION); return 0; } // Завершаем работу приложения case CM_FILEEXIT: { DestroyWindow(hwnd); return 0; } default: return 0; } } case WM_DESTROY: { PostQuitMessage(0); // Завершаем работу с DDEML DDEServerClose(); return 0; } default: break; } return DefWindowProc(hwnd, msg, wParam, lParam); } Приложение DDEMLSR создает обычное главное окно, имеющее меню. Однако в таком окне не всегда есть необходимость - сервер может вообще не рисовать на экране свое окно, так как его функции могут быть не связаны с отображением информации. Для того чтобы скрыть главное окно приложения (а точнее, не показывать его вовсе), достаточно убрать вызов функции ShowWindow перед запуском цикла обработки сообщений. Разумеется, для стиля окна в этом случае не следует использовать значение WS_VISIBLE. При создании главного окна приложения обработчик сообщения WM_CREATE инициализирует сервер, вызывая функцию DDEServerOpen, определенную в файле ddemlfn.cpp (листинг 3.2). Функции передается имена сервиса, раздела данных и элемента данных. Обработчики сообщений от меню предназначены для просмотра версии сервера и завершения работы приложения. Эти действия выполняются обычным образом. При завершении работы приложения обработчик сообщения WM_DESTROY закрывает канал DDEML и освобождает все занятые ресурсы, вызывая функцию DDEServerClose, определенную в файле ddemlfn.cpp. Для удобства мы собрали все функции, имеющие отношение к DDEML, в файле ddemlfn.cpp. Там же определена и функция обратного вызова для сервера DDEML. Листинг 3.2. Файл ddeml/ddemlfn.cpp // ----------------------------------------------------- // Функции для работы с библиотекой DDEML // Сервер DDEML // ----------------------------------------------------- #define STRICT #include <windows.h> #include <windowsx.h> #include <ddeml.h> #include <dde.h> #include <mem.h> #include <string.h> #pragma hdrstop #include "ddemlsr.hpp" HDDEDATA EXPENTRY _export DDEServerCallback(WORD wType, WORD wFmt, HCONV hConv, HSZ hsz1, HSZ hsz2, HDDEDATA hData, DWORD dwData1, DWORD dwData2 ); // Идентификатор приложения, полученный после регистрации в // библиотеке DDEML DWORD idInst = 0L; FARPROC lpDdeProc; HSZ hszService = NULL; HSZ hszTopic = NULL; HSZ hszItem = NULL; // Идентификатор канала HCONV hConvApp = NULL; // Буфер для приема данных char szDDEData[200]; // Версия сервера. Эта текстовая строка // передается клиенту по его запросу char szDDEServerVersion[] = "DDEML Server v.1.0, (C) Frolov A.V."; //----------------------------------------------------- // Функция DDEServerOpen // Инициализация библиотеки DDEML //----------------------------------------------------- BOOL DDEServerOpen(HINSTANCE hInst, LPSTR szService, LPSTR szTopic, LPSTR szItem) { int rc; // Создаем переходник для функции обратного вызова lpDdeProc = MakeProcInstance((FARPROC)DDEServerCallback, hInst); // Выполняем инициализацию if(DdeInitialize((LPDWORD)&idInst, (PFNCALLBACK)lpDdeProc, APPCLASS_STANDARD, 0L)) { return FALSE; } // После успешной инициализации получаем идентификаторы // строк для сервиса, раздела и элемента данных else { hszService = DdeCreateStringHandle(idInst, szService, CP_WINANSI); hszTopic = DdeCreateStringHandle(idInst, szTopic, CP_WINANSI); hszItem = DdeCreateStringHandle(idInst, szItem, CP_WINANSI); // Регистрируем сервис DdeNameService(idInst, hszService, (HSZ)NULL, DNS_REGISTER); return TRUE; } } //----------------------------------------------------- // Функция DDEServerClose // Завершение работы с DDEML //----------------------------------------------------- void DDEServerClose(void) { // Завершение работы канала связи if(hConvApp != NULL) { DdeDisconnect(hConvApp); hConvApp = NULL; } // Сервис больше не доступен DdeNameService(idInst, hszService, (HSZ)NULL, DNS_UNREGISTER); // Освобождение идентификаторов строк DdeFreeStringHandle(idInst, hszService); DdeFreeStringHandle(idInst, hszTopic); DdeFreeStringHandle(idInst, hszItem); // Удаление переходника функции обратного вызова FreeProcInstance(lpDdeProc); } //----------------------------------------------------- // Функция DDEServerCallback // Функция обратного вызова для сервера DDEML //----------------------------------------------------- #pragma argsused HDDEDATA EXPENTRY _export DDEServerCallback(WORD wType, WORD wFmt, HCONV hConv, HSZ hsz1, HSZ hsz2, HDDEDATA hData, DWORD dwData1, DWORD dwData2) { switch(wType) { // Создание канала передачи данных case XTYP_CONNECT: { // Если сервис поддерживается, возвращаем // признак успешного создания канала if((HSZ)hsz2==(HSZ)hszService) return((HDDEDATA)TRUE); else return((HDDEDATA)FALSE); } // Запрос данных от сервера case XTYP_REQUEST: { // Создаем идентификатор данных hData = DdeCreateDataHandle(idInst, szDDEServerVersion, lstrlen(szDDEServerVersion) + 1, 0L, hszItem, CF_TEXT, 0); // В случае успеха возвращаем созданный идентификатор if(hData != NULL) return(hData); else return(NULL); } // Запрос на выполнение команды, отрабатывается вхолостую case XTYP_EXECUTE: break; // Передача данных серверу case XTYP_POKE: { // Проверяем элемент данных if(hsz1 == hszTopic) { // Получаем данные DdeGetData(hData, (LPBYTE) szDDEData, 200L, 0L); // Отображаем принятые данные на экране if(szDDEData != NULL) { MessageBox(NULL, szDDEData, "DDEML Server", MB_OK | MB_SYSTEMMODAL | MB_ICONINFORMATION); // Признак успешного завершения транзакции return((HDDEDATA)DDE_FACK); } } else return((HDDEDATA)NULL); break; } // Подтверждение создания канала case XTYP_CONNECT_CONFIRM: { // Сохраняем идентификатор канала hConvApp = hConv; break; } // Завершение работы канала case XTYP_DISCONNECT: { hConvApp = NULL; break; } // Ошибка case XTYP_ERROR: { break; } } return((HDDEDATA)NULL); } Транзакция XTYP_ERROR передается в функцию обратного вызова в случае возникновения ошибки. Младшее слово параметра dwData1 содержит код ошибки. Библиотека DDEML в Windows версии 3.1 поддерживает только один код ошибки - DML_ERR_LOW_MEMORY. Эта ошибка возникает при нехватке оперативной памяти. В случае возникновения такой ошибки выполнение обработки транзакции может быть не завершено. Файл ddemlsr.hpp (листинг 3.3) содержит определения констант для приложения DDEMLSR. Листинг 3.3. Файл ddeml/ddemlsr.hpp #define CM_HELPABOUT 301 #define CM_FILEINFO 302 #define CM_FILECLOSE 303 #define CM_FILEEXIT 304 #define CM_EDITCOPY 305 В файле определения ресурсов (листинг 3.4) описано главное меню приложения и пиктограмма. Листинг 3.4. Файл ddeml/ddemlsr.rc #include "ddemlsr.hpp" APP_MENU MENU BEGIN POPUP "&File" BEGIN MENUITEM "E&xit", CM_FILEEXIT END POPUP "&Help" BEGIN MENUITEM "&About...", CM_HELPABOUT END END APP_ICON ICON "ddemlsr.ico" Файл определения модуля приложения DDEMLSR приведен в листинге 3.5. Листинг 3.5. Файл ddeml/ddemlsr.def NAME DDEMLSR DESCRIPTION 'Приложение DDEMLSR, (C) 1995, Frolov A.V.' EXETYPE windows STUB 'winstub.exe' STACKSIZE 8120 HEAPSIZE 1024 CODE preload moveable discardable DATA preload moveable multiple 3.5. Приложение DDEMLCLПриложение DDEMLCL создано нами специально для работы с сервером DDEMLSR, описанном в предыдущем разделе. Вы можете запустить сервер перед запуском клиента DDEMLCL, либо не делать этого. В последнем случае на экране появится предупреждающее сообщение о том, что сервер не запущен (рис. 3.7).
Рис. 3.7. Запрос на запуск сервера DDEML Вам будет предложено запустить сервер, для чего следует нажать на клавишу "Yes". Приложение DDEMLCL предпримет попытку запустить приложение DDEMLSR из текущего каталога или из каталогов, указанных в переменной среды PATH операционной системы MS-DOS. Главное окно приложения представлено на рис. 3.8.
Рис. 3.8. Приложение DDEMLCL С помощью меню "Action" вы можете послать серверу текстовую строку "c:\\nicebmp\\sky.bmp" (строка "Send Filename") или запросить версию сервера (строка "Get Server Version"). В последнем случае принятая строка будет отображена на экране (рис. 3.9).
Рис. 3.9. Клиент отображает текстовую строку, полученную от сервера Функция WinMain и функция главного окна приложения DDEMLCL определены в файле ddemlcl.cpp (листинг 3.6). Листинг 3.6. Файл ddeml/ddemlcl.cpp // ---------------------------------------- // Приложение DDEMLCL // Клиент DDEML // ---------------------------------------- #define STRICT #include <windows.h> #include <windowsx.h> #include <ddeml.h> #include <dde.h> #include <mem.h> #pragma hdrstop #include "ddemlcl.hpp" // Прототипы функций HCONV DDEClientOpen(HINSTANCE hInst, LPSTR szService, LPSTR szTopic, LPSTR szItem); void DDEClientClose(HCONV); BOOL DDESend(HCONV, LPSTR); BOOL DDEReceive(HCONV, LPSTR szBuf, int nBufSize); BOOL InitApp(HINSTANCE); LRESULT CALLBACK _export WndProc(HWND, UINT, WPARAM, LPARAM); // Имя класса окна char const szClassName[] = "DDEMLCLIENT"; // Заголовок окна char const szWindowTitle[] = "DDEML Client"; HWND hwnd; HINSTANCE hInst; HCONV hConv = NULL; // ===================================== // Функция WinMain // ===================================== #pragma argsused int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) { MSG msg; // Можно запускать только одну копию приложения if(hPrevInstance) return FALSE; // Инициализируем приложение if(!InitApp(hInstance)) return FALSE; // Сохраняем идентификатор приложения hInst = hInstance; hwnd = CreateWindow( szClassName, // имя класса окна szWindowTitle, // заголовок окна WS_OVERLAPPEDWINDOW, 20, 20, 250, 100, 0, 0, hInstance, NULL); if(!hwnd) return FALSE; ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); while(GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } // ===================================== // Функция InitApp // Выполняет регистрацию класса окна // ===================================== BOOL InitApp(HINSTANCE hInstance) { ATOM aWndClass; WNDCLASS wc; memset(&wc, 0, sizeof(wc)); wc.lpszMenuName = "APP_MENU"; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC) WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(hInstance, "APP_ICON"); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject(LTGRAY_BRUSH); wc.lpszClassName = (LPSTR)szClassName; aWndClass = RegisterClass(&wc); return (aWndClass != 0); } // ===================================== // Функция WndProc // ===================================== LRESULT CALLBACK _export WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { HDC hdc; PAINTSTRUCT ps; switch (msg) { case WM_CREATE: { // Инициализируем DDEML и создаем канал связи if(hConv == NULL) { hConv = DDEClientOpen(hInst, (LPSTR)"BMPServer", (LPSTR)"BMPFile", (LPSTR)"DDEData"); // Если сервер не запущен, предоставляем // пользователю возможность запустить его if(hConv == NULL) { if(IDYES == MessageBox(hwnd, "Сервер не запущен.\nЗапустить?", "DDEML Client", MB_YESNO | MB_ICONHAND)) { WORD rc; // Выполняем попытку запуска сервера rc = WinExec("DDEMLSR", SW_SHOW); if(rc < 32) { MessageBox(hwnd, "Невозможно запустить сервер", "DDEML Client", MB_ICONHAND); return -1; } else { // После удачного запуска повторяем // попытку инициализации DDEML // и создания канала связи hConv = DDEClientOpen(hInst, (LPSTR)"BMPServer", (LPSTR)"BMPFile", (LPSTR)"DDEData"); if(hConv == NULL) return -1; else return 0; } } return -1; } return 0; } return 0; } // Обработка сообщений от меню case WM_COMMAND: { switch (wParam) { case CM_HELPABOUT: { MessageBox(hwnd, "DDEML Client\nVersion 1.0\n" "(C) Frolov A.V., 1995", "About DDEML Client", MB_OK | MB_ICONINFORMATION); return 0; } // Завершаем работу приложения case CM_FILEEXIT: { DestroyWindow(hwnd); return 0; } // Посылаем текстовую строку серверу case CM_MSG_TO_SERVER: { if(!DDESend(hConv, (LPSTR)"c:\\nicebmp\\sky.bmp")) MessageBox(hwnd, "Сервер не отвечает", "DDEML Client", MB_OK); return 0; } // Принимаем текстовую строку от сервера case CM_MSG_FROM_SERVER: { BYTE szBuf[256]; if(DDEReceive(hConv, szBuf, 80L)) MessageBox(hwnd, szBuf, "DDEML Client", MB_OK); else MessageBox(hwnd, "Сервер не отвечает", "DDEML Client", MB_OK); return 0; } default: return 0; } } case WM_DESTROY: { PostQuitMessage(0); // Завершаем работу с DDEML DDEClientClose(hConv); return 0; } default: break; } return DefWindowProc(hwnd, msg, wParam, lParam); } Обработчик сообщения WM_CREATE при инициализации главного окна приложения вызывает функцию DDEClientOpen, определенную в файле ddemlcf.cpp (листинг 3.7). Эта функция регистрирует приложение в библиотеке DDEML и пытается создать канал связи с сервером. Если канал связи создать не удалось, приложение DDEMLCL делает вывод о том, что сервер не запущен, и предлагает пользователю запустить его. Запуск выполняется при помощи функции WinExec. После удачного запуска сервера делается еще одна попытка создать канал связи. Когда пользователь выбирает из меню "Action" строку "Send Filename", приложение вызывает функцию DDESend, определенную в файле ddemlcf.cpp. Этой функции передается идентификатор созданного канала связи и текстовая строка "c:\\nicebmp\\sky.bmp". Функция DDESend передаст строку серверу, который отобразит ее на экране. Если пользователь выбирает из меню "Action" строку "Get Server Version", вызывается функция DDEReceive, также определенная в файле ddemlcf.cpp. Этой функции помимо идентификатора канала связи передается адрес и размер буфера, в который нужно записать принятую информацию. После приема данные отображаются на экране в виде текстовой строки с помощью функции MessageBox. Когда приложение DDEMLCL завершает свою работу, вызывается функция DDEClientClose, закрывающая канал и освобождающая связанные с ним ресурсы. В файле ddemlcf.cpp (листинг 3.7) собраны все функции, имеющие отношение к DDEML. Листинг 3.7. Файл ddeml/ddemlcf.cpp // ----------------------------------------------------- // Функции для работы с библиотекой DDEML // Клиент DDEML // ----------------------------------------------------- #define STRICT #include <windows.h> #include <windowsx.h> #include <ddeml.h> #include <dde.h> #include <mem.h> #include <string.h> #pragma hdrstop #include "ddemlcl.hpp" HDDEDATA EXPENTRY _export DDEClientCallback(WORD wType, WORD wFmt, HCONV hConv, HSZ hsz1, HSZ hsz2, HDDEDATA hData, DWORD dwData1, DWORD dwData2); // Идентификатор приложения для DDEML DWORD idInst; // Функция обратного вызова для DDE FARPROC lpDdeProc; HSZ hszService; HSZ hszTopic; HSZ hszItem; HDDEDATA hData; DWORD dwResult; WORD wFmt = CF_TEXT; //----------------------------------------------------- // Функция DDEClientOpen // Инициализация DDEML, создание идентификаторов строк //----------------------------------------------------- HCONV DDEClientOpen(HINSTANCE hInst, LPSTR szService, LPSTR szTopic, LPSTR szItem) { HCONV hConv = NULL; // Создаем переходник для функции обратного вызова lpDdeProc = MakeProcInstance((FARPROC)DDEClientCallback, hInst); // Инициализируем DDEML if(DdeInitialize((LPDWORD)&idInst, (PFNCALLBACK)lpDdeProc, APPCMD_CLIENTONLY, 0L)) { return NULL; } else { // При успешной инициализации создаем // идентификаторы строк для сервиса, раздела // и элемента данных hszService = DdeCreateStringHandle(idInst, szService, CP_WINANSI); hszTopic = DdeCreateStringHandle(idInst, szTopic, CP_WINANSI); hszItem = DdeCreateStringHandle(idInst, szItem, CP_WINANSI); // Устанавливаем канал связи hConv = DdeConnect(idInst, hszService, hszTopic, (PCONVCONTEXT)NULL); // Возвращаем идентификатор созданного канала связи return hConv; } } //----------------------------------------------------- // Функция DDEClientClose // Завершение работы с библиотекой DDEML //----------------------------------------------------- void DDEClientClose(HCONV hConv) { // Закрываем канал связи if(hConv != NULL) { DdeDisconnect(hConv); hConv = NULL; } // Освобождаем идентификаторы строк DdeFreeStringHandle(idInst, hszService); DdeFreeStringHandle(idInst, hszTopic); DdeFreeStringHandle(idInst, hszItem ); // Удаляем переходник для функции обратного вызова FreeProcInstance(lpDdeProc); } //----------------------------------------------------- // Функция DDESend // Передача серверу текстовой строки //----------------------------------------------------- BOOL DDESend(HCONV hConv, LPSTR szString) { if(hConv != NULL) { // Создаем идентификатор данных hData = DdeCreateDataHandle (idInst, szString, lstrlen(szString) + 1, 0L, hszItem, CF_TEXT, 0); // Запускаем транзакцию записи данных if(hData != NULL) hData = DdeClientTransaction((LPBYTE)hData, -1, hConv, hszItem, CF_TEXT, XTYP_POKE, 1000, &dwResult); if(hData != NULL) return TRUE; else return FALSE; } else return FALSE; } //----------------------------------------------------- // Функция DDEReceive // Получение от сервера текстовой строки //----------------------------------------------------- BOOL DDEReceive(HCONV hConv, LPSTR szBuf, int nBufSize) { // Запускаем транзакцию чтения данных if(hConv != NULL) { hData = DdeClientTransaction(NULL, 0, hConv, hszItem, CF_TEXT, XTYP_REQUEST, 1000, &dwResult); // Получаем строку от сервера if(hData != NULL) { DdeGetData(hData, szBuf, nBufSize, 0L); return TRUE; } else return FALSE; } else return FALSE; } //----------------------------------------------------- // Функция DDEClientCallback // Функция обратного вызова для клиента DDEML //----------------------------------------------------- #pragma argsused HDDEDATA EXPENTRY DDEClientCallback(WORD wType, WORD wFmt, HCONV hConvX, HSZ hsz1, HSZ hsz2, HDDEDATA hData, DWORD dwData1, DWORD dwData2) { switch(wType) { case XTYP_DISCONNECT: return((HDDEDATA) NULL); case XTYP_ERROR: break; case XTYP_XACT_COMPLETE: break; } return((HDDEDATA)NULL); } Все символические константы определены в файле ddemlcf.hpp (листинг 3.8). Листинг 3.8. Файл ddeml/ddemlcf.hpp #define CM_HELPABOUT 301 #define CM_FILEEXIT 302 #define CM_EDITCOPY 303 #define CM_MSG_TO_SERVER 304 #define CM_MSG_FROM_SERVER 305 Файл описания ресурсов представлен в листинге 3.9. Листинг 3.9. Файл ddeml/ddemlcf.rc #include "ddemlcl.hpp" APP_MENU MENU BEGIN POPUP "&File" BEGIN MENUITEM "E&xit", CM_FILEEXIT END POPUP "&Action" BEGIN MENUITEM "&Send Filename", CM_MSG_TO_SERVER MENUITEM "&Get Server Version", CM_MSG_FROM_SERVER END POPUP "&Help" BEGIN MENUITEM "&About...", CM_HELPABOUT END END APP_ICON ICON "ddemlcl.ico" Файл определения модуля для приложения DDEMLCL вы найдете в листинге 3.10. Листинг 3.10. Файл ddeml/ddemlcf.def NAME DDEMLCL DESCRIPTION 'Приложение DDEMLCL, (C) 1995, Frolov A.V.' EXETYPE windows STUB 'winstub.exe' STACKSIZE 8120 HEAPSIZE 1024 CODE preload moveable discardable DATA preload moveable multiple 3.6. Отладка DDEML-приложенийБиблиотека DDEML позволяет создавать приложения, предназначенные для отладки DDEML-приложений. В частности, такие приложения могут перехватывать вызовы функций обратного вызова DDEML (как для сервера, так и для клиента), следить за использованием идентификаторов строк и данных, за регистрацией сервиса и так далее. Из-за ограниченного объема книги мы не сможем рассказать вам о том, как создавать такие приложения, однако вся необходимая информация есть в документации, которая входит в состав Microsoft SDK for Windows 3.1. Тем не менее, мы научим вас пользоваться готовым приложением DDESpy, которое поставляется в составе SDK и специально предназначено для отладки DDEML-приложений. Запустите приложение DDESpy и раскройте меню "Output" (рис. 3.10).
Рис. 3.10. Меню "Output" приложения DDESpy С помощью этого меню вы можете направить поток отладочной информации в файл (строка "File..."), на отладочный терминал (строка "Debug Terminal" или в окно приложения DDESpy (строка "Screen"). Кроме того, с помощью строки "Clear Screen" вы можете очистить содержимое окна приложения DDESpy от отладочной информации (если она там есть). В меню "Output" вам надо выбрать строку "File..." и с помощью появившейся на экране диалоговой панели задать путь к файлу, в который будет записана отладочная информация. Затем раскройте меню "Monitor" (рис. 3.11).
Рис. 3.11. Меню "Monitor" приложения DDESpy С помощью этого меню вы можете определить состав отладочной информации, отображаемой в окне приложения и сохраняемой в только что указанном вами файле. Отметьте все строки, как на рис. 3.11. Теперь вы сможете получить полную отладочную информацию. Затем раскройте меню "Track" (рис. 3.12).
Рис. 3.12. Меню "Track" приложения DDESpy С помощью этого меню вы сможете задать информацию о системе DDEML, отображаемую в отдельных окнах. Отметьте все строки в меню "Track". При этом в нижней части экрана монитора появится несколько новых пиктограмм, соответствующих отдельным окнам. Теперь все готово к отладке. Запустите приложение DDEMLSR, описанное нами раньше. В главном окне приложения DDESpy появятся текстовые строки описания происходящих событий. Эти строки одновременно записываются в файл, указанный нами ранее. Мы к ним еще вернемся, а пока давайте раскроем пиктограмму "Registered Service". Появится окно, в котором вы сможете увидеть имя сервиса "BMPServer", а также название и идентификатор копии приложения (рис. 3.13).
Рис. 3.13. Список серверов DDEML Раскройте окно "Active Conversation", в котором отображается информация об активных каналах связи. В этот момент времени ни один канал связи еще не создан, поэтому окно пустое. Запустите приложение DDEMLCL, предназначенное для совместной работы с приложением DDEMLSR. Оно создаст канал связи, используя сервис "BMPServer" и раздел "BMPFile". Теперь в окне "Active Conversation" есть информация о сервисе, разделе и идентификаторах копий приложений клиента и сервера, создавшего канал связи (рис. 3.14).
Рис. 3.14. Список активных каналов Итак, канал связи установлен. Раскройте окно "String Handles", отображающее список созданных идентификаторов строк (рис. 3.15).
Рис. 3.15. Список идентификаторов строк И сервер, и клиент создали каждый по три идентификатора, соответствующих строкам "BMPService, "BMPFile" и "DDEData". В столбце "Count" отображается счетчик использования идентификаторов. В нашем случае он равен 2, так как каждая строка была использована по 2 раза - сервером и клиентом. Для одинаковых строк не создается отдельных идентификаторов, а просто увеличивается счетчик использования. Сделайте активным окно клиента DDEMLCL и выполните пересылку данных, выбрав из меню "Action" строки "Send Filename" и затем "Get Server Version". Будет выполнена передача данных по каналу связи, причем информация о результате будет записана в отладочный файл. Закройте приложение DDEMLCL и сделайте активным окно "String Handles". Теперь счетчик использования всех трех идентификаторов строк будет равен 1, так как клиент освободил эти идентификаторы. Однако сервер продолжает их использовать, поэтому идентификаторы не уничтожаются. Окно "Active Conversation" очистится, так как теперь в системе нет активных каналов связи (если только их не создали какие-либо другие приложения). Теперь завершите приложение DDEMLSR. Окна "String Handles" и "Registered Service" теперь тоже очистятся, так как все строки уничтожены, а сервис больше не доступен. Завершите приложение DDESpy и загрузите в любой текстовый редактор файл, содержащий протокол отладки. Мы приведем фрагменты такого файла, полученного в результате выполнения описанных выше манипуляций с приложениями DDEMLSR и DDEMLCL. Первые три строки появились в результате запуска сервера DDEMLSR. Они содержат сведения о том, что были созданы три идентификатора строк для копии приложения с идентификатором 0x458f. Указано время события (от момента запуска Windows), идентификаторы созданных строк (c0d0, c0d5, c2d8) и содержимое строк. Task:0x458f,Time:7766779,String Handle Created:c0d0(BMPServer) Task:0x458f,Time:7766779,String Handle Created:c0d5(BMPFile) Task:0x458f,Time:7766834,String Handle Created:c2d8(DDEData) Далее идут строки, описывающие вызов функции обратного вызова сервера: Task:0x458f Time:7766834 Callback: Type=Register, fmt=0x0("?"), hConv=0x0, hsz1=0xc0d0("BMPServer") hsz2=0x5c40c0d0("BMPServer:(5c40)"), hData=0x0, dwData1=0x0, dwData2=0x0 return=0x0 Здесь функция обратного вызова была вызвана для выполнения регистрации сервиса "BMPServer". Вы можете определить содержимое всех параметров функции на момент вызова. В следующем фрагмента отражен факт посылки данных серверу: Task:0x458f Time:7769470 hwndTo=0x5f24 Message(Posted)=Poke: hwndFrom=0x5e44, lParam=0xc2d82aaf status=2000(fRelease ) fmt=0x1("CF_TEXT") Data= "c:\nicebmp\sky.bmp" Item=0xc2d8("DDEData") Следом идет обращение к функции обратного вызова: Task:0x458f Time:7770624 Callback: Type=Poke, fmt=0x1("CF_TEXT"), hConv=0x75f24, hsz1=0xc0d5("BMPFile") hsz2=0xc2d8("DDEData"), hData=0x4537232c, dwData1=0x0, dwData2=0x0 return=0x8000 Input data= "c:\nicebmp\sky.bmp" А вот запрос данных от сервера: Task:0x458f Time:7772052 hwndTo=0x5f24 Message(Posted)=Request: hwndFrom=0x5e44, lParam=0xc2d80001 fmt=0x1("CF_TEXT") Item=0xc2d8("DDEData") Task:0x458f Time:7772052 Callback: Type=Request, fmt=0x1("CF_TEXT"), hConv=0x75f24, hsz1=0xc0d5("BMPFile") hsz2=0xc2d8("DDEData"), hData=0x0, dwData1=0x0, dwData2=0x0 return=0x2daf02fc Output data= "DDEML Server v.1.0, (C) Frolov A.V." В следующем фрагменте листинга выполняется запрос на уничтожение канала связи, после чего управление получает функция обратного вызова: Task:0x458f Time:7774578 hwndTo=0x5f24 Message(Posted)=Terminate: hwndFrom=0x5e44, lParam=0x0 dFrom=0x5e44, lParam=0x0 Task:0x458f Time:7774688 Callback: Type=Disconnect, fmt=0x0("?"), hConv=0x75f24, hsz1=0x0("") hsz2=0x0(""), hData=0x0, dwData1=0x0, dwData2=0x0 return=0x0 Перед завершением работы сервер должен сообщить системе DDEML, что обеспечиваемый им сервис больше не доступен. Следующий фрагмент листинга отражает процедуру "изъятия" сервиса : Task:0x458f Time:7776281 Callback: Type=Unregister, fmt=0x0("?"), hConv=0x0, hsz1=0xc0d0("BMPServer") hsz2=0x5c40c0d0("BMPServer:(5c40)"), hData=0x0, dwData1=0x0, dwData2=0x0 return=0x0 Анализируя содержимое протокола отладки, вы
можете проследить за тем, чтобы при завершении
сервера освобождались все занимаемые им ресурсы,
такие как идентификаторы строк и сервис. Вы
можете проверить значения параметров функции
обратного вызова для всех или отдельных
транзакций, проследить формат передаваемых
данных и многое другое. |