Обратные вызовы в MIDAS через TSocketConnection (45806)

Посмотреть архив целиком

Обратные вызовы в MIDAS через TSocketConnection

Передача сообщений между клиентскими приложениями

Роман Игнатьев (Romkin)

Введение

Обратные вызовы в технологии СОМ – достаточно обычное дело. Клиент подключается к серверу, и сервер в некоторых случаях извещает клиента о событиях, происходящих в системе, просто вызывая методы интерфейса обратного вызова. Однако реализация механизма для TRemoteDataModule, который обычно применяется на сервере приложений, довольно загадочна. В этой статье как раз и описывается способ реализации вызовов клиентской части со стороны сервера приложений.

Все началось с того, что я обновил Delphi с 4 на 5 версию, и при этом обнаружил, что у TSocketConnection появилось свойство SupportCallbacks. В справочной системе написано, что при установке этого свойства в True сервер приложений может делать обратные вызовы методов клиента, и больше практически никаких подробностей. При этом возможность добавить поддержку обратных вызовов при создании Remote data module отсутствует, и не совсем ясно, как же реализовывать обратные вызовы клиента в этом случае. С одной стороны, способность сервера приложений извещать своих клиентов о каких-либо событиях очень привлекательна, с другой стороны – без этого как-то до сих пор обходились.

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

Итак, что же мне хотелось сделать. Мне хотелось сделать механизм, позволяющий серверу приложений посылать сообщения всем подключенным к нему клиентам, а заодно дать возможность одной клиентской части вызывать методы других клиентских частей, например, для организации простого обмена сообщениями. Как видно, вторая задача включает в себя первую, ведь если сервер приложений знает, как посылать сообщения всем клиентам, достаточно просто выделить эту процедуру в отдельный метод интерфейса, и любое клиентское приложение сможет делать то же самое. Поскольку обычно я работаю с серверами приложений, удаленные модули данных в которых работают по модели Apartment (в фабрике класса стоит параметр tmApartment), мне хотелось сделать метод, работающий именно в этой модели. Как будет видно ниже, это связано с некоторыми сложностями.

После нескольких попыток реализовать обратные вызовы, написав при этом как можно меньше кода, и при этом еще понять, что же именно делается, выяснилось следующее:

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

К сожалению, при модели Apartment каждый удаленный модуль данных работает в своем потоке, а просто так вызвать интерфейс из другого потока невозможно, и необходимо производить ручной маршалинг или пользоваться GIT. Такой механизм в COM есть, со способом вызова можно ознакомиться, например, на http://www.techvanguards.com/com/tutorials/tips.asp#Marshal%20interface%20pointers%20across%20apartments (на нашем сайте вы можете найти разбор тех же вопросов на русском языке). Мне так делать не захотелось, во-первых, это достаточно сложно и я оставил это "на сладкое", во-вторых, я попробовал маршалинг через механизм сообщений, что позволяет реализовать как синхронные вызовы, так и асинхронные. Вызывающий модуль в этом случае не ожидает обработки вызовов клиентами, что, как мне кажется, является дополнительным преимуществом. Впрочем, при стандартном маршалинге реализуется практически такой же механизм.

Вот что у меня получилось в итоге.

Сервер приложений

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

В библиотеке типов надо объявить собственно интерфейс обратного вызова, который станет известен клиентской части при импорте библиотеки типов сервера.

В результате библиотека типов приняла вид, приведенный на рисунке 1.



Рисунок 1.

Проект называется BkServer. Модуль данных называется rdmMain, и в его интерфейсе объявлены методы, описание которых приведено ниже.

procedure RegisterCallBack(const BackCallIntf: IDispatch); safecall;

В данный метод должен передаваться интерфейс обратного вызова IBackCall, метод OnCall которого и служит для обеспечения обратного вызова. Однако параметр объявлен как IDispatch, с другими типами соединение по сокетам просто не работает.

procedure Broadcast(const MsgStr: WideString); safecall;

Этот метод служит для широковещательной рассылки сообщений.

В интерфейсе обратного вызова (IBackCall) есть только один метод:

procedure OnCall(const MsgStr: WideString); safecall;

Этот метод получает сообщение.

Полученные клиентские интерфейсы надо где-то хранить, причем желательно обеспечить к ним доступ из глобального списка, тогда сообщение можно передать всем клиентским частям, просто пройдя по этому списку. Мне показалось удобным сделать класс-оболочку, и вставлять в список ссылку на класс. В качестве списка используется простой TThreadList, описанный как глобальная переменная в секции implementation:

var CallbackList: TThreadList;

и, соответственно, экземпляр списка создается в секции initialization модуля и освобождается при завершении работы приложения в секции finalization. Выбран именно TThreadList (потокобезопасный список), поскольку, как уже упоминалось, используется модель apartment, и обращения к списку будут идти из разных потоков.

В секции initialization записано следующее объявление фабрики класса:

TComponentFactory.Create(ComServer, TrdmMain, Class_rdmMain, ciMultiInstance, tmApartment);

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

В CallbackList хранятся ссылки на класс TCallBackStub, в котором и хранится ссылка на интерфейс клиента:

TCallBackStub = class(TObject)

private

// Callback-интерфейсы должны быть disp-интерфейсами.

// Вызовы должны идти через Invoke

FClientIntf: IBackCallDisp;

FOwner: TrdmMain;

FCallBackWnd: HWND;

public

constructor Create(AOwner: TrdmMain);

destructor Destroy; override;

procedure CallOtherClients(const MsgStr: WideString);

function OnCall(const MsgStr: WideString): BOOL;

property ClientIntf: IBackCallDisp read FClientIntf write FClientIntf;

property Owner: TrdmMain read FOwner write FOwner;

end;

Экземпляр этого класса создается и уничтожается rdmMain (в обработчиках OnCreate и OnDestroy). Ссылка на него сохраняется в переменной TrdmMain.FCallBackStub, при этом класс сразу вставляется в список:

procedure TrdmMain.RemoteDataModuleCreate(Sender: TObject);

begin

//Сразу делаем оболочку для callback-интерфейса

FCallbackStub := TCallBackStub.Create(Self);

//И сразу регистрируем в общем списке

CallbackList.Add(FCallBackStub);

end;


procedure TrdmMain.UnregisterStub;

begin

if Assigned(FCallbackStub) then

begin

CallbackList.Remove(FCallbackStub);

FCallBackStub.ClientIntf := nil;

FCallBackStub.Free;

FCallBackStub := nil;

end;

end;


procedure TrdmMain.RemoteDataModuleDestroy(Sender: TObject);

begin

UnregisterStub;

end;

Назначение полей довольно понятно: в FClientIntf хранится собственно интерфейс обратного вызова, в FOwner - ссылка на TRdmMain... А вот третье поле (FCallBackWnd) служит для маршалинга вызовов между потоками, об этом будет сказано немного ниже. В вызове метода RegisterCallBack интерфейс просто передается этому классу, где и производится непосредственный вызов callback-интерфейса (через Invoke):

procedure TrdmMain.RegisterCallBack(const BackCallIntf: IDispatch);

begin

lock;

try

FCallBackStub.ClientIntf := IBackCallDisp(BackCallIntf);

finally

unlock;

end;

end;


Случайные файлы

Файл
183991.doc
11908.rtf
47627.rtf
185289.rtf
32760.rtf




Чтобы не видеть здесь видео-рекламу достаточно стать зарегистрированным пользователем.
Чтобы не видеть никакую рекламу на сайте, нужно стать VIP-пользователем.
Это можно сделать совершенно бесплатно. Читайте подробности тут.