最近業余時間在搞Qt,其中的一個功能是實現FTP的上傳下載。
之前版本的Qt提供了一個FTP操作的類,但是5.x(4.x?)之后因為性能問題就棄用了。貌似CSDN上有人發帖問過這個問題,記得應該是put多大的文件時導致占用內存過大。現在Qt的官方手冊推薦使用QNetworkAccessManager用於TCP/IP以及FTP的傳輸。說實話這玩意對於FTP的兼容並不好。於是想到了CURL。
網上關於CURL和libCURL的文章很多,但是用於FTP操作的文章大多數停留於試驗性的代碼。別的不說,忽略了一個比較重要的問題就是FTP的傳輸模式。
FTP有兩種傳輸模式,ASCII和Binary,即TYPE A 和TYPE I。大家的文章里面都沒有提到這個事情。我用libCURL主要是傳輸文本文件,所以這個問題一下就浮現出來了。
libCURL的下載什么的就不說了,到官網上找windows的包就可以,記得下載develop版本。
在QT里面使用動態庫,首先要做的肯定是修改pro文件。
1 LIBS += C:\your-curl-lib-location\curl-7.34.0-devel-mingw32\lib\libcurldll.a 2 3 INCLUDEPATH += C:\your-curl-lib-location\curl-7.34.0-devel-mingw32\include
首先指定靜態庫位置,接着指定include路徑。這個沒什么好說的。然后把curl路徑下的bin目錄中的dll拷貝到debug文件夾。否則run的時候會“莫名其妙”的停止。
這里使用的是CURL的easy借口(貌似網上的文章都是使用的這個)。
先來看下下載代碼。
1 CURLcode ret; 2 curl_global_init(CURL_GLOBAL_ALL); 3 CURL* curl = curl_easy_init(); 4 5 curl_easy_setopt(curl,CURLOPT_URL,remoteFile.toLatin1().data()); 6 curl_easy_setopt(curl,CURLOPT_USERPWD,"username:password"); 7 ret = curl_easy_setopt(curl,CURLOPT_WRITEDATA,localFile); 8 ret = curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,curlWriteFunction); 9 10 curl_easy_setopt(curl,CURLOPT_TRANSFERTEXT,1); 11 12 curl_easy_setopt(curl,CURLOPT_VERBOSE,1); 13 ret = curl_easy_perform(curl); 14 15 curl_easy_cleanup(curl); 16 curl_global_cleanup();
解釋幾點。
1) 聲明CURLcode,獲取curl接口的返回值。
2) 使用 curl_global_init對CURL進行初始化。傳遞CURL_GLOBAL_ALL進去表明對所有平台進行初始化,這個是最保險的,也保證了代碼的可移植性。
3) curl_easy_setopt函數對curl的選項進行設定。
該函數的第一個參數為curl指針,第二個為CURL選項名稱(其實就是個整數),第三個為實際選項的值,可以是字符串、指針,函數指針等,根據不同的選項有不同的值。
CURLOPT_URL: 設定需要上傳/下載的文件URL。
CURLOPT_USERPWD:設定登錄服務器的賬號密碼,格式為user:passwd。
CURLOPT_WRITEDATA:傳遞DATA的指針。引用一句被用爛的話,“CURL對該指針不做任何處理,只做傳遞作用。”。這個指針有什么用,我們一會兒再看。
CURLOPT_WRITEFUNCTION:設定寫文件的函數。當curl從服務器獲取數據之后,便會調用這個函數做寫入處理。函數原型如下:
size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
看見最后一個參數了米有?這個參數就是CURLOPT_WRITEDATA中指定的指針。也就是,如果你想指定幾個參數貫穿CURL“獲取-寫入"這個過程,就把他們的指針通過CURLOPT_WRITEDATA傳給CURL。這里我給了個QFile指針進去。
需要說明的一點是,如果在C++中只用CURL,這個函數在類中應當被聲明為static的。
CURLOPT_TRANSFERTEXT: 指定CURL的FTP傳輸模式。后面的值為1,則為ASCII,0位二進制傳輸。如果使用了錯誤的傳輸模式,在下載的過程中,服務器可能給你一個553。最開始這個錯誤讓我很莫名其妙。因為我使用了CURLOPT_PREQUOTE去追加了一個TYPE A的命令給CURL。理論上應該是可行的,但實際上文件傳輸總是到一半就被服務器關閉鏈接。猜測這個可能與CURL的多線程有關?僅僅做猜測了。
CURLOPT_VERBOSE 設定調試級別。
在libCURL的網站上,有一個鏈接專門解釋這些宏。
1 http://curl.haxx.se/libcurl/c/curl_easy_setopt.html
4) curl_easy_perform 執行curl動作。默認為下載。
對CURL設置結束后,就可以編寫之前指定的寫入函數了。
1 int FTPThread::curlWriteFunction(void *buffer, size_t size, size_t nmemb, void *stream){ 2 QFile* file = static_cast<QFile*>(stream); 3 if(!file->isOpen()){ 4 file->open(QFile::ReadWrite | QIODevice::Text); 5 } 6 return file->write((char*)buffer,nmemb); 7 8 }
在QT中,這個函數是很容易被實現的。只要確保返回值為實際的寫入字節就可以,因為CURL會檢查返回值,如果與nmemb不相等的話,會報錯。
上傳的代碼與下載很類似,直接上代碼了。
1 CURLcode FTPThread::CURLUpload(){ 2 CURLcode ret; 3 curl_global_init(CURL_GLOBAL_ALL); 4 CURL* curl = curl_easy_init(); 5 QFile* localFile = new QFile("FTPCache/"+fileName); 6 qDebug()<<localFile->fileName()<<endl; 7 QString remoteFile = "ftp://your.ftp.server/"+fileName; 8 9 if(!localFile->open(QFile::ReadWrite | QIODevice::Text)){ 10 qDebug()<<"open file fail"<<endl; 11 return CURLE_READ_ERROR; 12 } 13 14 curl_easy_setopt(curl,CURLOPT_URL,remoteFile.toLatin1().data()); 15 curl_easy_setopt(curl,CURLOPT_USERPWD,"username:password"); 16 17 curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); 18 curl_easy_setopt(curl,CURLOPT_FTP_CREATE_MISSING_DIRS,CURLFTP_CREATE_DIR); 19 ret = curl_easy_setopt(curl,CURLOPT_READDATA,localFile); 20 curl_easy_setopt(curl,CURLOPT_INFILESIZE_LARGE,localFile->size()); 21 ret = curl_easy_setopt(curl,CURLOPT_READFUNCTION,curlReadFunction); 22 23 //Timeout 24 curl_easy_setopt(curl,CURLOPT_FTP_RESPONSE_TIMEOUT,60000); 25 26 27 //Set transfer mode to ASCII 28 curl_easy_setopt(curl,CURLOPT_TRANSFERTEXT,1); 29 curl_easy_setopt(curl,CURLOPT_BUFFERSIZE,200); 30 31 32 curl_easy_setopt(curl,CURLOPT_VERBOSE,1); 33 ret = curl_easy_perform(curl); 34 if(ret!=CURLE_OK) 35 qDebug()<<"perform fail"<<endl; 36 37 curl_easy_cleanup(curl); 38 curl_global_cleanup(); 39 localFile->close(); 40 return ret; 41 }
只需說明一點,curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); 指定了CURL的動作為上傳。
讀取文件的函數原型如下:
1 size_t read_callback(char *buffer, size_t size, size_t nitems, void *instream);
實現如下:
1 int FTPThread::curlReadFunction(void *buffer, size_t size, size_t nmemb, void *stream){ 2 QFile* file = static_cast<QFile*>(stream); 3 return file->read((char*)buffer,size*nmemb); 4 }
全文完。