Bob Swart (aka Dr.Bob)
Delphi 6 en MTS/COM+ Objecten

De vorige keer heb ik het over COM en OLE Automation gehad. Deze keer wil ik het over MTS en COM+ hebben - een run-time omgeving waar COM+ componenten in draaien, met een hoop extra eigenschappen zoals just-in-time activation, load balancing, security en meer. Waarschijnlijk teveel om in één keer te laten zien, maar ik zal deze keer in ieder geval demonstreren hoe we COM+ componenten kunnen maken, kunnen installeren, kunnen gebruiken, en wat het belang is van stateless components in dit verhaal.

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.

Object Repository

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.

New Transactional Object

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.
Het is met name de teller waar ik nu even bij stil wil staan. De implementatie die ik ervoor bedacht heb bestaat uit een veld FTeller (in de private sectie van TBank), dat bij iedere aanroep van Teller wordt toegekend aan het Tel argument, en vervolgens wordt opgehoogd. Hoe zetten we de interne FTeller dan op 0? Daar kunnen we de initialize methode voor gebruiken, die we kunnen overriden. De implemenatie (voor het FTeller deel) is daarmee als volgt:
  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).

Install COM+ Objects

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.
Door uiteindelijk op de OK-button van de Install COM+ Objects te klikken wordt het installeren daadwerkelijk uitgevoerd (dit kan even duren, en kan some foutmeldingen opleveren). Als alles goed is gegaan, is het Bank object nu geregistreerd (bij mij in de Delphi 6 Test COM+ Application). Dit is te controleren met behulp van de Component Services toepassing, die op mijn Windows 2000 machine onder de Administrative Tools te vinden is.

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.

Component Services

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.
Een beter manier is om de client zo te bouwen dat in de FormCreate een instantie van het IBank interface wordt opgevraagd, wat vervolgens gebruikt wordt gedurende de lifetime van de client (en bij afsluiten van de client toepassing automatisch weer opgeruimd zal worden). Dat betekent dat er op de server niet steeds een nieuw COM+ object geïnitialiseerd hoeft te worden, zodat de teller wel zal tellen.
  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.
Het compileren van de server kan overigens soms problemen geven, met name als dit binnen drie minuten gebeurt nadat de laatste client is afgesloten. In dat geval is het COM+ object namelijk nog steeds "aktief" (of in ieder geval geladen) binnen de COM+ runtime omgeving, zodat het paraat staat om nieuwe binnenkomende clients te bedienen. Een stukje performance optimalisatie die handig is in gebruik, maar minder handig tijdens het ontwikkelen (als je iedere keer drie minuten moet wachten voordat je de server opnieuw kan compileren). Gelukkig kun je via de Component Services toepassing de gehele COM+ applicatie eruit gooien (rechtermuisknop op Delphi 6 Test en dan Shutdown kiezen), en zelfs de idle shutdown tijd zelf is in te stellen, via de properties van de Delphi 6 Test package, op de Advanced tab, in de "Minutes until idle shutdown" optie.
Goed, inmiddels zijn er wel drie minuten verstreken, dus compileer de Bank42 toepassing opnieuw, en draai nu de client weer. Je zult zien dat de teller niet meer werkt - er wordt niet verder geteld dan 0. Wat gaat er mis?

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.
Voor effectief tellen zouden we het ophogen van de teller eigenlijk in de initialize method moeten stoppen, zodat we kunnen meten hoevaak een nieuwe instantie van het COM+ object gemaakt wordt (dat laat ik over als oefening voor de lezer). En dat is vaker dan je zou denken, en meteen een leuke overstap naar ons laatste onderwerp voor vandaag...

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):

dllhost en TBank.Initialize

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.
Het gevolg is echter wel dat COM+ objecten die transacties ondersteunen dus geen state kunnen bijhouden. Wie afhankelijk is daarvan, zal de client zelf de state moeten laten bijhouden en doorgeven. Voorbeeld hiervan is zijn de andere drie methoden van het Bank object - die ik nog helemaal niet geïmplementeerd heb, die namelijk alledrie het rekeningnummer als argument meegeven - daarbij aangeven dat het COM+ object via het rekeningnummer (de state) het saldo moet ophalen en eventueel een bedrag moet storten of opnemen. Hiervoor ligt het voor de hand een database als opslag te gebruiken, uiteraard, maar dat laat ik aan de verbeelding van de lezer over.

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 .


Dit artikel is eerder verschenen in SDGN Magazine #73 - augustus 2002

This webpage © 2002-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.