case studies white papers downloads technology process careers sitemap contact
about us products solutions domains services projects search clients
Technical papers

 

 

Binary File Transfer using SOAP


Abstract
This article talks about how to transfer files using Soap - explained using Delphi 6 Enterprise. You'll learn to transfer files, both upload and download.

Introduction
This is a paper about transferring binary data using the Web Services implementation in Delphi 6 Enterprise. This is a topic discussed frequently at news://newsgroups.borland.com/borland.public.delphi.webservices.soap but there wasn't a paper online (that I knew of). Well, here it is.

What I'm going to show you is how you can transfer binary data across the world using Soap. I'll create both Server and Client in Delphi 6, and transfer files both up and down.

Writing the Server

Let the show begin! Run Delphi 6, 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:
My very own rendition of what I believe Soap is, at SOAP Introduction

Nick Hodges' excellent walk-through of building your first Soap Server and Client : http://community.borland.com/article/0,1410,27399,00.asp.

Now I'm going to assume you're all fired up with this Soap thing. 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.

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 we don't want to do that)

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. 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:
(http://localhost:1024/uBinaryWad.BinaryServer/wsdl/ISoapBinary
)
(You'll see a unit quite similar to the Interface you had created.) Once we have that, here's a sample client form

 

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 a little while 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 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

 

BytesRead := Length(Request.Content);
// Fixed code:
if BytesRead < Request.ContentLength then
begin
SetLength(Buffer, Request.ContentLength);
Stream.Write(Request.Content[1], BytesRead);
repeat
// --> added "[BytesRead]"
ChunkSize := Request.ReadClient(Buffer[BytesRead],
Request.ContentLength - BytesRead);
if ChunkSize > 0 then
begin
// --> added "[BytesRead]"
Stream.Write(Buffer[BytesRead], ChunkSize);
Inc(BytesRead, ChunkSize);
end;
// --> changed from "until ChunkSize = -1" to:
until (BytesRead = Request.ContentLength) or
(ChunkSize <= 0);
end else
Stream.Write(Request.Content[1], BytesRead);
// End fixed code
Stream.Position := 0;

You'll need to include the changed WebBrokerSOAP.PAS in your project path.
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 text for another article.
Where? How? Etc.

The files are all at http://www.agnisoft.com/downloads (binarytransfer.zip). If you have any questions, mail us. Do let us know how you liked this article.
To make a real world application MORE clear, we've written a sample that does Compression and Decompression too! 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.

This should be fairly clear from this NEW sample - available at: http://www.agnisoft.com/downloads (binarycompress.zip)

-------------------------------------------------------------------------------------------------------

 

© 1998-2012, Agni Software (P) Ltd. All Rights reserved.