ftp下載的好處我在這里就不多說了,許多工程會把ftp下載作為一個(gè)重要的功能來實(shí)現(xiàn)。微軟提供的WinInet類可以利用下面這些函數(shù):
InternetOpen;
InternetConnect;
GetCurrentDirectory;
SetCurrentDirectory;
FtpGetFile;
很容易實(shí)現(xiàn)ftp的下載,網(wǎng)上關(guān)于這方面的文章也很多。但是要實(shí)現(xiàn)ftp的多線程下載,利用這些函數(shù)就顯得有些牽強(qiáng)了。用socket根據(jù)ftp協(xié)議來開發(fā)將會變的十分靈活。下面我就逐步的講解整個(gè)開發(fā)的過程:開發(fā)環(huán)境 BCB(組件模式),VC 環(huán)境下請自行稍作改動。看了這篇文章后對于BCB開發(fā)人員來說,不僅可以對 FlashGet 等軟件的開發(fā)原理有一定的了解,特別是在開發(fā)組件方面也有很大的指導(dǎo)作用,請耐心的將它看完。很簡單!!
首先介紹一下部分ftp協(xié)議:

圖一 FTP服務(wù)示意圖
用戶FTP和服務(wù)器FTP之間要傳送文件,需要有兩個(gè)連接:命令通道和數(shù)據(jù)連接,從名字上就可以看出命令通道是傳送命令的,數(shù)據(jù)通道是用于傳送文件。服務(wù)器與服務(wù)器之間的數(shù)據(jù)傳送在此就不多作解釋。
主要用到的命令為:USER,PASS,TYPE,SIZE,REST,CWD,PWD,RETR,PASV,PORT,QUIT;
USER:參數(shù)是標(biāo)記用戶的Telnet串。用戶標(biāo)記是訪問服務(wù)器必須的,此命令通常是控制連接后第一個(gè)發(fā)出的命令,有些主機(jī)還會要求口令和帳戶。服務(wù)器可以在任何時(shí)間接收新的USER命令以改變訪問控制和(或)帳戶信息。這可以重新開始登錄過程,所以傳輸參數(shù)不變,在進(jìn)行中的文件傳輸在過去的訪問控制參數(shù)下完成。
PASS:參數(shù)是標(biāo)記用戶口令的Telnet串。此命令緊跟USER命令,在某些站點(diǎn)它是完成訪問控制不可缺少的一步。因此口令是個(gè)重要的東西,因此不能顯示出來,服務(wù)器方?jīng)]有辦法隱藏口令,所以這一任務(wù)得由用戶FTP進(jìn)程完成。
TYPE:參數(shù)指定表示類型。有些類型需要第二個(gè)參數(shù),第一個(gè)參數(shù)由單個(gè)Telnet字符定義,第二個(gè)參數(shù)是十進(jìn)制整數(shù)指定字節(jié)大小,參數(shù)間以<SP>分隔。下面是格式:

圖二 TYPE參數(shù)示意圖
默認(rèn)表示類型是ASCII非打印字符,如果參數(shù)未改變,以后只改變了第一個(gè)參數(shù),則使用默認(rèn)值。
SIZE:參數(shù)從FTP服務(wù)器上返回指定文件的大小。
REST:參數(shù)域代表服務(wù)器要重新開始的那一點(diǎn),此命令并不傳送文件,而是略過指定點(diǎn)后的數(shù)據(jù),此命令后應(yīng)該跟其它要求文件傳輸?shù)腇TP命令。
CWD:此命令使用戶可以在不同的目錄或數(shù)據(jù)集下工作而不用改變它的登錄或帳戶信息。傳輸參數(shù)也不變。參數(shù)一般是目錄名或與系統(tǒng)相關(guān)的文件集合。
PWD:改變當(dāng)前的工作目錄。
RETR:開始傳送指定的文件。(從REST參數(shù)指定的偏移量開始傳送)
PASV:此命令要求服務(wù)器DTP在指定的數(shù)據(jù)端口偵聽,進(jìn)入被動接收請求的狀態(tài),參數(shù)是主機(jī)和端口地址。
PORT:參數(shù)是要使用的數(shù)據(jù)連接端口,通常情況下對此不需要命令響應(yīng)。如果使用此命令時(shí),要發(fā)送32位的IP地址和16位的TCP端口號。上面的信息以8位為一組,逗號間隔十進(jìn)制傳輸。
QUIT:退出登錄。
各個(gè)參數(shù)的具體用法舉例如下:
USER sandy \r\n //用戶名為sandy登錄
PASS sandy \r\n //密碼為sandy
TYPE I \r\n
SIZE sandy.txt \r\n //如果sandy.txt文件存在,則返回該文件的大小
REST 100 \r\n //重新指定文件傳送的偏移
CWD infor/ \r\n //獲取當(dāng)前的工作目錄
PWD temp/ \r\n //改變當(dāng)前的工作目錄
RETR \r\n //開始傳送文件
PASV \r\n //進(jìn)入被動模式
PORT h1,h2,h3,h4,p1,p2 \r\n //進(jìn)入主動模式,h1,h2,h3,h4為ip地址的4個(gè)部分。p1,p2是16進(jìn)制的端口號
下面介紹一下各個(gè)函數(shù)的使用順序和一些應(yīng)注意的地方:
使用這些命令的前提條件是客戶端和服務(wù)器端建立了連接。比如ftp服務(wù)器地址:192.168.1.81 ,端口:21。那么利用Winsock的API函數(shù)建立socket連接,然后使用USER,PASS登陸FTP服務(wù)器.需要下載文件,要確保文件必須在當(dāng)前工作目錄下,可以使用命令CWD和PWD。查看和更改當(dāng)前的工作目錄。使用SIZE命令獲取文件的大小。我們想要多線程下載那么就要求服務(wù)器支持該功能。一般我們都會在開頭先使用REST命令判斷該ftp站點(diǎn)是否支持多線程下載。PORT和PASV兩個(gè)命令是用來建立數(shù)據(jù)連接的。他們的主要區(qū)別是:PORT需要你指定一個(gè)ip地址和端口與服務(wù)器建立連接。PASV命令服務(wù)器會返回h1,h2,h3,h4,p1,p2樣式 的數(shù)據(jù)供客戶端連接。等數(shù)據(jù)連接建立后,就可以了使用REST,RETR進(jìn)行多線程和斷點(diǎn)續(xù)傳文件下載了。
上面講解了一點(diǎn)ftp下載的基本知識,下面主要介紹的是斷點(diǎn)續(xù)傳的文件保存技巧。
若要講斷點(diǎn)續(xù)傳的文件保存方式至少可以說出10種,但是各種方法都有利有弊,下面主要介紹一種我在工作中常常使用的一種文件保存方式:比如要下載一個(gè)364544字節(jié)的文件,文件名為:namelock.avi。因?yàn)橐獢帱c(diǎn)續(xù)傳,所以 在下載的過程中必須得保存文件的大小,已經(jīng)下載的文件的大小和各個(gè)線程的任務(wù)。
有兩種方法:
一、可以產(chǎn)生兩個(gè)文件:內(nèi)容文件和配置文件。
二、只需一個(gè)文件:把配置文件的數(shù)據(jù)加載到內(nèi)容文件的末尾。
這兩個(gè)都不失為好方法。我使用的是前一種,因?yàn)槲宜接邢蓿▽τ谂R界資源的訪問總是不能做到互坼,老出問題。)。這里 的后綴名希望大家要把它放在心上,后綴名是個(gè)象征性的東西。就拿我們公司來說,擁有自己的MPEG編碼、解碼技術(shù),比如原來5m的一首mp3歌曲,通過編碼可以 轉(zhuǎn)換成500K左右的.fun文件(funinhand的前三個(gè)字)。再利用我們自己的解碼播放器邊下載邊解碼邊播放, 音質(zhì)和mp3不相上下。真正實(shí)現(xiàn)了手機(jī)上的流媒體技術(shù)。受到國內(nèi)外高科技大公司的信賴。(不好意思,這里有點(diǎn)像做廣告了。)講這些的另外一個(gè)企圖是這樣的:
內(nèi)容文件所使用的后綴名是我女朋友的英文名(namelock)的前三個(gè)字母.nam 。配置文件使用的是我自己的英文名(sandy)的前三個(gè)字母.san 。所以說寫程序也可以很浪漫,因?yàn)檫@,女朋友又給了我的月生活零用錢增加了幾元,哈哈(大家也可以效仿)。言歸正傳,這兩個(gè)文件嚴(yán)格意義上來講是臨時(shí)文件,當(dāng)文件下載完畢的時(shí)候,namelock.avi.nam內(nèi)容文件應(yīng)該改名為:namelock.avi。namelock.avi.san配置文件也應(yīng)該及時(shí)的刪除。
FTP多線程下載技術(shù)部分:前面我介紹了文件的保存技巧,主要也是為了多線程服務(wù)。現(xiàn)在有個(gè)namelock.avi文件需要下載。文件的大小為:364544字節(jié)。要用8個(gè)下載線程。 第一步:將namelock.avi文件分成8個(gè)子模塊。這里要注意的地方是我所說的分成8個(gè)字模塊,并不是把文件的內(nèi)容分別存放到8個(gè)不同的緩沖區(qū)里。而是生成8個(gè)不同的文件偏移量。很多時(shí)候程序員為了偷懶往往容易一次性講文件讀入內(nèi)存,這樣帶來的后果是不堪設(shè)想的。一個(gè)比較理想的方法是這樣的。
bool DealFile(string fileName) //隨便寫個(gè)函數(shù)說明
{
FILE *file;
DWORD fileSize ,pos;
int readLen ;
//MAX_BUFFER_LEN 在頭文件里定義,這里能夠保證數(shù)據(jù)不丟失,也不至于內(nèi)存逸出
char *buffer = new char[MAX_BUFFER_LEN];
file = fopen(fileName.c_str(),"r+b");
if(file == NULL) return false;
fseek(file,0,2);
fileSize = ftell(file); //取得文件的大小
fseek(file,0,0);
do{
readLen = fread(buffer,sizeof(char),MAX_BUFFER_LEN,file);
if(readLen > 0)
{
pos += readLen;
//對讀取的文件做處理
}
}while(pos < fileSize); //循環(huán)讀取文件
delete[] buffer;
fclose(file); //釋放資源
return true;
}
8個(gè)線程下載文件時(shí),都要對內(nèi)容文件和配置文件進(jìn)行讀寫。這樣如果沒有處理好,很有可能會造成訪問文件失敗,我定義了一個(gè)全局變量FileLocked,如果FileLocked=true說明文件正在被某個(gè)線程訪問。所以使用Sleep(10)睡眠等待。當(dāng)某個(gè)線程進(jìn)入讀寫文件時(shí)必須設(shè)置FileLocked = true;訪問文件完畢必須將FileLocked = false;這樣就能很好的控制各個(gè)線程對文件的訪問了。(對臨界資源的訪問有API提供了很多很好的解決方法,請查閱)。
8個(gè)下載線程同時(shí)下載文件時(shí),完成部分下載是隨機(jī)的。那么怎么樣把隨機(jī)的文件數(shù)據(jù)按照偏移量正確的寫入文件呢?我是這樣實(shí)現(xiàn)的,當(dāng)要下載文件namelock.avi時(shí),首先查找文件namelock.avi.san配置文件是否存在。如果存在,說明上次已經(jīng)下載過部分該文件,就可以斷點(diǎn)續(xù)傳了。如果沒有找到該文件,那么生成和該文件的大小一樣大的文件,文件里所有的數(shù)據(jù)都為0,(可以使用函數(shù)memset(buffer,10000,''0''))和一個(gè)配置文件。然后利用fseek函數(shù)將數(shù)據(jù)正確的覆蓋原先的0;接下來要介紹一寫配置文件的格式了。
很簡單,配置文件的內(nèi)容主要包括:文件在本地保存的絕對路徑、文件的大小、線程的個(gè)數(shù)、已經(jīng)下載的文件大小,各個(gè)線程的任務(wù)(在原始文件起始位置和結(jié)束位置,中間使用''-''分開);如:
D:\mm\namelock.avi //文件保存在這里
364544 //文件大小
5 //有5個(gè)線程在下載
0 //已經(jīng)下載了0字節(jié)
0-72908 //線程1的下載任務(wù)
72908-145816 //線程2的下載任務(wù)
145816-218724 //線程3的下載任務(wù)
218724-291632 //線程4的下載任務(wù)
291632-364544 //線程5的下載任務(wù)
以上是開始下載時(shí)的各個(gè)線程的任務(wù)分配。
D:\mm\namelock.avi
364544
5
113868
72908-72908
113868-145816
145816-218724
218724-291632
291632-364544
以上是某一時(shí)刻各個(gè)線程的任務(wù)分配情況。
各個(gè)線程任務(wù)分配是這樣實(shí)現(xiàn)的。在開始下載時(shí),文件平均分成若干塊進(jìn)行下載。如第一個(gè)線程一開始的任務(wù)是從文件的0位置開始下載一直到72908位置處。線程1每次下載一塊數(shù)據(jù)后就要調(diào)整任務(wù),如第一次下載了20800字節(jié)的數(shù)據(jù),那么線程1的任務(wù)將改為:20800-72908。如此下去,直到任務(wù)為72908-72908時(shí)表示線程1完成了當(dāng)前的下載任務(wù)。此時(shí),線程1就分析各個(gè)線程的任務(wù),找出任務(wù)最為繁忙的一個(gè)線程:如線程3:14816-218724。那么線程1就自動去調(diào)整任務(wù),拿50%的任務(wù)來再次下載。周而復(fù)始直到各個(gè)線程都完成任務(wù)。不過這里有一點(diǎn)需要注意:為了避免重復(fù)下載部分?jǐn)?shù)據(jù),在調(diào)整任務(wù)的時(shí)候,起始的文件便移量必須加上接受緩沖器的字節(jié)數(shù),因?yàn)槿缜懊嫠e的列子來看。線程1和線程3在平衡負(fù)載的時(shí)候,線程正在下載數(shù)據(jù),如果所剩的數(shù)據(jù)比接受緩沖器的大小還小,線程1和線程3的部分下載數(shù)據(jù)將會重復(fù)。
在調(diào)整任務(wù)和分析任務(wù)的時(shí)候,會發(fā)現(xiàn)一個(gè)問題。就是讀取文件數(shù)據(jù)太過頻繁。于是我用了一個(gè)數(shù)據(jù)結(jié)構(gòu)。在下載文件的過程中始終打開配置文件,這樣速度提高了很多。在文件下載完畢后關(guān)閉文件。數(shù)據(jù)結(jié)構(gòu)如下:
typedef struct FromToImpl{
DWORD from; //任務(wù)起始位置
DWORD to; //任務(wù)結(jié)束位置
}m_fromTo;
typedef struct InfroImpl{
String fileLoad; //文件保存位置
DWORD fileSize; //文件大小
int threadCnt; //下載線程數(shù)
DWORD alreadyDownloadCnt; //已經(jīng)下載的文件大小
FromToImpl *fromToImpl; //各個(gè)線程的任務(wù)描述
}m_inforImpl;