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 !