现在的位置: 首页 > 综合 > 正文

qt FTP客户端编程

2013年12月17日 ⁄ 综合 ⁄ 共 10431字 ⁄ 字号 评论关闭

三、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;
    ...
}

除此之外,还有一种方法可以了解命令执行情况,那就是与QFtpstateChanged()信号相连接,因为每当该连接进入一个新的状态时(QFtp::ConnectingQFtp::ConnectedQFtp::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:一个封装QByteArrayQIODevice子类。举例来说:

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

我们还可以省略给

get()I/O设备参数,或者传给它一个空指针。这时,每当有新数据可用时,QFtp类都会发出一个readyRead()信号,之后就可以使用read()或者readAll()来读取这些数据了。(转)

抱歉!评论已关闭.