Wiki

Far-Reaching QFtp and QHttp

by Rainer M. Schmid

Qt's QUrlOperator provides simple and convenient access to remote files that are accessible using the FTP or HTTP protocols. New in Qt 3.1 are more direct interfaces to these protocols in the QFtp and QHttp classes. In this article we will explore how these classes can give Qt programmers more power and control over remote files and servers.

File System and Network Abstractions

The design idea behind the QUrlOperator interface is to provide a file system abstraction. The abstraction allows us to get the contents of a file, list directory entries, copy files, etc. All of these operations can be performed in a protocol and network independent way for any supported protocol. For example, to copy a Qt documentation page, you can simply write:

QUrlOperator op("http://doc.trolltech.com");
op.get("qhttp.html");

This is very convenient for handling individual files and for directory listings, but it has some limitations. For example, QUrlOperator does not know anything about persistent connections and this makes it difficult to implement a connection-oriented FTP client. Another limitation is that the abstraction doesn't provide support for the request-response model used by the HTTP protocol.

The QFtp and QHttp classes have been designed to provide an interface that presents a more concrete network abstraction, thereby avoiding the limitations of the file system abstraction used by QUrlOperator.

For the FTP protocol, this means that we start by connecting to a server, and then logging in. Once the connection is established we can issue whatever commands we want, and at the end disconnect. The QFtp class has functions that are similar to those provided by common interactive FTP client programs.

The HTTP protocol is a request-response oriented protocol. The QHttp class makes it easy to POST requests (for example a search expression for an online form), and to retrieve the response that contains the results of the request.

The Basics

Like QUrlOperator, the QFtp and QHttp classes work asynchronously. This means that when a command is issued, the function merely schedules the command for later execution, and returns immediately. This ensures that the application, and especially the user interface, isn't blocked, i.e. the user can continue interacting with the program without the application seeming to "lock up." Qt handles the processing behind the scenes, and issues signals to notify the program about the progress of the command.

It is possible to schedule multiple commands. All commands return a unique ID by which they can be identified (for example, by slots that receive their signals). But in many cases the fate of a single command isn't what matters, but rather the completion of an entire sequence of commands.

Here is a complete example that downloads the INSTALL file from Trolltech's FTP server:

#include <qapplication.h>
#include <qfile.h>
#include <qftp.h>
 
int main(int argc, char *argv[])
{
    QApplication app(argc, argv, false);
    QFtp ftp;
 
    QObject::connect(&ftp, SIGNAL(done(bool)), &app, SLOT(quit()));
 
    QFile file("INSTALL");
    if (!file.open(IO_WriteOnly))
        return -1;
 
    ftp.connectToHost("ftp.trolltech.com");
    ftp.login();
    ftp.cd("qt");
    ftp.get("INSTALL", &file);
    ftp.close();
 
    return app.exec();
}

Notice that we pass false to the QApplication constructor. This tells Qt that the application is not a GUI application. The connectToHost() function can also accept a port number. And if we were connecting to a site that required a user name and password, we could pass these in the login() call.

In a real application, we would also need to handle errors. This is quite easy to do: The QFtp::done() signal has a bool argument that specifies if the operations finished successfully or not. If an error did occur, you can get a textual description of the error by using QFtp::errorString().

The QHttp class has a very similar interface to the QFtp class. For example, to retrieve a file using the HTTP protocol you could modify the code shown above, by including , creating an QHttp object instead of a QFtp object, replacing ftp with http, and using the following lines to retrieve the file:

http.setHost("www.trolltech.com");
http.get("index.html", &file);

FTP specifics

The QFtp class provides interfaces for the most common commands, like downloading files, listing the contents of a directory, uploading files, removing files, etc. But what happens if you want to use a command that QFtp doesn't support, like appending data to a file on the server? This isn't a problem. QFtp lets you send arbitrary commands with its QFtp::rawCommand() function, although you must interpret the reply from the server yourself.

Here is a small example that changes the permissions of a file on the server. The FTP protocol does not have a function for doing this, but many FTP servers support the SITE CHMOD command:

class MyFtp : public QFtp
{
    Q_OBJECT
public:
    MyFtp(QObject *parent = 0, char *name = 0)
        : QFtp(parent, name)
    {
        connect(this, SIGNAL(rawCommandReply(int, const QString&)),
                this, SLOT(reply(int, const QString&)));
    }
 
    void uploadExecutable(const QByteArray &data, const QString &file)
    {
        put(data, file);
        rawCommand(QString("SITE CHMOD 755 ") + file);
    }
 
private slots:
    void reply(int code, const QString &detail)
    {
        if (code / 100 == 2)
            // success
        else
            // error
    }
};

This is a small QFtp subclass that adds a function to upload an executable to an FTP server. We want the uploaded file to have executable permission, so we try to change its default permission by sending a SITE CHMOD 755 filename command.

QFtp doesn't interpret the server's reply to a command sent with the rawCommand() function; instead it emits the signal rawCommandReply() with a three digit reply code and a "detail" string. The code indicates the status of the command, and the detail string provides the status in human readable form.

If the FTP server returns an error code as the result of a rawCommand() call, QFtp will not treat this as an error situation and will not set an error in the QFtp object. This means that you must interpret the reply code yourself, and if you get an error code you must handle the error. The first digit of an FTP reply code describes the generic status of the command. A reply code starting with 2 indicates a "positive completion reply." In the example we know that if the code begins with 2, the SITE CHMOD succeeded.

We can test our QFtp extension with the following main() function, where we try to upload a fictitious Python script that we want to make executable. Note that since we will be changing the script's permissions on the server, the server we connect to must be one for which we have write access.

int main(int argc, char *argv[])
{
    QApplication app(argc, argv, false);
    MyFtp ftp;
 
    QObject::connect(&ftp, SIGNAL(done(bool)), &app, SLOT(quit()));
 
    QFile file("script.py");
    if (!file.open(IO_ReadOnly))
        return -1;
 
    ftp.connectToHost("ftp.some-server.com");
    ftp.login();
    ftp.uploadExecutable(file.readAll(), "/bin/script.py");
    ftp.close();
 
    return app.exec();
}

Arbitrary HTTP Requests

The QHttp class has convenient functions to do the most common HTTP requests, GET, POST, and HEAD. If you need one of these, you don't need to worry about the header, QHttp will use typical defaults that work in most cases.

But for fine control you can send arbitrary HTTP requests using the QHttp::request() functions. In this section we present an example subclass that uses a QListView to search the qt-interest mailing list's archive. To achieve this, we send a POST request to http://www.trolltech.com/search.html with the search expression sent in the data. The reply to this request is an HTML page that contains a list of matching documents that we then present in the list view.

We cannot do this using QUrlOperator, since if we used QUrlOperator::put() (which does a POST request), we don't get any data back because of the abstraction QUrlOperator uses.

Here's how the example subclass, ArchiveSearch, is used to search for the term "QHttp":

ArchiveSearch *as = new ArchiveSearch(this);
as->search("QHttp");

The class's declaration is very small:

class ArchiveSearch : public QListView
{
    Q_OBJECT
public:
    ArchiveSearch(QWidget *parent = 0, char *name = 0, WFlags f = 0);
    void search(const QString &topic);
 
private slots:
    void done(bool error);
 
private:
    QHttp http;
};

We've made the class a QListView subclass so that it can show the list of results itself. It uses the private member variable http to do the HTTP request.

ArchiveSearch::ArchiveSearch(QWidget *parent, char *name, WFlags f)
    : QListView(parent, name, f)
{
    addColumn("Title");
    addColumn("URL");
    connect(&http, SIGNAL(done(bool)), this, SLOT(done(bool)));
}

In the constructor we add two columns to the list view to show the title and the URL of each matching page. The only QHttp signal we care about is done().

void ArchiveSearch::search(const QString &topic)
{
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
 
    QHttpRequestHeader header("POST", "/search.html");
    header.setValue("Host", "www.trolltech.com");
    header.setContentType("application/x-www-form-urlencoded");
    QString encodedTopic = topic;
    QUrl::encode(encodedTopic);
    QString searchString = "qt-interest=on&search=" + encodedTopic;
    http.setHost("www.trolltech.com");
    http.request(header, searchString.utf8());
}

The search is done asynchronously and might take some time, so we change the application's cursor to indicate that the application is busy. Then we set up the header and the search string. Although we are using a POST request, we can't use the QHttp::post() function because we need to set the content type of our data to application/x-www-form-urlencoded, so we use the more generic QHttp::request() instead. Using QHttp::request() means that we must set up the request header ourselves. Setting the Host header is necessary because we're using HTTP 1.1. The search string must be URL-encoded (to allow for spaces, ampersands, etc.), and given to request() as a QByteArray, hence the QString::utf8() call.

void ArchiveSearch::done(bool error)
{
    if (error)
        qDebug("error: %s", http.errorString().latin1());
    else {
        QString result(http.readAll());
        QRegExp rx("<a href=\"(http://lists\\.trolltech\\.com/qt-interest/.*)\">(.*)</a>");
        rx.setMinimal(true);
        int pos = 0;
        while (pos >= 0) {
            pos = rx.search(result, pos);
            if (pos > -1) {
                pos += rx.matchedLength();
                new QListViewItem(this, rx.cap(2), rx.cap(1));
            }
        }
    }
    QApplication::restoreOverrideCursor();
}

When the request is finished, the ArchiveSearch::done() slot is called. If no error occurred, we read the response data into a QString. We use a regular expression to extract the title and URL of each match from the returned HTML file.

But how would we use this class in practice? One simple way would be to pair it with a QTextBrowser. (This won't make a web browser; QTextBrowser's HTML handling is much too basic!)

Archiveapp

To create such an application we'd need to add a public slot, a signal and a private slot to the ArchiveSearch class:

public slots:
    void newSearch()
    {
        QString text = QInputDialog::getText("Search Term", "Term:");
        if (!text.isEmpty()) {
            clear();
            search(text);
        }
    }
signals:
    void display(const QString &url);
 
private slots:
	void display(QListViewItem *item)
    {
        emit display(item->text(1));
    }

With these additions we can connect some user interaction (e.g. pressing Enter) to invoke the newSearch() slot. And when an item is chosen, for example by being clicked, we can emit the display() signal with the URL we want shown.

We also need a suitable QTextBrowser subclass:

class ArchiveView : public QTextBrowser
{
    Q_OBJECT
public:
    ArchiveView(QWidget *parent = 0, char *name = 0)
        : QTextBrowser(parent, name)
    {
        connect(&http, SIGNAL(done(bool)), this, SLOT(done()));
    }
 
public slots:
    void fetch(const QString &page)
    {
        QUrl url(page);
        http.setHost(url.host());
        http.get(page);
    }
 
private slots:
    void done() { setText(http.readAll()); }
 
private:
    QHttp http;
};

Now we can put this all together in an application (we've omitted the #includes):

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
 
    QSplitter splitter(0);
    splitter.setCaption("qt-interest search");
    ArchiveSearch *search = new ArchiveSearch(&splitter);
    ArchiveView *view = new ArchiveView(&splitter);
 
    QObject::connect(search, SIGNAL(clicked(QListViewItem*)),
		 search, SLOT(display(QListViewItem*)));
    QObject::connect(search, SIGNAL(display(const QString&)),
		 view, SLOT(fetch(const QString&)));
    QObject::connect(search, SIGNAL(returnPressed(QListViewItem*)),
		 search, SLOT(newSearch()));
    QObject::connect(&app, SIGNAL(lastWindowClosed()),
		 &app, SLOT(quit()));
 
    splitter.show();
    search->newSearch();
    return app.exec();
}

The entire application's behavior is produced using signals and slots connections. We connect the ArchiveSearch's clicked() signal to its display() slot; the display() slot in turn emits the display() signal with the URL. We connect the display() signal to the ArchiveView's fetch() slot. This means that when the user clicks an item in the list view the text of the URL is given to the browser so that it can display the relevant page. We connect the list view's returnPressed() signal to its newSearch() slot: When the user presses Enter a small dialog will pop up in which they can enter their search text, and then a new search is started.

Summing Up

For fast and simple needs QUrlOperator is often sufficient. If you need more control you can use QFtp and QHttp: Both these classes provide a high-level interface that covers most common situations, and lower-level access if you need to fine-tune. You can combine Qt classes to handle other protocols, for example using Qt's XML and HTTP support you could handle SOAP. And by aggregating (as we did with QListView in the qt-interest search example) you can make your Qt widgets network-capable.


Copyright © 2003 Trolltech. Trademarks