Skip to content

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 :

Image

alors que Netscape Navigator affichera :

Image

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

Image

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] :

Image

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 :

Image

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 :

Image

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 :

Image

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 :

Image

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 :

Public Class Service1
    Inherits System.Web.Services.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 :

Image

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

Image

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].

Image

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 :

Image

Ouvrons avec un éditeur de texte (notepad ou autre). On obtient le contenu suivant :

<%@ WebService Language="vb" Codebehind="Service1.asmx.vb" Class="demo.Bonjour" %>

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 :

Image

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] :

Image

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 :

Image

Nous avons volontairement tronqué la page obtenue pour ne pas alourdir notre démonstration. Remarquons de nouveau l'url obtenue :

http://localhost/polyvbnet/demo/Service1.asmx?op=Bonjour

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 :

Image

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) :

dos>dir bin
02/03/2004  18:12                3 072 demo2.dll

Nous créons maintenant le fichier demo2.asmx. C'est lui qui sera appelé par les clients web. Son contenu est le suivant :

<%@ WebService Language="vb" class="Bonjour2,demo2"%>

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 :

Image

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

Image

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 :

Image

Nous utilisons le bouton [Appeler] ci-dessus :

Image

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 :

<%@ WebService Language="vb" class="Bonjour3"%>

Imports System.Web.Services

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> _
Public Class Bonjour3
    Inherits System.Web.Services.WebService

    <WebMethod()> Public Function getBonjour() As String
        Return "bonjour en version3 !"
    End Function
End Class

Nous constatons que le code source du service est maintenant directement dans le fichier source du fichier demo3.asmx. La directive

<%@ WebService Language="vb" class="Bonjour3"%>

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 :

Image

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

Image

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 :

Image

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 :

Image

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 :

Image

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 :

Image

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 :

Image

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

Image

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 :

  1. ajouter(a,b) qui rendra a+b
  2. soustraire(a,b) qui rendra a-b
  3. multiplier(a,b) qui rendra a*b
  4. diviser(a,b) qui rendra a/b
  5. 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 :

Image

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] :

Image

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

Image

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 :

      <WebMethod>  _
      Function ajouter(a As Double, b As Double) As Double
         Return a + b
      End Function 

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 :

Image

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

Image

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

Image

Nous obtenons la page suivante :

Image

Utilisons le bouton [Appeler] ci-dessus :

Image

Dans tous les cas, la réponse du serveur a la forme :

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • 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 :

Image

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

Image

Image

Image

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 :

Image

Commentons ce qui est écrit. Tout da'bord le client web doit envoyer les entêtes HTTP suivants :

POST /operations/operations.asmx/ajouter HTTP/1.1
Le client web fait une requête POST à l'URL /operations/operations.asmx/ajouter selon le protocole HTTP version 1.1
HOST: localhost
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
Content-Type: application/x-www-form-urlencoded
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.
Content-length: 7
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 :

a=2&b=3

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 :

dos>httpPost2 http://localhost/operations/operations.asmx

Ensuite, le client lit les commandes tapées au clavier et les exécute. Celles-ci sont au format :

fonction a b

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 :

ajouter 6 7

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 :

[résultat=13]

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

--> Content-Length: 7

on doit indiquer la taille des paramètres qui seront envoyés par le client derrière les entêtes HTTP :

--> a=6&b=7

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 %n1n2n1n2 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 :

--> a=6&b=7

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 :

  1. des entêtes HTTP terminés par une ligne vide
  2. 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 :

<-- <double xmlns="st.istia.univ-angers.fr">13</double>

au moyen là encore d'une expression régulière. Une fois le résultat trouvé, il est affiché.

[résultat=13]

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 :

Image

Image

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

    a=A&b=B

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 :

  1. l'URI du service web auquel il doit se connecter
  2. 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 :

  1. 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
  2. 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 :

  1. création d'un objet clientSOAP qui va créer la liaison avec le service web
  2. utilisation répétée de la méthode executeFonction
  3. 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 :

  1. l'URI du service web operations
  2. 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 :

type
nom
rôle
1
TextBox
txtURI
l'URI du service web operations
2
Button
btnOuvrir
ouvre la liaison avec le service Web
3
Button
btnFermer
ferme la liaison avec le service Web
4
ComboBox
cmbFonctions
la liste des fonction (ajouter, soustraire, multiplier, diviser)
5
TextBox
txtA
l'argument a des fonctions
6
TextBox
txtB
l'argument b des fonctions
7
TextBox
txtRésultat
le résultat de fonction(a,b)
8
Button
btnCalculer
lance le calcul de fonction(a,b)
9
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] :

Image

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

Image

Image

Image

Image

Image

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 :

dos>clientsoapgui

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 :

Image

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 :

Public Class operations
    Inherits System.Web.Services.Protocols.SoapHttpClientProtocol

La classe porte le nom operations du service web pour lequel elle a été construite. Elle dérive de la classe SoapHttpClientProtocol :

Image

Notre classe proxy a un constructeur unique :

    Public Sub New()
        MyBase.New
        Me.Url = "http://localhost/operations/operations.asmx"
    End Sub

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 :

dos>vbc /t:library /r:system.web.services.dll /r:system.xml.dll /r:system.dll operations.vb
dos>dir
04/03/2004  17:17                6 663 operations.vb
04/03/2004  17:24                7 680 operations.dll

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 :

<configuration>
    <appSettings>
...
    </appSettings>
</configuration>

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 :

        String P=ConfigurationSettings.AppSettings["V"];

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 :

dos>dir
09/03/2004  08:25               632 personne.asmx
09/03/2004  08:08               186 web.config

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 :

Image

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

Image

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

Image

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 :

Imports Microsoft.Data.Odbc

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 :

dos>dir impots\bin
30/01/2002  02:02           327 680 Microsoft.Data.Odbc.dll

Le service et son fichier de configuration ont été placés dans impots :

dos>dir impots
09/03/2004  10:13             4 669 impots.asmx
09/03/2004  10:19               431 web.config

Le dossier physique du service web a été associé au dossier virtuel /impots de IIS. La page du service est alors la suivante :

Image

Si on suit le lien id :

Image

Si on utilise le bouton Appeler :

Image

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 :

Image

Suivons le lien calculer :

Image

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

Image

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.