Учебная работа. Реферат: Обратные вызовы в 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.
]]>