Hello folks! We are about to dive into one of the hottest topics of our times: Web
services!
But wait -- aren't there already a lot of
articles about Web services and SOAP?
Probably... but this one is different. We will be talking about a specific
programming technique that is very important if you seek to write
stateful Web Services.
Why stateful?
Isn't a Web service an object that is instantiated on the Internet with
its methods called from all over the globe?
Well, yes, almost. The problem is
that a Web service, since it uses HTTP as its transport protocol, is stateless. So it's up to us developers to write some kind of state-maintenance
mechanism.
In this article we will be doing just that.
The solution
To implement state in a stateless framework, we must
save the state (context) between method calls using some kind of persistence
layer. An easy, lightweight one may be created using MyBase, the TClientDataset file-based
database. The general schema for the solution looks like this:
Starting is easy.. you've read about this in Nick Hodges's Shakespearean
insult generator Web service article. But we can repeat it here for clarity's
sake. Please follow the steps below:
- Register Delphi 6 (not just because it's the right thing to do, but because
that's how you get the eXtreme Components collection with the Invokable
Wizard).
- Select File | New | Other | Web Services | Soap
Server Application from Delphi 6's main menu.
- Choose ISAPI/NSAPI Dynamic Link Library.
Although you could normally use any kind of Web application to write your Web
Service, if you ever really implement a session/TClientDataset-dependent one,
please use an ISAPI DLL for your production code. The reason is that we
must have a single TClientDataset instance shared by all the client connections,
protected by a Critical Section from concurrent accesses. This way we don't lose
session information when two clients connect to the Web Service at the
same time. (Thanks to Deepak Shenoy for calling my attention to this bit of
technical arcana.)
Now you have an empty Web service. The files that were generated by Delphi up
to this point are these:
Project1.dpr: This is your project source file. Save it as AuthServer.dpr.
Unit1.pas: Save it as UwmAuthServer. Name your WebModule wmAuthServer. This is
your Web Service WebModule with Delphi 6's SOAP implementation three core
components:
- THTTPSoapDispatcher: Responsible for the response of SOAP messages
delegating their handling to an Invoker component.
- THTTPSoapPascalInvoker: Upon interpretation of a SOAP message,
executes the corresponding Pascal code which you implemented using
your Invokable Interface (explained below).
- TWSDLHTMLPublish: Produces WSDL (Web Service Definition Language)
code from the Invokable Interfaces you register within your
application.
The following step
is to provide it with some code. (Or did you think Delphi would do it for you
automatically?)
If you have successfully registered your copy of Delphi
6, you will be able to use the great Invokable Interface Class
Wizard and Delphi will provide you with two units: one for the interface
and another for the implementation class. Just select File | New |
Other | Web Services | Invokable Wizard from Delphi 6's main menu:
As you can see, using the wizard is pretty simple:
- Fill in the base name of the class and
interface you will be using. In our case, use Authenticator.
- Accept the default generated base unit identifier.
- Select InvokableClass in the drop down. (The other
option, TInterfacedObject, is not so easy to use. You would have to provide
a factory and register it later...oh no! I am wrong -- the Invokable Wizard
does this for you!)
- Press Generate.
Now you get a couple of units added to
your project automatically:
Here is the interface unit:
{ Invokable interface declaration unit for IAuthenticator }
unit AuthenticatorIntf;
interface
uses
Types, XSBuiltIns;
type
IAuthenticator = interface(IInvokable)
['{BC2B44FB-8161-4BE3-8836-50F1DEBCE7D9}']
// Declare your invokable logic here using standard Object Pascal code
// Remember to include a calling convention! (usually stdcall)
end;
implementation
uses
InvokeRegistry;
initialization
InvRegistry.RegisterInterface(TypeInfo(IAuthenticator), '', '');
end.
And here is the implementation unit:
{ Invokable implementation declaration unit for TAuthenticator,
which implements IAuthenticator }
unit AuthenticatorImpl;
interface
uses
AuthenticatorIntf, InvokeRegistry;
type
TAuthenticator = class(TInvokableClass, IAuthenticator)
// Make sure you have your invokable logic implemented in IAuthenticator
// first, then use CodeInsight(tm) to fill in this implementation
// section by pressing Ctrl+Space, marking all the interface
// declarations for IAuthenticator, and pressing Enter.
// Once the declarations are inserted here, use ClassCompletion(tm)
// to write the implementation stubs by pressing Ctrl+Shift+C
end;
implementation
initialization
InvRegistry.RegisterInvokableClass(TAuthenticator);
end.
Up until this point we have done everything as it should be done for each and every
Web service. The cool part begins now!
We must decide which methods to add to the IAuthenticator interface. Let's add
the following:
- Login: Log a user/password pair into the Web Service. Upon
success, returns a session handle.
- Logout: The opposite of Login. Duh!
- AddSessionData: Lets the Web consumer add custom data to the current session.
- GetSessionData: Retrieves data for the Web consumer.
Here is the final declaration of the IAuthenticator interface:
type
IAuthenticator = interface(IInvokable)
['{BD4E3554-A4BB-40FA-B5A7-60D2B599F101}']
function Login (const User, Password : String;
var SessionHandle: String) : boolean; stdcall;
function AddSessionData (const SessionHandle : String; const Data : String) : Boolean; stdcall;
function GetSessionData (const SessionHandle : String) : String; stdcall;
function IsValidSession (const SessionHandle : String) : boolean; stdcall;
function Logout (const SessionHandle : String): boolean; stdcall;
end;
Please be careful to use stdcall or cdecl instead of Object Pascal's default
register calling convention, or else there won't be RTTI enough left for the Web
Service infrastructure to manipulate your interface.
Now you can simply press Ctrl-Shift-C and start coding. Right?
Wrong!
Ever seen an interface with
code? I haven't. You must switch to the AuthenticatorIntf.pas unit and use
Ctrl-Space as much as you want up to the point when all the interface methods
are declared in the class definition. Then press
Ctrl-Shift-C and behold the wonders of CodeCompletion.
Your class declaration is now complete and the methods are correctly declared in
the implementation part of your unit.
Before we start writing the implementation of our methods, let's create
a new TDataModule named dtmSessions and drop a TClientDataset component named
cdsSessions onto it.
Add three FieldDefs to it: SessionHandle, SessionUser and SessionData, all
three with DataType ftString and Size 255.
Add the following code to the OnCreate event handler of the TDataModule:
procedure TdtmSessions.DataModuleCreate(Sender: TObject);
begin
cdsSessions.FileName := ExtractFilePath(ParamStr(0))+ 'AuthSessions.cds';
if not FileExists (cdsSessions.FileName) then
cdsSessions.CreateDataSet
else
cdsSessions.Open;
end;
This ensures that the ClientDataset is either created or opened, as
necessary.
To avoid problems with multiple TDataModules in your Web Application,
do not forget to remove dtmSessions DataModule from the auto-create list of
your project. To use this DataModule, let's create and free it ourselves in the
initialization and finalization parts of its unit:
initialization
dtmSessions := TdtmSessions.Create(nil);
finalization
dtmSessions.Free;
end.
To avoid concurrency problems, let's declare a
TCriticalSection object in the AuthenticatorImpl.pas implementation part:
implementation
uses
SyncObjs, //for the Critical Section
DB, //Locate constants
SysUtils, //GuidToString function
ActiveX, //CoCreateGuid call
UwmAuthServer,
UdtmSessions; //The Sessions DataModule
var
CS : TCriticalSection;
As with the TDataModule above, let's take care of the TCriticalSection
life-cycle in the initialization and finalization of its unit:
initialization
InvRegistry.RegisterInvokableClass(TAuthenticator);
CS := TCriticalSection.Create;
finalization
CS.Free;
end.
Now let's code each of our Web service methods:
Login
Here's the code:
function TAuthenticator.Login(const User, Password: String;
var SessionHandle: String): Boolean;
begin
//Here you would check for the validity of the User/Password pair
//The implementation below is just a simple substitute of a better
//mechanism.
Result := True;
if (CompareText (User,'daniel')<>0) or (password <> 'test') then
begin
Result := False;
Exit;
end;
//let's see if the user is already logged in
CS.Enter;
try
if dtmSessions.cdsSessions.Locate('SessionUser',User,[loCaseInsensitive]) then
raise Exception.Create('User already logged in');
//from here on, we will assume that the login is ok, let's create a session!
SessionHandle := GetStringGuid;
dtmSessions.cdsSessions.Insert;
dtmSessions.cdsSessions.FieldByName('SessionHandle').AsString := SessionHandle;
dtmSessions.cdsSessions.FieldByName('SessionUser').AsString := User;
dtmSessions.cdsSessions.Post;
dtmSessions.cdsSessions.SaveToFile;
finally
CS.Leave;
end;
end;
Notice that we are protecting the access to our TClientDataset data (even in
the call to its Locate method, since it's not thread-safe) with a critical
section. Critical sections work by blocking every thread from the same process from
entering its protected area concurrently. When a second (or third, or fourth...)
thread tries to enter a critical section, the operating system (Windows here,
but it could as easily be Linux) sets its state to WAIT until the critical section is
left by the first thread.
The GetStringGuid method implementation follows:
function TAuthenticator.GetStringGuid: String;
var
GUID : TGUID;
begin
CoCreateGuid(GUID);
Result := GUIDToString(GUID);
end;
Logout
Here comes the code:
function TAuthenticator.Logout(const SessionHandle: String): Boolean;
begin
//Locating the session
if not LocateSession(SessionHandle) then
begin
Result := False;
Exit;
end;
//deleting the session
CS.Enter;
try
wmAuthServer.cdsSessions.Delete;
wmAuthServer.cdsSessions.SaveToFile;
finally
CS.Leave;
end;
Result := True;
end;
The LocateSession implementation follows:
function TAuthenticator.LocateSession (const SessionHandle : String): boolean;
begin
//Locating the session
Result := IsValidSession(SessionHandle);
end;
Oh no! It uses IsValidSession! What is that?
IsValidSession
The code:
function TAuthenticator.IsValidSession(
const SessionHandle: String): Boolean;
begin
CS.Enter;
try
Result := dtmSessions.cdsSessions.Locate('SessionHandle',SessionHandle,[]);
finally
CS.Leave;
end;
end;
Difficult, eh?
AddSessionData
And the corresponding code is:
function TAuthenticator.AddSessionData(const SessionHandle: String;
const Data: String): Boolean;
begin
//checking if session is already established
Result := True;
if not LocateSession(SessionHandle) then
begin
Result :=False;
Exit;
end;
//ok, let's add the data
CS.Enter;
try
wmAuthServer.cdsSessions.Edit;
wmAuthServer.cdsSessions.FieldByName('SessionData').AsString := Data;
wmAuthServer.cdsSessions.Post;
wmAuthServer.cdsSessions.SaveToFile;
finally
CS.Leave;
end;
end;
GetSessionData
The code:
function TAuthenticator.GetSessionData(
const SessionHandle: String): String;
begin
Result := '';
if not LocateSession(SessionHandle) then
Exit;
Result := wmAuthServer.cdsSessions.FieldByName ('SessionData').AsString;
end;
And that's it. We're finished.
Oh, I almost forgot...the client.
That's the easy part. The client may be part of another
article. But here's a nice screen shot from one I wrote:
If you would like to get the full source code for the examples (and the client)
used in this article, get it here.
It's in CodeCentral. If you
have any further questions, send me an email at daniel@qualtech.com.br
or danpol@pobox.com.
Cheers!
Daniel Polistchuck
IT Director
QualTech IT - Brazil
daniel@qualtech.com.br