一、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()来读取这些数据了