``` delphi中利用Indy的TIdFtp控件實現FTP協議 版權聲明:本文為博主原創文章,未經博主允許不得轉載。 現在很多應用都需要上傳與下載大型文件,通過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; 使用此類我們的程序可以變得更簡單,如何修改就留給讀者自己去完善吧。 ```
Tags: 電影服務器 程序設計 程序員 緩沖區 局限性
文章來源: