Операционная система Microsoft Windows 3.1 для программиста. Дополнительные главы© Александр Фролов, Григорий ФроловТом 17, М.: Диалог-МИФИ, 1994, 287 стр. 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; } Все, что делает этот фрагмент кода - это запись нулевого значения в идентификатор не существующего больше канала связи. |