Зворотні виклики в MIDAS через TSocketConnection h2>
Передача повідомлень між клієнтськими додатками h2>
Роман Ігнатьєв (Romkin) p>
Введення
h2>
Зворотні виклики в технології СОМ - досить звичайне
справа. Клієнт підключається до сервера, і сервер в деяких випадках сповіщає
клієнта про події, що відбуваються в системі, просто викликаючи методи інтерфейсу
зворотного дзвінка. Однак реалізація механізму для TRemoteDataModule, який
звичайно застосовується на сервері додатків, досить загадкова. У цій статті як
раз і описується спосіб реалізації викликів клієнтської частини з боку сервера
додатків. p>
Все почалося з того, що я поновив Delphi з 4 на 5
версію, і при цьому виявив, що у TSocketConnection з'явилося властивість
SupportCallbacks. У довідковій системі написано, що при встановленні цього
властивості в True сервер додатків може робити зворотні виклики методів клієнта,
і більше практично ніяких подробиць. При цьому можливість додати підтримку
зворотних викликів при створенні Remote data module відсутній, і не зовсім ясно,
як же реалізовувати зворотні виклики клієнта в цьому випадку. З одного боку,
здатність сервера додатків повідомляти своїх клієнтів про які-небудь події
дуже приваблива, з іншого боку - без цього як-то до сих пір
обходилися. p>
Нарешті, дивлячись в черговий раз на цю властивість, я
вирішив провести деякі дослідження, результат яких викладено нижче. Хочу відразу
сказати, що всі нижчевикладене носить характер простого дослідження
можливостей, і практично поки не застосовується, так що рекомендую застосовувати
цей спосіб з обережністю. Справа в тому, що мені хотілося реалізувати всі як
можна більш простим і зрозумілим способом, не відволікаючись на тонкощі реалізації
викликів. Загалом, здається, все працює як треба, але поки цей механізм не
випробуваний на деле, я не можу поручитися за правильність цього підходу. p>
Отже, що ж мені хотілося зробити. Мені хотілося
зробити механізм, що дозволяє серверу додатків надсилати повідомлення всім
підключеним до нього клієнтам, а заразом дати можливість однієї клієнтської частини
викликати методи інших клієнтських частин, наприклад, для організації простого
обміну повідомленнями. Як видно, друге завдання включає в себе перший, адже якщо
сервер додатків знає, як надсилати повідомлення всім клієнтам, достатньо
просто виділити цю процедуру в окремий метод інтерфейсу, і будь-яке клієнтське
додаток зможе робити те ж саме. Оскільки зазвичай я працюю з серверами
додатків, віддалені модулі даних у яких працюють за моделлю Apartment (в
фабриці класу коштує параметр tmApartment), мені хотілося зробити метод,
що працює саме в цій моделі. Як буде видно нижче, це пов'язано з деякими
складнощами. p>
Після декількох спроб реалізувати зворотні виклики,
написавши при цьому якомога менше коду, і при цьому ще зрозуміти, що ж саме
робиться, з'ясувалося наступне: p>
Писати все довелося вручну, стандартні механізми
зворотних викликів змусити працювати мені не вдалося. Як відомо, при
реалізації зворотного виклику клієнтська частина просто неявно створює кокласс для
реалізації інтерфейсу зворотного виклику, і передає посилання на його інтерфейс
COM-сервера, який у міру потреби викликає його методи. Цього ж
результату можна домогтися, написавши об'єкт автоматизації на клієнті і передавши
його інтерфейс сервера. Нижче так і зроблено. P>
На жаль, при моделі Apartment кожен віддалений
модуль даних працює у своєму потоці, а просто так викликати інтерфейс з
іншого потоку неможливо, і необхідно виробляти ручної маршалинга або
користуватися GIT. Такий механізм в COM є, зі способом виклику можна
ознайомитися, наприклад, на http://www. techvanguards.com/com/tutorials/tips.asp # Marshal% 20interface% 20pointers% 20across% 20apartments
(на нашому сайті ви можете знайти розбір тих самих питань українською мовою). Мені
так робити не захотілося, по-перше, це досить складно і я залишив це
"на солодке", по-друге, я спробував маршалинга через механізм
повідомлень, що дозволяє реалізувати як синхронні виклики, так і асинхронні.
Зухвалий модуль в цьому випадку не очікує обробки викликів клієнтами, що,
як мені здається, є додатковою перевагою. Втім, при
стандартному маршалинга реалізується практично такий же механізм. p>
Ось що в мене вийшло в результаті. p>
Сервер додатків
h2>
Складається з одного віддаленого модуля даних, в якому
немає доступу до бази даних, тільки реалізація зворотних викликів (фактично,
ніяких компонентів на формі немає). Відповідно, у бібліотеці типів для нього
потрібно описати два методи: отримання інтерфейсу зворотних викликів від клієнтської
частини і метод для передачі повідомлення від однієї клієнтської частини всім іншим
(широкомовної розсилки повідомлень). Я зупинився на варіанті, коли в
зворотному виклику передається рядок, але ніщо не заважає реалізувати будь-який розділ
параметрів. p>
У бібліотеці типів треба оголосити власне інтерфейс
зворотного виклику, який стане відомий клієнтської частини при імпорті
бібліотеки типів сервера. p>
У результаті бібліотека типів прийняла вигляд, наведений
на малюнку 1. p>
p>
Малюнок 1. p>
Проект називається BkServer. Модуль даних називається
rdmMain, і в його інтерфейсі оголошені методи, опис яких наведено нижче. p>
procedure
RegisterCallBack (const BackCallIntf: IDispatch); safecall; p>
В даний метод має передаватися інтерфейс зворотного
виклику IBackCall, метод OnCall якого і служить для забезпечення зворотного
дзвінка. Однак параметр оголошений як IDispatch, з іншими типами підключення по
сокета просто не працює. p>
procedure
Broadcast (const MsgStr: WideString); safecall; p>
Цей метод служить для широкомовної розсилки
повідомлень. p>
В інтерфейсі зворотного виклику (IBackCall) є тільки
один метод: p>
procedure
OnCall (const MsgStr: WideString); safecall; p>
Цей метод отримує повідомлення. p>
Отримані клієнтські інтерфейси треба десь зберігати,
причому бажано забезпечити до них доступ з глобального списку, тоді
повідомлення можна передати всім клієнтських частинах, просто пройшовши за цим списком.
Мені здалося зручним зробити клас-оболонку, і вставляти в список посилання на
клас. Як списку використовується простий TThreadList, описаний як
глобальна мінлива в секції implementation: p>
var CallbackList: TThreadList; p>
і, відповідно, примірник списку створюється в секції
initialization модуля і звільняється під час завершення роботи програми в секції
finalization. Обрано саме TThreadList (потокобезопасний список), оскільки,
як уже згадувалося, використовується модель apartment, і звернення до списку будуть
йти з різних потоків. p>
У секції initialization записано наступне оголошення
фабрики класу: p>
TComponentFactory.Create (ComServer,
TrdmMain, Class_rdmMain, ciMultiInstance, tmApartment); p>
На сервері додатків створюється один модуль даних на
кожне з'єднання, і кожен модуль даних працює у своєму потоці. p>
У CallbackList зберігаються посилання на клас TCallBackStub,
в якому і зберігається посилання на інтерфейс клієнта: p>
TCallBackStub =
class (TObject) p>
private p>
// Callback-інтерфейси повинні бути
disp-інтерфейсами. p>
// Виклики повинні йти через Invoke p>
FClientIntf: IBackCallDisp; p>
FOwner: TrdmMain; p>
FCallBackWnd: HWND; p>
public p>
constructor Create (AOwner:
TrdmMain); p>
destructor Destroy; override; p>
procedure
CallOtherClients (const MsgStr: WideString); p>
function OnCall (const MsgStr:
WideString): BOOL; p>
property ClientIntf:
IBackCallDisp read FClientIntf write FClientIntf; p>
property Owner: TrdmMain read
FOwner write FOwner; p>
end; p>
Примірник цього класу створюється і знищується
rdmMain (в обробника OnCreate і OnDestroy). Посилання на нього зберігається в
змінної TrdmMain.FCallBackStub, при цьому клас відразу вставляється в список: p>
procedure TrdmMain.RemoteDataModuleCreate (Sender: TObject); p>
begin p>
// Відразу робимо оболонку для
callback-інтерфейсу p>
FCallbackStub: = TCallBackStub.Create (Self); p>
// І відразу реєструємо в загальному списку p>
CallbackList.Add (FCallBackStub); p>
end; p>
procedure TrdmMain.UnregisterStub; p>
begin p>
if Assigned (FCallbackStub) then p>
begin p>
CallbackList.Remove (FCallbackStub); p>
FCallBackStub.ClientIntf: =
nil; p>
FCallBackStub.Free; p>
FCallBackStub: = nil; p>
end; p>
end; p>
procedure TrdmMain.RemoteDataModuleDestroy (Sender: TObject); p>
begin p>
UnregisterStub; p>
end; p>
Призначення полів досить зрозуміло: у FClientIntf
зберігається власне інтерфейс зворотного дзвінка, в FOwner - посилання на
TRdmMain ... А от третє полі (FCallBackWnd) служить для маршалинга викликів
між потоками, про це буде сказано трохи нижче. У виклику методу
RegisterCallBack інтерфейс просто передається цього класу, де і виробляється
безпосередній виклик callback-інтерфейсу (через Invoke): p>
procedure TrdmMain.RegisterCallBack (const BackCallIntf: IDispatch); p>
begin p>
lock; p>
try p>
FCallBackStub.ClientIntf: =
IBackCallDisp (BackCallIntf); p>
finally p>
unlock; p>
end; p>
end; p>
Всього цього цілком достатньо для дзвінків клієнтської
частини з віддаленого модуля даних, до якого вона приєднана. Однак завдання
полягає саме в тому, щоб викликати інтерфейси клієнтських частин, що працюють з
іншими модулями. Це забезпечується двома методами класу TCallBackStub:
CallOtherClients і OnCall. p>
Перший метод досить простий, і викликається з процедури
Broadcast: p>
procedure TrdmMain.Broadcast (const MsgStr: WideString); p>
begin p>
lock; p>
try p>
if Assigned (FCallbackStub)
then// переводимо стрілки:) p>
FCallbackStub.CallOtherClients (MsgStr); p>
finally p>
unlock; p>
end; p>
end; p>
procedure TCallBackStub.CallOtherClients (const MsgStr: WideString); p>
var p>
i: Integer; p>
LastError: DWORD; p>
ErrList: string; p>
begin p>
ErrList: =''; p>
with Callbacklist.LockList do p>
try p>
for i: = 0 to Count - 1 do p>
if Items [i] <> Self
then// для всіх, крім себе p>
if not
TCallbackStub (Items [i]). OnCall (MsgStr) then p>
begin p>
LastError: = GetLastError; p>
if LastError <>
ERROR_SUCCESS then p>
ErrList: = ErrList +
SysErrorMessage (LastError) + # 13 # 10 p>
else p>
ErrList: = ErrList + 'Щось незрозуміле' + # 13 # 10; p>
end; p>
if ErrList <>''then p>
raise Exception.Create ( 'Виникли помилки:' # 13 # 10 +
ErrList); p>
finally p>
Callbacklist.UnlockList; p>
end; p>
end; p>
Організовується прохід за списком Callbacklist, і для всіх
TCallbackStub у списку викликається метод OnCall. Якщо виклик не вийшов,
збираємо помилки і видаємо повідомлення. Помилка може бути системною, як видно
нижче. Я не став створювати свій клас виняткову ситуацію, на клієнті вона
все одно буде виглядати як EOLEException. p>
Якщо б модель потоків була tmSingle, у методі OnCall
достатньо було б просто викликати відповідний метод інтерфейсу
IBackCallDisp, але при створенні віддаленого модуля даних була обрана модель
tmApartment, і прямий виклик IBackcallDisp.OnCall негайно призводить до помилки,
потоки-то різні. Тому доводиться робити виклики інтерфейсу з його
власного потоку. Для цього використовується вікно, що створюється кожним
екземпляром класу TCallBackStub, handle якого і зберігається у змінній
FCallBackWnd. Основна ідея така: замість прямого виклику інтерфейсу послати
повідомлення у вікно, і викликати метод інтерфейсу в процедурі обробки повідомлень
цього вікна, яка опрацює повідомлення в контексті потоку, який створив вікно: p>
function TCallBackStub.OnCall (const MsgStr: WideString): BOOL; p>
var p>
MsgClass: TMsgClass; p>
begin p>
Result: = True; p>
if Assigned (FClientIntf) and
(FCallbackWnd <> 0) then p>
begin p>
// MsgClass - це просто оболонка
повідомлення, тут же можна передавати p>
// додаткову службову інформацію. p>
MsgClass: = TMsgClass.Create; p>
// А ось звільнений об'єкт буде в
обробнику повідомлення. p>
MsgClass.MsgStr: = MsgStr; p>
// Синхронізація - послав і забув :-))
Виходимо відразу. P>
// При SendMessage викликав клієнт буде
чекати, поки всі інші клієнти p>
// опрацюють повідомлення, а це небажано p>
Result: = PostMessage (FCallBackWnd,
CM_CallbackMessage, p>
Longint (MsgClass), Longint (Self )); p>
if not Result then// то й не треба:) p>
MsgClass.Free; p>
end; p>
end; p>
Що виходить: повідомлення посилається в чергу кожного
потоку, і там повідомлення накопичуються. Коли модуль даних звільняється від
поточної обробки даних, а вона може бути досить довгою, всі повідомлення в
черги обробляються і передаються на клієнтську частину в порядку надходження.
Побічним ефектом є те, що клієнт, який викликав Broadcast, не очікує
закінчення обробки повідомлень усіма іншими клієнтськими частинами, так як
PostMessage повертає керування негайно. У підсумку виходить досить
симпатична система, коли один клієнт надсилає повідомлення всім іншим і тут
ж продовжує роботу, не чекаючи закінчення передачі. Інші ж клієнти
отримують це повідомлення в момент, коли ніякої обробки даних не відбувається,
можливо - набагато пізніше. Клас TMsgClass оголошений в секції implementation
наступним чином: p>
type p>
TMsgClass = class (TObject) p>
public p>
MsgStr: WideString; p>
end; p>
і служить просто конвертом для рядки повідомлення, в
принципі, в нього можна додати будь-які інші дані. Посилання на екземпляр цього
класу зберігається тільки в параметрі wParam повідомлення, і теоретично можлива
ситуація, коли повідомлення буде надіслано модулю, який вже знищується
(клієнт від'єднався). І, природно, повідомлення оброблено не буде, і не
буде знищений екземпляр класу TMsgClass, що призведе до витоку пам'яті.
Виходячи з цього, при знищенні клас TCallBackStub вибирає за допомогою
PeekMessage всі повідомлення, що залишилися, і знищує MsgClass до знищення
вікна. FCallbackWnd створюється в конструкторі TCallBackStub і знищується в
деструктор: p>
constructor TCallBackStub.Create (AOwner: TrdmMain); p>
var p>
WindowName: string; p>
begin p>
inherited Create; p>
Owner: = AOwner; p>
// створюємо вікно синхронізації p>
WindowName: = 'CallbackWnd' + p>
IntToStr (InterlockedExchangeAdd (@ WindowCounter, 1 )); p>
FCallbackWnd: = p>
CreateWindow (CallbackWindowClass.lpszClassName, PChar (WindowName), 0, p>
0, 0, 0, 0, 0, 0, HInstance,
nil); p>
end; p>
destructor TCallBackStub.Destroy; p>
var p>
Msg: TMSG; p>
begin p>
// Можуть залишитися повідомлення - видаляємо p>
while PeekMessage (Msg, FCallbackWnd, CM_CallbackMessage, p>
CM_CallbackMessage,
PM_REMOVE) do p>
if Msg.wParam <> 0 then p>
TMsgClass (Msg.wParam). Free; p>
DestroyWindow (FCallbackWnd); p>
inherited; p>
end; p>
Зрозуміло, перед створенням вікна потрібно оголосити і
зареєструвати його клас, що і зроблено в секції implementation модуля.
Процедура обробки повідомлень вікна викликає метод OnCall інтерфейсу при
отриманні повідомлення CM_CallbackMessage: p>
var p>
CM_CallbackMessage: Cardinal; p>
function CallbackWndProc (Window: HWND; Message: Cardinal; p>
wParam, lParam: Longint):
Longint; stdcall; p>
begin p>
if Message = CM_CallbackMessage
then p>
with TCallbackStub (lParam) do p>
begin p>
Result: = 0; p>
try p>
if wParam <> 0 then p>
with TMsgClass (wParam) do p>
begin p>
Owner.lock; p>
try p>
// Безпосередній виклик інтерфейсу клієнта p>
if
Assigned (ClientIntf) then p>
ClientIntf.OnCall (MsgStr); p>
finally p>
Owner.unlock; p>
end; p>
end; p>
except p>
end; p>
if wParam <> 0 then// повідомлення відпрацьовано - знищуємо p>
TMsgClass (wParam). Free; p>
end p>
else p>
Result: =
DefWindowProc (Window, Message, wParam, lParam); p>
end; p>
Номер повідомленням CM_CallbackMessage присвоюється
викликом p>
RegisterWindowMessage ( 'bkServer
Callback SyncMessage'); p>
також у секції ініціалізації. p>
Ось, власне, і все - зворотний виклик здійснюється
з потрібного потоку. Тепер можна приступати до реалізації клієнтської частини. P>
Клієнтська частина
p>
Складається з однієї форми, просто щоб спробувати
механізм передачі повідомлень. На етапі розробки форма виглядає таким
чином (Малюнок 2): p>
p>
Малюнок 2 p>
Тут присутній TSocketConnection (scMain), яка
з'єднується з сервером BkServer. Кнопка "Помилка з'єднання" (btnConnect)
призначена для установки з'єднання, кнопка "Відправити" (btnSend) --
для відправлення повідомлення, записаного у вікні редагування (eMessage) іншим
клієнтським частинах. p>
Код клієнтської частини досить короткий: p>
procedure TfrmClient.btnConnectClick (Sender: TObject); p>
begin p>
with scMain do p>
Connected: = not Connected; p>
end; p>
procedure TfrmClient.btnSendClick (Sender: TObject); p>
var p>
AServer: IrdmMainDisp; p>
begin p>
if not scMain.Connected then p>
raise Exception.Create ( 'Немає з'єднання'); p>
AServer: =
IrdmMainDisp (scMain.GetServer); p>
AServer.Broadcast (eMessage.Text); p>
end; p>
procedure TfrmClient.scMainAfterConnect (Sender: TObject); p>
var p>
AServer: IrdmMainDisp; p>
begin p>
FCallBack: = TBackCall.Create; p>
AServer: =
IrdmMainDisp (scMain.GetServer); p>
AServer.RegisterCallBack (FCallBack); p>
lConnect.Caption: = 'З'єднання встановлено'; p>
btnConnect.Caption: = 'Від'єднатись'; p>
end; p>
procedure TfrmClient.scMainAfterDisconnect (Sender: TObject); p>
begin p>
FCallBack: = nil; p>
lConnect.Caption: = 'Немає з'єднання'; p>
btnConnect.Caption: = 'З'єднатися'; p>
end; p>
Фактично все управляється 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 я реалізував
найпростішим чином: p>
procedure TBackCall.OnCall (const MsgStr: WideString); p>
begin p>
ShowMessage (MsgStr); p>
end; p>
Після компіляції додаток можна запустити в
двох-трьох примірниках і перевірити його працездатність. Необхідно враховувати,
що повідомлення отримують всі клієнти, крім того, хто вислав його. p>
Таким чином, вийшло хоч і мінімальний, але
працездатний додаток із зворотними викликами та передачею повідомлень між
клієнтськими частинами. Хоча практично все реалізовано вручну, без
використання готових методик COM, мені цей спосіб видається найбільш
кращим, я просто реалізував зворотні виклики і маршалинга так, як мені
хотілося. У результаті вся реалізація досить зрозуміла і дозволяє
програмувати виклики так, як хочеться. p>
Хоча мої друзі обізвали цей спосіб маршалинга
викликів "хакерів", мені все одно хотілося б висловити їм глибоку
вдячність за поради і терпіння, з яким вони відповідали на мої запитання ;-)). p>
ПРИМІТКА p>
Виконувані модулі були створені в
Delphi5 SP1. Для роботи програми, природно, необхідно запустити Borland
Socket Server, що входить в постачання Delphi. P>
Список літератури h2>
Для підготовки даної роботи були використані
матеріали з сайту http://www.rsdn.ru/
p>