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