Статьи Королевства Дельфи

Два простых способа уведомления.


Раздел Подземелье Магов Алексей Еремеев ,
дата публикации 14 декабря 2000

В своей работе мне частенько приходиться делать разного рода клиент-серверные системы.
И совсем не обязательно на уровне глобальных сетей. Речь пойдет о внутренних подсистемах.
Например, имеем компонент, который эмулирует секундомер. Запустили его с параметром типа "а напомни мне, что будет полночь" и забыли. Ну и конечно событие есть типа OnAlert. И обработчик его честно будет вызван по достижении нужной нам полуночи. Но обработчик один, а захотели узнать об этом событии сразу десять разных объектов. Не вешать же десять будильников?
Конечно, проще в одном обработчике перебрать методы уведомления этих десяти объектов да и дело с концом. Но можно поступить хитрее - заставить объект-будильник самому напоминать всем кто попросит его об этом. Вот о способах такого уведомления и пойдет речь.

Как условие - объект "сервер" ничего не знает об объекте "клиенте". После некоторого размышления и перебрав несколько вариантов я пришел к выводу, что наиболее приемлимые для практики есть два способа. Первый подсмотрен в WinAPI а второй - чисто Дельфи. Оба способа основаны на простой идее регистрации клиента на сервере и оповещении сервером клиентов по внутреннему списку зарегистрированных клиентов.

Способ 1. Оповещение через механизм сообщений Windows.

в модуле объекта-сервера в интерфейсной части определяется пользовательский номер события: const WM_NOTIFY_MSG = WM_USER + 123; в объекте-сервере реализуются две интерфейсные процедуры (вкупе с объявленным в приватной секции и созданным в конструкторе TList, в деструкторе не забудем его разрушить, естественно) procedure RegisterHandle(HW: THandle); var i: integer; begin i := FWindList.IndexOf(pointer(HW)); if i < 0 then FWinList.Add(pointer(HW)); end; procedure UnregisterHandle(HW: THandle) var i: integer; begin i := FWindList.IndexOf(pointer(HW)); if i >= 0 then FWinList.Delete(i); end; и создается функция оповещения в приватной секции: procedure SendNotify(wParam, lParam: integer); var i: integer; begin i := 0; while i < FWinList.Count do begin SendMessage(integer(FWinList.Items[i]), WM_NOTIFY_MSG, wParam, lParam); Inc(i); end; end; можно вместо SendMessage использовать PostMessage, будет асинхронное сообщение, иногда это выгодней, например для исключения возможности бесконечной рекурсии.


Объект- клиент должен иметь хэндл окна, который регистрируется на объекте-сервере и обработчик событий этого окна, который будет вызыватся при оповещении сервером списка клиентов (окон).
У объекта-клиента можно поступить двояко. Если объект-клиент уже имеет хэндл окна (например, форма) то пишется обработчик фиксированного номера события: procedure ServMsg(var Msg: TMessage); message WM_NOTIFY_MSG; или если окна нет, то создается универсальный метод-обработчик и невидимое окно при помощи функции AllocateHWND() (пример смотрите в исходниках VCL - объект TTimer)

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

Способ 2. Оповещение через объект-посредник.

В отдельном модуле создаем объект-посредник, который имеет один метод типа SendEvent и одну ссылку на обработчик события OnEvent. Я назвал такой объект TSynaps (да простят меня нейрохирурги) unit Synaps; interface uses Windows, Messages, SysUtils, Classes; type TSynaps = class(TObject) private FOnEvent : TNotifyEvent; public procedure SendEvent; property OnEvent : TNotifyEvent read FOnEvent write FOnEvent; end; implementation procedure SendEvent; begin if Assigned(FOnEvent) then try FOnEvent(Self); except end; end; end; Причем методов и событий может быть много разных на любой вкус. С очередями, асинхронными "прослойками", задержками и другими наворотами. Тут уж кто на что горазд. Я лишь демонстрирую идею. Модуль с объектом-сервером и модуль с объектом-клиентом имеют право знать о модуле Synaps. В объекте-сервере реализуются уже знакомые нам три функции (чуть иначе):
в интерфейсе объекта: procedure RegisterSynaps(Syn: TSynaps); var i: integer; begin i := FSynapsList.IndexOf(pointer(Syn)); if i < 0 then FSynapsList.Add(pointer(Syn)); end; procedure UnregisterSynaps(Syn: TSynaps); var i: integer; begin i := FSynapsList.IndexOf(pointer(Syn)); if i >= 0 then FSynapsList.Delete(i); end; и приватная функция: procedure NotifySynapses; var i: integer; begin i := 0; while i < FSynapsList.Count do begin TSynaps(FSynapsList.Items[i]).SendEvent; Inc(i); end; end; Объект-клиент создает в себе объект-синапс, назначает его событию OnEvent свой внутренний обработчик и регистрирует этот синапс на объекте-сервере. Вуаля! И получает оттуда уведомления. Кстати, в деструктор синапса можно встроить вызов события OnDestroy, и тогда объект-сервер, при регистрации клиента, может назначить ему обработчик и автоматически разрегистрировать его при уничтожении. Но это уже навороты.



Такой подход позволяет строить обратные вызовы любой сложности. К тому-же это чистый паскаль-код без привязки к операционке. (а вдруг Kylix :о)

Итог.

Как вы могли заметить, оба способа базируются на двух базовых идеях. Первое - это регистрация клиента на сервере, и второе - вызов сервером некой функции внутри клиента. Разница только в механизмах. И выбирать тут можно исходя из вкусов, предпочтений и неких требований, связанных с ресурсоемкостью, переносимостью и т. п.
На самом деле есть очень широко распространенный и давно известный метод под названием CallBack-функция.
Мы вызываем кого-то и передаем как один из параметров адрес другой функции. И этот метод частенько используется в WinAPI (смотрите, к примеру, справку по функции EnumFonts). Но! Механизм прямого CallBack-а довольно некрасиво ложится на объектную модель Дельфи, так что я не стал описывать его здесь. Тем более, что оба способа - то-же самое, но красивше. И самое последнее - не забывайте разрегистрировать клиента в конце работы и освобождать ресурсы в деструкторе. И да известят вас ваши сервера только о хорошем!

Алексей Еремеев


Содержание раздела