利用Qt進行FTP網絡編程

Qt是一個用於桌面系統和嵌入式開發的跨平臺應用程序框架。它包括一個直觀的API和一個豐富的類庫,並且對通用網絡協議提供了很好的支持。在本文中,我們將向讀者介紹如何利用Qt提供的網絡編程有關的類來進行快速的FTP編程,下面首先介紹FTP協議的基礎知識,然後用實例講解FTP的客戶端編程。

一、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的客戶端編程。
 
 

三、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();
}
我們看到,這裏使用的是QCoreApplication,而不是QApplication,這樣做是爲了防止在編譯時連接QtGui程序庫。另外,函數QCoreApplication::arguments()返回的命令行參數用作QStringList,其第一項是被調用的程序的名稱,這裏的任何Qt參數,比如-style 等,都將被刪除。Main()函數的重點在於建立MyFtpGet對象並調用getFile(),如果調用成功,就進入事件循環,直到下載結束爲止。我們看到,所有的活都是由MyFtpGet子類來乾的,其定義如下:

 

 


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;
};
這個類具有一個公共函數getFile(),用來檢索URL指定的文件。類QUrl提供了一個高級接口,用來提取URL的各個部分,如文件名、路徑、協議和端口等等。MyFtpGet具有一個私有的槽,即ftpDone(),當我們的文件傳輸完成時,就會調用該函數;另外,MyFtpGet還有一個信號,即done(),當文件下載後就會發出該信號。除此之外,這個類還有兩個私有變量,分別是變量ftp和變量file。前者類型爲QFtp,用來封裝至FTP服務器的連接;後者用來將下載的文件寫入硬盤

 

 


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;
}
GetFile()函數首先檢查傳遞給它的URL,如果有問題,它會向cerr打印錯誤信息,並返回false,指示下載失敗。注意,在這裏,我們沒有要求用戶建立一個文件名,相反,我們設法利用URL本身來生成一個文件名。如果文件打開失敗,會打印錯誤信息並返回false。

 

接下來,我們使用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();
}
FTP命令執行後,我們馬上關閉該文件,併發出我們自己done()信號。你也許覺得奇怪,爲什麼會在這裏關閉文件呢?好像應該在getFile()函數末尾調用ftp.close()後關閉纔對呀?別忘了,FTP命令是異步執行的,也許它們在執行時函數getFile()早就已經返回了。只有當QFtp對象的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;
    ...
}
除此之外,還有一種方法可以瞭解命令執行情況,那就是與QFtp的stateChanged()信號相連接,因爲每當該連接進入一個新的狀態時(QFtp::Connecting、QFtp::Connected、QFtp::LoggedIn等等),QFtp總會發出相應的stateChanged()信號。

 

不過在大部分情況下,我們只對命令序列的整體情況感興趣,而不是單獨的某條命令,這時就可以直接與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;
};
這裏的起始目錄由QUrl指定,然後使用getdirectory()函數進行設置。

 

 


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 &)));
}

在構造函數中,我們建立了兩個信號–槽連接。當爲每個檢索的文件請求目錄清單時,QFtp就會發出listInfo(const QUrlInfo)信號。這個信號連接到一個稱爲ftplistinfo()的槽上,該函數會下載給定URL相關聯的文件。

 


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;
}
調用getDirectory()函數時,它首先進行必要的檢查,如果一切正常的話,它就嘗試建立一個FTP連接。它記下必須處理的路徑,並調用processNextDirectory()開始下載根目錄。

 

 


void Yourftpget::processNextDirectory()
{
    if (!pendingDirs.isEmpty()) {
        currentDir = pendingDirs.takeFirst();
        currentLocalDir = "downloads/" + currentDir;
        QDir(".").mkpath(currentLocalDir);
 
        ftp.cd(currentDir);
        ftp.list();
    } else {
        emit done();
    }
}
函數processNextDirectory()從pendingDirs列表中取得第一個遠程目錄,然後在本地文件系統中創建一個對應的目錄,之後指示QFtp對象切換到該遠程目錄中來列出目錄下的文件。List()每處理一個文件,它都會發出一個listInfo()信號,該信號會調用ftpListInfo()槽。如果處理完所有目錄,該函數會發出一個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());
    }
}
槽ftpListInfo()的參數urlInfo提供了遠程文件的詳細信息,如果該文件是一個常規文件並且可讀的話,我們就調用get()來下載之。爲此,用new分配一個QFile對象來處理下載,並將指向它的指針放在openedFiles列表中。如果QUrlInfo存放的是遠程目錄的詳細信息,而非一個符號連接的信息,那麼就把這個目錄添加到pendingDirs列表中。之所以不用符號連接,是因爲它常常導致無限遞歸。

 

 


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();
}
當全部FTP命令都結束時,或者出現錯誤時,將會調用槽ftpDone()。爲了防止內存泄漏,需要刪除QFile對象。最後,我們調用processNextDirectory()。只要還有未處理的目錄,就要對列表中的下一個目錄進行新一輪的處理;否則,停止下載併發出done()信號。如果沒有出現錯誤的話,FTP命令序列和信號會像下面一樣:

 

 


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()
如果一個文件不是常規文件,而是一個目錄的話,它就會被添加到pendingDirs列表中,並且在當前list()命令的最後一個文件下載好後,將發出一個cd()命令,接着用一個新的list()命令來處理下一個尚未處理的目錄——新一輪的處理又將開始。對於新文件則下載,對於新目錄則添加到pendingDirs列表,如此反覆,直至從所有目錄中將所有文件全部下載,這時,pendingDirs列表才最終變成一個空的。

 

如果下載時出現網絡錯誤,比如一個目錄中有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()
...
 
另一個方法是爲每個文件使用一個QFtp對象,這樣一來我們就能夠通過單獨的FTP連接來並行的下載文件了。

 


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();
}
如果用戶沒有在命令行規定一個URL的話,我們就要給出一個錯誤信息並且終止該程序。在這兩個FTP示例程序中,使用get()檢索的數據寫到了一個QFile中,但這不是必需的。如果我們想要將數據放在內存中的話,我們可以使用一個Qbuffer:一個封裝QByteArray的QIODevice子類。舉例來說:

 

 


QBuffer *buffer = new QBuffer;
buffer->open(QIODevice::WriteOnly);
ftp.get(urlInfo.name(), buffer);

我們還可以省略給

get()的I/O設備參數,或者傳給它一個空指針。這時,每當有新數據可用時,QFtp類都會發出一個readyRead()信號,之後就可以使用read()或者readAll()來讀取這些數據了
 
 
發佈了25 篇原創文章 · 獲贊 6 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章