QFtp类在Qt中实现了FTP协议的客户端程序,它提供了非常多的函数来执行多数常见的FTP操作,同时还可以执行任意的FTP指令。
QFtp类是异步工作的。若调用一个像get()或者put()这样的函数,它会立即返回并且仅在控制权回到Qt的事件循环时才发生数据传输。这样就确保了在执行FTP指令时,用户界面可以保持响应。
我们将从如何使用get()函数获取一个单一文件的例子 开始讲起。该例子是一个名为ftpget的控制台应用程序,它可以下载指定命令行上的远程文件。首先看看main()主函数:
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QStringList args = QCoreApplication::arguments();
if (args.count() != 2) {
std::cerr << "Usage: ftpget url" << std::endl
<< "Example:" << std::endl
<< " ftpget ftp://ftp.trolltech.com/mirrors"
<< std::endl;
return 1;
}
FtpGet getter;
if (!getter.getFile(QUrl(args[1])))
return 1;
QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit()));
return app.exec();
}
我们要创建一个QCoreApplication而不是它的子类QAplication,以避免连接到QtGui库。QCoreApplication::arguments()函数返回命令行参数作为一个QStingList列表,采用第一项作为程序被调用时的名称,同时删除掉诸如-style型的所有Qt指定的参数。main()函数的核心是构建FtpGet对象和getFile()调用。如果该函数调用成功,就会让事件循环一直运行下去,直到下载完毕。
FtpGet子类完成了所有的工作,其子类的定义如下:
class FtpGet : public QObject
{
Q_OBJECT
public:
FtpGet(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的不同部分,如文件名称、路径、协议和端口。
FtpGet有一个私有槽fitpDone(),当文件传输完成时,就会调用它;而当文件下载完成后,它将发送一个done()信号。这个类还有两个私有变量:ftp变量和file变量,前者的类型为QFtp,封装一个到FTP服务器的连接;后者用来向磁盘写入所下载的文件。
FtpGet::FtpGet(QObject *parent)
: QObject(parent)
{
connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool)));
}
在构造函数中,我们把QFtp::done( bool)信号与ftpDone( bool)私有槽连接起来。当所有的请求都已处理完时,QFtp就发射done( bool)信号,bool参数指明是否有错误发生。
bool FtpGet::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 = "ftpget.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以表明下载失败。
通过对ftpget.out的备份,我们可以试图利用URL自身创建一个合理的本地文件名而不是让用户自己虚构一个文件名。如果打开文件失败,就会打印出错信息并返回false。
接着,利用QFtp对象执行一个含4个FTP指令的序列。url.port(21)调用返回在URL中指定的端口号;如果在URL中并未指定任何端口号,则返回端口21。因为没有给login()函数任何用户名或者密码,所以此处需要一个匿名登录。get()中的第二个参数指定了输出信号的I/O设备。
这些FTP指令在Qt的事件循环中排队并等待执行。QFtp 的done(bool)信号表明这些指令的完成情况,构造函数中已经把这个信号与ftpDone(bool)连接起来了。
void FtpGet::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()来关闭文件,显得有些不同寻常;但是请牢记:在返回getFile()函数之后,FTP指令会异步执行并且整个执行过程会非常顺利。
QFtp提供了一些FTP指令,包括connectToHost()、login()、close()、list()、cd()、get()、put()、remove()、mkdir()、rmdir()和rename()。所有这些函数都可以调度一个 FTP指令,并且可以返回一个标识这个指令的ID号。对于传输模式(默认为被动模式)和传输类型(默认为二进制数据)的控制,也是可能的。
使用rawCommand()可以执行任意的FTP指令。例如,以下是如何执行一个SITE CHMOD指令的代码:
ftp. rawCommand(“SITE CHMOD 755 fortune”);
当QFtp开始执行一个指令的时候,它发射commandStarted(int)信号,而当这个指令完成的时候,它发射commandFinished(int, bool)信号。int 参数是标识一个指令的ID号。如果对个别指令的结果感兴趣,则当调用这些指令的时候,可以保存这些ID号。了解并记录这些ID号可以为用户提供详细的反馈信息。例如:
bool FtpGet::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 FtpGet::ftpCommandStarted(int id)
{
if(id == connectId)
{
std::cerr << "Connecting..." << std::endl;
}
else if(id == loginId)
{
std::cerr << "Logging in..." << std::endl;
}
...
}
#ifndef FTPGET_H
#define FTPGET_H
#include
#include
class QUrl;
class FtpGet : public QObject
{
Q_OBJECT
public:
FtpGet(QObject *parent = 0);
bool getFile(const QUrl &url);
signals:
void done();
private slots:
void ftpDone(bool error);
private:
QFtp ftp;
QFile file;
};
#endif
#include
#include
#include
#include "ftpget.h"
FtpGet::FtpGet(QObject *parent)
: QObject(parent)
{
connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool)));
}
bool FtpGet::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 = "ftpget.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;
}
void FtpGet::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();
}
#include
#include
#include "ftpget.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QStringList args = QCoreApplication::arguments();
if (args.count() != 2) {
std::cerr << "Usage: ftpget url" << std::endl
<< "Example:" << std::endl
<< " ftpget ftp://ftp.trolltech.com/mirrors"
<< std::endl;
return 1;
}
FtpGet getter;
if (!getter.getFile(QUrl(args[1])))
return 1;
QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit()));
return app.exec();
}
提供反馈的另一种方式是与QFtp的stateChanged()信号连接,只要连接进入了一个新状态(QFtp::Connecting、QFtp::Connected、QFtp::LoggedIn,等等),就会发射stateChanged()信号。
在绝大多数应用程序中,我们仅仅对整个指令序列的结果而不是对其中某个特别的指令感兴趣。这时,可以只简单地连接done(bool)信号,一旦指令队列变空,就会发射这个信号。
当错误发生时,QFtp会自动清空指令队列。也就是说,如果这次连接或者登录失败,位于队列后面的指令将不会执行。但如果在错误发生之后使用同一个QFtp对象调用新的指令序列,那么这些指令将会排队等待执行。
在应用程序的.pro文件中,需要如下的命令行来连接到QtNetwork数据库:
QT += network
现在来看一个更高级的例子。Spider 命令行程序下载一个FTP目录下的所有文件,它将从所有目录的子目录中递归下载这些文件。在Spider类中体现了网络的逻辑联系:
#ifndef SPIDER_H
#define SPIDER_H
#include
#include
class QFile;
class Spider : public QObject
{
Q_OBJECT
public:
Spider(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;
};
#endif
我们将开始目录指定为QUrl,并且使用getDirectory()来设置开始目录:
Spider::Spider(QObject *parent)
: QObject(parent)
{
connect(&ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool)));
connect(&ftp, SIGNAL(listInfo(const QUrlInfo &)),
this, SLOT(ftpListInfo(const QUrlInfo &)));
}
在构造函数中,建立了两个信号-槽的连接。当为所获取的每一个文件请求一个目录列表[在getDirectory()中]时,QFtp 就会发射listInfo(const QUrlInfo &)信号。这个信号被连接到一个名为ftpListInfo()的槽,它会下载与URL相关的给定文件。
bool Spider::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 Spider::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()处理的每一个文件,它都将发射一个促使ftpListInfo()槽被调用的listInfo()信号 。
如果没有需要处理的目录了,该函数将发射done()信号以表明下载完成。
void Spider::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()来下载它。这个用于下载的QFile对象是利用new函数以及一个指向它在openedFiles 列表中存储的指针来分配的。
如果QUlInfo持有一个不是符号连接的远程目录的细节信息,就将这个目录加到pendingDirs列表中。之所以要跳过符号连接,是因为它容易导致无穷递归循环。
void Spider::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();
}
当所有这些FIP指令都完成后,或者如果有错误发生时,就会调用ftpDone()槽。我们删除QFile对象以防止内存泄漏,同时也关闭每一个文件。最后,调用processNextDirectory()。如果还有需要处理的目录,整个过程将从列表中的下一个目录重新开始;否则,下载过程停止并且发射done()信号。
如果没有错误发生,FTP指令和信号的序列如下:
connectToHost(host, port)
login()
cd(directory_1)
list()
emit listInfo(file_1_2)
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 列表也最终将变空。
如果要下载一个目录下的20个文件,当下载到第5个文件的时候,网络发生错误,那么剩下的文件将不会被下载。如果想下载尽可能多的文件,一个解决方案是一次只调用一个GET操作来下载一个文件,并且在调用下一个新的GET操作之前等待done( bool)信号。在listInfo()中,将简单地把文件名追加到一个QStringList中,而不是立即调用get(),而且在done(bool)中,我们将对下一个文件调用get()函数以在QStringList中下载。于是,整个执行顺序看起来可以归结如下:
connectToHost(host, port)
login()
cd(directory_1)
list()
...
cd(directory_N)
list()
emit listInfo(file_1_2)
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)
...
另外一个解决方案是对每一个文件使用一个QFtp对象。这样可以通过一些单独的FTP连接来并行下载这些文件。
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QStringList args = QCoreApplication::arguments();
if (args.count() != 2) {
std::cerr << "Usage: spider url" << std::endl
<< "Example:" << std::endl
<< " spider ftp://ftp.trolltech.com/freebies/"
<< "leafnode" << std::endl;
return 1;
}
Spider spider;
if (!spider.getDirectory(QUrl(args[1])))
return 1;
QObject::connect(&spider, SIGNAL(done()), &app, SLOT(quit()));
return app.exec();
}
main()函数完成了这个程序。如果用户没有在命令行中指定一个URL,就给出一个出错信息并终止程序。
在这两个FTP实例中,利用get()函数获得的数据都被写入QFile 中。但这样的做法并不是必需的。如果想把这些数据放到内存中,则可以使用QBuffer,它是一个在QByteAray上操作的QIODevice的子类。例如:
QBuffer *buffer = new QBuffer;
buffer->open(QIODevice::WriteOnly);
ftp.get(urlInfo.name(), buffer);
也可以省略get()中的输入/输出(I/O)设备参数,或者只传递一个空指针。然后每次当有新数据可读的时候,QFtp类就会发射一个readyRead()信号,并且可以使用read()或者readAIl()来读取这些数据。
#ifndef SPIDER_H
#define SPIDER_H
#include
#include
class QFile;
class Spider : public QObject
{
Q_OBJECT
public:
Spider(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;
};
#endif
#include
#include
#include
#include "spider.h"
Spider::Spider(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 Spider::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 Spider::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();
}
void Spider::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 Spider::processNextDirectory()
{
if (!pendingDirs.isEmpty()) {
currentDir = pendingDirs.takeFirst();
currentLocalDir = "downloads/" + currentDir;
QDir(".").mkpath(currentLocalDir);
ftp.cd(currentDir);
ftp.list();
} else {
emit done();
}
}
#include
#include
#include "spider.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QStringList args = QCoreApplication::arguments();
if (args.count() != 2) {
std::cerr << "Usage: spider url" << std::endl
<< "Example:" << std::endl
<< " spider ftp://ftp.trolltech.com/freebies/"
<< "leafnode" << std::endl;
return 1;
}
Spider spider;
if (!spider.getDirectory(QUrl(args[1])))
return 1;
QObject::connect(&spider, SIGNAL(done()), &app, SLOT(quit()));
return app.exec();
}