10. Services Web
10.1. Introduction
Nous avons présenté dans le chapitre précédent plusieurs applications client-serveur tcp-ip. Dans la mesure où les clients et le serveur échangent des lignes de texte, ils peuvent être écrits en n'importe quel langage. Le client doit simplement connaître le protocole de dialogue attendu par le serveur. Les services Web sont des applications serveur tcp-ip présentant les caractéristiques suivantes :
- Elles sont hébergées par des serveurs web et le protocole d'échanges client-serveur est donc HTTP (HyperText Transport Protocol), un protocole au-dessus de TCP-IP.
- Le service Web a un protocole de dialogue standard quelque soit le service assuré. Un service Web offre divers services S1, S2, .., Sn. Chacun d'eux attend des paramètres fournis par le client et rend à celui-ci un résultat. Pour chaque service, le client a besoin de savoir :
- le nom exact du service Si
- la liste des paramètres qu'il faut lui fournir et leur type
- le type de résultat retourné par le service
Une fois, ces éléments connus, le dialogue client-serveur suit le même format quelque soit le service web interrogé. L'écriture des clients est ainsi normalisée.
- Pour des raisons de sécurité vis à vis des attaques venant de l'internet, beaucoup d'organisations ont des réseaux privés et n'ouvrent sur Internet que certains ports de leurs serveurs : essentiellement le port 80 du service web. Tous les autres ports sont verrouillés. Aussi les applications client-serveur telles que présentées dans le chapitre précédent sont-elles construites au sein du réseau privé (intranet) et ne sont en général pas accessibles de l'extérieur. Loger un service au sein d'un serveur web le rend accessible à toute la communauté internet.
- Le service Web peut être modélisé comme un objet distant. Les services offerts deviennent alors des méthodes de cet objet. Un client peut avoir accès à cet objet distant comme s'il était local. Cela cache toute la partie communication réseau et permet de construire un client indépendant de cette couche. Si celle-ci vient à changer, le client n'a pas à être modifié. C'est là un énorme avantage et probablement le principal atout des services Web.
- Comme pour les applications client-serveur tcp-ip présentées dans le chapitre précédent, le client et le serveur peuvent être écrits dans un langage quelconque. Ils échangent des lignes de texte. Celles-ci comportent deux parties :
- les entêtes nécessaires au protocole HTTP
- le corps du message. Pour une réponse du serveur au client, celui-ci est au format XML (eXtensible Markup Language). Pour une demande du client au serveur, le corps du message peut avoir plusieurs formes dont XML. La demande XML du client peut avoir un format particulier appelé SOAP (Simple Object Access Protocol). Dans ce cas, la réponse du serveur suit aussi le format SOAP.
10.2. Les navigateurs et XML
Les services Web envoient du XML à leurs clients. Les navigateurs peuvent réagir différemment à la réception de ce flux XML. Internet Explorer a une feuille de style prédéfinie qui permet de l'afficher. Netscape Communicator n'a pas lui cette feuille de style et n'affiche pas le code XML reçu. Il faut visualiser le code source de la page reçue pour avoir accès au XML. Voici un exemple. pour le code XML suivant :
<?xml version="1.0" encoding="utf-8"?>
<string xmlns="st.istia.univ-angers.fr">bonjour de nouveau !</string>
Internet Explorer affichera la page suivante :

alors que Netscape Navigator affichera :

Si on visualise le code source de la page reçue par Netscape, on obtient :

Netscape a bien reçu la même chose que Internet Explorer mais il l'a affiché différemment. Dans la suite, nous utiliserons Internet Explorer pour les copies d'écran.
10.3. Un premier service Web
Nous allons découvrir les services web au travers d'un exemple simplissime décliné en trois versions.
10.3.1. Version 1
Pour cette première version nous allons utiliser VS.NET qui présente l'avantage de pouvoir générer un squelette de service web immédiatement opérationnel. Une fois comprise cette architecture, nous pourrons commencer à voler de nos propres ailes. Ce sera l'objet des versions suivantes.
Avec VS.NET, construisons un nouveau projet avec l'option [Fichier/Nouveau/Projet] :

On notera les points suivants :
- le type du projet est Visual Basic (cadre de gauche)
- le modèle du projet est Service Web ASP.NET (cadre de droite)
- l'emplacement est libre. Ici, le service web sera hébergé par un serveur Web IIS local. Son URL sera donc http://localhost/[chemin] où [chemin] est à définir. Ici, nous choisissons le chemin http://localhost/polyvbnet/demo. VS.NET va alors créer un dossier pour ce projet. Où ? Le serveur IIS a une racine pour l'arborescence des documents web qu'il délivre. Appelons cette racine <IISroot>. Elle correspond à l'URL http://localhost. On en déduit que l'URL http://localhost/polyvbnet/demo sera associée au dossier <IISroot>/polyvbnet/demo. <IISroot> est normalement le dossier \inetpub\wwwroot sur le disque où a été installé IIS. Dans notre exemple c'est le disque E. Le dossier créé par VS.NET est donc le dossier e:\inetpub\wwwroot\polyvbnet\demo :

Comme toujours, il y a une surabondance de dossiers créés. Ils n'ont pas toujours un intérêt. Nous n'expliciterons que ceux dont nous avons besoin à un moment donné. VS.NET a créé un projet :

Nous retrouvons certains des fichiers présents dans le dosier physique du projet. Le plus intéressant pour nous est le fichier de suffixe asmx. C'est le suffixe des services web. Un service web est géré par VS.NET comme une application windows, c.a.d. une application qui a une interface graphique et du code pour la gérer. C'est pourquoi, nous avons une fenêtre de conception :

Un service web n'a normalement pas d'interface graphique. Il représente un objet qu'on peut appeler à distance. Il possède des méthodes et les applications appellent celles-ci. Nous le verrons donc comme un objet classique avec cette particularité qu'il a de pouvoir être instancié à distance via le réseau. Aussi, n'utiliserons-nous pas la fenêtre de conception présentée par VS.NET. Intéressons-nous plutôt au code du service en utilisant l'option Affichage/Code :

Plusieurs points sont à noter :
- le fichier s'appelle Service1.asmx.vb et non Service1.asmx. Nous reviendrons sur le contenu du fichier Service1.asmx un peu plus loin.
- on retrouve une fenêtre de code analogue à celle qu'on avait lorsqu'on construisait des applications windows avec VS.NET
Le code généré par VS.NET est le suivant :
Imports System.Web.Services
<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
Inherits System.Web.Services.WebService
#Region " Code généré par le Concepteur des services Web "
Public Sub New()
MyBase.New()
'Cet appel est requis par le Concepteur des services Web.
InitializeComponent()
'Ajoutez votre code d'initialisation après l'appel InitializeComponent()
End Sub
'Requis par le Concepteur des services Web
Private components As System.ComponentModel.IContainer
'REMARQUE : la procédure suivante est requise par le Concepteur des services Web
'Elle peut être modifiée en utilisant le Concepteur des services Web.
'Ne la modifiez pas en utilisant l'éditeur de code.
<System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
components = New System.ComponentModel.Container()
End Sub
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
'CODEGEN : cette procédure est requise par le Concepteur des services Web
'Ne la modifiez pas en utilisant l'éditeur de code.
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub
#End Region
' EXEMPLE DE SERVICE WEB
' L'exemple de service HelloWorld() retourne la chaîne Hello World.
' Pour générer, ne commentez pas les lignes suivantes, puis enregistrez et générez le projet.
' Pour tester ce service Web, assurez-vous que le fichier .asmx est la page de démarrage
' et appuyez sur F5.
'
'<WebMethod()> Public Function HelloWorld() As String
' HelloWorld = "Hello World"
' End Function
End Class
Tout d'abord, remarquons que nous avons là une classe, la classe Service1 qui dérive de la classe WebService :
Cela nous amène à importer l'espace de noms System.Web.Services :
Imports System.Web.Services
La déclaration de la classe est précédée d'un attribut de compilation :
<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
Inherits System.Web.Services.WebService
L'attribut System.Web.Services.WebService() indique que la classe qui suit est un service web. Cet attribut admet divers paramètres dont un appelé NameSpace. Il sert à placer le service web dans un espace de noms. En effet, on peut imaginer qu'il y ait plusieurs services web appelés meteo dans le monde. Il nous faut un moyen de les différentier. C'est l'espace de noms qui le permet. L'un pourra s'appeler [espacenom1].meteo et un autre [espacenom2].meteo. On retrouve là, un concept analogue aux espaces de noms des classes. VS.NET a automatiquement généré du code qu'il a mise dans une région du source :
#Region " Code généré par le Concepteur des services Web "
Si on regarde ce code, on retrouve celui que le concepteur générait lorsqu'on construisait des applications windows. C'est un code que l'on pourra purement et simplement supprimer si on n'a pas d'interface graphique, ce qui sera notre cas pour les services web.
La classe se termine par un exemple de ce que pourrait être un service web :
#End Region
' EXEMPLE DE SERVICE WEB
' L'exemple de service HelloWorld() retourne la chaîne Hello World.
' Pour générer, ne commentez pas les lignes suivantes, puis enregistrez et générez le projet.
' Pour tester ce service Web, assurez-vous que le fichier .asmx est la page de démarrage
' et appuyez sur F5.
'
'<WebMethod()> Public Function HelloWorld() As String
' HelloWorld = "Hello World"
' End Function
Fort de ce qui vient d'être dit, nous nettoyons le code pour qu'il devienne le suivant :
Imports System.Web.Services
<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour
Inherits System.Web.Services.WebService
<WebMethod()> Public Function Bonjour() As String
Return "bonjour !"
End Function
End Class
Nous y voyons un peu plus clair.
- un service web est une classe dérivant de la classe WebService
- la classe est qualifiée par l'attribut <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. On place donc notre service dans l'espace de noms st.istia.univ-angers.fr.
- les méthodes de la classe sont qualifiées par un attribut <WebMethod()> indiquant qu'on a affaire à une méthode qui peut être appelée à distance via le réseau
La classe assurant notre service web s'appelle donc Bonjour et a une seule méthode s'appelant elle-aussi Bonjour qui rend une chaîne de caractères. Nous sommes prêts pour un premier test.
- lançons le serveur web IIS si ce n'est fait
- utilisons l'option Déboguer/Exécuter sans débogage. VS.NET
VS.NET va alors compiler l'ensemble de l'application, lancer un navigateur (souvent Internet Explorer s'il est présent), et afficher l'url http://localhost/polyvbnet/demo/Service1.asmx :

Pourquoi l'url http://localhost/polyvbnet/demo/Service1.asmx ? Parce que c'était le seul fichier .asmx du projet :

S'il y avait eu plusieurs fichiers .asmx, il nous aurait fallu préciser celui qui devait être exécuté en premier. Cela se fait en cliquant droit sur le fichier .asmx concerné et en prenant l'option [Définir comme page de démarrage].

On pourrait être intéressé par savoir ce que contient le fichier service1.asmx. En effet, avec VS.NET nous avons travaillé sur le fichier service1.asmx.vb et non sur le fichier service1.asmx. Ce fichier se trouve dans le dossier du projet :

Ouvrons avec un éditeur de texte (notepad ou autre). On obtient le contenu suivant :
Le fichier contient une simple directive à l'intention du serveur IIS indiquant :
- qu'on a affaire à un service web (mot clé WebService)
- que le langage de la classe de ce service est Visual Basic (Language="vb")
- que le source de cette classe sera trouvée dans le fichier Service1.asmx.vb (Codebehind="Service1.asmx.vb")
- que la classe implémentant le service s'appelle demo.Bonjour (Class="demo.Bonjour"). On remarquera que VS.NET a placé la classe Bonjour dans l'espace de noms demo qui est aussi le nom du projet.
Revenons à la page obtenue à l'url http://localhost/polyvbnet/demo/Service1.asmx :

Qui a écrit le code HTML de la page ci-dessus ? Pas nous, nous le savons. C'est IIS, qui présente les services web d'une façon standard. Cette page nous propose deux liens. Suivons le premier [Description du service] :

Ooops... c'est du XML plutôt abscons. Remarquons quand même l'URL
http://localhost/polyvbnet/demo/Service1.asmx?WSDL. Prenez un navigateur, et tapez directement cette url. Vous obtenez la même chose que précédemment. On se rappellera donc que l'url http://serviceweb?WSDL donne accès à la description XML du service web. Revenons à la page de départ et prenons le lien [Bonjour]. Rappelons-nous que Bonjour est une méthode du service web. Si nous avions eu plusieurs méthodes, elles auraient été toutes présentées ici. Nous obtenons la nouvelle page suivante :

Nous avons volontairement tronqué la page obtenue pour ne pas alourdir notre démonstration. Remarquons de nouveau l'url obtenue :
Si nous tapons directement cette url dans un navigateur, nous obtiendrons la même chose que ci-dessus. On nous incite à utiliser le bouton [Appeler]. Faisons-le. Nous obtenons une nouvelle page :

C'est de nouveau du XML. On y retrouve deux informations qui étaient présentes dans notre service web :
- l'espace de noms st.istia.univ-angers.fr de notre service
<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
- la valeur rendue par la méthode Bonjour :
Return "bonjour !"
Qu'avons-nous appris ?
- la façon d'écrire un service web S
- la façon de l'appeler
Nous nous intéressons maintenant à l'écriture d'un service web sans l'aide de VS.NET.
10.3.2. Version 2
Dans l'exemple précédent, VS.NET a fait beaucoup de choses tout seul. Est-il possible de construire un service web sans cet outil ? La réponse est oui et nous le montrons maintenant. Avec un éditeur de texte, nous construisons le service web suivant :
Imports System.Web.Services
<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour2
Inherits System.Web.Services.WebService
<WebMethod()> Public Function getBonjour() As String
Return "bonjour de nouveau !"
End Function
End Class
La classe s'appelle Bonjour2 et a une méthode qui s'appelle getBonjour. Elle a été placée dans le fichier demo2.vb lui même placé dans l'arborescence du serveur IIS dans le dossier E:\Inetpub\wwwroot\polyvbnet\demo2. C'est une classe VB.NET classique qu'on peut donc compiler :
dos>vbc /out:demo2 /t:library /r:system.dll /r:system.web.services.dll demo2.vb
dos>dir
02/03/2004 18:04 286 demo2.vb
02/03/2004 18:10 77 demo2.asmx
02/03/2004 18:12 3 072 demo2.dll
Nous mettons l'assemblage demo2.dll dans un dossier bin (ce nom est obligatoire) :
Nous créons maintenant le fichier demo2.asmx. C'est lui qui sera appelé par les clients web. Son contenu est le suivant :
Nous avons déjà rencontré cette directive. Elle indique que :
- la classe du service web s'appelle Bonjour2 et se trouve dans l'assemblage demo2.dll. IIS cherchera cet assemblage dans différents endroits et notamment dans le dossier bin du service web. C'est pourquoi, nous avons placé là l'assemblage demo2.dll.
Maintenant nous pouvons faire divers tests. On s'assure que IIS est actif et on demande avec un navigateur l'url http://localhost/polyvbnet/demo2/demo2.asmx :

Puis l'url http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Puis l'URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, où getBonjour est le nom de l'unique méthode de notre service web :

Nous utilisons le bouton [Appeler] ci-dessus :

Nous obtenons bien le résultat de l'appel à la méthode getBonjour du service web. Nous savons maintenant comment construire un service web sans vs.net. Nous ferons désormais abstraction de la façon dont est construit le service web pour ne nous intéresser qu'aux fichiers fondamentaux.
10.3.3. Version 3
Les deux versions précédentes du service web [Bonjour] utilisaient deux fichiers :
- un fichier .asmx, point d'entrée du service web
- un fichier .vb, code source du service web
Nous montrons ici, qu'on peut se contenter du seul fichier .asmx. Le code du service demo3.asmx est le suivant :
Nous constatons que le code source du service est maintenant directement dans le fichier source du fichier demo3.asmx. La directive
ne référence plus une classe dans un assemblage externe, mais une classe se trouvant dans le même fichier source. Plaçons celui-ci dans le dossier <IISroot>\polyvbnet\demo3 :

Lançons IIS et demandons l'url http://localhost/polyvbnet/demo3/demo3.asmx :

Nous constatons une différence importante par rapport à la version précédente : nous n'avons pas eu à compiler le code VB du service. IIS a opéré cette compilation lui-même par l'intermédiaire du compilateur VB.NET installé sur la même machine. Puis il a délivré la page. S'il y a une erreur de compilation, celle-ci sera signalée par IIS :

10.3.4. Version 4
Nous nous intéressons ici à la configuration du serveur IIS. Nous avons toujours, jusqu'à maintenant, placé nos services web dans l'arborescence de racine <IISroot> du serveur IIS, ici [e:\inetpub\wwwroot]. Nous montrons ici que nous pouvons placer le service web n'importe où. Cela se fait à l'aide des dossiers virtuels de IIS. Plaçons notre service dans le dossier suivant :

Le dossier [D:\data\devel\vbnet\poly\chap9\demo3] ne se trouve pas dans l'arborescence du serveur IIS. On doit l'indiquer à celui-ci en créant un dossier IIS virtuel. Lançons IIS et prenons l'option [Avancé] ci-dessous :

Nous avons une liste de répertoires virtuels qui nous est présentée. Nous ne nous attarderons pas sur celle-ci. On crée un nouveau répertoire virtuel avec le bouton [Ajouter] ci-dessus :

A l'aide du bouton [Parcourir], nous désignons le dossier physique contenant le service web, ici le dossier [D:\data\devel\vbnet\poly\chap9\demo3]. Nous donnons un nom logique (virtuel) à ce dossier : [virdemo3]. Cela signifie que les documents à l'intérieur du dossier physique [D:\data\devel\vbnet\poly\chap9\demo3] seront accessibles sur le réseau via l'url [http://<machine>/virdemo3]. La boîte de dialogue ci-dessus comporte d'autres paramètres qu'on laisse en l'état. Nous validons la boîte. Le nouveau dossier virtuel apparaît dans la liste des dossiers virtuels de IIS :

Maintenant, nous prenons un navigateur et nous demandons l'url [http://localhost/virdemo3/demo3.asmx]. Nous obtenons la même chose qu'auparavant :

10.3.5. Conclusion
Nous avons montré plusieurs façons de procéder pour créer un service web. Par la suite, nous utiliserons la méthode de la version 3 pour la création du service et la méthode 4 pour sa localisation. Nous n'aurons pas ainsi besoin de VS.NET. Néanmoins notons l'intérêt d'utiliser VS.NET pour l'aide qu'il apporte au débogage. Il existe des outils gratuits pour développer des application web, notamment le produit WebMatrix sponsorisé par Microsoft et qu'on trouvera à l'URL [http://www.asp.net/webmatrix]. C'est un outil excellent pour démarrer la programmation web sans investissement préalable.
10.4. Un service web d'opérations
Nous considérons un service Web qui offre cinq fonctions :
- ajouter(a,b) qui rendra a+b
- soustraire(a,b) qui rendra a-b
- multiplier(a,b) qui rendra a*b
- diviser(a,b) qui rendra a/b
- toutfaire(a,b) qui rendra le tableau [a+b,a-b,a*b,a/b]
Le code VB.NET de ce service est le suivant :
<%@ WebService language="VB" class=operations %>
imports system.web.services
<WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class operations
Inherits WebService
<WebMethod> _
Function ajouter(a As Double, b As Double) As Double
Return a + b
End Function
<WebMethod> _
Function soustraire(a As Double, b As Double) As Double
Return a - b
End Function
<WebMethod> _
Function multiplier(a As Double, b As Double) As Double
Return a * b
End Function
<WebMethod> _
Function diviser(a As Double, b As Double) As Double
Return a / b
End Function
<WebMethod> _
Function toutfaire(a As Double, b As Double) As Double()
Return New Double() {a + b, a - b, a * b, a / b}
End Function
End Class
Nous reprenons ici certaines explications déjà données mais qui méritent d'être rappelées ou complétées. La classe operations ressemble à une classe VB.NET avec cependant quelques points à noter :
- les méthodes sont précédées d'un attribut <WebMethod()> qui indique au compilateur les méthodes qui doivent être "publiées" c.a.d. rendues disponibles au client. Une méthode non précédée de cet attribut serait invisible aux clients distants. Ce pourrait être une méthode interne utilisée par d'autres méthodes mais pas destinée à être publiée.
- la classe dérive de la classe WebService définie dans l'espace de noms System.Web.Services. Cet héritage n'est pas toujours obligatoire. Dans cet exemple notamment on pourrait s'en passer.
- la classe elle-même est précédée d'un attribut <WebService(Namespace="st.istia.univ-angers.fr")> destiné à donner un espace de noms au service web. Un vendeur de classes donne un espace de noms à ses classes afin de leur donner un nom unique et éviter ainsi des conflits avec des classes d'autres vendeurs qui pourraient porter le même nom. Pour les services Web, c'est pareil. Chaque service web doit pouvoir être identifié par un nom unique, ici par st.istia.univ-angers.fr.
- nous n'avons pas défini de constructeur. C'est donc implicitement le constructeur de la classe parent qui sera utilisé.
Le code source précédent n'est pas destiné directement au compilateur VB.NET mais au serveur Web IIS. Il doit porter le suffixe .asmx et sauvegardé dans l'arborescence du serveur Web. Ici nous le sauvegardons sous le nom operations.asmx dans le dossier <IISroot>\polyvbnet\operations :

Nous associons à ce dossier physique, le dossier virtuel IIS [operations] :
![]() | ![]() |
Accédons au service avec un navigateur. L'URl à demander est [http://localhost/operations/operations.asmx] :

Nous obtenons un document Web avec un lien pour chacune des méthodes définies dans le service web operations. Suivons le lien ajouter :

La page obtenue nous propose de tester la méthode ajouter en lui fournissant les deux arguments a et b dont elle a besoin. Rappelons la définition de la méthode ajouter :
On notera que la page a repris les noms des arguments a et b utilisés dans la définition de la méthode. On utilise le bouton Appeler et on obtient la réponse suivante dans une fenêtre séparée du navigateur :

Si ci-dessus, on fait [Affichage/Source] on obtient le code suivant :

Refaisons l'opération pour la méthode [toutfaire] :

Nous obtenons la page suivante :

Utilisons le bouton [Appeler] ci-dessus :

Dans tous les cas, la réponse du serveur a la forme :
- la réponse est au format XML
- la ligne 1 est standard et est toujours présente dans la réponse
- les lignes suivantes dépendent du type de résultat (double,ArrayOfDouble), du nombre de résultats, et de l'espace de noms du service web (st.istia.univ-angers.fr ici).
Il existe plusieurs méthodes pour interroger un service web et obtenir sa réponse . Revenons à l'URL du service :

et suivons le lien [ajouter]. Dans page présentée, sont exposées deux méthodes pour interroger la fonction [ajouter] du service web :



Ces deux méthodes d'accès aux fonctions d'un service web sont appelées respectivement : HTTP-POST et SOAP. Nous les examinons maintenant l'une après l'autre.
Note : dans les premières versions de VS.NET, il existait une 3ième méthode appelée HTTP-GET. Au jour d'écriture de ce document (mars 2004), cette méthode ne semble plus être disponible. Cela veut dire que le service web généré par VS.NET n'accepte pas de requêtes GET. Cela ne veut pas dire qu'on ne peut pas écrire de services web acceptant les requêtes GET, notamment avec d'autres outils que VS.NET ou simplement à la main.
10.5. Un client HTTP-POST
Nous suivons la méthode proposée par le service web :

Commentons ce qui est écrit. Tout da'bord le client web doit envoyer les entêtes HTTP suivants :
Le client web fait une requête POST à l'URL /operations/operations.asmx/ajouter selon le protocole HTTP version 1.1 | |
On précise la machine cible de la requête. Ici localhost. Cet entête a été rendu obligatoire par la version 1.1 du protocole HTTP | |
On précise ici qu'après les entêtes HTTP on va envoyer des paramètres supplémentaires au format urlencoded. Ce format remplace certains caractères par leur code hexadécimal. | |
C'est la taille en caractères de la chaîne de paramètres qui sera envoyée après les entêtes HTTP. |
Les entêtes HTTP sont suivis d'une ligne vide puis de la chaîne de paramètres du POST de [Content-Length] caractères sous la forme a=XX&b=YY où XX et YY sont les chaînes "urlencodées" des valeurs des paramètres a et b. Nous en savons assez pour reproduire ce qui ci-dessus avec notre client tcp générique déjà utilisé dans le chapitre sur la programmation tcp-ip :
- nous lançons IIS
- le service est disponible à l'url [http://localhost/operations/operations.asmx]
- nous utilisons le client tcp générique dans une fenêtre DOS
dos>clttcpgenerique localhost 80
Commandes :
POST /operations/operations.asmx/ajouter HTTP/1.1
HOST: localhost
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-length: 7
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--
a=2&b=3
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:26 GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Remarquons tout d'abord que nous avons ajouté l'entête [Connection: close] pour demander au serveur de fermer la connexion après avoir envoyé la réponse. Cela est nécessaire ici. Si on ne le dit pas, par défaut le serveur va garder la connexion ouverte. Or sa réponse est une suite de lignes de texte dont la dernière n'est pas terminée par une marque de fin de ligne. Il se trouve que notre client TCP générique lit des lignes de texte terminées par la marque de fin de ligne avec la méthode ReadLine. Si le serveur ne ferme pas la connexion après envoi de la dernière ligne, le client est bloqué parce qu'il attend une marque de fin de ligne qui ne vient pas. Si le serveur ferme la connexion, la méthode ReadLine du client se termine et le client ne reste pas bloqué.
Aussitôt après avoir reçu la ligne vide signalant la fin des entêtes HTTP, le serveur IIS envoie une première réponse :
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--
Cette réponse formée uniquement d'entêtes HTTP indique au client qu'il peut envoyer les 7 caractères qu'il a dit vouloir envoyer. Ce que nous faisons :
Il faut voir ici que notre client tcp envoie plus de 7 caractères puisqu'il les envoie avec une marque de fin de ligne (WriteLine). Ca ne gêne pas le serveur qui des caractères reçus ne prendra que les 7 premiers et parce qu'ensuite la connexion est fermée (Connection: close). Ces caractères en trop auraient été gênants si la connexion était restée ouverte car alors ils auraient été pris comme venant de la commande suivante du client. Une fois les paramètres reçus, le serveur envoie sa réponse :
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:26 GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>
Nous avons maintenant les éléments pour écrire un client programmé pour notre service web. Ce sera un client console appelé httpPost2 et s'utilisant comme suit :
dos>httpPost2 http://localhost/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 6 7
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[résultat=13]
soustraire 8 9
--> POST /operations/operations.asmx/soustraire HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=8&b=9
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">-1</double>
[résultat=-1]
fin
dos>
Le client est appelé en lui passant l'URL du service web :
Ensuite, le client lit les commandes tapées au clavier et les exécute. Celles-ci sont au format :
où fonction est la fonction du service web appelée (ajouter, soustraire, multiplier, diviser) et a et b les valeurs sur lesquelles va opérer cette fonction. Par exemple :
A partir de là, le client va faire la requête HTTP nécessaire au serveur Web et obtenir une réponse. Les échanges client-serveur sont dupliqués à l'écran pour une meilleure compréhension du processus :
ajouter 6 7
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<--
--> a=6&b=7
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
[résultat=13]
On retrouve ci-dessus l'échange déjà rencontré avec le client tcp générique à une différence près : l'entête HTTP Connection: Keep-Alive demande au serveur de ne pas fermer la connexion. Celle-ci reste donc ouverte pour l'opération suivante du client qui n'a donc pas besoin de se reconnecter de nouveau au serveur. Cela l'oblige cependant à utiliser une autre méthode que ReadLine() pour lire la réponse du serveur puisqu'on sait que celle-ci est une suite de lignes dont la dernière n'est pas terminée par une marque de fin de ligne. Une fois toute la réponse du serveur obtenue, le client l'analyse pour y trouver le résultat de l'opération demandée et l'afficher :
Examinons le code de notre client :
' espaces de noms
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web
' client d'un service web operations
Public Module clientPOST
Public Sub Main(ByVal args() As String)
' syntaxe
Const syntaxe As String = "pg URI"
Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
' nombre d'arguments
If args.Length <> 1 Then
erreur(syntaxe, 1)
End If
' on note l'URI demandée
Dim URIstring As String = args(0)
' on se connecte au serveur
Dim uri As Uri = Nothing ' l'URI du service web
Dim client As TcpClient = Nothing ' la liaison tcp du client avec le serveur
Dim [IN] As StreamReader = Nothing ' le flux de lecture du client
Dim OUT As StreamWriter = Nothing ' le flux d'écriture du client
Try
' connexion au serveur
uri = New Uri(URIstring)
client = New TcpClient(uri.Host, uri.Port)
' on crée les flux d'entrée-sortie du client TCP
[IN] = New StreamReader(client.GetStream())
OUT = New StreamWriter(client.GetStream())
OUT.AutoFlush = True
Catch ex As Exception
' URI incorrecte ou autre problème
erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
End Try
' création d'un dictionnaire des fonctions du service web
Dim dicoFonctions As New Hashtable
Dim i As Integer
For i = 0 To fonctions.Length - 1
dicoFonctions.Add(fonctions(i), True)
Next i
' les demandes de l'utilisateur sont tapées au clavier
' sous la forme fonction a b
' elles se terminent avec la commande fin
Dim commande As String = Nothing ' commande tapée au clavier
Dim champs As String() = Nothing ' champs d'une ligne de commande
Dim fonction As String = Nothing ' nom d'une fonction du service web
Dim a, b As String ' les arguments des fonctions du service web
' invite à l'utilisateur
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")
' gestion des erreurs
Dim erreurCommande As Boolean
Try
' boucle de saisie des commandes tapées au clavier
While True
' pas d'erreur au départ
erreurCommande = False
' lecture commande
commande = Console.In.ReadLine().Trim().ToLower()
' fini ?
If commande Is Nothing Or commande = "fin" Then
Exit While
End If
' décomposition de la commande en champs
champs = Regex.Split(commande, "\s+")
Try
' il faut trois champs
If champs.Length <> 3 Then
Throw New Exception
End If
' le champ 0 doit être une fonction reconnue
fonction = champs(0)
If Not dicoFonctions.ContainsKey(fonction) Then
Throw New Exception
End If
' le champ 1 doit être un nombre valide
a = champs(1)
Double.Parse(a)
' le champ 2 doit être un nombre valide
b = champs(2)
Double.Parse(b)
Catch
' commande invalide
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
erreurCommande = True
End Try
' on fait la demande au service web
If Not erreurCommande Then executeFonction([IN], OUT, uri, fonction, a, b)
End While
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
' fin liaison client-serveur
Try
[IN].Close()
OUT.Close()
client.Close()
Catch
End Try
End Sub
...........
' affichage des erreurs
Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
' affichage erreur
System.Console.Error.WriteLine(msg)
' arrêt avec erreur
Environment.Exit(exitCode)
End Sub
End Module
On a là des choses déjà rencontrées plusieurs fois et qui ne nécessitent pas de commentaires particuliers. Examinons maintenant le code de la méthode executeFonction où résident les nouveautés :
' executeFonction
Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
' exécute fonction(a,b) sur le service web d'URI uri
' les échanges client-serveur se font via les flux IN et OUT
' le résultat de la fonction est dans la ligne
' <double xmlns="st.istia.univ-angers.fr">double</double>
' envoyée par le serveur
' construction de la chaîne de requête
Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
Dim nbChars As Integer = requête.Length
' construction du tableau des entêtes HTTP à envoyer
Dim entetes(5) As String
entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
entetes(1) = "Host: " & uri.Host & ":" & uri.Port
entetes(2) = "Content-Type: application/x-www-form-urlencoded"
entetes(3) = "Content-Length: " & nbChars
entetes(4) = "Connection: Keep-Alive"
entetes(5) = ""
' on envoie les entêtes HTTP au serveur
Dim i As Integer
For i = 0 To entetes.Length - 1
' envoi au serveur
OUT.WriteLine(entetes(i))
' écho écran
Console.Out.WriteLine(("--> " + entetes(i)))
Next i
' on lit la 1ere réponse du serveur Web HTTP/1.1 100
Dim ligne As String = Nothing
' une ligne du flux de lecture
ligne = [IN].ReadLine()
While ligne <> ""
'écho
Console.Out.WriteLine(("<-- " + ligne))
' ligne suivante
ligne = [IN].ReadLine()
End While
'écho dernière ligne
Console.Out.WriteLine(("<-- " + ligne))
' envoi paramètres de la requête
OUT.Write(requête)
' echo
Console.Out.WriteLine(("--> " + requête))
' construction de l'expression régulière permettant de retrouver la taille de la réponse XML
' dans le flux de la réponse du serveur web
Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
Dim RegexLength As New Regex(modèleLength) '
Dim MatchLength As Match = Nothing
Dim longueur As Integer = 0
' lecture seconde réponse du serveur web après envoi de la requête
' on mémorise la valeur de la ligne Content-Length
ligne = [IN].ReadLine()
While ligne <> ""
' écho écran
Console.Out.WriteLine(("<-- " + ligne))
' Content-Length ?
MatchLength = RegexLength.Match(ligne)
If MatchLength.Success Then
longueur = Integer.Parse(MatchLength.Groups(1).Value)
End If
' ligne suivante
ligne = [IN].ReadLine()
End While
' écho dernière ligne
Console.Out.WriteLine("<--")
' construction de l'expression régulière permettant de retrouver le résultat
' dans le flux de la réponse du serveur web
Dim modèle As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
Dim ModèleRésultat As New Regex(modèle)
Dim MatchRésultat As Match = Nothing
' on lit le reste de la réponse du serveur web
Dim chrRéponse(longueur) As Char
[IN].Read(chrRéponse, 0, longueur)
Dim strRéponse As String = New [String](chrRéponse)
' on décompose la réponse en lignes de texte
Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
' on parcourt les lignes de texte à la recherche du résultat
Dim strRésultat As String = "?" ' résultat de la fonction
For i = 0 To lignes.Length - 1
' suivi
Console.Out.WriteLine(("<-- " + lignes(i)))
' comparaison ligne courante au modèle
MatchRésultat = ModèleRésultat.Match(lignes(i))
' a-t-on trouvé ?
If MatchRésultat.Success Then
' on note le résultat
strRésultat = MatchRésultat.Groups(1).Value
End If
Next i
' on affiche le résultat
Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
End Sub
Tout d'abord, le client HTTP-POST envoie sa demande au format POST :
' construction de la chaîne de requête
Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
Dim nbChars As Integer = requête.Length
' construction du tableau des entêtes HTTP à envoyer
Dim entetes(5) As String
entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
entetes(1) = "Host: " & uri.Host & ":" & uri.Port
entetes(2) = "Content-Type: application/x-www-form-urlencoded"
entetes(3) = "Content-Length: " & nbChars
entetes(4) = "Connection: Keep-Alive"
entetes(5) = ""
' on envoie les entêtes HTTP au serveur
Dim i As Integer
For i = 0 To entetes.Length - 1
' envoi au serveur
OUT.WriteLine(entetes(i))
' écho écran
Console.Out.WriteLine(("--> " + entetes(i)))
Next i
Dans l'entête
on doit indiquer la taille des paramètres qui seront envoyés par le client derrière les entêtes HTTP :
Pour cela on utilise le code suivant :
' construction de la chaîne de requête
Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
Dim nbChars As Integer = requête.Length
La méthode HttpUtility.UrlEncode(string chaine) transforme certains des caractères de chaîne en %n1n2 où n1n2 est le code ASCII du caractère transformé. Les caractères visés par cette transformation sont tous les caractères ayant un sens particulier dans une requête POST (l'espace, le signe =, le signe &, ...). Ici la méthode HttpUtility.UrlEncode est normalement inutile puisque a et b sont des nombres qui ne contiennent aucun de ces caractères particuliers. Elle est ici employée à titre d'exemple. Elle a besoin de l'espace de noms System.Web. Une fois que le client a envoyé ses entêtes HTTP :
--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
le serveur répond par l'entête HTTP 100 Continue :
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:47 GMT
<-- X-Powered-By: ASP.NET
<--
Le code se contente de lire et d'afficher à l'écran cette première réponse :
' on lit la 1ere réponse du serveur Web HTTP/1.1 100
Dim ligne As String = Nothing
' une ligne du flux de lecture
ligne = [IN].ReadLine()
While ligne <> ""
'écho
Console.Out.WriteLine(("<-- " + ligne))
' ligne suivante
ligne = [IN].ReadLine()
End While
'écho dernière ligne
Console.Out.WriteLine(("<-- " + ligne))
Une fois cette première réponse lue, le client doit envoyer ses paramètres :
Il le fait avec le code suivant :
' envoi paramètres de la requête
OUT.Write(requête)
' echo
Console.Out.WriteLine(("--> " + requête))
Le serveur va alors envoyer sa réponse. Celle-ci est composée de deux parties :
- des entêtes HTTP terminés par une ligne vide
- la réponse au format XML
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>
Dans un premier temps, le client lit les entêtes HTTP pour y trouver la ligne Content-Length et récupérer la taille de la réponse XML (ici 90). Celle-ci est récupérée au moyen d'une expression régulière. On aurait pu faire autrement et sans doute de façon plus efficace.
' construction de l'expression régulière permettant de retrouver la taille de la réponse XML
' dans le flux de la réponse du serveur web
Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
Dim RegexLength As New Regex(modèleLength) '
Dim MatchLength As Match = Nothing
Dim longueur As Integer = 0
' lecture seconde réponse du serveur web après envoi de la requête
' on mémorise la valeur de la ligne Content-Length
ligne = [IN].ReadLine()
While ligne <> ""
' écho écran
Console.Out.WriteLine(("<-- " + ligne))
' Content-Length ?
MatchLength = RegexLength.Match(ligne)
If MatchLength.Success Then
longueur = Integer.Parse(MatchLength.Groups(1).Value)
End If
' ligne suivante
ligne = [IN].ReadLine()
End While
' écho dernière ligne
Console.Out.WriteLine("<--")
Une fois qu'on a la longueur N de la réponse XML, on n'a plus qu'à lire N caractères dans le flux IN de la réponse du serveur. Cette chaîne de N caractères est redécomposée en lignes de texte pour les besoins du suivi écran. Parmi ces lignes on cherche la ligne du résultat :
au moyen là encore d'une expression régulière. Une fois le résultat trouvé, il est affiché.
La fin du code du client est la suivante :
' construction de l'expression régulière permettant de retrouver le résultat
' dans le flux de la réponse du serveur web
Dim modèle As String = "<double xmlns=""st.istia.univ-angers.fr"">(.+?)</double>"
Dim ModèleRésultat As New Regex(modèle)
Dim MatchRésultat As Match = Nothing
' on lit le reste de la réponse du serveur web
Dim chrRéponse(longueur) As Char
[IN].Read(chrRéponse, 0, longueur)
Dim strRéponse As String = New [String](chrRéponse)
' on décompose la réponse en lignes de texte
Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
' on parcourt les lignes de texte à la recherche du résultat
Dim strRésultat As String = "?" ' résultat de la fonction
For i = 0 To lignes.Length - 1
' suivi
Console.Out.WriteLine(("<-- " + lignes(i)))
' comparaison ligne courante au modèle
MatchRésultat = ModèleRésultat.Match(lignes(i))
' a-t-on trouvé ?
If MatchRésultat.Success Then
' on note le résultat
strRésultat = MatchRésultat.Groups(1).Value
End If
Next i
' on affiche le résultat
Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
End Sub
10.6. Un client SOAP
Nous étudions ici un second client qui va lui utiliser un dialogue client-serveur de type SOAP (Simple Object Access Protocol). Un exemple de dialogue nous est présenté pour la fonction ajouter :


La demande du client est une demande POST. On va donc retrouver certains des mécanismes du client précédent. La principale différence est qu'alors que le client HTTP-POST envoyait les paramètres a et b sous la forme
le client SOAP les envoie dans un format XML plus complexe :
POST /operations/operations.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "st.istia.univ-angers.fr/ajouter"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>double</a>
<b>double</b>
</ajouter>
</soap:Body>
</soap:Envelope>
Il reçoit en retour une réponse XML également plus complexe que les réponses vues précédemment :
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouterResponse xmlns="st.istia.univ-angers.fr">
<ajouterResult>double</ajouterResult>
</ajouterResponse>
</soap:Body>
</soap:Envelope>
Même si la demande et la réponse sont plus complexes, il s'agit bien du même mécanisme HTTP que pour le client HTTP-POST. L'écriture du client SOAP peut être ainsi calqué sur celle du client HTTP-POST. Voici un exemple d'exécution :
dos>clientsoap1 http://localhost/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 3 4
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--
--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:33 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 345
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>7</ajouterResult></ajouterResponse
></soap:Body></soap:Envelope>
[résultat=7]
Seule la méthode executeFonction change. Le client SOAP envoie les entêtes HTTP de sa demande. Ils sont simplement un peu plus complexes que ceux de HTTP-POST :
ajouter 3 4
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
Le code qui les génère :
' executeFonction
Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
' exécute fonction(a,b) sur le service web d'URI uri
' les échanges client-serveur se font via les flux IN et OUT
' le résultat de la fonction est dans la ligne
' <double xmlns="st.istia.univ-angers.fr">double</double>
' envoyée par le serveur
' construction de la chaîne de requête SOAP
Dim requêteSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
requêteSOAP += "<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" + ControlChars.Lf
requêteSOAP += "<soap:Body>" + ControlChars.Lf
requêteSOAP += "<" + fonction + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
requêteSOAP += "<a>" + a + "</a>" + ControlChars.Lf
requêteSOAP += "<b>" + b + "</b>" + ControlChars.Lf
requêteSOAP += "</" + fonction + ">" + ControlChars.Lf
requêteSOAP += "</soap:Body>" + ControlChars.Lf
requêteSOAP += "</soap:Envelope>"
Dim nbCharsSOAP As Integer = requêteSOAP.Length
' construction du tableau des entêtes HTTP à envoyer
Dim entetes(6) As String
entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
entetes(1) = "Host: " & uri.Host & ":" & uri.Port
entetes(2) = "Content-Type: text/xml; charset=utf-8"
entetes(3) = "Content-Length: " & nbCharsSOAP
entetes(4) = "Connection: Keep-Alive"
entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
entetes(6) = ""
' on envoie les entêtes HTTP au serveur
Dim i As Integer
For i = 0 To entetes.Length - 1
' envoi au serveur
OUT.WriteLine(entetes(i))
' écho écran
Console.Out.WriteLine(("--> " + entetes(i)))
Next i
En recevant cette demande, le serveur envoie sa première réponse que le client affiche :
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--
Le code de lecture de cette première réponse est le suivant :
' on lit la 1ere réponse du serveur Web HTTP/1.1 100
Dim ligne As String = Nothing
' une ligne du flux de lecture
ligne = [IN].ReadLine()
While ligne <> ""
'écho
Console.Out.WriteLine(("<-- " + ligne))
' ligne suivante
ligne = [IN].ReadLine()
End While 'while
'écho dernière ligne
Console.Out.WriteLine(("<-- " + ligne))
Le client va maintenant envoyer ses paramètres au format XML dans quelque chose qu'on appelle une enveloppe SOAP :
--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</soap:Body>
</soap:Envelope>
Le code :
' envoi paramètres de la requête
OUT.Write(requêteSOAP)
' echo
Console.Out.WriteLine(("--> " + requêteSOAP))
Le serveur va alors envoyer sa réponse définitive :
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:33 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 345
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>7</ajouterResult></ajouterResponse
></soap:Body></soap:Envelope>
Le client affiche à l'écran les entêtes HTTP reçus tout en y cherchant la ligne Content-Length :
' construction de l'expression régulière permettant de retrouver la taille de la réponse XML
' dans le flux de la réponse du serveur web
Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
Dim RegexLength As New Regex(modèleLength) '
Dim MatchLength As Match = Nothing
Dim longueur As Integer = 0
' lecture seconde réponse du serveur web après envoi de la requête
' on mémorise la valeur de la ligne Content-Length
ligne = [IN].ReadLine()
While ligne <> ""
' écho écran
Console.Out.WriteLine(("<-- " + ligne))
' Content-Length ?
MatchLength = RegexLength.Match(ligne)
If MatchLength.Success Then
longueur = Integer.Parse(MatchLength.Groups(1).Value)
End If
' ligne suivante
ligne = [IN].ReadLine()
End While 'while
' écho dernière ligne
Console.Out.WriteLine("<--")
Une fois la taille N de la réponse XML connue, le client lit N caractères dans le flux de la réponse du serveur, décompose la chaîne récupérée en lignes de texte pour les afficher à l'écran et y chercher la balise XML du résultat : <ajouterResult>7</ajouterResult> et afficher ce dernier :
' construction de l'expression régulière permettant de retrouver le résultat
' dans le flux de la réponse du serveur web
Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
Dim ModèleRésultat As New Regex(modèle)
Dim MatchRésultat As Match = Nothing
' on lit le reste de la réponse du serveur web
Dim chrRéponse(longueur) As Char
[IN].Read(chrRéponse, 0, longueur)
Dim strRéponse As String = New [String](chrRéponse)
' on décompose la réponse en lignes de texte
Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
' on parcourt les lignes de texte à la recherche du résultat
Dim strRésultat As String = "?" ' résultat de la fonction
For i = 0 To lignes.Length - 1
' suivi
Console.Out.WriteLine(("<-- " + lignes(i)))
' comparaison ligne courante au modèle
MatchRésultat = ModèleRésultat.Match(lignes(i))
' a-t-on trouvé ?
If MatchRésultat.Success Then
' on note le résultat
strRésultat = MatchRésultat.Groups(1).Value
End If
'ligne suivante
Next i
' on affiche le résultat
Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
End Sub
10.7. Encapsulation des échanges client-serveur
Imaginons que notre service web operations soit utilisé par diverses applications. Il serait intéressant de mettre à disposition de celles-ci une classe qui ferait l'interface entre l'application cliente et le service web et qui cacherait la majeure partie des échanges réseau qui, pour la plupart des développeurs, ne sont pas triviaux. On aurait ainsi le schéma suivant :
![]() |
L'application cliente s'adresserait à l'interface client-serveur pour faire ses demandes au service web. Celle-ci ferait tous les échanges réseau nécessaires avec le serveur et rendrait le résultat obtenu à l'application cliente. Celle-ci n'aurait plus à s'occuper des échanges avec le serveur ce qui faciliterait grandement son écriture.
10.7.1. La classe d'encapsulation
Après ce qui a été vu dans les paragraphes précédents, nous connaissons bien maintenant les échanges réseau entre le client et le serveur. Nous avons même vu trois méthodes. Nous choisissons d'encapsuler la méthode SOAP. La classe est la suivante :
' espaces de noms
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic
' clientSOAP du service Web operations
Public Class clientSOAP
' variables d'instance
Private uri As uri = Nothing ' l'URI du service web
Private client As TcpClient = Nothing ' la liaison tcp du client avec le serveur
Private [IN] As StreamReader = Nothing ' le flux de lecture du client
Private OUT As StreamWriter = Nothing ' le flux d'écriture du client
' dictionnaire des fonctions
Private dicoFonctions As New Hashtable
' liste des fonctions
Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
' verbose
Private verbose As Boolean = False ' à vrai, affiche à l'écran les échanges client-serveur
' constructeur
Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)
' on note verbose
Me.verbose = verbose
' connexion au serveur
uri = New Uri(uriString)
client = New TcpClient(uri.Host, uri.Port)
' on crée les flux d'entrée-sortie du client TCP
[IN] = New StreamReader(client.GetStream())
OUT = New StreamWriter(client.GetStream())
OUT.AutoFlush = True
' création du dictionnaire des fonctions du service web
Dim i As Integer
For i = 0 To fonctions.Length - 1
dicoFonctions.Add(fonctions(i), True)
Next i
End Sub
' fermeture de la connexion au serveur
Public Sub Close()
' fin liaison client-serveur
[IN].Close()
OUT.Close()
client.Close()
End Sub
' executeFonction
Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String
' exécute fonction(a,b) sur le service web d'URI uri
' les échanges client-serveur se font via les flux IN et OUT
' le résultat de la fonction est dans la ligne
' <double xmlns="st.istia.univ-angers.fr">double</double>
' envoyée par le serveur
' fonction valide ?
fonction = fonction.Trim().ToLower()
If Not dicoFonctions.ContainsKey(fonction) Then
Return "[fonction [" + fonction + "] indisponible : (ajouter, soustraire,multiplier,diviser)]"
End If
' arguments a et b valides ?
Dim doubleA As Double = 0
Try
doubleA = Double.Parse(a)
Catch
Return "[argument [" + a + "] incorrect (double)]"
End Try
Dim doubleB As Double = 0
Try
doubleB = Double.Parse(b)
Catch
Return "[argument [" + b + "] incorrect (double)]"
End Try
' division par zéro ?
If fonction = "diviser" And doubleB = 0 Then
Return "[division par zéro]"
End If
' construction de la chaîne de requête SOAP
Dim requêteSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
requêteSOAP += "<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" + ControlChars.Lf
requêteSOAP += "<soap:Body>" + ControlChars.Lf
requêteSOAP += "<" + fonction + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
requêteSOAP += "<a>" + a + "</a>" + ControlChars.Lf
requêteSOAP += "<b>" + b + "</b>" + ControlChars.Lf
requêteSOAP += "</" + fonction + ">" + ControlChars.Lf
requêteSOAP += "</soap:Body>" + ControlChars.Lf
requêteSOAP += "</soap:Envelope>"
Dim nbCharsSOAP As Integer = requêteSOAP.Length
' construction du tableau des entêtes HTTP à envoyer
Dim entetes(6) As String
entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
entetes(1) = "Host: " + uri.Host + ":" + uri.Port.ToString
entetes(2) = "Content-Type: text/xml; charset=utf-8"
entetes(3) = "Content-Length: " + nbCharsSOAP.ToString
entetes(4) = "Connection: Keep-Alive"
entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
entetes(6) = ""
' on envoie les entêtes HTTP au serveur
Dim i As Integer
For i = 0 To entetes.Length - 1
' envoi au serveur
OUT.WriteLine(entetes(i))
' écho écran
If verbose Then
Console.Out.WriteLine(("--> " + entetes(i)))
End If
Next i
' on lit la 1ere réponse du serveur Web HTTP/1.1 100
Dim ligne As String = Nothing
' une ligne du flux de lecture
ligne = [IN].ReadLine()
While ligne <> ""
'écho
If verbose Then
Console.Out.WriteLine(("<-- " + ligne))
End If
' ligne suivante
ligne = [IN].ReadLine()
End While
'écho dernière ligne
If verbose Then
Console.Out.WriteLine(("<-- " + ligne))
End If
' envoi paramètres de la requête
OUT.Write(requêteSOAP)
' echo
If verbose Then
Console.Out.WriteLine(("--> " + requêteSOAP))
End If
' construction de l'expression régulière permettant de retrouver la taille de la réponse XML
' dans le flux de la réponse du serveur web
Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
Dim RegexLength As New Regex(modèleLength) '
Dim MatchLength As Match = Nothing
Dim longueur As Integer = 0
' lecture seconde réponse du serveur web après envoi de la requête
' on mémorise la valeur de la ligne Content-Length
ligne = [IN].ReadLine()
While ligne <> ""
' écho écran
If verbose Then
Console.Out.WriteLine(("<-- " + ligne))
End If
' Content-Length ?
MatchLength = RegexLength.Match(ligne)
If MatchLength.Success Then
longueur = Integer.Parse(MatchLength.Groups(1).Value)
End If
' ligne suivante
ligne = [IN].ReadLine()
End While
' écho dernière ligne
If verbose Then
Console.Out.WriteLine("<--")
End If
' construction de l'expression régulière permettant de retrouver le résultat
' dans le flux de la réponse du serveur web
Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
Dim ModèleRésultat As New Regex(modèle)
Dim MatchRésultat As Match = Nothing
' on lit le reste de la réponse du serveur web
Dim chrRéponse(longueur) As Char
[IN].Read(chrRéponse, 0, longueur)
Dim strRéponse As String = New [String](chrRéponse)
' on décompose la réponse en lignes de texte
Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
' on parcourt les lignes de texte à la recherche du résultat
Dim strRésultat As String = "?" ' résultat de la fonction
For i = 0 To lignes.Length - 1
' suivi
If verbose Then
Console.Out.WriteLine(("<-- " + lignes(i)))
End If ' comparaison ligne courante au modèle
MatchRésultat = ModèleRésultat.Match(lignes(i))
' a-t-on trouvé ?
If MatchRésultat.Success Then
' on note le résultat
strRésultat = MatchRésultat.Groups(1).Value
End If
Next i
' on renvoie le résultat
Return strRésultat
End Function
End Class
Nous ne retrouvons rien de neuf par rapport à ce qui a été déjà vu. Nous avons simplement repris le code du client SOAP étudié et l'avons réaménagé quelque peu pour en faire une classe. Celle-ci a un constructeur et deux méthodes :
' constructeur
Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)
' executeFonction
Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String
' fermeture de la connexion au serveur
Public Sub Close()
et a les attributs suivants :
' variables d'instance
Private uri As Uri = Nothing ' l'URI du service web
Private client As TcpClient = Nothing ' la liaison tcp du client avec le serveur
Private [IN] As StreamReader = Nothing ' le flux de lecture du client
Private OUT As StreamWriter = Nothing ' le flux d'écriture du client
' dictionnaire des fonctions
Private dicoFonctions As New Hashtable
' liste des fonctions
Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
' verbose
Private verbose As Boolean = False ' à vrai, affiche à l'écran les échanges client-serveur
On passe au constructeur deux paramètres :
- l'URI du service web auquel il doit se connecter
- un booléen verbose qui, à vrai, demande que les échanges réseau soient affichés à l'écran, sinon ils ne le seront pas.
Au cours de la construction, on construit les flux IN de lecture réseau, OUT d'écriture réseau, ainsi que le dictionnaire des fonctions gérées par le service. Une fois l'objet construit, la connexion client-serveur est ouverte et ses flux IN et OUT utilisables.
La méthode Close permet de fermer la liaison avec le serveur.
La méthode ExecuteFonction est celle qu'on a écrite pour le client SOAP étudié, à quelques détails près :
- Les paramètres uri, IN et OUT qui étaient auparavant passés en paramètres à la méthode n'ont plus besoin de l'être, puisque ce sont maintenant des attributs d'instance accessibles à toutes les méthodes de l'instance
- La méthode ExecuteFonction qui rendait auparavant un type void et affichait le résultat de la fonction à l'écran, rend maintenant ce résultat et donc un type string.
Typiquement un client utilisera la classe clientSOAP de la façon suivante :
- création d'un objet clientSOAP qui va créer la liaison avec le service web
- utilisation répétée de la méthode executeFonction
- fermeture de la liaison avec le service Web par la méthode Close.
Etudions un premier client.
10.7.2. Un client console
Nous reprenons ici le client SOAP étudié alors que la classe clientSOAP n'existait pas et nous le réaménageons afin qu'il utilise maintenant cette classe :
' espaces de noms
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic
Public Module testClientSoap
' demande l'URI du service web operations
' exécute de façon interactive les commandes tapées au clavier
Public Sub Main(ByVal args() As String)
' syntaxe
Const syntaxe As String = "pg URI [verbose]"
' nombre d'arguments
If args.Length <> 1 And args.Length <> 2 Then
erreur(syntaxe, 1)
End If
' verbose ?
Dim verbose As Boolean = False
If args.Length = 2 Then
verbose = args(1).ToLower() = "verbose"
End If
' on se connecte au service web
Dim client As clientSOAP = Nothing
Try
client = New clientSOAP(args(0), verbose)
Catch ex As Exception
' erreur de connexion
erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
End Try
' les demandes de l'utilisateur sont tapées au clavier
' sous la forme fonction a b - elles se terminent avec la commande fin
Dim commande As String = Nothing ' commande tapée au clavier
Dim champs As String() = Nothing ' champs d'une ligne de commande
' invite à l'utilisateur
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b" + ControlChars.Lf)
' gestion des erreurs
Dim erreurCommande As Boolean
Try
' boucle de saisie des commandes tapées au clavier
While True
' au départ pas d'erreur
erreurCommande = False
' lecture commande
commande = Console.In.ReadLine().Trim().ToLower()
' fini ?
If commande Is Nothing Or commande = "fin" Then
Exit While
End If
' décomposition de la commande en champs
champs = Regex.Split(commande, "\s+")
' il faut trois champs
If champs.Length <> 3 Then
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
' on note l'erreur
erreurCommande = True
End If
' on fait la demande au service web
If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
' demande suivante
End While
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
' fin liaison client-serveur
Try
client.Close()
Catch
End Try
End Sub
' affichage des erreurs
Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
' affichage erreur
System.Console.Error.WriteLine(msg)
' arrêt avec erreur
Environment.Exit(exitCode)
End Sub
End Module
La client est maintenant considérablement plus simple et on n'y retrouve aucune communication réseau. Le client admet deux paramètres :
- l'URI du service web operations
- le mot clé facultatif verbose. S'il est présent, les échanges réseau seront affichés à l'écran.
Ces deux paramètres sont utilisés pour construire un objet clientSOAP qui va assurer les échanges avec le service web.
' on se connecte au service web
Dim client As clientSOAP = Nothing
Try
client = New clientSOAP(args(0), verbose)
Catch ex As Exception
' erreur de connexion
erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
End Try
Une fois ouverte la connexion avec le service web, le client peut envoyer ses demandes. Celles-ci sont tapées au clavier, analysées puis envoyées au serveur par appel à la méthode executeFonction de l'objet clientSOAP.
' on fait la demande au service web
If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
La classe clientSOAP est compilée dans un "assemblage" :
dos>vbc /r:clientSOAP.dll testClientSOAP.vb
dos>dir
04/03/2004 08:46 6 913 clientSOAP.vb
04/03/2004 09:07 7 168 clientSOAP.dll
L'application client testClientSoap est ensuite compilée par :
dos>vbc /r:clientSOAP.dll /r:system.dll testClientSOAP.vb
dos>dir
04/03/2004 09:08 2 711 testClientSOAP.vb
04/03/2004 09:08 4 608 testClientSOAP.exe
Voici un exemple d'exécution non verbeux :
dos>testclientsoap http://localhost/st/operations/operations.asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 1 3
résultat=4
soustraire 6 7
résultat=-1
multiplier 4 5
résultat=20
diviser 1 2
résultat=0.5
x
syntaxe : [ajouter|soustraire|multiplier|diviser] a b
x 1 2
résultat=[fonction [x] indisponible : (ajouter, soustraire,multiplier,diviser)]
ajouter a b
résultat=[argument [a] incorrect (double)]
ajouter 1 b
résultat=[argument [b] incorrect (double)]
diviser 1 0
résultat=[division par zéro]
fin
On peut suivre les échanges réseau en demandant une exécutions "verbeuse" :
dos>testClientSOAP http://localhost/operations/operations.asmx verbose
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 4 8
--> POST /operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 08:15:25 GMT
<-- X-Powered-By: ASP.NET
<--
--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>4</a>
<b>8</b>
</ajouter>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 08:15:25 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 346
<--
<-- <?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><soap:Body><ajouterResponse xmlns="st.istia.univ-angers.fr"><ajouterResult>12</ajouterResult></ajouterResponse></soap:Body></soap:Envelope>
résultat=12
fin
Construisons maintenant un client graphique.
10.7.3. Un client graphique windows
Nous allons maintenant interroger notre service web avec un client graphique qui utilisera lui aussi la classe clientSOAP. L'interface graphique sera la suivante :
![]() |
Les contrôles sont les suivants :
n° | type | nom | rôle |
TextBox | txtURI | l'URI du service web operations | |
Button | btnOuvrir | ouvre la liaison avec le service Web | |
Button | btnFermer | ferme la liaison avec le service Web | |
ComboBox | cmbFonctions | la liste des fonction (ajouter, soustraire, multiplier, diviser) | |
TextBox | txtA | l'argument a des fonctions | |
TextBox | txtB | l'argument b des fonctions | |
TextBox | txtRésultat | le résultat de fonction(a,b) | |
Button | btnCalculer | lance le calcul de fonction(a,b) | |
TextBox | txtErreur | affiche un msg d'état de la liaison |
Il y a quelques contraintes de fonctionnement :
- le bouton btnOuvrir n'est actif que si le champ txtURI est non vide et qu'une liaison n'est pas déjà ouverte
- le bouton btnFermer n'est actif que lorsqu'une liaison avec le service web a été ouverte
- le bouton btnCalculer n'est actif que lorsqu'une liaison est ouverte et ques champs txtA et txtB sont non vides
- les champs txtRésultat et txtErreur ont l'attribut ReadOnly à vrai
Le client commence par ouvrir la connexion avec le service web à l'aide du bouton [Ouvrir] :

Ensuite l'utilisateur peut choisir une fonction et des valeurs pour a et b :





Le code de l'application suit. Nous avons omis le code du formulaire qui ne présente pas d'intérêt ici.
'espaces de noms
Imports System
Imports System.Windows.Forms
' la classe du formulaire
Public Class FormClientSOAP
Inherits System.Windows.Forms.Form
' attributs d'instance
Dim client As clientSOAP ' client SOAP du service web operations
#Region " Code généré par le Concepteur Windows Form "
Public Sub New()
MyBase.New()
'Cet appel est requis par le Concepteur Windows Form.
InitializeComponent()
' autres initialisations
myInit()
End Sub
'La méthode substituée Dispose du formulaire pour nettoyer la liste des composants.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
End Sub
...
Private Sub InitializeComponent()
....
End Sub
#End Region
Private Sub myInit()
' init formulaire
cmbFonctions.SelectedIndex = 0
btnOuvrir.Enabled = False
btnFermer.Enabled = True
btnCalculer.Enabled = False
End Sub
Private Sub txtURI_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtURI.TextChanged
' le contenu du champ de saisie a changé - on fixe l'état du bouton ouvrir
btnOuvrir.Enabled = txtURI.Text.Trim <> ""
End Sub
Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
' demande d'ouverture d'une connexion avec le service web
Try
' création d'un objet de type [clientSOAP]
client = New clientSOAP(txtURI.Text.Trim, False)
' états des boutons
btnOuvrir.Enabled = False
btnFermer.Enabled = True
' l'URI ne peut plus être modifiée
txtURI.ReadOnly = True
' état client
txtErreur.Text = "Liaison au service web ouverte"
Catch ex As Exception
' il y a eu une erreur - on l'affiche
txtErreur.Text = ex.Message
End Try
End Sub
Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
' fermeture le la connexion au service web
client.Close()
' états boutons
btnOuvrir.Enabled = True
btnFermer.Enabled = False
' URI
txtURI.ReadOnly = False
' état client
txtErreur.Text = "Liaison au service web fermée"
End Sub
Private Sub btnCalculer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnCalculer.Click
' calcul d'une fonction f(a,b)
' on efface le résultat précédent
txtRésultat.Text = ""
Try
txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
Catch ex As Exception
' il y a eu une erreur réseau
txtErreur.Text = ex.Message
' on ferme la liaison
btnFermer_Click(Nothing, Nothing)
End Try
End Sub
Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
' changement de la valeur de A
btnCalculer.Enabled = txtA.Text.Trim <> "" And txtB.Text.Trim <> ""
End Sub
Private Sub txtB_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtB.TextChanged
' changement de la valeur de B
txtA_TextChanged(Nothing, Nothing)
End Sub
' méthode principale
Public Shared Sub main()
Application.Run(New FormClientSOAP)
End Sub
End Class
Là encore la classe clientSOAP cache tout l'aspect réseau de l'application. L'application a été construite de la façon suivante :
- l'assemblage clientSOAP.dll contenant la classe clientSOAP a été placé dans le dossier du projet
- l'interface graphique clientsoapgui.vb a été construite avec VS.NET puis compilée dans une fenêtre dos :
dos>vbc /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll /r:clientSOAP.dll clientsoapgui.vb
dos>dir
04/03/2004 09:13 7 168 clientSOAP.dll
04/03/2004 16:44 9 866 clientsoapgui.vb
04/03/2004 16:44 11 264 clientsoapgui.exe
L'interface graphique a été ensuite lancée par :
10.8. Un client proxy
Rappelons ce qui vient d'être fait. Nous avons créé une classe intermédiaire encapsulant les échanges réseau entre un client et un service web selon le schéma ci-dessous :
![]() |
La plate-forme .NET pousse cette logique plus loin. Une fois connu le service Web à atteindre, nous pouvons générer automatiquement la classe qui va nous servir d'intermédiaire pour atteindre les fonctions du service Web et qui cachera toute la partie réseau. On appelle cette classe un proxy pour le service web pour lequel elle a été générée.
Comment générer la classe proxy d'un service web ? Un service web est toujours accompagné d'un fichier de description au format XML. Si l'URI de notre service web operations est http://localhost/operations/operations.asmx, son fichier de description est disponible à l'URL http://localhost/operations/operations.asmx?wsdl comme le montre la copie d'écran suivante :

On a là un fichier XML décrivant précisément toutes les fonctions du service web avec pour chacune d'elles le type et le nombre de paramètres, le type du résultat. On appelle ce fichier le fichier WSDL du service parce qu'il utilise le langage WSDL (Web Services Description Language). A partir de ce fichier, une classe proxy peut être générée à l'aide de l'outil wsdl :
dos>wsdl http://localhost/operations/operations.asmx?wsdl /language=vb
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Écriture du fichier 'D:\data\devel\vbnet\poly\chap9\clientproxy\operations.vb'.
dos>dir
04/03/2004 17:17 6 663 operations.vb
L'outil wsdl génère un fichier source VB.NET (option /language=vb) portant le nom de la classe implémentant le service web, ici operations. Examinons une partie du code généré :
'------------------------------------------------------------------------------
' <autogenerated>
' This code was generated by a tool.
' Runtime Version: 1.1.4322.573
'
' Changes to this file may cause incorrect behavior and will be lost if
' the code is regenerated.
' </autogenerated>
'------------------------------------------------------------------------------
Option Strict Off
Option Explicit On
Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Xml.Serialization
'
'Ce code source a été automatiquement généré par wsdl, Version=1.1.4322.573.
'
'<remarks/>
<System.Diagnostics.DebuggerStepThroughAttribute(), _
System.ComponentModel.DesignerCategoryAttribute("code"), _
System.Web.Services.WebServiceBindingAttribute(Name:="operationsSoap", [Namespace]:="st.istia.univ-angers.fr")> _
Public Class operations
Inherits System.Web.Services.Protocols.SoapHttpClientProtocol
'<remarks/>
Public Sub New()
MyBase.New
Me.Url = "http://localhost/operations/operations.asmx"
End Sub
'<remarks/>
<System.Web.Services.Protocols.SoapDocumentMethodAttribute("st.istia.univ-angers.fr/ajouter", RequestNamespace:="st.istia.univ-angers.fr", ResponseNamespace:="st.istia.univ-angers.fr", Use:=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle:=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)> _
Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
Return CType(results(0),Double)
End Function
'<remarks/>
Public Function Beginajouter(ByVal a As Double, ByVal b As Double, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult
Return Me.BeginInvoke("ajouter", New Object() {a, b}, callback, asyncState)
End Function
'<remarks/>
Public Function Endajouter(ByVal asyncResult As System.IAsyncResult) As Double
Dim results() As Object = Me.EndInvoke(asyncResult)
Return CType(results(0),Double)
End Function
....
Ce code est un peu complexe au premier abord. Nous n'avons pas besoin d'en comprendre les détails pour pouvoir l'utiliser. Examinons tout d'abord la déclaration de la classe :
La classe porte le nom operations du service web pour lequel elle a été construite. Elle dérive de la classe SoapHttpClientProtocol :

Notre classe proxy a un constructeur unique :
Le constructeur affecte à l'attibut url l'URL du service web associé au proxy. La classe operations ci-dessus ne définit pas elle-même l'attribut url. Celui-ci est hérité de la classe dont dérive le proxy : System.Web.Services.Protocols.SoapHttpClientProtocol. Examinons maintenant ce qui se rapporte à la méthode ajouter :
Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
Return CType(results(0),Double)
End Function
On peut constater qu'elle a la même signature que dans le service Web operations où elle était définie comme suit :
<WebMethod> _
Function ajouter(a As Double, b As Double) As Double
Return a + b
End Function 'ajouter
La façon dont cette classe dialogue avec le service Web n'apparaît pas ici. Ce dialogue est entièrement pris en charge par la classe parent System.Web.Services.Protocols.SoapHttpClientProtocol. On ne trouve dans le proxy que ce qui le différencie des autres proxy :
- l'URL du service web associé
- la définition des méthodes du service associé.
Pour utiliser les méthodes du service web operations, un client n'a besoin que de la classe proxy operations générée précédemment. Compilons cette classe dans un fichier assembly :
Maintenant écrivons un client console. Il est appelé sans paramètres et exécute les demandes tapées au clavier :
dos>testclientproxy
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b
ajouter 4 5
résultat=9
soustraire 9 8
résultat=1
multiplier 10 4
résultat=40
diviser 6 7
résultat=0,857142857142857
toutfaire 10 20
résultats=[30,-10,200,0,5]
diviser 5 0
résultat=+Infini
fin
Le code du client est le suivant :
' espaces de noms
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Public Module testClientProxy
' exécute de façon interactive les commandes tapées au clavier
' et les envoie au service web operations
Public Sub Main()
' il n'y a plus d'arguments - l'URL du service web étant codée en dur dans le proxy
' création d'un dictionnaire des fonctions du service web
Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser", "toutfaire"}
Dim dicoFonctions As New Hashtable
Dim i As Integer
For i = 0 To fonctions.Length - 1
dicoFonctions.Add(fonctions(i), True)
Next i
' on crée un objet proxy operations
Dim myOperations As operations = Nothing
Try
myOperations = New operations
Catch ex As Exception
' erreur de connexion
erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
End Try
' les demandes de l'utilisateur sont tapées au clavier
' sous la forme fonction a b - elles se terminent avec la commande fin
Dim commande As String = Nothing ' commande tapée au clavier
Dim champs As String() = Nothing ' champs d'une ligne de commande
' invite à l'utilisateur
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)
' qqs données locales
Dim erreurCommande As Boolean
Dim fonction As String
Dim a, b As Double
' boucle de saisie des commandes tapées au clavier
While True
' au départ pas d'erreur
erreurCommande = False
' lecture commande
commande = Console.In.ReadLine().Trim().ToLower()
' fini ?
If commande Is Nothing Or commande = "fin" Then
Exit While
End If
' décomposition de la commande en champs
champs = Regex.Split(commande, "\s+")
Try
' il faut trois champs
If champs.Length <> 3 Then
Throw New Exception
End If
' le champ 0 doit être une fonction reconnue
fonction = champs(0)
If Not dicoFonctions.ContainsKey(fonction) Then
Throw New Exception
End If
' le champ 1 doit être un nombre valide
a = Double.Parse(champs(1))
' le champ 2 doit être un nombre valide
b = Double.Parse(champs(2))
Catch
' commande invalide
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
erreurCommande = True
End Try
' on fait la demande au service web
If Not erreurCommande Then
Try
Dim résultat As Double
Dim résultats() As Double
If fonction = "ajouter" Then
résultat = myOperations.ajouter(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "soustraire" Then
résultat = myOperations.soustraire(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "multiplier" Then
résultat = myOperations.multiplier(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "diviser" Then
résultat = myOperations.diviser(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "toutfaire" Then
résultats = myOperations.toutfaire(a, b)
Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
résultats(2).ToString + "," + résultats(3).ToString + "]"))
End If
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
End If
End While
End Sub
' affichage des erreurs
Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
' affichage erreur
System.Console.Error.WriteLine(msg)
' arrêt avec erreur
Environment.Exit(exitCode)
End Sub
End Module
Nous n'examinons que le code propre à l'utilisation de la classe proxy. Tout d'abord un objet proxy operations est créé :
' on crée un objet proxy operations
Dim myOperations As operations = Nothing
Try
myOperations = New operations
Catch ex As Exception
' erreur de connexion
erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
End Try
Des lignes fonction a b sont tapées au clavier. A partir de ces informations, on appelle les méthodes appropriées du proxy :
' on fait la demande au service web
If Not erreurCommande Then
Try
Dim résultat As Double
Dim résultats() As Double
If fonction = "ajouter" Then
résultat = myOperations.ajouter(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "soustraire" Then
résultat = myOperations.soustraire(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "multiplier" Then
résultat = myOperations.multiplier(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "diviser" Then
résultat = myOperations.diviser(a, b)
Console.Out.WriteLine(("résultat=" + résultat.ToString))
End If
If fonction = "toutfaire" Then
résultats = myOperations.toutfaire(a, b)
Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
résultats(2).ToString + "," + résultats(3).ToString + "]"))
End If
Catch e As Exception
Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
End Try
On traite ici pour la première fois, l'opération toutfaire qui fait les quatre opérations. Elle avait été ignorée jusqu'à maintenant car elle envoie un tableau de nombres encapsulé dans une enveloppe XML plus compliquée à gérer que les réponses XML simples des autres fonctions ne délivrant qu'un unique résultat. On voit qu'ici avec la classe proxy, utiliser la méthode toutfaire n'est pas plus compliqué qu'utiliser les autres méthodes. L'application a été compilée dans une fenêtre dos de la façon suivante :
dos>vbc /r:operations.dll /r:system.dll /r:system.web.services.dll testClientProxy.vb
dos>dir
04/03/2004 17:17 6 663 operations.vb
04/03/2004 17:24 7 680 operations.dll
04/03/2004 17:41 4 099 testClientProxy.vb
04/03/2004 17:41 5 632 testClientProxy.exe
10.9. Configurer un service Web
Un service Web peut avoir besoin d'informations de configuration pour s'initialiser correctement. Avec le serveur IIS, ces informations peuvent être placées dans un fichier appelé web.config et situé dans le même dossier que le service web. Supposons qu'on veuille créer un service web ayant besoin de deux informations pour s'initialiser : un nom et un âge. Ces deux informations pevent être placées dans le fichier web.config sous la forme suivante :
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Les paramètres d'initialisation sont placées dans une enveloppe XML :
Un paramètre d'initialisation de nom P ayant la valeur V sera déclarée avec la ligne :
<add key="P" value="V"/>
Comment le service Web récupère-t-il ces informations ? Lorsque IIS charge un service web, il regarde s'il y a dans le même dossier un fichier web.config. Si oui, il le lit. La valeur V d'un paramètre P est obtenue par l'instruction :
où ConfigurationSettings est une classe dans l'espace de noms System.Configuration.
Vérifions cette technique sur le service web suivant :
<%@ WebService language="VB" class=personne %>
Imports System.Web.Services
imports System.Configuration
<WebService([Namespace] := "st.istia.univ-angers.fr")> _
Public Class personne
Inherits WebService
' attributs
Private nom As String
Private age As Integer
' constructeur
Public Sub New()
' init attributs
nom = ConfigurationSettings.AppSettings("nom")
age = Integer.Parse(ConfigurationSettings.AppSettings("age"))
End Sub
<WebMethod> _
Function id() As String
Return "[" + nom + "," + age.ToString + "]"
End Function
End Class
Le service web personne a deux attributs nom et age qui sont initialisés dans son constructeur sans paramètres à partir des valeurs lues dans le fichier de configuration web.config du service personne. Ce fichier est le suivant :
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Le service web a par ailleurs une <WebMethod> id sans paramètres et qui se contente de rendre les attributs nom et age. Le service est enregistré dans le fichier source personne.asmx qui est placé avec son fichier de configuration dans le dossier c:\inetpub\wwwroot\st\personne :
Associons un dossier virtuel IIS /config au dossier physique précédent. Lançons IIS puis avec un navigateur demandons l'url http://localhost/config/personne.asmx du service personne :

Suivons le lien de l'unique méthode id :

La méthode id n'a pas de paramètres. Utilisons le bouton Appeler :

Nous avons bien récupéré les informations placées dans le fichier web.config du service.
10.10. Le service Web IMPOTS
Nous reprenons l'application IMPOTS maintenant bien connue. La dernière fois que nous avons travaillé avec, nous en avions fait un serveur distant qu'on pouvait appeler sur l'internet. Nous en faisons maintenant un service web.
10.10.1. Le service web
Nous partons de la classe impôt créée dans le chapitre sur les bases de données et qui se construit à partir des informations contenues dans une base de données ODBC :
' options
Option Strict On
Option Explicit On
' espaces de noms
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Public Class impôt
' les données nécessaires au calcul de l'impôt
' proviennent d'une source extérieure
Private limites(), coeffR(), coeffN() As Decimal
' constructeur
Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
' on vérifie que les 3 tablaeux ont la même taille
Dim OK As Boolean = LIMITES.Length = COEFFR.Length And LIMITES.Length = COEFFN.Length
If Not OK Then
Throw New Exception("Les 3 tableaux fournis n'ont pas la même taille(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
End If
' c'est bon
Me.limites = LIMITES
Me.coeffR = COEFFR
Me.coeffN = COEFFN
End Sub
' constructeur 2
Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
' initialise les trois tableaux limites, coeffR, coeffN à partir
' du contenu de la table Timpots de la base ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN sont les trois colonnes de cette table
' peut lancer une exception
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Dim impotsConn As OdbcConnection = Nothing ' la connexion
Dim sqlCommand As OdbcCommand = Nothing ' la commande SQL
' la requête SELECT
Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
' tableaux pour récupérer les données
Dim tLimites As New ArrayList
Dim tCoeffR As New ArrayList
Dim tCoeffN As New ArrayList
' on tente d'accéder à la base de données
impotsConn = New OdbcConnection(connectString)
impotsConn.Open()
' on crée un objet command
sqlCommand = New OdbcCommand(selectCommand, impotsConn)
' on exécute la requête
Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
' Exploitation de la table récupérée
While myReader.Read()
' les données de la ligne courante sont mis dans les tableaux
tLimites.Add(myReader(colLimites))
tCoeffR.Add(myReader(colCoeffR))
tCoeffN.Add(myReader(colCoeffN))
End While
' libération des ressources
myReader.Close()
impotsConn.Close()
' les tableaux dynamiques sont mis dans des tableaux statiques
Me.limites = New Decimal(tLimites.Count) {}
Me.coeffR = New Decimal(tLimites.Count) {}
Me.coeffN = New Decimal(tLimites.Count) {}
Dim i As Integer
For i = 0 To tLimites.Count - 1
limites(i) = Decimal.Parse(tLimites(i).ToString())
coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
Next i
End Sub
' calcul de l'impôt
Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
' calcul du nombre de parts
Dim nbParts As Decimal
If marié Then
nbParts = CDec(nbEnfants) / 2 + 2
Else
nbParts = CDec(nbEnfants) / 2 + 1
End If
If nbEnfants >= 3 Then
nbParts += 0.5D
End If
' calcul revenu imposable & Quotient familial
Dim revenu As Decimal = 0.72D * salaire
Dim QF As Decimal = revenu / nbParts
' calcul de l'impôt
limites((limites.Length - 1)) = QF + 1
Dim i As Integer = 0
While QF > limites(i)
i += 1
End While
' retour résultat
Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
End Function
End Class
Dans le service web, on ne peut utiliser qu'un constructeur sans paramètres. Aussi le constructeur de la classe va-t-il devenir le suivant :
' constructeur
Public Sub New()
' initialise les trois tableaux limites, coeffR, coeffN à partir
' du contenu de la table Timpots de la base ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN sont les trois colonnes de cette table
' peut lancer une exception
' on récupère les paramètres de configuration du service
Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")
' on exploite la base de données
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Les cinq paramètres du constructeur de la classe précédente sont maintenant lus dans le fichier web.config du service. Le code du fichier source impots.asmx est le suivant. Il reprend la majeure partie du code précédent. Nous nous sommes contentés d'encadrer les portions de code propres au service web :
<%@ WebService language="VB" class=impots %>
' création d'un servie web impots
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Imports System.Configuration
Imports System.Web.Services
<WebService([Namespace]:="st.istia.univ-angers.fr")> _
Public Class impôt
Inherits WebService
' les données nécessaires au calcul de l'impôt
' proviennent d'une source extérieure
Private limites(), coeffR(), coeffN() As Decimal
Private OK As Boolean = False
Private errMessage As String = ""
' constructeur
Public Sub New()
' initialise les trois tableaux limites, coeffR, coeffN à partir
' du contenu de la table Timpots de la base ODBC DSNimpots
' colLimites, colCoeffR, colCoeffN sont les trois colonnes de cette table
' peut lancer une exception
' on récupère les paramètres de configuration du service
Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")
' on exploite la base de données
Dim connectString As String = "DSN=" + DSNimpots + ";" ' chaîne de connexion à la base
Dim impotsConn As OdbcConnection = Nothing ' la connexion
Dim sqlCommand As OdbcCommand = Nothing ' la commande SQL
Dim myReader As OdbcDataReader ' lecteur de données Odbc
' la requête SELECT
Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
' tableaux pour récupérer les données
Dim tLimites As New ArrayList
Dim tCoeffR As New ArrayList
Dim tCoeffN As New ArrayList
' on tente d'accéder à la base de données
Try
impotsConn = New OdbcConnection(connectString)
impotsConn.Open()
' on crée un objet command
sqlCommand = New OdbcCommand(selectCommand, impotsConn)
' on exécute la requête
myReader = sqlCommand.ExecuteReader()
' Exploitation de la table récupérée
While myReader.Read()
' les données de la ligne courante sont mis dans les tableaux
tLimites.Add(myReader(colLimites))
tCoeffR.Add(myReader(colCoeffR))
tCoeffN.Add(myReader(colCoeffN))
End While
' libération des ressources
myReader.Close()
impotsConn.Close()
' les tableaux dynamiques sont mis dans des tableaux statiques
Me.limites = New Decimal(tLimites.Count) {}
Me.coeffR = New Decimal(tLimites.Count) {}
Me.coeffN = New Decimal(tLimites.Count) {}
Dim i As Integer
For i = 0 To tLimites.Count - 1
limites(i) = Decimal.Parse(tLimites(i).ToString())
coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
Next i
' c'est bon
OK = True
errMessage = ""
Catch ex As Exception
' erreur
OK = False
errMessage += "[" + ex.Message + "]"
End Try
End Sub
' calcul de l'impôt
<WebMethod()> _
Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
' calcul du nombre de parts
Dim nbParts As Decimal
If marié Then
nbParts = CDec(nbEnfants) / 2 + 2
Else
nbParts = CDec(nbEnfants) / 2 + 1
End If
If nbEnfants >= 3 Then
nbParts += 0.5D
End If
' calcul revenu imposable & Quotient familial
Dim revenu As Decimal = 0.72D * salaire
Dim QF As Decimal = revenu / nbParts
' calcul de l'impôt
limites((limites.Length - 1)) = QF + 1
Dim i As Integer = 0
While QF > limites(i)
i += 1
End While
' retour résultat
Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
End Function
' id
<WebMethod()> _
Function id() As String
' pour voir si tout est OK
Return "[" + OK + "," + errMessage + "]"
End Function
End Class
Expliquons les quelques modifications faites à la classe impots en-dehors de celles nécessaires pour en faire un service web :
- la lecture de la base de données dans le constructeur peut échouer. Aussi avons-nous ajouté deux attributs à notre classe et une méthode :
- le booléen OK est à vrai si la base a pu être lue, à faux sinon
- la chaîne errMessage contient un message d'erreur si la base de données n'a pu être lue.
- la méthode id sans paramètres permet d'obtenir la valeur ces deux attributs.
- pour gérer l'erreur éventuelle d'accès à la base de données, la partie du code du constructeur concernée par cet accès a été entourée d'un try-catch.
Le fichier web.config de configuration du service est le suivant :
<configuration>
<appSettings>
<add key="DSN" value="mysql-impots" />
<add key="TABLE" value="timpots" />
<add key="COL_LIMITES" value="limites" />
<add key="COL_COEFFR" value="coeffr" />
<add key="COL_COEFFN" value="coeffn" />
</appSettings>
</configuration>
Lors d'un premier essai de chargement du service impots, le compilateur a déclaré qu'il ne trouvait pas l'espace de noms Microsoft.Data.Odbc utilisé dans la directive :
Après consultation de la documentation
- une directive de compilation a été ajoutée dans web.config pour indiquer qu'il fallait utiliser l'assembly Microsoft.Data.odbc
- une copie du fichier microsoft.data.odbc.dll a été placée dans le dossier bin du projet. Celui-ci est systématiquement exploré par le compilateur d'un service web lorsqu'il recherche un "assembly".
D'autres solutions semblent possibles mais n'ont pas été creusées ici. Le fichier de configuration est donc devenu :
<configuration>
<appSettings>
<add key="DSN" value="mysql-impots" />
<add key="TABLE" value="timpots" />
<add key="COL_LIMITES" value="limites" />
<add key="COL_COEFFR" value="coeffr" />
<add key="COL_COEFFN" value="coeffn" />
</appSettings>
<system.web>
<compilation>
<assemblies>
<add assembly="Microsoft.Data.Odbc" />
</assemblies>
</compilation>
</system.web>
</configuration>
Le contenu du dossier impots\bin :
Le service et son fichier de configuration ont été placés dans impots :
Le dossier physique du service web a été associé au dossier virtuel /impots de IIS. La page du service est alors la suivante :

Si on suit le lien id :

Si on utilise le bouton Appeler :

Le résultat précédent affiche les valeurs des attributs OK (true) et errMessage (""). Dans cet exemple, la base a été chargée correctement. Ca n'a pas toujours été le cas et c'est pourquoi nous avons ajouté la méthode id pour avoir accès au message d'erreur. L'erreur était que le nom DSN de la base avait été définie comme DSN utilisateur alors qu'il fallait le définir comme DSN système. Cette distinction se fait dans le gestionnaire de sources ODBC 32 bits :
![]() |
Revenons à la page du service :

Suivons le lien calculer :

Nous définissons les paramètres de l'appel et nous exécutons celui-ci :

Le résultat est correct.
10.10.2. Générer le proxy du service impots
Maintenant que nous avons un service web impots opérationnel, nous pouvons générer sa classe proxy. On rappelle que celle-ci sera utilisée par des applications clientes pour atteindre le service web impots de façon transparente. On utilise d'abord l'utilitaire wsdl pour générer le fichier source de la classe proxy puis celui-ci est compilé dans un une dll.
dos>wsdl /language=vb http://localhost/impots/impots.asmx
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
Écriture du fichier 'D:\data\serge\devel\vbnet\poly\chap9\impots\impots.vb'.
D:\data\serge\devel\vbnet\poly\chap9\impots>dir
09/03/2004 10:20 <REP> bin
09/03/2004 10:58 4 651 impots.asmx
09/03/2004 11:05 3 364 impots.vb
09/03/2004 10:19 431 web.config
dos>vbc /t:library /r:system.dll /r:system.web.services.dll /r:system.xml.dll impots.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4
pour Microsoft (R) .NET Framework version 1.1.4322.573
Copyright (C) Microsoft Corporation 1987-2002. Tous droits réservés.
dos>dir
09/03/2004 10:20 <REP> bin
09/03/2004 10:58 4 651 impots.asmx
09/03/2004 11:09 5 120 impots.dll
09/03/2004 11:05 3 364 impots.vb
09/03/2004 10:19 431 web.config
10.10.3. Utiliser le proxy avec un client
Dans le chapitre sur les bases de données nous avions créé une application console permettant le calcul de l'impôt :
dos>dir
27/02/2004 16:56 5 120 impots.dll
27/02/2004 17:12 3 586 impots.vb
27/02/2004 17:08 6 144 testimpots.exe
27/02/2004 17:18 3 328 testimpots.vb
dos>testimpots
pg DSNimpots tabImpots colLimites colCoeffR colCoeffN
dos>testimpots odbc-mysql-dbimpots impots limites coeffr coeffn
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F
Le programme testimpots utilisait alors la classe impôt classique celle contenue dans le fichier impots.dll. Le code du programme testimpots.vb était le suivant :
Option Explicit On
Option Strict On
' espaces de noms
Imports System
Imports Microsoft.VisualBasic
' pg de test
Module testimpots
Sub Main(ByVal arguments() As String)
' programme interactif de calcul d'impôt
' l'utilisateur tape trois données au clavier : marié nbEnfants salaire
' le programme affiche alors l'impôt à payer
Const syntaxe1 As String = "pg DSNimpots tabImpots colLimites colCoeffR colCoeffN"
Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"
' vérification des paramètres du programme
If arguments.Length <> 5 Then
' msg d'erreur
Console.Error.WriteLine(syntaxe1)
' fin
Environment.Exit(1)
End If 'if
' on récupère les arguments
Dim DSNimpots As String = arguments(0)
Dim tabImpots As String = arguments(1)
Dim colLimites As String = arguments(2)
Dim colCoeffR As String = arguments(3)
Dim colCoeffN As String = arguments(4)
' création d'un objet impôt
Dim objImpôt As impôt = Nothing
Try
objImpôt = New impôt(DSNimpots, tabImpots, colLimites, colCoeffR, colCoeffN)
Catch ex As Exception
Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
Environment.Exit(2)
End Try
' boucle infinie
While True
' au départ pas d'erreurs
Dim erreur As Boolean = False
' on demande les paramètres du calcul de l'impôt
Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
Dim paramètres As String = Console.In.ReadLine().Trim()
' qq chose à faire ?
If paramètres Is Nothing Or paramètres = "" Then
Exit While
End If
' vérification du nombre d'arguments dans la ligne saisie
Dim args As String() = paramètres.Split(Nothing)
Dim nbParamètres As Integer = args.Length
If nbParamètres <> 3 Then
Console.Error.WriteLine(syntaxe2)
erreur = True
End If
Dim marié As String
Dim nbEnfants As Integer
Dim salaire As Integer
If Not erreur Then
' vérification de la validité des paramètres
' marié
marié = args(0).ToLower()
If marié <> "o" And marié <> "n" Then
Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
erreur = True
End If
' nbEnfants
nbEnfants = 0
Try
nbEnfants = Integer.Parse(args(1))
If nbEnfants < 0 Then
Throw New Exception
End If
Catch
Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
erreur = True
End Try
' salaire
salaire = 0
Try
salaire = Integer.Parse(args(2))
If salaire < 0 Then
Throw New Exception
End If
Catch
Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
erreur = True
End Try
End If
If Not erreur Then
' les paramètres sont corrects - on calcule l'impôt
Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
End If
End While
End Sub
End Module
Nous reprenons le même programme pour lui faire utiliser maintenant le service web impots au travers de la classe proxy impots créée précédemment. Nous sommes obligés de modifier quelque peu le code :
- alors que la classe impôt d'origine avait un constructeur à cinq arguments, la classe proxy impots a un constructeur sans paramètres. Les cinq paramètres, nous l'avons vu, sont maintenant fixés dans le fichier de configuration du service web.
- il n'y a donc plus besoin de passer ces cinq paramètres en arguments au programme test
Le nouveau code est le suivant :
Imports System
Imports Microsoft.VisualBasic
' pg de test
Module testimpots
Public Sub Main(ByVal arguments() As String)
' programme interactif de calcul d'impôt
' l'utilisateur tape trois données au clavier : marié nbEnfants salaire
' le programme affiche alors l'impôt à payer
Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"
' création d'un objet impôt
Dim objImpôt As impôt = Nothing
Try
objImpôt = New impôt
Catch ex As Exception
Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
Environment.Exit(2)
End Try
' boucle infinie
Dim erreur As Boolean
While True
' au départ pas d'erreur
erreur = False
' on demande les paramètres du calcul de l'impôt
Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
Dim paramètres As String = Console.In.ReadLine().Trim()
' qq chose à faire ?
If paramètres Is Nothing Or paramètres = "" Then
Exit While
End If
' vérification du nombre d'arguments dans la ligne saisie
Dim args As String() = paramètres.Split(Nothing)
Dim nbParamètres As Integer = args.Length
If nbParamètres <> 3 Then
Console.Error.WriteLine(syntaxe2)
erreur = True
End If
If Not erreur Then
' vérification de la validité des paramètres
' marié
Dim marié As String = args(0).ToLower()
If marié <> "o" And marié <> "n" Then
Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
erreur = True
End If
' nbEnfants
Dim nbEnfants As Integer = 0
Try
nbEnfants = Integer.Parse(args(1))
If nbEnfants < 0 Then
Throw New Exception
End If
Catch
Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument nbEnfants incorrect : tapez un entier positif ou nul"))
erreur = True
End Try
' salaire
Dim salaire As Integer = 0
Try
salaire = Integer.Parse(args(2))
If salaire < 0 Then
Throw New Exception
End If
Catch
Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument salaire incorrect : tapez un entier positif ou nul"))
erreur = True
End Try
' si les paramètres sont corrects - on calcule l'impôt
If Not erreur Then Console.Out.WriteLine(("impôt=" + objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
End If
End While
End Sub
End Module
Nous avons le proxy impots.dll et le source testimpots dans le même dossier.
dos>dir
09/03/2004 11:28 <REP> bin
09/03/2004 11:09 5 120 impots.dll
09/03/2004 11:34 3 396 testimpots.vb
09/03/2004 10:19 431 web.config
Nous compilons le source testimpots.vb :
dos>vbc /r:impots.dll /r:microsoft.visualbasic.dll /r:system.web.services.dll /r:system.dll testimpots.vb
dos>dir
09/03/2004 11:28 <REP> bin
09/03/2004 11:09 5 120 impots.dll
09/03/2004 11:05 3 364 impots.vb
09/03/2004 11:35 5 632 testimpots.exe
09/03/2004 11:34 3 396 testimpots.vb
09/03/2004 10:19 431 web.config
puis l'exécutons :
dos>testimpots
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F
Nous obtenons bien le résultat attendu.





