现在很多应用都需要上传与下载大型文件,通过HTTP方式上传大文件有一定的局限性。幸好FTP作为一个非常老而且非常成熟的协议可以高效稳定地完 成大文件的上传下载,并且可以完美地实现续传。就拿我写的电影服务器管理端程序来说,各种方案比较后,发现使用FTP可以完美地实现要求。但是要通过 WinSocket库实现FTP比较麻烦,幸好有Indy--一个包装了大多数网络协议的组件包。
通过Indy,程序设计人员可以通过阻塞方式进行编程,可以抛开蹩脚的Winsocket异步模式,采用与Unix系统上等同的阻塞编程模式进行。这样,程序员就可以很好的处理程序的运行流程。
下面,我们进入到Indy的TIdFtp世界。
1.控件的说明
使用Indy 9中的TIdFtp控件可以实现通过FTP方式进行文件的上传与下载。
2.控件的具体使用
(1)控件属性设置
默 认属性即可,与服务器连接直接相关的属性如主机名与用户等在建立连接时进行设定。需要设定的是RecvBufferSize和 SendBufferSize两属性的值。另外需要根据要传输的文件类型指定TransferType属性,而其他属性按默认值设定即可。
RecvBufferSize说明(默认值为8192字节):该属性为整型变量,用于指定连接所用的接受缓冲区大小。
SendBufferSize 说明(默认值为32768字节):该属性也为整型变量,用于指定连接所用的发送缓冲区的最大值。该属性在WriteStream方法中时,可用于 TStream指定要发送内容的块数。如果要发送的内容大于本属性值,则发送内容被分为多个块发送。
TransferType说明(默认 值为ftBinary):该属性为TIdFTPTransferType型变量。用于指定传输内容是二进制文件(ftBinary )还是ASCII文件(ftASCII)。应用程序需要使用二进制方式传输可执行文件、压缩文件和多媒体文件等;而使用ASCII方式传输文本或超文本等 文本型数据。
(2)控件的事件响应
OnDisconnected响应:TNotifyEvent类,用于响应断开(disconnect)事件。当Disconnect方法被调用用来关闭Socket的时候,触发该响应。应用程序必须指定该事件响应的过程,以便对该断开事件进行相应。
OnStatus 响应:TIdStatusEvent类。该响应在当前连接的状态变化时被触发。该事件可由DoStatus方法触发并提供给事件控制器属性。 axStatus是当前连接的TIdStatus值;aaArgs是一个可选的参数用于格式化函数,它将用于构造表现当前连接状态的文本消息。
OnWork 响应:OnWord是TWorkEvent类事件的响应控制器。OnWork用于关联DoWork方法当缓冲区读写操作被调用时通知Indy组件和类。它 一般被用于控制进度条和视窗元素的更新。AWorkMode表示当前操作的模式,其中:wmRead-组件正在读取数据;wmWrite-组件正在发送数 据。AWorkCount指示当前操作的字节计数。
OnWorkBegin响应:TWorkBeginEvent类。当缓冲区读 写操作初始化时,该事件关联BeginWork方法用于通知Indy组件和类。它一般被用于控制进度条和视窗元素的更新。AWorkMode表示当前操作 的模式,其中:wmRead-组件正在读取数据;wmWrite-组件正在发送数据。AWorkCountMax用于指示发送到OnWorkBegin事 件的操作的最大字节数,0值代表未知。
OnWorkEnd响应:TWorkEndEvent类。当缓冲区读写操作终止时,该事件 关联EndWork方法用于通知Indy组件和类。AWorkMode表示当前操作的模式,其中:wmRead-组件正在读取数据;wmWrite-组件 正在发送数据。AWorkCount表示操作的字节数。
在事件响应中,主要通过上述五种事件响应来控制程序。在一般情况下,在 OnDisconnected中设定连接断开的界面通知;在OnStatus中设定当前操作的状态;在OnWork中实现传输中状态条和其他参数的显示; 而在OnWorkBegin和OnWorkEnd中分别设定开始传输和传输结束时的界面。
(3)连接远程服务器
完 成了设定控件属性和实现了控件的事件响应后,就可以与服务器进行交互和传输了。在连接之前,应首先判断IdFtp是否处于连接状态,如果 Connected为False,则通过界面控件或其他方式指定与服务器连接相关的一些TCP类属性的设置,分别是:Host(主机名):String、 Username(用户名):String、Password(密码):String,也可以指定Port(端口)。之后调用Connect方法连接远程 服务器,如果无异常出现则连接成功建立。
过程说明:procedure Connect(AAutoLogin: boolean; const ATimeout: Integer);
该过程连接远程FTP服务器
属性:AAutoLogin: boolean = True
连接后自动登录,该参数默认为True。
const ATimeout: Integer = IdTimeoutDefault
超时时间,单位:秒。
示例代码:
if IdFTP1.Connected then try
if TransferrignData then IdFTP1.Abort;
IdFTP1.Quit;
finally
end
else with IdFTP1 do try
Username := UserIDEdit.Text;
Password := PasswordEdit.Text;
Host := FtpServerEdit.Text;
Connect;
ChangeDir(CurrentDirEdit.Text);
finally
end;
(4)改变目录
连 接建立后,可以改变当前FTP会话所在的目录。对于已知绝对路径的情况下,可以直接调用ChangeDir(const ADirName: string)方法来转换目录,ADirName表示服务器上的文件系统目录,另外还可以调用ChangeDirUp回到上级目录。
如 果未知路径,则可以通过List(ADest: TStrings; const ASpecifier: string; const ADetails: boolean)过程获取远程服务器的当前目录结构,此时必须设定TransferType为ftASCII(ASCII模式),其中:ADest保存当 前目录结构,可以在后续程序中调用该列表。另外可以通过RetrieveCurrentDir方法获取当前目录名。
过程说明:
procedure ChangeDir(const ADirName: string);
改变工作目录
属性
const ADirName: string
远程服务器的目录描述
说明:该过程实际上是实现了FTP CWD命令。
procedure ChangeDirUp;
到上一级目录
function RetrieveCurrentDir: string;
该函数返回当前目录名
procedure List(ADest: TStrings; const ASpecifier: string; const ADetails: boolean);
列出当前目录所有文件和子目录及其属性
参数:
ADest: TStrings
保存文件及子目录的返回结果
const ASpecifier: string = ''
文件掩码,用于列出符合条件的文件
const ADetails: boolean = true
包含文件和子目录属性
property DirectoryListing: TIdFTPListItems;
返回文件及目录结构的列表
示例代码:
LS := TStringList.Create;
try
IdFTP1.ChangeDir(DirName);
IdFTP1.TransferType := ftASCII;
CurrentDirEdit.Text := IdFTP1.RetrieveCurrentDir;
DirectoryListBox.Items.Clear;
IdFTP1.List(LS);
DirectoryListBox.Items.Assign(LS);
if DirectoryListBox.Items.Count > 0 then
if AnsiPos('total', DirectoryListBox.Items[0]) > 0 then DirectoryListBox.Items.Delete(0);
finally
LS.Free;
end;
(5)下载的实现
在 下载之前,必须查看DirectoryListing.Items[sCurrFile].ItemType是否为文件,如返回为 ditDirectory则代表当前文件名为目录,不能下载,必须导向到文件才可。如为文件,则可以进行下载。在下载前,设定传输的类型为二进制文件,并 且指定本地要保存的路径。通过调用Get方法,实现文件的下载。下载过程较慢,可以考虑将其放到线程中实现。
过程说明:
procedure Get(const ASourceFile: string; ADest: TStream; AResume: Boolean); overload;
procedure Get(const ASourceFile: string; const ADestFile: string; const ACanOverwrite: boolean; AResume: Boolean); overload;
从远程服务器上获取文件。
属性说明:
const ASourceFile: string
远程服务器上的源文件名
const ADestFile: string
保存到客户机上的文件名
const ACanOverwrite: boolean = false
重写同名文件
AResume: Boolean = false
是否进行断点续传
示例代码:
SaveDialog1.FileName := Name;
if SaveDialog1.Execute then begin
SetFunctionButtons(false);
IdFTP1.TransferType := ftBinary;
BytesToTransfer := IdFTP1.Size(Name);
if FileExists(Name) then begin
case MessageDlg('File aready exists. Do you want to resume the download operation?',
mtConfirmation, mbYesNoCancel, 0) of
mrYes: begin
BytesToTransfer := BytesToTransfer - FileSizeByName(Name);
IdFTP1.Get(Name, SaveDialog1.FileName, false, true);
end;
mrNo: begin
IdFTP1.Get(Name, SaveDialog1.FileName, true);
end;
mrCancel: begin
exit;
end;
end;
end
else begin
IdFTP1.Get(Name, SaveDialog1.FileName, false);
end;
(6)上传的实现
上传的实现与下载类似,通过put方法即可。
过程说明:
procedure Put(const ASource: TStream; const ADestFile: string; const AAppend: boolean); overload;
procedure Put(const ASourceFile: string; const ADestFile: string; const AAppend: boolean); overload;
上传文件至服务器
属性说明:
const ASourceFile: string
将要被上传的文件
const ADestFile: string = ''
服务器上的目标文件名
const AAppend: boolean = false
是否继续上传
代码示例:
if IdFTP1.Connected then begin
if UploadOpenDialog1.Execute then try
IdFTP1.TransferType := ftBinary;
IdFTP1.Put(UploadOpenDialog1.FileName, ExtractFileName(UploadOpenDialog1.FileName));
//可以在此添加改变目录的代码;
finally
//完成清除工作
end;
end;
(7)删除的实现
删除文件使用Delete方法,该方法删除指定的文件,删除对象必须为文件。如果要删除目录则使用RemoveDir方法。
过程说明:
procedure Delete(const AFilename: string);
删除文件
procedure RemoveDir(const ADirName: string);
删除文件夹,根据不同的服务器删除文件夹有不同的要求。有些服务器不允许删除非空文件夹,程序员需要添加清空目录的代码。
上述两个过程的参数均为目标名称
代码示例:
if not IdFTP1.Connected then exit;
Name := IdFTP1.DirectoryListing.Items[iCurrSelect].FileName;
if IdFTP1.DirectoryListing.Items[iCurrSelect].ItemType = ditDirectory then try
idftp1.RemoveDir(Name);
finally
end
else
try
idftp1.Delete(Name);
finally
end;
(8)后退的实现
后退在实际上是目录操作的一种,可以简单的改变当前目录为..来实现,也可以通过回到上级目录来实现。
(9)取消的实现
在IdFtp的传输过程中,可以随时使用abort方法取消当前操作。可以的OnWork事件的实现中来确定何时取消操作。
代码示例:
//取消按钮的OnClick响应
procedure TMainForm.AbortButtonClick(Sender: TObject);
begin
AbortTransfer := true;
end;
//IdFTP的OnWork事件响应
procedure TMainForm.IdFTP1Work(Sender: TObject; AWorkMode: TWorkMode;
const AWorkCount: Integer);
begin
...
if AbortTransfer then IdFTP1.Abort;
AbortTransfer := false;
end;
(10)断点续传的实现
断点续传就是在上传或下载过程开始时,判断已经传输过的文件是否上传输完毕,如果传输没有成功完成,则在上次中断处继续进行传输工作。实现该功能需要两个重要的操作,首先是判断文件的大小信息,其次是在传输过程Get和Put中指定上传的行为。
判断服务器上文件的大小使用函数Size(FileName)。在下载过程中,比较本地文件和远程文件的信息,然后在Get中指定AResume := True即可。而上传也一样,指定Put的AAppend := True就可以了。
在 前面我们讲过,Indy的网络操作大部分是阻塞模式的,TIdFtp也不例外。这样在上述各个操作运行过程的时候用户界面被暂时冻结,必须要等待调用返回 才能继续用户操作界面响应。所以在实际编程中,需要使用多线程的方式来保证户界面的响应。Windows系统可以使用CreateThread系统调用来 创建线程,但是在使用的时候需要开发人员做很多额外的工作来保证线程的同步等问题。而Indy中也包含了实现多线程的控件 TIdThreadComponent,相对比之下该控件实现多线程时更加方便,也更容易控制。我将在后续的文章里为大家介绍 TIdThreadCOmponent的使用方法。
下载的一些代码
接下来我们来写最主要的代码,也就是下载部分了,首先来看HTTP协议的:
procedure TForm1.HttpDownLoad(aURL, aFile: string; bResume: Boolean);
var
tStream: TFileStream;
begin //Http方式下载
if FileExists(aFile) then //如果文件已经存在
tStream := TFileStream.Create(aFile, fmOpenWrite) else
tStream := TFileStream.Create(aFile, fmCreate);
if bResume then //续传方式
begin
IdHTTP1.Request.ContentRangeStart := tStream.Size - 1;
tStream.Position := tStream.Size - 1; //移动到最后继续下载
IdHTTP1.Head(aURL);
IdHTTP1.Request.ContentRangeEnd := IdHTTP1.Response.ContentLength;
end else //覆盖或新建方式
begin
IdHTTP1.Request.ContentRangeStart := 0;
end;
try
IdHTTP1.Get(aURL, tStream); //开始下载
finally
tStream.Free;
end;
end;
这里我们同样使用IdHTTP的Get过程,函数的aURL是网址,aFile是保存的文件名,bResume确定是否续传,需要注意的就是续传方式时的代码:
IdHTTP1.Request.ContentRangeStart := tStream.Size - 1;
tStream.Position := tStream.Size - 1; //移动到最后继续下载
IdHTTP1.Head(aURL);
IdHTTP1.Request.ContentRangeEnd := IdHTTP1.Response.ContentLength;
第 一行我们将下载开始位置设置为读入文件流的末尾,也就是设置为已经下载了的那部分文件的大小,第二行我们将文件流本身也指向自己的末尾,第三行我们通过 Head过程得到网址头信息,在第四行将头信息的文件总大小赋值给下载的结束的位置,至于这里为什么第一行和第二行代码最后都要-1,我当时没有加-1的 时候在续下载一个完整的已经下载的文件的时候总是提示错误,最后跟踪IdHTTP的代码发现他在处理下载范围的时候如果开始的位置和结束位置一样时会引发 将浮点数转为整数的错误,因而这里加上-1防止这种错误发生,另外一种处理方法就是比较如果开始位置等于结束位置就退出也是可以的。
再来看FTP协议的下载过程:
procedure TForm1.FtpDownLoad(aURL, aFile: string; bResume: Boolean);
var
tStream: TFileStream;
sName, sPass, sHost, sPort, sDir: string;
begin //ftp方式下载
if FileExists(aFile) then //建立文件流
tStream := TFileStream.Create(aFile, fmOpenWrite) else
tStream := TFileStream.Create(aFile, fmCreate);
GetFTPParams(aURL, sName, sPass, sHost, sPort, sDir);
with IdFTP1 do
try
if Connected then Disconnect; //重新连接
Username := sName;
Password := sPass;
Host := sHost;
Port := StrToInt(sPort);
Connect;
except
exit;
end;
IdFTP1.ChangeDir(sDir); //改变目录
BytesToTransfer := IdFTP1.Size(aFile);
try
if bResume then //续传
begin
tStream.Position := tStream.Size;
IdFTP1.Get(aFile, tStream, True);
end else
begin
IdFTP1.Get(aFile, tStream, False);
end;
finally
tStream.Free;
end;
end;
这 个过程中我们就用到了GetFTPParams()函数将网址的用户名、密码、主机地址、端口、路径等信息分离出来,IdFTP利用这些信息登陆服务器并 到相应目录,最后利用Get()过程就很容易实现下载了,它的续传就比HTTP协议要简单很多,因为IdFTP的Get()本身就支持续传。
这里我简单穿插一点的内容,一个服务器是否支持断点续传,我们可以通过发送"REST 1"FTP指令来检测,如果返回350则表示支持。
最后我们根据网址来确定使用什么协议来下载:
function TForm1.GetProt(aURL: string): Byte;
begin //检测下载的地址是http还是ftp
Result := 0;
if Pos('http', LowerCase(aURL)) = 1 then
Result := 1; //http协议
if Pos('ftp', LowerCase(aURL)) = 1 then
Result := 2; //ftp协议
end;
这个函数根据网址返回整数供我们使用。
procedure TForm1.MyDownLoad(aURL, aFile: string; bResume: Boolean);
begin
case GetProt(aURL) of
0: ShowMessage('不可识别的地址!');
1: HttpDownLoad(aURL, aFile, bResume);
2: FtpDownLoad(aURL, aFile, bResume);
end;
end;
这个过程就利用GetProt()函数返回的整数执行相应的协议下载过程。
(2) 接下来看看每个按钮的代码,有了上面的函数,按钮的代码就简单多了:
下载按钮:
procedure TForm1.Button1Click(Sender: TObject);
var
aURL, aFile: string;
begin
aURL := ComboBox1.Text; //下载地址,例如" http://www.2ccc.com/update/demo.exe";
aFile := GetURLFileName(aURL); //得到文件名,例如"demo.exe"
if FileExists(aFile) then
begin
case MessageDlg('文件已经存在,是否续传?', mtConfirmation, mbYesNoCancel, 0) of
mrYes: MyDownLoad(aURL, aFile, True); //续传
mrNo: MyDownLoad(aURL, aFile, False); //覆盖
mrCancel: Exit; //取消
end;
end else MyDownLoad(aURL, aFile, False); //建立新文件下载
end;
MessageDlg()函数弹出一个对话框让用户选择续传、覆盖还是取消下载。
中断按钮:
procedure TForm1.Button2Click(Sender: TObject);
begin
AbortTransfer := True;
end;
前 面忘了介绍,所以这里大家看不明白,AbortTransfer是我们定义的一个私有变量,在开始下载的时候将它设为False,下载的过程中随时监测这 个变量,一旦变为True就利用IdHTTP的Disconnect和IdFTP1的Abort方法中断下载,如果没有下载完就中断,那程序的目录中就会 有一个下载不完整的程序或者其他东西,下次再下载的时候我们就可以选择续传来完成剩下的下载过程。
procedure TForm1.IdHTTP1WorkBegin(Sender: TObject; AWorkMode: TWorkMode;
const AWorkCountMax: Integer);
begin
AbortTransfer := False;
……
end;
在IdHTTP1和IdFTP的OnWorkBegin事件我们就将AbortTransfer设置为False了,在他们的Work事件中,我们检测AbortTransfer变量来完成是否中断的操作。
procedure TForm1.IdHTTP1Work(Sender: TObject; AWorkMode: TWorkMode;
const AWorkCount: Integer);
begin
if AbortTransfer then
begin //中断下载
IdHTTP1.Disconnect;
IdFTP1.Abort;
end;
ProgressBar1.Position := AWorkCount;
Application.ProcessMessages;
end;
(3) 最后是连接状态等信息的代码:
在IdHTTP和IdFTP的OnStatus事件写入:
procedure TForm1.IdHTTP1Status(ASender: TObject; const AStatus: TIdStatus;
const AStatusText: string);
begin
ListBox1.ItemIndex := ListBox1.Items.Add(AStatusText);
end;
因为IdHTTP和IdFTP在OnWork、OnStatus等事件上执行的代码都是一样的,所以我们只用写其中一个的代码,然后另外一个选择相同的事件就OK了。
图8.3.4
3.全部代码写完收工,F9运行一下看看效果,是不是能断点续传。
【程序小结】
本程序主要的功能由IdHTTP和IdFTP组件完成,主要掌握他们的Get过程实现断点续传的方法以及字符串的分析分解方法,这里我们同样使用了流格式,不过这次不是内存流而是文件流。通过本例,读者应该初步掌握调试程序时断点的使用,事件代码的共用等。
【作者后话】
在写完这篇文章不久,作者偶然间察看了Indy系列组件的帮助,发现一个封装了分析URL结构的类TIdURI,在IdURI单元,这个类可以很轻松的将我们上面的GetFTPParams()函数的功能实现,例如:
var
URI: TIdURI;
begin
URI := TIdURI.Create(aURL); //建立
try
sProtocol := URI.Protocol; //协议
sHost := URI.Host; //主机
//……等等都可以通过URI的属性得到
finally
URI.Free;
end;
end;
使用此类我们的程序可以变得更简单,如何修改就留给读者自己去完善吧。