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:
(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 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/soap/binarytransfer.zip. If you
have any questions, mail me at shenoy@agnisoft.com. Do let me know how
you liked this article.
To make a real world application MORE clear, I'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/soap/binarycompress.zip.
About the Author
Deepak Shenoy is one of the
founders of Agni Software, and has been working with Web Services in Delphi
and Microsoft's .NET for a while now.