Communications longue distance avec QFtp et QHttp

| Rainer M. Schmid | traducteur : Thibaut Cuvelier | Qt Quarterly |

Les deux classes QHttp et QFtp, bien que toujours supportées, ne sont plus recommandées, depuis Qt 4.4 : on leur préfère désormais QNetworkAccessManager.

Cet article est une traduction autorisée de Far Reaching QFtp and QHttp, par Rainer M. Schmid.

Commentez

- Titre : Communications longue distance avec QFtp et QHttp

- Auteurs : Rainer M. Schmid, traducteur : Thibaut Cuvelier, Qt Quarterly

- Parution : 15 février 2009

- Mise à jour : 1 août 2011

- Licence : Copyright ® 2003 Rainer M. Schmid. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Introduction

- III. Système de fichiers et couche d'abstraction réseau

- IV. Les bases

- V. Spécificités du FTP

- VI. Requêtes HTTP

- VII. Résumé

- VIII. Divers

I - L'article original

Qt Quarterly est une revue trimestrielle électronique proposée par Nokia à destination des développeurs et utilisateurs de Qt. Vous pouvez trouver les versions originales.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'articleFar Reaching QFtp and QHttp de par Rainer M. Schmid paru dans la Qt Quarterly Issue 6.

Cet article est une traduction d'un des tutoriels écrits par Nokia Corporation and/or its subsidiary(-ies) inclus dans la documentation de Qt, en anglais. Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia.

II - Introduction

Les QUrlOperator fournissent un accès simple et pratique aux fichiers distants, accessibles via les protocoles FTP ou HTTP.

Apparu dans Qt 3.1, disparu de Qt 4.0, il fournit des interfaces plus directes pour ces protocoles via les classes QFtp et QHttp.

Cet article va traiter de la manière dont ces classes peuvent donner plus de pouvoir et de contrôle sur des fichiers et serveurs distants

III - Système de fichiers et couche d'abstraction réseau

L'idée à la base de l'interface QUrlOperator est de fournir une couche d'abstraction du système de fichiers. Cette abstraction nous permet de lire le contenu d'un fichier, de lister les entrées d'un répertoire, de copier des fichiers ...
Toutes ces opérations peuvent être réalisées d'une manière indépendante du réseau et du protocole, pour tous les protocoles supportés. Par exemple, vous pouvez écrire tout simplement ceci :

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

Ce qui est très pratique, vous en conviendrez, pour vous occuper de fichiers individuels, et pour lister des répertoires. Cependant, cette manière de faire a ses propres limitations.
Par exemple, QUrlOperator n'a aucune conscience des connexions persistantes, ce qui rend difficile l'implémentation d'un client FTP. Une autre limitation est que l'abstraction ne supporte pas le modèle de questions - réponses du HTTP.

Les classes QFtp et QHttp sont prévues pour des couches d'abstraction réseau plus concrètes, qui évitent les limitations de QUrlOperator.

Concernant le protocole FTP, cela signifie que nous commençons par nous connecter à un serveur, et puis nous nous identifions à ce serveur. Dès que la connexion est établie, nous pouvons utiliser toutes les commandes que nous voulons, et nous déconnecter à la fin.
La classe QFtp a des fonctions similaires à celles fournies par des clients interactifs FTP habituels.

Le protocole HTTP est orienté requête-réponse. La classe QHttp rend les requêtes POST très faciles (par exemple, une recherche pour un formulaire en ligne), ainsi que la réception de la réponse, qui contient les résultats de la requête.

IV - Les bases

Comme QUrlOperator, les classes QFtp et QHttp fonctionnent de manière asynchrone. Ce qui veut dire que, dès qu'une commande est appelée, la fonction la planifie pour exécution ultérieure, et retourne immédiatement. Ceci permet d'éviter que l'interface utilisateur ne reste bloquée pendant l'attente de la réponse, ce qui permet à l'utilisateur de continuer à interagir avec le programme, sans qu'il semble bloqué. Qt s'occupe de tout sans que vous n'ayez à vous en préoccuper, et envoie un signal pour notifier le programme de l'avancée du traitement.

Il est possible de planifier plusieurs commandes. Toute commande retourne un identifiant unique avec lequel elle peut être identifiée (par exemple, par les slots qui reçoivent leurs signaux). Mais, dans bien des cas, l'exécution d'une seule commande n'est pas très intéressante, c'est l'achèvement de tout le paquet de commandes qui importe.

Voici un exemple qui télécharge le fichier INSTALL du serveur FTP de Nokia Trolltech :

#include <QApplication>
#include <QFile>
#include <QFtp>

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

Remarquez que nous passons l'argument false au constructeur de la QApplication. Ceci indique à Qt qu'il ne s'agit pas d'une application avec interface graphique.
La fonction connectToHost() accepte aussi un numéro de port.
Et, si nous nous connections à un site qui requiert un nom d'utilisateur et un mot de passe, nous pourrions les passer à la fonction login().

Dans une vraie application, nous nous occuperions aussi des erreurs, ce qui peut être facilement effectué : le signal QFtp a un argument booléen, qui spécifie si les opérations ont connu le succès attendu. Si une erreur apparaît, on peut en avoir une description textuelle en utilisant QFtp::errorString.

La classe QHttp a une interface très semblable à QFtp. Par exemple, pour rapatrier un fichier avec le protocole HTTP, vous pouvez modifier le code ci-dessus, en incluant qhhtp.h, en créant un objet QHttp au lieu d'un QFtp, en remplaçant toutes les occurrences de ftp par http, et en utilisant ces lignes de code pour recevoir le fichier.

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

V - Spécificités du FTP

La classe QFtp fournit une interface pour les commandes les plus répandues, comme le téléchargement de fichier, le listing de répertoire, l'upload de fichiers, leur suppression ...
Mais que se passe-t-il lorsque vous essayez d'utiliser une commande que QFtp ne supporte pas, comme ajouter des données à la fin d'un fichier sur le serveur ?
Ceci n'est pas un problème. QFtp vous laisse envoyer des commandes arbitraires avec la fonction QFtp::rawCommand, même si vous devez interpréter la réponse du serveur vous-même.

Voici un petit exemple des changements de permission d'un fichier sur le serveur.
Le protocole FTP ne possède pas de fonction dans ce but, mais la majorité des serveurs FTP supporte la commande SITE CHMOD.

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

Ceci est une petite classe dérivée de QFtp pour ajouter une fonction permettant d'uploader un exécutable (pour le serveur) sur le serveur. Nous voulons que le fichier transféré ait les droits d'exécution, nous essayons donc de changer ses permissions pas défaut en envoyant la commande SITE CHMOD 755 [nom du fichier]

QFtp n'interprète alors pas la réponse du serveur à une requête envoyée par la fonction rawCommand(). A la place, il émet le signal rawCommandReply() avec une réponse à trois chiffres suivie d'une chaîne de détails. Le code indique le statut de la commande, et la chaîne, sa traduction, plus compréhensible.

Si le serveur renvoie un code d'erreur comme résultat d'un appel à rawCommand(), QFtp ne traitera pas ceci comme une situation d'erreur et ne transmettra pas d'erreur au sein de l'objet QFtp.

Ceci signifie que vous devez aussi interpréter le code réponse vous-même, et, si vous recevez un code d'erreur, vous devez vous charger de cette erreur.

Le premier chiffre d'un code de réponse FTP décrit le statut générique de la commande. Une réponse commençant par un 2 indique une "réponse positive". Dans cet exemple, nous savons que, si le code commence par 2, la commande SITE CHMOD a réussi.

Nous pouvons tester notre extension à QFtp avec la fonction main() suivante, où nous essayons d'uploader un script Python fictif que nous voulons rendre exécutable. Notez que, si nous changeons les permissions du script sur le serveur, nous devons y être connectés avec des droits en écriture.

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

VI - Requêtes HTTP

La classe QHttp a des fonctions utiles pour la majorité des requêtes HTTP, comme GET, POST et HEAD. Si vous avez besoin d'une d'elles, vous n'avez pas à vous soucier de l'en-tête, QHttp utilisera des valeurs par défaut, qui fonctionnent dans la majorité des cas.

Mais si vous avez besoin d'un contrôle plus fin, vous pouvez envoyer des requêtes HTTP via la fonction QHttp::request. Dans cette section, nous présentons un exemple de classe dérivée de QListView, et qui utilise cette filiation pour recherche l'archive de qt-interest. Pour parvenir à ce but, nous envoyons une requête POST à http://qt.nokia.com/search.html. La réponse à cette requête est une page HTML qui contient une liste de documents correspondants que nous présentons dans une vue de liste.

Nous ne pouvons faire ceci avec QUrlOperator, car, si nous utilisions QUrlOperator::put(), ce qui résulte en une requête POST, nous ne recevrions aucune donnée en retour à cause de l'abstraction que QUrlOperator utilise.

Voici un exemple de classe dérivée, ArchiveSearch, qui est utilisée pour une recherche sur le terme QHttp.

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

La déclaration de la classe est très petite.

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

Nous avons fait de la classe un dérivé de QListView, pour qu'elle puisse afficher les résultats elle-même. Elle utilise la variable privée http pour faire ses requêtes HTTP.

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

Nous ajoutons deux colonnes dans le constructeur à la vue de liste pour montrer le titre et l'URL de chaque page. Le seul signal qui nous intéresse est 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());
}

La recherche, asynchrone, peut prendre un certain temps, nous changeons donc le curseur pour montrer que l'application est occupée. Ensuite, nous préparons l'en-tête et la chaîne de recherche. Même si nous utilisons une requête POST, nous ne pouvons pas utiliser la fonction QHttp::post, parce que nous avons besoin de mettre le type de contenu de nos données à application/x-www-form-urlencoded, nous pourrons donc utiliser la fonction plus générique QHttp::request. Utiliser QHttp::request signifie que nous devons mettre l'en-tête de la requête nous-mêmes. Mettre l'en-tête Host est nécessaire, car nous utilisons HTTP/1.1. La recherche doit être encodée (pour permettre les espaces, entre autres), et donnée à request() en tant que QByteArray, malgré l'appel à QString::utf.

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

Quand la requête est finie, le slot ArchiveSearch::done() est appelé. Si une erreur survient, nous lisons les données de réponse dans un QString. Nous utilisons une expression régulière pour extraire le titre et l'URL de chaque correspondance du fichier HTML retourné.

Mais comment pourrions-nous utiliser cette classe, pratiquement ? Un moyen très simple serait de le coupler avec un QTextBrowser. Ce qui ne ferait pas un navigateur : la compréhension du HTML du QTextBrowser est beaucoup trop basique. WebKit est prévu à cet effet.

Pour créer une telle application, nous aurions besoin d'ajouter un slot public, un signal et un slot privé à la classe ArchiveSearch.

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

Avec ces quelques ajouts, nous pouvons connecter n'importe quelle interaction de l'utilisateur (un appui sur Enter, par exemple), pour invoquer le slot newSearch(). Et, quand un item est choisi (par exemple, par un clic), nous pouvons émettre le signal display avec l'URL que nous voulons montrer.

Nous aurons aussi besoin d'une classe dérivant de QTextBrowser.

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

Nous pouvons désormais mettre le tout dans une application (nous avons omis les 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();
}

Le comportement entier de l'application est produit avec des signaux et des slots. Nous connectons le signal clicked() de ArchiveSearch au slot display(). Ce slot émet, en retour, le signal display() avec l'URL. Nous connectons le signal display() au slot fetch(). Au final, quand l'utilisateur clique sur un item dans la vue de liste, le texte de l'URL est donné au navigateur, pour qu'il puisse afficher la page. Nous connectons le signal returnPressed() à son slot newSearch(). Quand l'utilisateur appuie sur Enter, un petit dialogue apparaît, dans lequel il pourra entrer sa recherche. Ensuite, une nouvelle recherche est entamée.

VII - Résumé

Pour des besoins très simples, QUrlOperator est très souvent suffisant. Si vous avez besoin de plus de contrôle, vous pouvez utiliser QFtp et QHttp. En effet, ces deux classes proposent une interface de haut niveau, qui couvre la plupart des besoins courants, et un accès très bas niveau si vous avez besoin d'aller aussi loin. Vous pouvez combiner des classes Qt pour d'autres protocoles, comme XML et HTTP pour supporter SOAP.

VIII - Divers

Ceci a été écrit pour Qt3. Cependant, la majorité du contenu reste d'actualité pour Qt4.

J'adresse ici de chaleureux remerciements à Ikipou, kinji1, yan, pour leur aide lors de la traduction, et à matrix788, pour sa rapide relecture !

Au nom de toute l'équipe Qt, j'aimerais adresser le plus grand remerciement à Nokia pour nous avoir autorisé la traduction de cet article !