这篇文章讲述如何用 WebService 下载一个文件。它采用的是 TByteDynArray 来传递文件的二进制数据。重点在
FileToByteArray 这个函数和另一个反向操作的函数。
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)