Учебная работа. Реферат: Обратные вызовы в MIDAS через TSocketConnection

1 Звезда2 Звезды3 Звезды4 Звезды5 Звезд (5 оценок, среднее: 4,80 из 5)
Загрузка...
Контрольные рефераты

Учебная работа. Реферат: Обратные вызовы в MIDAS через TSocketConnection

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

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

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

Введение

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

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

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

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

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

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

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

Вот что у меня вышло в итоге.

приложений

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

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

В итоге библиотека типов приняла вид, приведенный на рисунке 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;




Всего этого полностью довольно для вызовов клиентской части из удаленного модуля данных, к которому она присоединена. Но задачка состоит конкретно в том, чтоб вызывать интерфейсы клиентских частей, работающих с иными модулями. Это обеспечивается 2-мя способами класса TCallBackStub: CallOtherClients и OnCall.

1-ый способ достаточно прост, и вызывается из процедуры Broadcast:

procedure TrdmMain.Broadcast(const MsgStr: WideString);

begin

lock;

try

if Assigned(FCallbackStub) then //переводимстрелки 🙂

FCallbackStub.CallOtherClients(MsgStr);

finally

unlock;

end;

end;

procedure TCallBackStub.CallOtherClients(const MsgStr: WideString);

var

i: Integer;

LastError: DWORD;

ErrList: string;

begin

ErrList := »;

with Callbacklist.LockList do

try

for i := 0 to Count — 1 do

if Items[i] <> Self then // длявсех, кромесебя

if not TCallbackStub(Items[i]).OnCall(MsgStr) then

begin

LastError := GetLastError;

if LastError <> ERROR_SUCCESS then

ErrList := ErrList + SysErrorMessage(LastError) + #13#10

else

ErrList := ErrList + ‘Что-тонепонятное’ + #13#10;

end;

if ErrList <> » then

raise Exception.Create(‘Возниклиошибки:’#13#10 + ErrList);

finally

Callbacklist.UnlockList;

end;

end;




Организуется проход по списку Callbacklist, и для всех TCallbackStub в перечне вызывается способ OnCall. Если вызов не вышел, собираем ошибки и выдаем сообщение. Ошибка быть может системной, как видно ниже. Я не стал создавать собственный класс исключительной ситуации, на клиенте она все равно будет смотреться как EOLEException.

Если б модель потоков была tmSingle, в способе OnCall довольно было бы просто вызвать соответственный способ интерфейса IBackCallDisp, но при разработке удаленного модуля данных была выбрана модель tmApartment, и прямой вызов IBackcallDisp.OnCall немедля приводит к ошибке, потоки-то различные. Потому приходится созодать вызовы интерфейса из его собственного потока. Для этого употребляется окно, создаваемое каждым экземпляром класса TCallBackStub, handle которого и хранится в переменной FCallBackWnd. Основная мысль таковая: заместо прямого вызова интерфейса отправить сообщение в окно, и вызвать способ интерфейса в процедуре обработки сообщений этого окна, которая обработает сообщение в контексте потока, создавшего окно:

function TCallBackStub.OnCall(const MsgStr: WideString): BOOL;

var

MsgClass: TMsgClass;

begin

Result := True;

if Assigned(FClientIntf) and (FCallbackWnd <> 0) then

begin

//MsgClass — это просто оболочка для сообщения, тут же можно передавать

//доп служебную информацию.

MsgClass := TMsgClass.Create;

//А вот освобожден объект будет в обработчике сообщения.

MsgClass.MsgStr := MsgStr;

//синхронизация — послал и запамятовал :-)) Выходим сходу.

//При SendMessage вызвавший клиент будет ожидать, пока все другие клиенты

//обработают сообщение, а это не нужно

Result := PostMessage(FCallBackWnd, CM_CallbackMessage,

Longint(MsgClass),Longint(Self));

if not Result then //нуиненадо 🙂

MsgClass.Free;

end;

end;




Что выходит: сообщение посылается в очередь всякого потока, и там сообщения скапливаются. Когда модуль данных освобождается от текущей обработки данных, а она быть может довольно долгой, все сообщения в очереди обрабатываются и передаются на клиентскую часть в порядке поступления. Побочным эффектом будет то, что клиент, вызвавший Broadcast, не ждет окончания обработки сообщений всеми иными клиентскими частями, потому что PostMessage возвращает управление немедля. В итоге выходит довольно привлекательная система, когда один клиент отправляет сообщение всем остальным и здесь же продолжает работу, не ждя окончания передачи. Другие же клиенты получают это сообщение в момент, когда никакой обработки данных не происходит, может быть – еще позднее. Класс TMsgClass объявлен в секции implementation последующим образом:

type

TMsgClass = class(TObject)

public

MsgStr: WideString;

end;




и служит просто конвертом для строчки сообщения, в принципе, в него можно добавить любые остальные данные. ссылка на экземпляр этого класса сохраняется лишь в параметре wParam сообщения, и на теоретическом уровне вероятна ситуация, когда сообщение будет послано модулю, который уже уничтожается (клиент отсоединился). И, естественно, сообщение обработано не будет, и не будет уничтожен экземпляр класса TMsgClass, что приведет к утечке памяти. Исходя из этого, при ликвидировании класс TCallBackStub выбирает при помощи PeekMessage все оставшиеся сообщения, и уничтожает MsgClass до поражения окна. FCallbackWnd создается в конструкторе TCallBackStub и уничтожается в деструкторе:

constructor TCallBackStub.Create(AOwner: TrdmMain);

var

WindowName: string;

begin

inherited Create;

Owner := AOwner;

//создаемокносинхронизации

WindowName := ‘CallbackWnd’ +

IntToStr(InterlockedExchangeAdd(@WindowCounter,1));

FCallbackWnd :=

CreateWindow(CallbackWindowClass.lpszClassName, PChar(WindowName), 0,

0, 0, 0, 0, 0, 0, HInstance, nil);

end;

destructor TCallBackStub.Destroy;

var

Msg: TMSG;

begin

//Могут остаться сообщения — удаляем

while PeekMessage(Msg, FCallbackWnd, CM_CallbackMessage,

CM_CallbackMessage, PM_REMOVE) do

if Msg.wParam <> 0 then

TMsgClass(Msg.wParam).Free;

DestroyWindow(FCallbackWnd);

inherited;

end;




Очевидно, перед созданием окна необходимо объявить и зарегистрировать его класс, что и изготовлено в секции implementation модуля. Процедура обработки сообщений окна вызывает способ OnCall интерфейса при получении сообщения CM_CallbackMessage:

var

CM_CallbackMessage: Cardinal;

function CallbackWndProc(Window: HWND; Message: Cardinal;

wParam, lParam: Longint): Longint; stdcall;

begin

if Message = CM_CallbackMessage then

with TCallbackStub(lParam) do

begin

Result := 0;

try

if wParam <> 0 then

with TMsgClass(wParam) do

begin

Owner.lock;

try

//Конкретный вызов интерфейса клиента

if Assigned(ClientIntf) then

ClientIntf.OnCall(MsgStr);

finally

Owner.unlock;

end;

end;

except

end;

if wParam <> 0 then // сообщениеотработано — уничтожаем

TMsgClass(wParam).Free;

end

else

Result := DefWindowProc(Window, Message, wParam, lParam);

end;




Номер сообщению CM_CallbackMessage присваивается вызовом


RegisterWindowMessage(‘bkServer Callback SyncMessage’);

также в секции инициализации.

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

Клиентская часть

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

Набросок 2

тут находится TSocketConnection (scMain), которая соединяется с сервером BkServer. Клавиша «Объединиться» (btnConnect) создана для установки соединения, клавиша «Отправить» (btnSend) – для отправки сообщения, записанного в окне редактирования (eMessage) остальным клиентским частям.

Код клиентской части достаточно короток:

procedure TfrmClient.btnConnectClick(Sender: TObject);

begin

with scMain do

Connected := not Connected;

end;

procedure TfrmClient.btnSendClick(Sender: TObject);

var

AServer: IrdmMainDisp;

begin

if not scMain.Connected then

raise Exception.Create(‘Нетсоединения’);

AServer := IrdmMainDisp(scMain.GetServer);

AServer.Broadcast(eMessage.Text);

end;

procedure TfrmClient.scMainAfterConnect(Sender: TObject);

var

AServer: IrdmMainDisp;

begin

FCallBack := TBackCall.Create;

AServer := IrdmMainDisp(scMain.GetServer);

AServer.RegisterCallBack(FCallBack);

lConnect.Caption := ‘Соединениеустановлено’;

btnConnect.Caption := ‘Отключиться’;

end;

procedure TfrmClient.scMainAfterDisconnect(Sender: TObject);

begin

FCallBack := nil;

lConnect.Caption := ‘Нетсоединения’;

btnConnect.Caption := ‘Объединиться’;

end;




Практически все управляется scMain, обработчиками OnAfterConnect (регистрирующим callback-интерфейс) и OnAfterDisconnect (производящим оборотное действие). Очевидно, библиотека типов сервера подключена к проекту, но не через Import Type Library. Дело в том, что в проекте находится activeX Object TBackCall, который реализует интерфейс IBackCall, описанный в библиотеке типов сервера. Создать таковой объект весьма просто: нужно просто избрать New -> Automation Object и в диалоге ввести имя BackCall (можно и другое, это не принципно), избрать ckSingle, и надавить ОК. В получившейся библиотеке типов сходу удалить интерфейс IBackCall, и на вкладке uses библиотеки типов подключить библиотеку типов сервера (есть локальное меню). Опосля этого на вкладке Implements кокласса избрать из перечня интерфейс IBackCall. Опосля обновления в модуле будет сотворен заглушка для способа OnCall, а в каталоге проекта клиента организуется файл импорта библиотеки типов сервера BkServer_TLB.pas, который остается лишь подключить к проекту и прописать в секциях uses модулей главной формы и СОМ-объекта. способ OnCall я воплотил простым образом:

procedure TBackCall.OnCall(const MsgStr: WideString);

begin

ShowMessage(MsgStr);

end;




Опосля компиляции приложение можно запустить в 2-3 экземплярах и проверить его работоспособность. нужно учесть, что сообщения получают все клиенты, не считая пославшего его.

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

Хотя мои друзья обругали этот метод маршалинга вызовов «хакерским», мне все равно хотелось бы выразить им глубокую признательность за советы и терпение, с каким они отвечали на мои вопросцы ;-)).

ПРИМЕЧАНИЕ

Исполняемые модули были сделаны в Delphi5 SP1. Для работы приложения, естественно, нужно запустить Borland Socket Server, который заходит в поставку Delphi.

]]>