Delphi 6 en MTS/COM+ Objecten |
MTS en COM+
Eerst maar even beginnen met het verschil tussen MTS (Microsoft Transaction Server) en COM+.
Voor het artikel van deze keer is er eigenlijk geen verschil.
MTS is onderdeel van Windows NT4 (zit in de Option Pack), en COM+ komt met Windows 2000 en later.
COM+ kan gezien worden als de opvolger van MTS, en bevat nog enkele extra zaken.
Wie WinNT4 gebruikt kan echter de voorbeelden uit dit artikel gewoon nabouwen (ik zelf zal Windows 2000 gebruiken voor de screenshots in dit artikel overigens).
COM+ Component
De vorige keer bouwde ik een Euro converter als COM Object.
Deze keer wil ik iets verder gaan, en een eigen bank object implementeren (met rekeningnummers, en mogelijkheden om geld te storten, geld op te nemen en het bedrag van de rekening op de vragen).
Bovendien wil ik bijhouden hoeveel keer er gebruik wordt gemaakt van het bank object, dus bouw ik een teller in (die niet zal werken, maar tegen het eind van het artikel zal ik uitleggen waarom niet).
Een nieuw COM+ Component in Delphi 6 maken we met File | New - Other, door in de ActiveX tab van de Object Repository voor een Transactional Object te kiezen.
Er zal nu eerst een nieuwe ActiveX Library (achter de schermen) worden aangemaakt, en vervolgens krijgen we de New Transactional Object dialoog te zien. In deze dialoog kunnen we natuurlijk weer de CoClass Name opgeven, zoals Bank, maar ook of het COM+ object transacties ondersteunt of niet. Laat deze optie voorlopig op de default waarde staan (does not support transactions), want hier wil ik straks pas gebruik van maken.
Een druk op de OK-knop genereert een nieuwe unit met daarin de definitie van de class TBank, afgeleid van TMtsAutoObject, die het IBank interface implementeert. Bewaar het project in iets als Bank42, en de unit voor TBank in Bank.
Type Library Editor
De Type Library Editor stond al meteen open, en kunnen we gebruiken om enkele nieuwe methoden aan het IBank interface toe te voegen.
Ik voeg er vier toe, waarvan de Pascal equivalenten als volgt zijn (het moet niet moeilijk zijn om deze methoden via de Type Library Editor toe te voegen - zie anders het artikel in het vorige nummer van SDGN Magazine voor de stappen in detail):
procedure Opnemen(RekNr: Integer; Bedrag: Currency); safecall; procedure Saldo(RekNr: Integer; out Saldo: Currency); safecall; procedure Storten(RekNr: Integer; Bedrag: Currency); safecall; procedure Teller(out Tel: Integer); safecall;De eerste drie zijn voor het gebruik van de bankrekening: het opnemen van geld, bekijken van het saldo en storten van een bedrag op de rekening worden ondersteund. De vierde methode is een teller die bij aanroep de huidige waarde van de teller teruggeeft (het aantal tellen tot nu toe), en de teller intern eentje ophoogt.
unit Bank;
{$WARN SYMBOL_PLATFORM OFF}
interface
uses
ActiveX, Mtsobj, Mtx, ComObj, Bank42_TLB, StdVcl;
type
TBank = class(TMtsAutoObject, IBank)
protected
procedure Teller(out Tel: Integer); safecall;
private
FTeller: LongInt;
public
procedure Initialize; override;
end;
implementation
uses
ComServ;
procedure TBank.Initialize;
begin
inherited;
FTeller := 0
end;
procedure TBank.Teller(out Tel: Integer);
begin
Tel := FTeller;
Inc(FTeller)
end;
initialization
TAutoObjectFactory.Create(ComServer, TBank, Class_Bank,
ciMultiInstance, tmApartment);
end.
Als een client toepassing een instantie gebruik van het TBank object, zal dus iedere aanroep van Teller resulteren in een hogere waarde van de interne teller.
Maar voor we dat kunnen uitproberen moeten we eerst het COM+ object registreren.
COM+ Registreren
In tegenstelling tot "normale" COM en Automation objecten, hoef je een COM+ object niet via Run | Register ActiveX Server te registreren.
In plaats daarvan moet je de Run | Install COM+ Objects gebruiken (onder WinNT4 heet deze menu optie overigens Run | Install MTS Objects).
Je krijgt nu een speciale dialoog waarin alle COM+ objecten uit het huidige ActiveX Library project worden opgenoemd (in ons geval uiteraard alleen Bank).
Door te klikken op de checkbox voor een COM+ object naam geef je aan dit betreffende COM+ object te willen installeren in COM+. Er volgt dan een tweede dialoog waarin je kan kiezen in welke COM+ Application je het COM+ object wilt installeren. Ik kan je aanraden om voor je eigen (test) COM+ objecten een eigen (nieuwe) COM+ Apppication te specificeren die je alleen gebruikt voor je eigen test objecten. Klik hiertoe op de tab "Install Into New Application" en tik daar de naam in van de nieuwe COM+ Application (zoals Delphi 6 Test ofzo). Nadat je op OK hebt geklikt krijg je dan voor het geselecteerde COM+ object te zien dat deze in de opgegeven COM+ Application geïnstalleerd zal worden.
Component Services
De Component Services bestaat net als de Windows Explorer uit een treeview links, waarin we via Component Services, Computers, My Computer, en COM+ Applications naar onze Delphi 6 Test COM+ Application kunnen afdalen.
We kunnen behoorlijk diep komen, en zelfs de vier verschillende methoden van het IBank interface bekijken. Straks gaan we hier nog wat verder mee doen, maar eerst gaan we nu het COM+ Bank object gebruiken.
Gebruik van COM+ Componenten
Het gebruik van een COM+ object lijkt veel op het gebruik van een Automation Object (wat we de vorige keer zagen), met de CreateCOMObject routine.
Voor een hele simpele COM+ client die de Teller van onze Bank gebruikt, hebben we alleen maar een TLabel (genaamd lbTeller) en een TButton (genaamd btnTeller) nodig.
De eerste poging tot een implementatie van de OnClick event handler van de btnTeller is dan als volgt:
procedure TForm1.btnTellerClick(Sender: TObject); var Bank: IBank; Tel: Integer; begin (Sender as TButton).Enabled := False; try Bank := CreateCOMObject(Class_Bank) as IBank; Bank.Teller(Tel); lbTeller.Caption := Format('Teller: %d',[Tel]) finally (Sender as TButton).Enabled := True end end;Dit lijkt leuk, maar telt niet echt. Voor iedere aanroep van de OnClick event handler wordt er namelijk een nieuwe instantie van het IBank interface opgevraagd, die na afloop van de OnClick event handler out-of-scope raakt en netjes wordt opgeruimd. Bij de volgende binnenkomst staat de teller dus weer vrolijk op 0, en zo kun je tellen tot je een ons weegt, maar kom je niet echt veel verder.
unit ClientForm;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, Bank42_TLB, ComObj;
type
TForm1 = class(TForm)
btnTeller: TButton;
lbTeller: TLabel;
procedure btnTellerClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
Bank: IBank;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormCreate(Sender: TObject);
begin
Bank := CreateCOMObject(Class_Bank) as IBank;
end;
procedure TForm1.btnTellerClick(Sender: TObject);
var
Tel: Integer;
begin
(Sender as TButton).Enabled := False;
try
Bank.Teller(Tel);
lbTeller.Caption := Format('Teller: %d',[Tel])
finally
(Sender as TButton).Enabled := True
end
end;
end.
Als ik twee clients tegelijkertijd draai, zullen ze overigens allebei hun eigen teller krijgen, omdat ze allebei met een eigen instantie van een COM+ object praten.
Iedere client praat dus met zijn eigen COM+ object! Dit is niet echt schaalbaar (in het geval er een paar honderd of meer clients met de COM+ objecten willen praten), en dat is een mooi moment om transacties te introduceren.
Requires a new transaction
We moeten nu op twee plaatsen een wijziging doorvoeren.
Allereerst moeten we in ons Bank42 project naar de Type Library Editor gaan, daar de Bank CoClass selecteren, en dan op de COM+ tab de keuze van Transaction Model veranderen van "Does not support transactions" in "Requires a new transaction" (dit heeft tevens tot gevolg dat de keuze van Call Synchronization zal veranderen van "None" naar "Required").
Nu zal iedere methode van ons Bank object een nieuwe transactie starten, en moet deze derhalve (expliciet) eindigen via ofwel SetComplete (als het gelukt is) of SetAbort (als de aktie mislukt is).
De code voor de methode Teller krijgt de SetComplete regel erbij, en zal er dan nu als volgt uitzien:
procedure TBank.Teller(out Tel: Integer); begin Tel := FTeller; Inc(FTeller); SetComplete end;De client hoeven we niet aan te passen. Alleen de server opnieuw compileren, en dan de client opnieuw draaien.
Stateless
Gelukkig gaat er niks mis, alles gaat nog steeds goed (of liever gezegd: alles gaat zoals het hoort).
Wat we hier zien is het gevolg van het feit dat het COM+ Bank Object nu transacties ondersteunt, niet langer gekoppeld is aan één bepaalde client, en derhalve stateless is geworden.
Dit heeft behoorlijk wat gevolgen, met name voor clients die er vanuit gingen dat de server wel "de toestand in de wereld" zou onthouden (zoals de teller).
Dat was wel leuk en handig, maar niet schaalbaar (zoals we hiervoor al zagen).
Door de server stateless te maken is het wel mogelijk om met honderden clients tegelijkertijd met de COM+ server te praten - zelfs als ze allemaal denken een eigen instantie te hebben gemaakt.
De COM+ server zal er zijn voor wie hem nodig heeft, maar niet eerder, en ook niet langer.
Dit voorkomt dat er een paar honderd COM+ objecten in de lucht zullen zijn (en resources op de server in beslag nemen), terwijl de clients (bij wijze van spreken) alleen maar uit hun neus zitten te eten.
Saving State
In een stateless wereld is de koppeling tussen de client en de server verbroken (dat wil zeggen dat er niet een exclusieve server object is voor iedere client).
De state zal niet langer door de server zelf worden bijgehouden.
Er zijn twee alternatieven om de state nu bij te laten houden: door de client (iedere client houdt zijn eigen state bij), of met behulp van één-of-ander opslagmechanisme bij de server (zoals een database of .ini file).
Beide opties hebben voordelen en nadelen, en zijn niet altijd allebei mogelijk.
In ons teller voorbeeld, is het namelijk niet mogelijk om de client de teller te laten bijhouden (het was namelijk oorspronkelijk de bedoeling om een globale teller te krijgen, waarmee we het globale gebruik van het COM+ Bank object kunnen meten).
De server-side opslag oplossing met behulp van een .ini file is gelukkig eenvoudig te implementeren, waardoor de code voor de Teller methode nu als volgt is:
procedure TBank.Teller(out Tel: Integer); const TellerKey = 'teller'; begin with TIniFile.Create('.\bank.ini') do try Tel := ReadInteger(TellerKey, TellerKey, 0); WriteInteger(TellerKey, TellerKey, Succ(Tel)) finally UpdateFile; Free end; SetComplete end;Als we het COM+ server project opnieuw compileren, en dan de client opnieuw draaien zien we dat de teller weer werkt. En nog fijner: als we nu twee of meer clients draaien dan zien we dat de teller gedeeld wordt door alle clients (dus we kunnen nu echt het gebruik van het COM+ object tellen). Merk op dat ik .\bank.ini gebruik als locatie van de .ini file. Dat wil dus zeggen de zogenaamde working directory van het COM+ object, en dat is de Window's system32 directory.
Just-in-Time Activation
Een bijkomend effect van het gebruik van transactions wordt ook wel aangegeven met de naam Just-in-Time Activation.
Dit geeft aan dat de COM+ server pas zal worden aangemaakt op het moment dat het echt nodig is (tussentijds kan de server opgeruimd worden, aangezien hij toch geen state hoeft te bewaren).
Dus de client kan wel denken dat hij de hele tijd een instantie naar een COM+ object heeft wijzen, in praktijk is er wellicht nog helemaal niks aangemaakt op de server, tot het moment waarop er daadwerkelijk een methode wordt aangeroepen.
Dan wordt er vlug een COM+ object geïnitialiseerd of uit een pool met objecten gehaald.
Allemaal erg gericht op performance en schaalbaarheid.
We kunnen deze eigenschap overigens op eenvoudige wijze nader demonstreren, door de initialize methode van de TBank class een extra message dialog te laten geven om aan te geven dat er zojuist een nieuw COM+ object aangemaakt is.
De nieuwe code is als volgt:
procedure TBank.Initialize;
begin
inherited;
ShowMessage('TBank.Initialize ' + DateTimeToStr(Now));
// doe hier het eventueel ophogen van de echte teller...
end;
Met deze code erbij (en na het opnieuw compileren van de server en opnieuw draaien van de client), krijgen we meteen bij het opstarten van de client een message te zien (merk op dat de caption "dllhost" is - een indicatie dat het niet de client is, maar de server geladen door COM+, die deze melding geeft):
Het feit dat we deze dialoog krijgen komt door de initialisatie in de OnCreate event handler van het client form. Het COM+ object is nu klaar om gebruikt te worden, en de eerste keer dat we op de btnTeller klikken, krijgen we dan ook geen nieuwe melding meer van TBank.Initialize. Echter, iedere volgende klik op de btnTeller levert eerst een melding op van een nieuwe TBank.Initialize. En als we naast de Initialize ook de destructor Destroy van TBank overriden, zien we dat direct na afloop van iedere methode aanroep, het COM+ object (bij de server) weer wordt opgeruimd. Terwijl de client dus nog steeds denkt met een geldige en bruikbare instantie van IBank rond te lopen. Die is ook nog steeds bruikbaar, want een aanroep van een methode zal automatisch zorgen dat het COM+ object weer opnieuw wordt geïnitialiseerd, etc.
Behalve zaken die ervoor zorgen dat COM+ objecten stateless zijn en door vele clients gebruikt kunnen worden, zijn er nog meer fijne eigenschappen terug te vinden in COM+, zoals ondersteuning voor beveiliging.
Die wou ik een volgende keer aan de orde laten komen, net als deployment en debugging opties.
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .