Author: Deepak Shenoy (shenoy@agnisoft.com) (Presented at the Annual Borland Conference, San Jose, 2003)

Download the source code for this article (AdvancedWs.zip, 319 KB)

Download the powerpoint presentation for this article. (Advancedws.ppt, 239 KB)

Abstract

Learn about more advanced concepts in web services, such as Binary Transfer, Attachments, Compression, Encryption and Interoperability.

Introduction

This is a paper about Advanced Concepts in Web Services and how to use them in Delphi. I'll be talking about the following:

  1. Binary data Transfer
  2. Compression of parameters and datapackets
  3. Encryption
  4. Interoperability
  5. Attachments
  6. Headers
Let's take each of these concepts individually.

Binary Transfer

We've worked with standard SOAP servers, which have taken some parameters and then returned some more. What you might require to do in a real world application is to transfer files and binary data. What you need to use is a Delphi class called "TByteDynArray" - a Dynamic array of bytes, which can be used to transfer any binary data.

What I'm going to show you is how you can use TByteDynArray to transfer binary data across server and client using SOAP. I'll create both Server and Client in Delphi, and transfer files both up and down.

Writing the Server

Run Delphi 7, and hit File | New | Web Services | Soap Server Application. But before we continue let me say that this is not a beginner article - if you're already wondering what this whole thing might be about, here's a few links to help:

Now I'm going to assume you already know what SOAP is all about and have written a SOAP server/client. Let's go on - When you've hit the new Soap Server Application wizard, you need to choose "Web App Debugger Executable" and give the CoClass name as "BinaryServer". Now, let's create a new unit and write the Interface (File | New | Unit, save it as BinIntf.pas).

Here's the code:


unit BinIntf;
 
interface
uses
   Types, XSBuiltIns;
type
   ISoapBinary = interface(IInvokable)
     procedure UploadFile( const FileName : string; 
                           const FileData : TByteDynArray );stdcall;
     function GetFileList : TStringDynArray;stdcall;
     function DownloadFile( const FileName : string ) : TByteDynArray; stdcall;
  end;

 implementation

 uses
  InvokeRegistry;

initialization
  InvRegistry.RegisterInterface(TypeInfo(ISoapBinary), '', '');

end.
 
 
That's about the Interface. The functions are fairly intuitive:
  • UploadFile: Uploads a TByteDynArray (defined as an array of bytes in Types.pas) to the server
  • GetFileList: Gives you a list of currently available files at the server
  • DownloadFile: Downloads a specific file from the server, as a dynamic array of bytes.

Now what we're going to look at is the implementation:


unit BinImpl;

interface

uses InvokeRegistry, Windows, Classes, BinIntf, Types;

type
  TSoapBinary = class( TInvokableClass , ISoapBinary )
  protected
    procedure UploadFile( const FileName : string; 
                          const FileData : TByteDynArray );stdcall;
    function GetFileList : TStringDynArray;stdcall;
    function DownloadFile( const FileName : string ) : TByteDynArray; stdcall;
  public
  end;
implementation

uses WebBrokerSoap, uWeb;

{ TSoapBinary }

function TSoapBinary.DownloadFile(const FileName: string): TByteDynArray;
var i : integer;
begin
  SetLength(Result, 0);
  with (GetSoapWebModule as TBinWebModule) do
  begin
    i:= FileList.IndexOf(FileName);
    if i >=0 then
      Result := FileDataArray[i];
  end;
end;

function TSoapBinary.GetFileList: TStringDynArray;
var lst : TStringList;
  i : integer;
begin
  lst := (GetSoapWebModule as TBinWebModule).FileList;

  SetLength( Result, lst.Count );
  for i := 0 to lst.Count-1 do
    Result[i] := lst[i];
end;

procedure TSoapBinary.UploadFile(const FileName: string;
  const FileData: TByteDynArray);
begin
  with (GetSoapWebModule as TBinWebModule) do
  begin
    FileList.Add(FileName);
    SetLength(FileDataArray, Length(FileDataArray)+1);
    FileDataArray[Length(FileDataArray)-1] := FileData;
  end;
end;

initialization
  InvRegistry.RegisterInvokableClass(TSoapBinary);

end.

Upload file simply stores the byte array in a variable in the Web Unit - you will notice the call to GetSoapWebModule: this call is new in the Update Pack 1. This gets the web unit (the one that has the WSDLPublisher etc.) from the Soap implementation class.

If you're wondering what the GetSoapWebModule call is for, here's an explanation. In web applications, web modules are created on the fly - i.e. for every simultaneous request, there's a new webmodule created to handle it. SO, at any given time, your SOAP Server implementation class is not bound to any particular web module - any of the active webmodules may be creating an instance of the class - in this case, TSOAPBinary. But, if you wanted to access the webmodule for, say, the Request and the Response parameters, you would not be right in referencing a particular instance. To access the "currently active" Webmodule, i.e. the web module that actually instantiated TSOAPBinary, we must use "GetSOAPWebModule" and cast it to your webmodule type.

In this particular case, we're only going to be demonstrating with a single client - and with no simultaneous clients, only one instance of the WebModule is created. Therefore we're storing the FileList in the single WebModule class. In a production environment you might want to create a separate singleton class for this purpose. And of course, you must synchronize access to the singleton instance to avoid multiple webmodules accessing it simultaneously.

You might wonder why I've not used local member variables in the TSoapBinary class - the reason is that this class is created and destroyed as it is invoked - and we expect three separate invokations here (each method call is a separate invoke) so we'd lose all the data after every invokation.

Note: I could have used a global variable but that is not recommended, because a global variable is accessible across all instances of the web module. This gives us no protection if there are even two clients hitting the server at the same time - if both try to access the global variable in different threads, the state of the global variable might be undefined and could result in some nasty errors.

Here's how it's all defined in the web unit:


  private
    { Private declarations }
    FFileList : TStringList;

  public
    { Public declarations }
    FileDataArray : array of TByteDynArray;
    property FileList : TStringList read FFileList;

The DownloadFile and GetFileList functions are fairly intuitive too. DownloadFile looks up the FileList in the WebModule for the requested file, and if it finds it, returns the corresponding entry in FileDataArray (which is an array of "TByteDynArray" elements). GetFileList iterates through the FileList member and returns the filenames as a "TStringDynArray" (array of string). Note that I first set the length of the return variable to the length of the List - this is so that Delphi doesn't have to keep increasing the size every time we add an entry.

We need to run this server once to register - and keep it running we're going to need it later. You might need to head out to Windows explorer for that because if you run it from the IDE, you're going to have to shut it down before starting another project (the Client). Oh and while you're at it, you might want to run the Web App Debugger too, from the Tools menu.

 
Writing the Client
 
Now that we have a server, let's check out a simple little client. I'm going to start a new application and import the WSDL from the server by using the Web Services Importer (File | New | Web Services tab) on the following URL:
 
(You'll see a unit quite similar to the Interface you had created.) Once we have that, here's a sample client form and the client code for the project:


procedure TForm1.Button1Click(Sender: TObject);
var FileData : TByteDynArray;
begin
  if OpenDialog1.Execute then
  begin
    FileData := FileToByteArray( OPenDialog1.FileName );

    (HTTPRIO1 as ISoapBinary).UploadFile(ExtractFileName(OpenDialog1.FileName), 
	                                 FileData);
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var StrArray : TStringDynArray;
   i : integer;
begin
  StrArray := (HTTPRIO1 as ISOapBinary).GetFileList;
  for i := 0 to Length(StrArray)-1 do
    ListBox1.Items.Add( StrArray[i] );
end;

procedure TForm1.Button3Click(Sender: TObject);
var ByteArray : TByteDynArray;
begin
  if ListBox1.ItemIndex = -1 then Exit;

  SaveDialog1.FileName := ListBox1.Items[ListBox1.ItemIndex];

  if SaveDIalog1.Execute then
  begin
    ByteArray := (HTTPRIO1 as ISoapBinary).DownloadFile(
	                              ListBox1.Items[ListBox1.ItemIndex]);
    ByteArrayToFile( ByteArray, SaveDialog1.FileName );
  end;
end;

Each function as you see casts the HTTPRio to the ISoapBinary interface and calls a function on it.

  • The button marked "Upload" reads each byte of the file into a dynamic array of bytes, using a library function called FileToByteArray which I've listed below. This is sent to the server using the Upload call to HTTPRio.
  • The Get File List button gets the list of available files as a dynamic array of strings, and loads the file list into the List Box.
  • The Download File button asks for the selected file (in the list box) from the server, gets the dynamic array of bytes, saves this dynamic array to a file using the library function ByteArrayToFile.

Here's the library functions:


procedure ByteArrayToFile( const ByteArray : TByteDynArray;
                            const FileName : string );
var Count : integer;
    F : FIle of Byte;
    pTemp : Pointer;
begin
  AssignFile( F, FileName );
  Rewrite(F);
  try
    Count := Length( ByteArray );
    pTemp := @ByteArray[0];
    BlockWrite(F, pTemp^, Count );
  finally
    CloseFile( F );
  end;
end;

function FileToByteArray( const FileName : string ) : TByteDynArray;
const BLOCK_SIZE=1024;
var BytesRead, BytesToWrite, Count : integer;
    F : FIle of Byte;
    pTemp : Pointer;
begin
  AssignFile( F, FileName );
  Reset(F);
  try
    Count := FileSize( F );
    SetLength(Result, Count );
    pTemp := @Result[0];
    BytesRead := BLOCK_SIZE;
    while (BytesRead = BLOCK_SIZE ) do
    begin
      BytesToWrite := Min(Count, BLOCK_SIZE);
      BlockRead(F, pTemp^, BytesToWrite , BytesRead );
      pTemp := Pointer(LongInt(pTemp) + BLOCK_SIZE);
      Count := Count-BytesRead;
    end;
  finally
    CloseFile( F );
  end;
end;

This is all that's needed. Run the client project and see the result for yourself. Remember that if you don't have the server project running (as in you must see the server form on your task bar) you're not going to get the desired result. Also, of course, you'll need the Web App Debugger running.

Real World

So we're done? Well, if you're going to write a real world application using this code, you might have to remember a few things:

  • I'm storing the "files" as byte arrays on the server, in another array (FFileData in the web module). You probably don't want this because it's a) going to flush the files if you have to bring down the server in any way and b) you probably want to store large files, which are not so great to do in-memory. So what you really need to do is to save the files to disk or a database.
  • I've written a Web App Debugger module. Now what you'll want to do is to convert this to an ISAPI DLL. In ISAPI DLLs if the data sent is large, the data comes in as "chunks" rather than one big blob of data. There's a small bug in the Delphi 6 implementation here, so files larger than 49KB don't upload correctly. Here's the fix:
    Source: http://groups.google.com/groups?hl=en&selm=3c2f151c_2%40dnews

  • The byte array serialization and deserialization has some bugs so you need to make some code changes in TypInfo.pas - the details are at : http://groups.google.com/groups?hl=en&selm=3beafcf8%241_1%40dnews

    Remember that you must include $(DELPHI)\Source\Soap and $(DELPHI)\Source\Internet in your project's search path, for both Client and Server if you make these changes. Or, copy these changed files into your project directories.

  • You could also compress the files before sending, and decompress them when you receive - both during Upload and Download. That's what I'll be talking about next.

Compression

If huge binary files are being sent in XML - which is a text format - the data sent is actually expanded beyond the actual binary size, since the binary data is MIME encoded (or UUEncoded). Now this might not be acceptable for a web server, so you could reduce data size by compressing data before sending or receiving. I've written a sample that does Compression and Decompression. Plus, it stores the files in an Interbase database, so that you don't lose it. You'll have to tweak the database location etc. to make it all work, but you'll get the idea, which is:

  • The selected file is compressed before upload at the client side and then sent.
  • The Server always decompresses the file and then stores in the database.
  • Before a download the server compresses the file and then sends.
  • Client always decompresses after download.
First, let's see what I've done to compress and decompress data. I've used ZLib, a Delphi implementation of which is available on your Delphi CD. I've created the following helper functions for compression and decompression:
 
  { Compresses an InputStream and stores compressed data in Output Stream
  Assumes both streams are created }
  procedure CompressStream(inpStream,outStream: TStream);
  { Copies a TByteDynArray to a TStream }
  procedure CopyToStream( const InArray : TByteDynArray ; outStream : TStream );
  { Decompresses input stream and stores in output stream. 
  Assumes both are created}
  procedure ExpandStream(inpStream,outStream: TStream);

  { Converts a TStream into a TByteDynArray}
  function ByteArrayFromStream( inStream : TMemoryStream ) : TByteDynArray;

  { Stores an array of bytes into a file}
  procedure ByteArrayToFIle( const ByteArray : TByteDynArray;
                              const FileName : string );
  { Decompresses an array of bytes, and stores result into a file}
  procedure ByteArrayCompressedToFIle( const ByteArray : TByteDynArray;
                                       const FileName : string );
  { Loads a file into an array of bytes}
  function FileToByteArray( const FileName : string ) : TByteDynArray;

  {Loads a file, compresses it and stores compressed data in an array of bytes}
  function FileToByteArrayCompressed( const FileName : string ) : TByteDynArray;

Now, how do we compress data before sending? On the client, before sending the file, it's compressed using:

    FileData := FileToByteArrayCompressed( OpenDialog1.FileName );
    (HTTPRIO1 as ISoapBinary).UploadFile(ExtractFileName(OpenDialog1.FileName), 
                                         FileData);
Essentially, the File is compressed into a byte array using the FileToByteArrayCompressed function. This "compressed" byte array is sent to the server.

On the server, UploadFile, which is what receives this compressed content, needs to decompress it before storing it. Here's what looks like:

  with (GetSoapWebModule as TBinWebModule) do
  begin
    IBDatabase.Connected := True;
    qryFile.ParamByName('FILENAME').AsString := FileName;
    qryFile.Open;
    if qryFile.EOF then
      qryFile.Insert
    else
      qryFile.Edit;

    // first decompress
    InStream := TMemoryStream.Create;
    DeCompressedStream := TMemoryStream.Create;
    try
      CopyToStream(FileData, InStream);
      InStream.Position := 0;
      ExpandStream( InStream, DecompressedStream );
      DecompressedStream.Position := 0;
    finally
      InStream.Free;
    end;

    (qryFile.FieldByName('FILEDATA') as TBlobField).LoadFromStream( 
                                                       DecompressedStream );
    qryFile.FieldByName('FILENAME').AsString := FileName;
    DecompressedStream.Free;
    // store in database
    qryFile.Post;
    qryFile.Transaction.Commit;
    qryFile.Close;
    IBDatabase.Connected := False;
  end;

The CopyToStream function copies the incoming Byte array into a TStream (InStream). Then the ExpandStream function decompresses the data and saves it in another TStream DecompressedStream (These are all in-memory TStreams. Your mileage might differ, so use a TFileStream with temporary files where necessary) Next, this decompressed data needs to be stored in a database - so we use the TBlobField.LoadFromStream functions to load data from the decompressed TStream.

Notice that we now store the data in an Interbase database. This is much more efficient and robust compared to using an in-memory dynamic array solution (plus, it takes care of the synchronization issues)

The download function is the same concept but the other way around - you compress at the server, and expand at the client.

Datapacket Compression

Assume you're not passing large files and you don't want to compress each individual parameter. And you think the SOAP datapacket is HUGE and it's unnecessary bandwidth on each response. You can compress the response (or the request) datapacket by using some events. But before that we must see the gains we will get, so let's see the response XML for the GetFileList Call:

HTTP/1.1 200 OK
Content-Type: text/xml
Content-Length: 594
Content:

<?xml version="1.0"?>

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">
  <SOAP-ENV:Body SOAP-ENC:encodingStyle="http://schemas.xmlsoap.org/soap/envelope/">
   <NS1:GetFileListResponse xmlns:NS1="urn:BinIntf-ISoapBinary">
     <return xsi:type="SOAP-ENC:Array" 
            SOAP-ENC:arrayType="xsd:string[2]">
		<item>file.doc.rtf</item>
		<item>file.rtf</item>
	 </return>
    </NS1:GetFileListResponse>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Now, let's add the following code in the server, in the main web unit's SoapPascalInvoker component - in the hander for AfterDispatchEvent .

procedure TBinWebModule.HTTPSoapPascalInvoker1AfterDispatchEvent(
  const MethodName: String; SOAPResponse: TStream);
var memStream : TMemoryStream;
begin
  // we must compress the data in the response stream
  SOAPResponse.Position := 0;
  memStream := TMemoryStream.Create;
  try
    CompressHelper.CompressStream(SoapResponse, memStream);
    SoapResponse.Position := 0;
    memStream.Position := 0;
    SoapResponse.CopyFrom(memStream, memStream.Size);
    SoapResponse.Size := memStream.Size;
    Response.ContentLength := memStream.Size;
  finally
    memStream.Free;   
  end;
end;
And in the client, add a handler for HTTPRio.AfterExecute:

procedure TForm1.HTTPRIO1AfterExecute(const MethodName: String;
  SOAPResponse: TStream);
var memStream : TMemoryStream;
begin
  // need to decompress stream here
  memStream := TMemoryStream.Create;
  try
    SoapResponse.Position := 0;
    CompressHelper.ExpandStream(SoapResponse, memStream);
    SoapResponse.Position := 0;
    memStream.Position := 0;
    SoapResponse.CopyFrom(memStream, memStream.Size);
    SoapResponse.Size := memStream.Size;
    SoapResponse.Position := 0;
  finally
    memStream.Free;
  end;
end;
This is it! Check out the response now, for the same call:

HTTP/1.1 200 OK
Content-Type: text/xml
Content-Length: 287
Content:

…binary stuff…
You can see that the content length has reduced from 594 down to 287, which is a reduction of nearly 50%! And all this in less than 15 lines of code.

You can do the same thing for the "Request" too (note here that the request is not compressed). You should use the "Before" events instead of the "After" events above, and remember to compress from the client, and expand on the server.

Using HTTP headers
You might want to write a server that can handle compressed requests/responses and uncompressed ones too. But the request or response datapacket needs to indicate that the data is compressed. Now you can add custom HTTP Headers from the client (for requests) and from the server (for responses) indicating that there is compression. For this, you have to do different things on the client and server:

  1. Server: Use the Response Object's AddCustomHeaders() function to add your own headers.
  2. Client: If you're not using INDY, then you can add headers using the HttpAddRequestHeaders (WinInet.pas). But there is a big problem in this since the parameter that you need to pass (a Handle to the request connection) is not exposed to you. You will need to modify SOAPHttpTrans.pas, the THTTPReqResp.Send() function. THere is a "Request:HINTERNET" local variable there - add it to the class as a member variable instead, and expose it using a public property. (This could be fixed in the next release of Delphi) Then you can use something like:
    
    HttpAddRequestHeaders( HttpRio1.HttpWebNode.Request, 
                           'Compression:zlib', 
                           Length('Compression:zlib'), 
                           HTTP_ADDREQ_FLAG_ADD);
    
    (I don't use Indy, but I'm sure there's something similar there for you to use)

Encryption

To ensure that wire tapping will not expose your data, you might need to encrypt it. Now you have many options.

Use HTTPS: Simply setting up a secure server ensures your datapacket is encrypted during the transfer. You needn't do anything special from the client or server, only you need to make sure your URL references have a "https" instead of "http".

You can encrypt specific parameters. The operation is very similar to compression above - just call the appropriate encryption function you may have, instead of the Compress/ExpandStream functions. Also you may want to encrypt the datapacket: again, my earlier example of compression may be used in principle.

The w3c also has drafts for encryption that you can look at. I haven't seen this implemented yet in either IE or Indy, but you might be able to use this in your application. The link you can start with is:
http://www.w3.org/TR/xmlenc-core/

Headers

The SOAP data usually goes under the <SOAP:Body> tag. There is also a SOAP:Header tag that allows you to add custom headers to the SOAP packet. SOAP Headers work exactly like cookies do in HTTP - if a server sends headers, the client should retain the sent headers with further calls.

How to use headers in Delphi soap

Delphi 7 supports SOAP headers. You can see the headers being used in the SOAPHeaders demo under Demos\WebServices in the Delphi installation folder. Here is how the client sets headers:


  H := AuthHeader.Create;
  try
    H.AccNumber := FAuthKey;
    H.TimeStamp := DateTimeToXSDateTime(FTimeStamp, True);
    { Add the Header to the outgoing message }
    if UseHeader then
      (svc as ISOAPHeaders).send(H);
    { Call the getBalance method }
    BalanceEdit.Text := CurrToStrF(svc.getBalance, ffCurrency, 2);
  finally
    H.Free
  end;
AuthHeader is a class inherited from TSOAPHeader. This class has published properties for AccountNumber and TimeStamp. Note also that you use :

(Svc as ISOAPHeaders).send(H);

which only sets the SOAPHeader - the header is sent only during the actual call (svc.getBalance) in this case. You can set multiple headers, using different classes inherited from TSOAPHeader.

If a client needs to see the headers that a server has sent, here is how the code would look like:


Headers := svc as ISoapHeaders;
  { Get the header from the incoming message }
  Headers.Get(AuthHeader, TSoapHeader(H));
  try
    if H <> nil then
    begin
      FAuthKey := H.AccNumber;
      FTimeStamp := H.TimeStamp.AsDateTime;
      { Refresh the balance edit }
      BalanceButton.Click;
    end
    else
      ShowMessage('No authentication header received from server');
  finally
    H.Free;
  end;
Note that you just cast the service (the HTTPRio component) as ISOAPHeaders, and use the Get function to get various classes of headers. The server can set multiple headers, and to retrieve a particular header class, simply send the header class as a parameter to the Get function.

At the server end, if you need to get the headers a client has sent:


var
  Headers: ISOAPHeaders;
  H: AuthHeader;
begin
  Result := 0;
  Headers := Self as ISoapHeaders;
  Headers.Get(authHeader, TSoapHeader(H));
Again, you cast "Self" (which is the implementation class in this case) as ISOAPHeaders, and retrieve the headers using the "Get" function.

To set headers from the server, the code would be:


  Headers := Self as ISoapHeaders;

  { Send back header }
  H:= AuthHeader.Create;
  { Return the header with the generated account info }
  H.AccNumber := AccNumber;
  H.TimeStamp := Now;
  Headers.Send(H);
Again, the "Send" function sets the headers, ready to be returned at the end of the call.

You can use headers in any service, as an authentication and authorization mechanism. You can use this in conjunction with encryption and compression, to ensure greater security to your data. You can also use headers as "cookies" - to maintain state across multiple web service calls. The first time a call comes in, the server sets a GUID as a header. The next calls have the GUID header, so the server can use this information to maintain state across calls. In fact, this is what is demonstrated in the Delphi sample used.

Attachments

In Delphi 7, you also have the ability to use SOAP attachments, a new standard that allows binary transfer. If you look at the SOAP packets in the earlier example, they contain the entire binary data encoded into the SOAP Payload itself, which isn't very coherant. To overcome this, the SOAP specification now includes support for Attachments. A SOAP attachment is separate from the SOAP payload itself, in the sense that it arrives in a different part of the transport packet. For instance, in HTTP, there is a facility to send multiple payloads to the server is the same connection - i.e. one part of it is the SOAP payload and the other could be attachment or binary data. In effect this is a "multipart" message, and the receiving end must understand this and decode the individual parts separately. SOAP Attachments come in as separate parts of the multipart message.

Note: Some web services use DIME encoding, such as MS SOAP SDK and .NET - Delphi attachments won't work with such services.

In Delphi 7, there's a new class called "TSOAPAttachment" - this is all you need in order to use attachments. If you open the SOAP Attachments demo that comes with Delphi 7(in the Demos\WebServices\SOAPAttachments folder in your Delphi installation) you will see a function definition as follows:

    
function  GetSpeciesInfo(const CommonName: WideString; 
                         out SpeciesInfo: TSpeciesInfo): TSOAPAttachment; stdcall;
The return value of this function is TSOAPAttachment which is essentially a binary file transferred through SOAP. In this case, the client gets a JPEG image as a TSOAPAttachment. The code used to then display the file is:
    
var FishPict : TSOAPAttachment;
  FishInfo: TSpeciesInfo;
  Src, Target: String;
begin
  CommonName := GetSelected(ListBox1);
  FishPict := GetAttachService.GetSpeciesInfo(CommonName, FishInfo);
  Src := Picture.CacheFile;
  Target := Src + '.bmp';
  RenameFile(src, Target);
  Image1.Picture.LoadFromFile(Target);
  FishPict.Free;
end;
FishPict is the SOAP Attachment returned by the Service (by the GetSpeciesInfo function). This attachment is automatically stored by Delphi in a temporary file referenced in its property "CacheFile". In the example above, the temporary file is renamed as a .bmp file and then loaded into a TImage.

This cached file is available for use as long as the TSOAPAttachment is in memory - if you need to use the file even after you have freed the TSOAPAttachment, then you must set the CacheFilePersist property to True. You can also get the type of content in the attachment (if it's set by the server) by using the TSOAPAttachment.ContentType property.

Also, if you are using IIS, the security on the Windows Temporary directory may not allow you to write there - if you are getting a TSOAPAttachment as a parameter to a function on the server. To change the default folder that a TSOAPAttachment writes these files in, go to your web module, and change the THTTPSoapDispatcher.Dispatcher.Converter.TempDir to a folder where you do have access.

From the server, you can return an attachment by doing:


        Result := TSoapAttachment.Create;
        Result.SetSourceFile(<filename>);
The TSoapAttachment.SetSourceFile function ensures that the TAttachment is loaded from a particular file before sending to the client. You can also use the SetSourceString or SetSourceStream methods.

Remember here that if you inherit a class (on the server) from TSOAPAttachment and send that instead, the client will only receive a TSOAPAttachment. You can't cast this to the descendant type at the client - any additional properties will not be marshalled.

Use TSOAPAttachment when you need to pass large files as attachments. Note that you can also use parameters, but Attachments have a few advantages:

  1. Attachments is a SOAP standard to send binary data, and this will result in a lot more web service vendors supporting it rather than using parameters.
  2. If you use attachments you separate the data from the SOAP XML packet. This gives you greater flexibility.
  3. Web servers might be able to use different methodologies to handle attachments (such as compress/encrypt only attachments) which aren't possible with parameter based attachments.

Interoperability

Web services are being supported by nearly every development tool, and in different ways. Regardless of the fact that SOAP is a standard, a SOAP implementation can have trouble talking to another one.

Various implementors have come together to address interoperability issues and have build an Interoperability Lab, where they demonstrate what works and what does not work between any two SOAP implementations. Check out:

http://www.whitemesa.com/interop.htm

for details. Delphi SOAP has entries here, and you can see that certain tests fail with certain implementations. The Borland Interop site is located at http://soap-server.borland.com. Most problems lie in the fact that nearly every implementation needs to handle details in a different way, especially arrays.

RPC|Encoded vs. Document|Literal

The biggest problem in interoperability is the fact that some servers use Document|Literal encoding, which most others use RPC|Encoded. Delphi servers only understand RPC|Encoded packages, but Delphi Clients can talk to both RPC|Encoded and Document|Literal servers.

To communicate with a Doc|Lit server (Most .NET web services are doc|lit) you must set HTTPRio1.Converter.Options.soLiteralParams to true. This ensures that parameters encoded with "literal" are not unwound, which is necessary in doc|lit cases where a parameter name might be encapsulated under a sub tag under the XML element representing the function.

In Delphi 7, Document encoding is supported in one way - you can make Delphi 7 client applications (consumers) for Document based webservices. What you cannot do is write Document based servers. If you're writing a client for a document based server, you can import the WSDL and Delphi's WSDL Importer will recognize that this is a document based server and generate necessary code for the conversion. FOr instance it will add the following call to the initialization section of your imported .pas file:


InvRegistry.RegisterInvokeOptions(TypeInfo(DataServiceSoap), ioDocument);
This ensures that an Invoke call that uses the interface DataServiceSoap will be formatted using Document rules (not RPC). You can also use ioLiteral for doc-lit services. Using ioLiteral also ensures that your input and output types won't be "unwound" - this needs some explanation. In RPC encoding, each method call is preceded by a method node in the XML datapacket. In Literal encoding, the method name is skipped and the parameters are unwound into XML. This and many small differences are actually handled directly by the Delphi SOAP runtime itself, so you don't have to worry about them individually. But, use a proxy server to actually find out what's happening behind the scenes in case there's a problem.

Examples: You will find an example of accessing a .NET service over the web. This is a .NET service, available at http://webservices.empowered.com/statsws/stats.asmx?WSDL and the example demonstrates a simple WSDL and function call - I had to do no other code changes. There are more complex servers like Terraserver and MapPoint, which unfortunately require a subscription and therefore cannot be demonstrated. Nevertheless, try them out if you have a subscription.

Conclusion

We have seen some of the more advanced areas in Web Services, but there are a number of areas still being developed. There's Universal Description, Discovery, and Integration (UDDI), which is a "directory" of Webservices. You can search for Web Services in a global UDDI registry, or in a local intranet registry, and find services that fulfil what you need. Delphi 7 comes with a UDDI browser, which you can access through the WSDL importer. Click the "Search UDDI..." button to look for web services in universal registries, and you can directly choose a service you like, import it, and start using it in Delphi.

There's also "WSIL" - Web Services Inspection Language" which is a format for Inspection of a site for available services, and to provide information about how to access them. For instance, a web site may provide a service available through a WSDL and through UDDI, and another service which is SMTP (e-mail) based whose WSDL is available only through FTP. This can be exposed through a WSIL document.

We have traditionally used HTTP as the transport for Web Services. There are more protocols getting defined for other transports - like SOAP over SMTP etc. There are more standards coming up for Web services, and more advances will be made in WSDL, SOAP, Encryption, Compression etc. This has been a really interesting time for Web Services, and hopefully we'll be able to keep in touch with this massive flow of information!