一、FTP
我們都知道,FTP協議是互聯網上的文件傳輸協議,利用它我們可以將一個文件的副本從一臺計算機傳輸到另一臺計算機上。就像許多其他網絡應用一樣,FTP使用客戶/服務器模式。FTP客戶打開一個控制連接與服務器連接,通過該連接,客戶發送請求並接收應答。控制連接在整個會話期間一直保持開放。FTP並不通過控制連接來發送數據,而是當客戶請求文件傳輸時,服務器形成一個獨立的數據連接。由於FTP使用兩個不同的協議端口號,所以數據連接與控制連接不會發生混亂。
在進行文件傳輸時,用戶運行一個本地FTP應用程序,該程序將解釋用戶輸入的命令。當用戶輸入open命令並指定一個遠程計算機時,本地計算機變成一個使用TCP與指定計算機上的FTP服務器程序建立控制連接的FTP客戶。客戶與服務器在通過控制連接進行通信時使用FTP協議。也就是說,客戶並不直接將用戶的鍵擊傳遞給服務器方。相反,當用戶輸入命令時,客戶首先解釋該命令。如果命令要求與服務器交互,那麼客戶形成一個使用FTP協議的請求,並將請求送到服務器方。服務器在應答時也使用FTP協議。
二、Qt爲FTP提供的類
實際上,爲了方便網絡編程,Qt已經提供了許多有關的類,比如QFtp就使我們能夠更加輕鬆使用FTP協議進行網絡編程。此外,Qt還用兩個低級的類QTcpSocket和QudpSocket,它們實現了TCP和UDP傳輸協議。我們知道,TCP是一種可靠的面向連接的協議,它用來在兩個網絡節點之間傳輸數據流;UDP則是一種不可靠的無連接協議,它用於在網絡節點之間發送非連續的數據包。兩者都可以用來建立網絡客戶/服務器模式的應用程序,對於服務器,還需要QTcpServer類來處理進入的TCP連接。如果不用QTcpSocket,而使用QSslSocket的話,還可以建立安全的SSL/TLS連接。
三、FTP客戶端編程
在Qt中,QFtp類爲我們實現了FTP協議的客戶端所需要的功能,比如它不僅提供了完成最常用的各種FTP操作的函數,還能執行任意的FTP命令。需要注意,QFtp類以異步方式工作,比如當我們調用諸如get()或者put()函數時,會立即返回,當控制權返還給Qt的事件循環後,方纔進行數據傳輸。這樣做的好處是,當FTP命令執行過程中,用戶界面仍能對客戶的動作作出迅速的響應。
現在,我們將用實例來說明如何利用get()來檢索一個文件。我們的示例是一個控制檯程序,名爲myftpget,用於下載命令行指定的遠程文件。下面讓我們首先來看一下該程序的main()函數:
int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = QCoreApplication::arguments(); if (args.count() != 2) { std::cerr << "Usage: myftpget url" << std::endl << "Example:" << std::endl << " myftpget ftp://ftp.xxxxx.com/yyyyyy" << std::endl; return 1; } MyFtpGet getter; if (!getter.getFile(QUrl(args[1]))) return 1; QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); } |
class MyFtpGet : public QObject { Q_OBJECT public: MyFtpGet(QObject *parent = 0); bool getFile(const QUrl &url); signals: void done(); private slots: void ftpDone(bool error); private: QFtp ftp; QFile file; }; |
MyFtpGet::MyFtpGet(QObject *parent) : QObject(parent) { connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); } 在構造函數中,我們將信號QFtp::done(bool)連到了私有的槽ftpDone(bool)上,當處理完所有請求後,QFtp就會發出信號done (bool)。參數bool的作用是指示有沒有出錯。 現在讓我們看看getFile()函數: bool MyFtpGet::getFile(const QUrl &url) { if (!url.isValid()) { std::cerr << "Error: Invalid URL" << std::endl; return false; } if (url.scheme() != "ftp") { std::cerr << "Error: URL must start with 'ftp:'" << std::endl; return false; } if (url.path().isEmpty()) { std::cerr << "Error: URL has no path" << std::endl; return false; } QString localFileName = QFileInfo(url.path()).fileName(); if (localFileName.isEmpty()) localFileName = "myftpget.out"; file.setFileName(localFileName); if (!file.open(QIODevice::WriteOnly)) { std::cerr << "Error: Cannot write file " << qPrintable(file.fileName()) << ": " << qPrintable(file.errorString()) << std::endl; return false; } ftp.connectToHost(url.host(), url.port(21)); ftp.login(); ftp.get(url.path(), &file); ftp.close(); return true; } |
接下來,我們使用QFtp對象執行一個由四條FTP命令組成的命令序列。調用url.port(21)後,將返回URL中指定的端口號,如果URL中沒有指定端口號的話,將返回端口21。此外,因爲沒有向函數login()提供用戶名或者口令,所以該函數將嘗試匿名登錄。給get()的第二個參數規定用於輸出的I/O設備。
在Qt的事件循環中,將對FTP命令進行排隊並執行它們。當所有命令執行完畢後,QFtp將發出信號done(bool),前面已經看到,該信號已經在構造函數中連到了ftpDone(bool)上,那就再看看該函數到底做什麼:
void MyFtpGet::ftpDone(bool error) { if (error) { std::cerr << "Error: " << qPrintable(ftp.errorString()) << std::endl; } else { std::cerr << "File downloaded as " << qPrintable(file.fileName()) << std::endl; } file.close(); emit done(); } |
QFtp提供了許多FTP命令,它們是connectToHost()、login()、close()、list()、cd()、get()、put()、remove()、mkdir()、rmdir()和rename()。這些函數都會發出一個FTP命令,並返回一個標識該命令的ID號。此外,還可以控制傳輸模式,默認爲被動模式,以及傳輸類型,默認時爲二進制類型。另外,所有FTP命令都可以通過rawCommand()來執行,舉例來說,可以像下面這樣執行SITE CHMOD命令:
ftp.rawCommand("SITE CHMOD 755 fortune");
當QFtp執行一個命令時,它會發出commandStarted(int)信號;當命令執行完成後,它會發出commandFinished(int,bool)信號,其中參數int表示該命令的ID號。如果想了解某個命令的執行情況,可以在調度該命令時記下其ID號,然後通過跟蹤ID號就能瞭解相關情況。舉例來說:
bool MyFtpGet::getFile(const QUrl &url) { ... connectId = ftp.connectToHost(url.host(), url.port(21)); loginId = ftp.login(); getId = ftp.get(url.path(), &file); closeId = ftp.close(); return true; } void MyFtpGet::ftpCommandStarted(int id) { if (id == connectId) { std::cerr << "Connecting..." << std::endl; } else if (id == loginId) { std::cerr << "Logging in..." << std::endl; ... } |
不過在大部分情況下,我們只對命令序列的整體情況感興趣,而不是單獨的某條命令,這時就可以直接與done(bool)信號連接,因爲命令隊列爲空時,就會發出該信號。
當遇到錯誤時,QFtp會自動清空命令隊列,也就是說如果連接或者註冊失敗的話,隊列後面的命令就沒有機會執行了。如果我們在出錯之後使用同一個QFtp對象重新發出命令的話,這些命令將被重新排隊並執行。在本程序的.pro文件中,需要用下列行來連接QtNetwork庫:
QT += network
現在,我們將考察一個更加複雜的例子:命令行程序yourftpget,它將下載一個FTP目錄中的所有文件,並遞歸下載該目錄下的所有子目錄中的文件。有關代碼如下所示:
class Yourftpget : public QObject { Q_OBJECT public: Yourftpget(QObject *parent = 0); bool getDirectory(const QUrl &url); signals: void done(); private slots: void ftpDone(bool error); void ftpListInfo(const QUrlInfo &urlInfo); private: void processNextDirectory(); QFtp ftp; QList<QFile *> openedFiles; QString currentDir; QString currentLocalDir; QStringList pendingDirs; }; |
Yourftpget::Yourftpget(QObject *parent) : QObject(parent) { connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); connect(&ftp, SIGNAL(listInfo(const QUrlInfo &)), this, SLOT(ftpListInfo(const QUrlInfo &))); } |
bool Yourftpget::getDirectory(const QUrl &url) { if (!url.isValid()) { std::cerr << "Error: Invalid URL" << std::endl; return false; } if (url.scheme() != "ftp") { std::cerr << "Error: URL must start with 'ftp:'" << std::endl; return false; } ftp.connectToHost(url.host(), url.port(21)); ftp.login(); QString path = url.path(); if (path.isEmpty()) path = "/"; pendingDirs.append(path); processNextDirectory(); return true; } |
void Yourftpget::processNextDirectory() { if (!pendingDirs.isEmpty()) { currentDir = pendingDirs.takeFirst(); currentLocalDir = "downloads/" + currentDir; QDir(".").mkpath(currentLocalDir); ftp.cd(currentDir); ftp.list(); } else { emit done(); } } |
void Yourftpget::ftpListInfo(const QUrlInfo &urlInfo) { if (urlInfo.isFile()) { if (urlInfo.isReadable()) { QFile *file = new QFile(currentLocalDir + "/" + urlInfo.name()); if (!file->open(QIODevice::WriteOnly)) { std::cerr << "Warning: Cannot write file " << qPrintable(QDir::toNativeSeparators( file->fileName())) << ": " << qPrintable(file->errorString()) << std::endl; return; } ftp.get(urlInfo.name(), file); openedFiles.append(file); } } else if (urlInfo.isDir() && !urlInfo.isSymLink()) { pendingDirs.append(currentDir + "/" + urlInfo.name()); } } |
void Yourftpget::ftpDone(bool error) { if (error) { std::cerr << "Error: " << qPrintable(ftp.errorString()) << std::endl; } else { std::cout << "Downloaded " << qPrintable(currentDir) << " to " << qPrintable(QDir::toNativeSeparators( QDir(currentLocalDir).canonicalPath())); } qDeleteAll(openedFiles); openedFiles.clear(); processNextDirectory(); } |
connectToHost(host, port) login() cd(directory_1) list() emit listInfo(file_1_1) get(file_1_1) emit listInfo(file_1_2) get(file_1_2) ... emit done() ... cd(directory_N) list() emit listInfo(file_N_1) get(file_N_1) emit listInfo(file_N_2) get(file_N_2) ... emit done() |
如果下載時出現網絡錯誤,比如一個目錄中有10個文件,當下載第6個文件時出錯,那麼剩餘的文件就無法下載了。如果想下載儘可能多的文件的話,一個辦法是一次調用一個GET操作,然後等待,直到收到done(bool)信號後才發出下一個GET操作。這時,在listInfo()中,我們只要簡單地把文件名添加到QStringList中進行了,但是不直接調用get(),而是應該在done(bool)中調用get()來下載QStringList中的下一個文件,運行順序如下所示:
connectToHost(host, port) login() cd(directory_1) list() ... cd(directory_N) list() emit listInfo(file_1_1) emit listInfo(file_1_2) ... emit listInfo(file_N_1) emit listInfo(file_N_2) ... emit done() get(file_1_1) emit done() get(file_1_2) emit done() ... get(file_N_1) emit done() get(file_N_2) emit done() ... |
int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QStringList args = QCoreApplication::arguments(); if (args.count() != 2) { std::cerr << "Usage: yourftpget url" << std::endl << "Example:" << std::endl << " yourftpget ftp://ftp.xxxxxx.com/yyyyyy/" << "leafnode" << std::endl; return 1; } Yourftpget yourftpget; if (!yourftpget.getDirectory(QUrl(args[1]))) return 1; QObject::connect(&yourftpget, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); } |
QBuffer *buffer = new QBuffer; buffer->open(QIODevice::WriteOnly); ftp.get(urlInfo.name(), buffer); |
我們還可以省略給
get()的I/O設備參數,或者傳給它一個空指針。這時,每當有新數據可用時,QFtp類都會發出一個readyRead()信號,之後就可以使用read()或者readAll()來讀取這些數據了