Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методическое пособие по СПр.doc
Скачиваний:
7
Добавлен:
16.12.2018
Размер:
690.69 Кб
Скачать

1 Разработка приложений с элементами системного программирования на основе использования технологий межпрограммного обмена данными

1.1 Лабораторная работа №1. Обмен данными между потоками с использованием сообщения wm_copydata

Цель работы: приобрести навыки создания клиентских и пользовательских приложений, осуществляющих обмен большими объемами данных с помощью посылки и обработки сообщения WM_COPYDATA; регистрации и обработка пользовательских сообщений.

      1. Краткие теоретические сведения

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

Сообщения и асинхронный ввод

Время жизни процесса напрямую связано с его потоками. Одни потоки появляются, другие приостанавливаются и возобновляются, третьи завершаются. Когда все потоки в процессе завершены, завершается и сам процесс; освобождаются принадлежавшие ему ресурсы и система удаляет его из памяти. Большинство созданных потоком объектов считаются собственностью процесса, которому принадлежит этот поток: блоки памяти, глобальные и статические переменные, объекты GDI - перья, кисти и т.д. Однако большая часть объектов модуля User - окна, меню, таблицы акселераторов и т.д. (кроме пиктограмм, курсоров и оконных классов) - принадлежит создавшему их потоку.

Очереди потока и обработка сообщений

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

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

При создании потока система создает структуру THREADINFO и связывает ее с этим потоком. Эта структура идентифицирует очередь сообщений потока (thread's message queue), виртуальную очередь ввода (virtualized input queue) и флаги пробуждения (wake flags), а также ряд других переменных, используемых для хранения информации о локальном состоянии ввода данного потока.

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

Посылка сообщения окну

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

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

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

Приемы работы с сообщениями

Посылка сообщений может быть способом работы с элементами управления из состава библиотеки COMCTL32.DLL. Все они — оконные (потомки TWinControl), что является необходимым условием такой работы. Если элемент управления создает окно, и нам известен его дескриптор, можно ознакомиться с документацией на обрабатываемые им сообщения и управлять им. Именно так и сделано в Delphi — желающие могут ознакомиться с исходным текстом модуля COMCTRLS.PAS.

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

Приемником сообщений может быть любое из созданных окон — форма или оконный элемент управления. Кто же из них "главный"? Минимальный анализ свойств объекта Application показывает, что главная форма приложения — это не то же самое, что его окно. Самому приложению (объекту Application) соответствует специально создаваемое невидимое окно с именем, совпадающим с именем текущего проекта (впредь будем называть его главным окном приложения). Именно его дескриптор содержится в свойстве Application.Handle. Чтобы найти главное окно приложения, нужно вызвать функцию API FindWindow:

srvhandle:= FindWindow(nil,'server');

if srvhandle=0 then ShowMessage('Сервер не запущен');

Подразумевается, что 'server' — это имя серверного приложения.

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

Но как обработать события, поступающие главному окну приложения? Порождать потомка от TApplication? Можно поступить проще. У класса TApplication есть полезное событие — OnMessage. Оно возникает тогда, когда из очереди главного окна приложения извлекается очередное сообщение:

type TMessageEvent = procedure (var Msg: TMsg;

var Handled: Boolean) of object;

property OnMessage: TMessageEvent;

Само сообщение передается в параметре Msg, а в параметре Handled программист должен вернуть признак того, обработано ли им самим данное сообщение. Если нет (Handled=False), то оно обрабатывается дальше обычным путем. Обрабатывая событие OnMessage, мы попадаем в "святая святых" механизмов функционирования оконных приложений. Здесь нельзя употреблять многие из процедур и функций. Попытавшись внутри обработчика OnMessage вывести сообщение на экран, вы рискуете зациклить программу.

Пусть сервер, как мы и договаривались, рассылает клиентам два вида информации (о времени и доступной памяти). Клиенты должны подписаться на ту или иную информацию и отказаться от нее. Для этого введем четыре новых сообщения: WM_SUBSCRIBETIME, WM_UNSUBSCRIBETIME, WM_SUBSCRIBEMEM и WM_UNSUBSCRIBEMEM.

Примечание. Номера сообщений, определяемых разработчиком, должны лежать в диапазоне WM_USER...$7FFF. Многие элементы управления сами резервируют номера сообщений в этом диапазоне, и совпадение номеров может привести к непредсказуемым последствиям. Чтобы избежать этого, надо воспользоваться функцией:

Function RegisterWindowMessage(lpString: PChar):UINT;

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

Зарегистрировав номера сообщений, можно отреагировать на событие OnMessage в сервере так:

type

TDummyObject = class

class procedure AppMsgHandler(var Msg:TMsg;varHandled:Boolean);

end;

class procedure TDummyObject.AppMsgHandler(var Msg: TMsg;

var Handled: Boolean);

var i: Integer; ClientWnd: THandle;

begin

ClientWnd := THandle(msg.WParam);

if Msg.message=WM_SUBSCRIBETIME then

for i:=0 to MaxClients-1 do begin

if TimeClientList[i]=0 then begin

TimeClientList[i] := ClientWnd; Handled := True;

Exit;

end

end

else if Msg.message=WM_UNSUBSCRIBETIME then

for i:=0 to MaxClients-1 do begin

if TimeClientList[i]=ClientWnd then

begin

TimeClientList[i] := 0; Handled := True;

Exit;

end

end

else if Msg.message=WM_SUBSCRIBEMEM then

for i:=0 to MaxClients-1 do begin

if MemClientList[i]=0 then

begin

MemClientList[i] := ClientWnd; Handled := True;

Exit;

end

end

else if Msg.message=WM_UNSUBSCRIBEMEM then

for i:=0 to MaxClients-1 do begin

if MemClientList[i]=ClientWnd then

begin

MemClientList[i] := 0; Handled := True;

Exit;

end

end;

end;

begin

WM_SUBSCRIBETIME:= RegisterWindowMessage('WM_SUBSCRIBETIME') ; WM_UNSUBSCRIBETIME:= RegisterWindowMessage('WM_UNSUBSCRIBETIME'); WM_SUBSCRIBEMEM:= RegisterWindowMessage('WM_SUBSCRIBEMEM'); WM_UNSUBSCRIBEMEM:= RegisterWindowMessage('WM_UNSUBSCRIBEMEM');

Application.Initialize;

Application.OnMessage:= TDummyObject.AppMsgHandler;

Application.CreateForm(TForm1, Form1);

Application.Run;

end.

Для хранения дескрипторов окон клиентских приложений в нашем примере предусмотрены два массива: MemClientList и TimeClientList. Обратите внимание, что обработчик OnMessage приписан к некоторому пустому классу TDummyObject. Поскольку обработчик может понадобиться еще до создания первой из форм, значение свойства Application.OnMessage устанавливается еще до вызова метода Application.Run. Клиент устроен таким образом, что по нажатии кнопки серверу посылается сообщение о подписке на информацию, а по отжатию — об отказе от нее. В качестве параметра он посылает дескриптор окна главной формы TClientForm.Handle. Этому окну и будут посылаться необходимые сообщения.

procedure TClientForm.MemSpeedButtonClick(Sender:TObject);

var srvhandle: THandle;.

begin

srvhandle:= FindWindow(nil,'Srvproj');

if srvhandle<>0 then if MemSpeedButton.Down then

PostMessage(srvhandle,WM_SUBSCRIBEMEM,

integer(Self.Handle),0)

else

begin PostMessage(srvhandle,WM_UNSUBSCRIBEMEM,

integer(Self.Handle),0);

MemSpeedButton.Caption:= 'Memory';

end;

end;

Предыдущие четыре сообщения — от клиента к серверу — несли информационный характер и представляли важность только самим фактом своего появления. В обратную сторону должен идти больший поток данных. В само сообщение может поместиться только 8 байт информации (в структуре TMessage имеются поля wParam и lParam длиной по 4 байта). А если нужно передать больше? Именно для этого в Windows предусмотрено специальное сообщение wm_copydata. Посылаемые данные нужно упаковать в следующую структуру (описанную в WINDOWS.PAS):

PCopyDataStruct = ^TCopyDataStruct;

TCopyDataStruct = packed record

dwData: DWORD;

cbData: DWORD;

lpData: Pointer;

end;

А затем передать ее адрес в качестве параметра lParam сообщения wm_copydata:

procedure TServerForm.Timer1Timer(Sender: TObject);

var dt: TDateTime; i: Integer; ms: TMemoryStatus;

CDS: TCopyDataStruct;

begin

dt:= Now;

GlobalMemoryStatus(ms);

CDS.dwData:= ms.dwAvailPageFile + ms.dwAvailPhys;

CDS.cbData:= SizeOf(dt);

CDS.lpData:= @dt;

for i:=0 to MaxClients-1 do

begin if TimeClientList[i]<>0 then

SendMessage(TimeClientList[i],WM_COPYDATA,

Self.Handle,longint(@CDS));

if (MemClientList[i]<>0) and

(TimeClientList[i]<>MemClientList[i] ) then

SendMessage(MemClientList[i],WM_COPYDATA,

Self.Handle, longint(@CDS));

end;

end;

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

Способ расшифровки wm_copydata может выглядеть так:

procedure TClientForm.WMCopyData(var msg: TMessage);

var m: cardinal;dt: TDateTime;

begin

m:= PCopyDataStruct(msg.lParam)^.dwData;

if PCopyDataStruct(msg.lParam)^.lpData<>nil then

dt:=TDateTime(PCopyDataStruct(msg.lParam)^.lpData^);

if MemSpeedButton.Down then

MemSpeedButton.Caption:= Format('%8dK',[m div 1024]);

if TimeSpeedButton.Down then

TimeSpeedButton.Caption:=TimeToStr(dt);

end;

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