Skip to content

10. Servizi web

10.1. Introduzione

Nel capitolo precedente abbiamo presentato diverse applicazioni client-server TCP/IP. Poiché i client e il server si scambiano righe di testo, possono essere scritti in qualsiasi linguaggio. Il client deve semplicemente conoscere il protocollo di comunicazione previsto dal server. I servizi web sono applicazioni server TCP/IP con le seguenti caratteristiche:

  • Sono ospitati da server web e il protocollo di comunicazione client-server è quindi HTTP (HyperText Transfer Protocol), un protocollo che gira su TCP/IP.
  • Il servizio Web ha un protocollo di comunicazione standard indipendentemente dal servizio fornito. Un servizio Web offre vari servizi S1, S2, .., Sn. Ciascuno di essi richiede parametri forniti dal client e restituisce un risultato al client. Per ogni servizio, il client deve conoscere:
    • il nome esatto del servizio Si
    • l'elenco dei parametri da fornire e i loro tipi
    • il tipo di risultato restituito dal servizio

Una volta noti questi elementi, l'interazione client-server segue lo stesso formato indipendentemente dal servizio web interrogato. Il codice client è quindi standardizzato.

  • Per motivi di sicurezza legati agli attacchi provenienti da Internet, molte organizzazioni mantengono reti private e aprono solo determinate porte dei propri server verso Internet: principalmente la porta 80 per il servizio web. Tutte le altre porte sono bloccate. Di conseguenza, le applicazioni client-server come quelle presentate nel capitolo precedente sono costruite all'interno della rete privata (intranet) e generalmente non sono accessibili dall'esterno. Ospitare un servizio su un server web lo rende accessibile all'intera comunità di Internet.
  • Il servizio web può essere modellato come un oggetto remoto. I servizi offerti diventano quindi metodi di questo oggetto. Un client può accedere a questo oggetto remoto come se fosse locale. Ciò nasconde l'intero livello di comunicazione di rete e consente lo sviluppo di un client indipendente da quel livello. Se il livello cambia, il client non deve essere modificato. Questo è un enorme vantaggio e probabilmente il principale beneficio dei servizi web.
  • Come per le applicazioni client-server TCP/IP presentate nel capitolo precedente, il client e il server possono essere scritti in qualsiasi linguaggio. Essi si scambiano righe di testo. Queste consistono di due parti:
    • le intestazioni richieste dal protocollo HTTP
    • il corpo del messaggio. Per una risposta del server al client, il corpo è in formato XML (eXtensible Markup Language). Per una richiesta del client al server, il corpo del messaggio può assumere diverse forme, incluso l'XML. La richiesta XML del client può utilizzare un formato specifico chiamato SOAP (Simple Object Access Protocol). In questo caso, anche la risposta del server segue il formato SOAP.

10.2. Browser e XML

I servizi Web inviano XML ai propri client. I browser possono reagire in modo diverso quando ricevono questo flusso XML. Internet Explorer dispone di un foglio di stile predefinito che gli consente di visualizzare l'XML. Netscape Communicator, tuttavia, non dispone di questo foglio di stile e non visualizza il codice XML ricevuto. È necessario visualizzare il codice sorgente della pagina ricevuta per accedere all'XML. Ecco un esempio. Per il seguente codice XML:

<?xml version="1.0" encoding="utf-8"?>
<string xmlns="st.istia.univ-angers.fr">bonjour de nouveau !</string>

Internet Explorer visualizzerà la seguente pagina:

Image

mentre Netscape Navigator visualizzerà:

Image

Se visualizziamo il codice sorgente della pagina ricevuta da Netscape, otteniamo:

Image

Netscape ha effettivamente ricevuto lo stesso contenuto di Internet Explorer, ma lo ha visualizzato in modo diverso. D'ora in poi, useremo Internet Explorer per le schermate.

10.3. Un primo servizio web

Esploreremo i servizi web attraverso un esempio molto semplice disponibile in tre versioni.

10.3.1. Versione 1

Per questa prima versione, useremo VS.NET, che ha il vantaggio di poter generare uno scheletro di servizio web immediatamente operativo. Una volta compresa questa architettura, saremo in grado di iniziare a lavorare in modo indipendente. Questo sarà l'obiettivo delle versioni successive.

Utilizzando VS.NET, creiamo un nuovo progetto tramite l'opzione [File/Nuovo/Progetto]:

Image

Si notino i seguenti punti:

  • il tipo di progetto è Visual Basic (riquadro sinistro)
  • il modello di progetto è Servizio Web ASP.NET (riquadro destro)
  • la posizione è flessibile. In questo caso, il servizio web sarà ospitato da un server web IIS locale. Il suo URL sarà quindi http://localhost/[path], dove [path] deve essere definito. Qui scegliamo il percorso http://localhost/polyvbnet/demo. VS.NET creerà quindi una cartella per questo progetto. Dove? Il server IIS ha una directory radice per l'albero dei documenti web che serve. Chiamiamo questa radice <IISroot>. Essa corrisponde all'URL http://localhost. Possiamo dedurre che l'URL http://localhost/polyvbnet/demo sarà associato alla cartella <IISroot>/polyvbnet/demo. <IISroot> è normalmente la cartella \inetpub\wwwroot sull'unità in cui è stato installato IIS. Nel nostro esempio, si tratta dell'unità E. La cartella creata da VS.NET è quindi la cartella e:\inetpub\wwwroot\polyvbnet\demo:

Image

Come sempre, vengono create numerose cartelle. Non sempre sono utili. Spiegheremo solo quelle di cui abbiamo bisogno in un dato momento. VS.NET ha creato un progetto:

Image

Troviamo alcuni dei file presenti nella cartella fisica del progetto. Quello più interessante per noi è il file con estensione .asmx. Questa è l'estensione per i servizi web. Un servizio web è gestito da VS.NET come un'applicazione Windows, ovvero un'applicazione che ha un'interfaccia utente grafica e del codice per gestirla. Ecco perché abbiamo una finestra di progettazione:

Image

Un servizio web normalmente non ha un'interfaccia utente grafica. Rappresenta un oggetto che può essere chiamato in remoto. Ha dei metodi, e le applicazioni chiamano questi metodi. Lo considereremo quindi come un oggetto classico con la caratteristica unica di poter essere istanziato in remoto attraverso la rete. Pertanto, non useremo la finestra di progettazione fornita da VS.NET. Concentriamoci invece sul codice del servizio utilizzando l'opzione Visualizza/Codice:

Image

Vale la pena notare diversi punti:

  • Il file si chiama Service1.asmx.vb, non Service1.asmx. Torneremo sul contenuto del file Service1.asmx più avanti.
  • Vediamo una finestra di codice simile a quella che avevamo quando sviluppavamo applicazioni Windows con VS.NET

Il codice generato da VS.NET è il seguente:


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()
 
        'This call is required by the Web Services Designer.
        InitializeComponent()
 
        'Add your initialization code after the InitializeComponent() call
 
    End Sub
 
    'Required by the Web Services Designer
    Private components As System.ComponentModel.IContainer
 
    'REMARQUE: the following procedure is required by the Web Services Designer
    'It can be modified using the Web Services Designer.  
    'Do not modify it using the code editor.
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
    End Sub
 
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        'CODEGEN: this procedure is required by the Web Services Designer
        'Do not modify it using the code editor.
        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
    ' The HelloWorld() service example returns the string Hello World.
    ' To generate, leave the following lines uncommented, then save and generate the project.
    ' To test this Web service, make sure that the .asmx file is the start page
    ' and press F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World
    ' End Function
 
End Class

Innanzitutto, si noti che qui abbiamo una classe, la classe Service1, che deriva dalla classe WebService:

Public Class Service1
    Inherits System.Web.Services.WebService

Questo ci porta a importare lo spazio dei nomi System.Web.Services:


Imports System.Web.Services

La dichiarazione della classe è preceduta da un attributo di compilazione:


<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
    Inherits System.Web.Services.WebService

L'attributo System.Web.Services.WebService() indica che la classe seguente è un servizio web. Questo attributo accetta vari parametri, tra cui uno chiamato NameSpace. Viene utilizzato per collocare il servizio web in uno spazio dei nomi. Infatti, è facile immaginare che esistano diversi servizi web denominati "weather" nel mondo. Abbiamo bisogno di un modo per distinguerli. Lo spazio dei nomi lo rende possibile. Uno potrebbe essere denominato [namespace1].weather e un altro [namespace2].weather. Si tratta di un concetto analogo agli spazi dei nomi delle classi. VS.NET ha generato automaticamente il codice e lo ha inserito in una regione del codice sorgente:


#Region " Code généré par le Concepteur des services Web "

Se osserviamo questo codice, è lo stesso codice generato dal designer quando abbiamo creato applicazioni Windows. Si tratta di codice che possiamo semplicemente eliminare se non disponiamo di un'interfaccia utente grafica, come nel caso dei servizi web.

La lezione si conclude con un esempio di come potrebbe presentarsi un servizio 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

Sulla base di quanto appena detto, ripuliamo il codice in modo che diventi il seguente:


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

Ora abbiamo un quadro più chiaro.

  • Un servizio web è una classe derivata dalla classe WebService
  • La classe è qualificata dall'attributo <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. Collochiamo quindi il nostro servizio nello spazio dei nomi st.istia.univ-angers.fr.
  • I metodi della classe sono qualificati da un attributo <WebMethod()> che indica che si tratta di un metodo che può essere chiamato in remoto attraverso la rete

La classe che fornisce il nostro servizio web si chiama quindi Bonjour e ha un unico metodo, anch'esso denominato Bonjour, che restituisce una stringa. Siamo pronti per un primo test.

  • Avviare il server web IIS se non è già stato fatto
  • Utilizza l'opzione Debug/Esegui senza debug. VS.NET

VS.NET compilerà quindi l'intera applicazione, avvierà un browser (spesso Internet Explorer, se disponibile) e visualizzerà l'URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

Perché l'URL http://localhost/polyvbnet/demo/Service1.asmx? Perché era l'unico file .asmx nel progetto:

Image

Se ci fossero stati più file .asmx, avremmo dovuto specificare quale eseguire per primo. Per farlo, basta cliccare con il tasto destro del mouse sul file .asmx in questione e selezionare l'opzione [Imposta come pagina iniziale].

Image

Potresti essere interessato a sapere cosa contiene il file service1.asmx. Infatti, con VS.NET, abbiamo lavorato sul file service1.asmx.vb e non sul file service1.asmx. Questo file si trova nella cartella del progetto:

Image

Apriamolo con un editor di testo (Blocco note o altro). Otteniamo il seguente contenuto:

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

Il file contiene una semplice direttiva per il server IIS che indica:

  • che si tratta di un servizio web (parola chiave WebService)
  • che il linguaggio della classe di questo servizio è Visual Basic (Language="vb")
  • che il codice sorgente di questa classe si trova nel file Service1.asmx.vb (Codebehind="Service1.asmx.vb")
  • che la classe che implementa il servizio si chiama demo.Bonjour (Class="demo.Bonjour"). Si noti che VS.NET ha collocato la classe Bonjour nello spazio dei nomi demo, che è anche il nome del progetto.

Torniamo alla pagina accessibile all'URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

Chi ha scritto il codice HTML della pagina sopra riportata? Non noi, questo lo sappiamo. È stato IIS, che presenta i servizi web in modo standard. Questa pagina ci offre due collegamenti. Seguiamo il primo [Descrizione del servizio]:

Image

Ops... è un XML piuttosto oscuro. Notate, tuttavia, l'URL

http://localhost/polyvbnet/demo/Service1.asmx?WSDL. Apri un browser e digita direttamente questo URL. Otterrai lo stesso risultato di prima. Ricorda quindi che l'URL http://serviceweb?WSDL consente di accedere alla descrizione XML del servizio web. Torniamo alla home page e clicchiamo sul link [Hello]. Ricordate che Hello è un metodo del servizio web. Se ci fossero stati più metodi, sarebbero stati tutti elencati qui. Otteniamo la seguente nuova pagina:

Image

Abbiamo intenzionalmente troncato la pagina risultante per mantenere concisa la nostra dimostrazione. Notate nuovamente l'URL:

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

Se digitiamo questo URL direttamente nel browser, otterremo lo stesso risultato di cui sopra. Ci viene chiesto di utilizzare il pulsante [Call]. Facciamolo. Si apre una nuova pagina:

Image

Si tratta ancora di XML. Contiene due informazioni presenti nel nostro servizio web:

  • lo spazio dei nomi st.istia.univ-angers.fr del nostro servizio

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
  • il valore restituito dal metodo Bonjour:

        Return "bonjour !"

Cosa abbiamo imparato?

  • come scrivere un servizio web S
  • come richiamarlo

Ora vedremo come scrivere un servizio web senza utilizzare VS.NET.

10.3.2. Versione 2

Nell'esempio precedente, VS.NET ha svolto molte operazioni autonomamente. È possibile creare un servizio web senza questo strumento? La risposta è sì, e ora vi mostreremo come. Utilizzando un editor di testo, creeremo il seguente servizio web:


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 si chiama Bonjour2 e ha un metodo chiamato getBonjour. Si trova nel file demo2.vb, che a sua volta si trova nella struttura di directory del server IIS nella cartella E:\Inetpub\wwwroot\polyvbnet\demo2. Si tratta di una classe VB.NET standard che può quindi essere compilata:

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

Inseriamo l'assembly demo2.dll in una cartella denominata bin (questo nome è obbligatorio):

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

Ora creiamo il file demo2.asmx. Questo è il file che verrà richiamato dai client web. Il suo contenuto è il seguente:

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

Abbiamo già incontrato questa direttiva. Essa indica che:

  • la classe del servizio web si chiama Bonjour2 e si trova nell'assembly demo2.dll. IIS cercherà questo assembly in varie posizioni, inclusa la cartella bin del servizio web. Ecco perché abbiamo collocato l'assembly demo2.dll in quella posizione.

Ora possiamo eseguire vari test. Ci assicuriamo che IIS sia in esecuzione e richiediamo l'URL http://localhost/polyvbnet/demo2/demo2.asmx in un browser:

Image

Quindi l'URL http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Quindi l'URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, dove getBonjour è il nome dell'unico metodo nel nostro servizio web:

Image

Utilizziamo il pulsante [Call] in alto:

Image

Otteniamo con successo il risultato della chiamata al metodo getBonjour del servizio web. Ora sappiamo come creare un servizio web senza Visual Studio .NET. Da questo punto in poi, tralasceremo i dettagli relativi alla creazione del servizio web e ci concentreremo esclusivamente sui file fondamentali.

10.3.3. Versione 3

Le due versioni precedenti del servizio web [Hello] utilizzavano due file:

  • un file .asmx, il punto di ingresso del servizio web
  • un file .vb, il codice sorgente del servizio web

Qui mostriamo che è sufficiente un singolo file .asmx. Il codice per il servizio demo3.asmx è il seguente:

<%@ 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

Possiamo notare che il codice sorgente del servizio si trova ora direttamente nel file sorgente demo3.asmx. La direttiva

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

non fa più riferimento a una classe in un assembly esterno, ma a una classe situata nello stesso file sorgente. Mettiamo questo file nella cartella <IISroot>\polyvbnet\demo3:

Image

Avviamo IIS e richiediamo l'URL http://localhost/polyvbnet/demo3/demo3.asmx:

Image

Notiamo una differenza significativa rispetto alla versione precedente: non abbiamo dovuto compilare il codice VB del servizio. IIS ha eseguito questa compilazione autonomamente utilizzando il compilatore VB.NET installato sulla stessa macchina. Ha quindi visualizzato la pagina. Se si verifica un errore di compilazione, IIS lo segnalerà:

Image

10.3.4. Versione 4

Qui ci concentriamo sulla configurazione del server IIS. Finora abbiamo sempre collocato i nostri servizi web nella directory radice <IISroot> del server IIS, in questo caso [e:\inetpub\wwwroot]. Qui dimostriamo che è possibile collocare il servizio web ovunque. Ciò avviene utilizzando le directory virtuali di IIS. Collochiamo il nostro servizio nella seguente directory:

Image

La cartella [D:\data\devel\vbnet\poly\chap9\demo3] non si trova nella struttura di directory del server IIS. Dobbiamo indicarla a IIS creando una cartella virtuale IIS. Avviamo IIS e selezioniamo l'opzione [Avanzate] qui sotto:

Image

Viene visualizzato un elenco di directory virtuali. Non ci soffermeremo su questo elenco. Creiamo una nuova directory virtuale utilizzando il pulsante [Aggiungi] in alto:

Image

Utilizzando il pulsante [Sfoglia], selezioniamo la cartella fisica contenente il servizio web, in questo caso la cartella [D:\data\devel\vbnet\poly\chap9\demo3]. Assegniamo a questa cartella un nome logico (virtuale): [virdemo3]. Ciò significa che i documenti all'interno della cartella fisica [D:\data\devel\vbnet\poly\chap9\demo3] saranno accessibili in rete tramite l'URL [http://<machine>/virdemo3]. La finestra di dialogo sopra riportata contiene altre impostazioni che lasciamo così come sono. Facciamo clic su OK. La nuova cartella virtuale appare nell'elenco delle cartelle virtuali in IIS:

Image

Ora, apriamo un browser e richiediamo l'URL [http://localhost/virdemo3/demo3.asmx]. Otteniamo lo stesso risultato di prima:

Image

10.3.5. Conclusione

Abbiamo illustrato diversi modi per creare un servizio web. D'ora in poi, useremo il metodo della versione 3 per creare il servizio e il metodo 4 per la sua distribuzione. In questo modo, non avremo bisogno di VS.NET. Tuttavia, vale la pena notare i vantaggi dell'uso di VS.NET per l'assistenza al debug che fornisce. Esistono strumenti gratuiti per lo sviluppo di applicazioni web, in particolare il prodotto WebMatrix sponsorizzato da Microsoft, disponibile all'URL [http://www.asp.net/webmatrix]. Si tratta di uno strumento eccellente per avvicinarsi alla programmazione web senza alcun investimento iniziale.

10.4. Un servizio web per le operazioni

Consideriamo un servizio web che offre cinque funzioni:

  1. add(a,b), che restituisce a+b
  2. subtract(a,b), che restituisce a-b
  3. multiply(a,b), che restituisce a*b
  4. divide(a,b), che restituisce a/b
  5. doAll(a,b), che restituisce l'array [a+b, a-b, a*b, a/b]

Il codice VB.NET per questo servizio è il seguente:


<%@ 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 

Ripetiamo qui alcune spiegazioni già fornite in precedenza, ma che vale la pena riprendere o approfondire. La classe delle operazioni assomiglia a una classe VB.NET, con alcuni punti da notare:

  • i metodi sono preceduti da un attributo <WebMethod()> che indica al compilatore quali metodi devono essere "pubblicati", ovvero resi disponibili al client. Un metodo non preceduto da questo attributo risulterebbe invisibile ai client remoti. Potrebbe trattarsi di un metodo interno utilizzato da altri metodi ma non destinato alla pubblicazione.
  • La classe deriva dalla classe WebService definita nello spazio dei nomi System.Web.Services. Questa ereditarietà non è sempre obbligatoria. In questo esempio, in particolare, potremmo farne a meno.
  • La classe stessa è preceduta da un attributo <WebService(Namespace="st.istia.univ-angers.fr")> destinato a fornire uno spazio dei nomi per il servizio web. Un fornitore di classi assegna uno spazio dei nomi alle proprie classi per dare loro un nome univoco ed evitare così conflitti con classi di altri fornitori che potrebbero avere lo stesso nome. Lo stesso vale per i servizi web. Ogni servizio web deve essere identificabile tramite un nome univoco, in questo caso st.istia.univ-angers.fr.
  • Non abbiamo definito un costruttore. Pertanto, verrà utilizzato implicitamente il costruttore della classe padre.

Il codice sorgente sopra riportato non è destinato direttamente al compilatore VB.NET, ma al server web IIS. Deve avere l'estensione .asmx ed essere salvato nella struttura delle directory del server web. Qui lo salviamo come operations.asmx nella cartella <IISroot>\polyvbnet\operations:

Image

Associamo la directory virtuale IIS [operations] a questa directory fisica:

Accediamo al servizio utilizzando un browser. L'URL da richiedere è [http://localhost/operations/operations.asmx]:

Image

Otteniamo una pagina web con un link per ciascuno dei metodi definiti nel servizio web delle operazioni. Seguiamo il link "Aggiungi":

Image

La pagina che appare ci invita a provare il metodo add fornendo i due argomenti a e b richiesti. Ricordiamo la definizione del metodo *add*:

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

Si noti che la pagina ha utilizzato i nomi degli argomenti a e b dalla definizione del metodo. Fare clic sul pulsante Chiama e la seguente risposta apparirà in una finestra separata del browser:

Image

Se si seleziona [Visualizza/Sorgente] in alto, si ottiene il codice seguente:

Image

Ripetiamo la procedura per il metodo [toutfaire]:

Image

Otteniamo la seguente pagina:

Image

Utilizziamo il pulsante [Chiama] in alto:

Image

In tutti i casi, la risposta del server ha il seguente formato:

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • la risposta è in formato XML
  • La riga 1 è standard ed è sempre presente nella risposta
  • Le righe seguenti dipendono dal tipo di risultato (double, ArrayOfDouble), dal numero di risultati e dallo spazio dei nomi del servizio web (st.istia.univ-angers.fr in questo caso).

Esistono diversi metodi per interrogare un servizio web e ottenere la sua risposta. Torniamo all'URL del servizio:

Image

e seguiamo il link [Add]. Nella pagina che appare, vengono presentati due metodi per interrogare la funzione [Add] del servizio web:

Image

Image

Image

Questi due metodi per accedere alle funzioni di un servizio web sono denominati, rispettivamente: HTTP-POST e SOAP. Li esamineremo ora uno per uno.

Nota: nelle prime versioni di VS.NET era presente un terzo metodo denominato HTTP-GET. Alla data di questo documento (marzo 2004), tale metodo non sembra più essere disponibile. Ciò significa che il servizio web generato da VS.NET non accetta richieste GET. Ciò non significa che non sia possibile scrivere servizi web che accettino richieste GET, in particolare utilizzando strumenti diversi da VS.NET o semplicemente a mano.

10.5. Un client HTTP-POST

Seguiremo il metodo proposto dal servizio web:

Image

Analizziamo ciò che è scritto. Innanzitutto, il client web deve inviare le seguenti intestazioni HTTP:

POST /operations/operations.asmx/add HTTP/1.1
Il client web effettua una richiesta POST all'URL /operations/operations.asmx/add secondo il protocollo HTTP versione 1.1
HOST: localhost
Specifichiamo la macchina di destinazione della richiesta. In questo caso, localhost. Questa intestazione è stata resa obbligatoria dalla versione 1.1 del protocollo HTTP
Content-Type: application/x-www-form-urlencoded
Questo specifica che, dopo le intestazioni HTTP, verranno inviati parametri aggiuntivi in formato urlencoded. Questo formato sostituisce determinati caratteri con i loro codici esadecimali.
Content-Length: 7
Questo è il numero di caratteri della stringa del parametro che verrà inviata dopo le intestazioni HTTP.

Le intestazioni HTTP sono seguite da una riga vuota, poi dalla stringa dei parametri POST composta da [Content-Length] caratteri nella forma a=XX&b=YY, dove XX e YY sono le stringhe "codificate in URL" dei valori dei parametri a e b. Abbiamo già le conoscenze necessarie per riprodurre quanto sopra con il nostro client TCP generico già utilizzato nel capitolo sulla programmazione TCP/IP:

  • Avviamo IIS
  • il servizio è disponibile all'URL [http://localhost/operations/operations.asmx]
  • utilizziamo il client TCP generico in una finestra 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]

Innanzitutto, si noti che abbiamo aggiunto l'intestazione [Connection: close] per indicare al server di chiudere la connessione dopo aver inviato la risposta. Ciò è necessario in questo caso. Se non lo specifichiamo, per impostazione predefinita il server manterrà aperta la connessione. Tuttavia, la sua risposta è una sequenza di righe di testo, l'ultima delle quali non è terminata da un carattere di fine riga. Risulta che il nostro client TCP generico legga le righe di testo terminate da un carattere di fine riga utilizzando il metodo ReadLine. Se il server non chiude la connessione dopo aver inviato l'ultima riga, il client viene bloccato perché è in attesa di un carattere di fine riga che non arriva mai. Se il server chiude la connessione, il metodo ReadLine del client viene completato e il client non viene bloccato.

Subito dopo aver ricevuto la riga vuota che segnala la fine delle intestazioni HTTP, il server IIS invia una risposta iniziale:

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--

Questa risposta, costituita esclusivamente da intestazioni HTTP, comunica al client che può inviare i 7 caratteri che aveva dichiarato di voler inviare. Cosa facciamo:

a=2&b=3

Si noti che il nostro client TCP invia più di 7 caratteri in questo caso, poiché li invia con un marcatore di fine riga (WriteLine). Ciò non interferisce con il server, che prenderà solo i primi 7 caratteri tra quelli ricevuti, e poiché la connessione viene poi chiusa (Connection: close). Questi caratteri extra sarebbero stati problematici se la connessione fosse rimasta aperta, poiché sarebbero stati interpretati come provenienti dal comando successivo del client. Una volta ricevuti i parametri, il server invia la sua risposta:

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

Ora disponiamo degli elementi necessari per scrivere un programma client per il nostro servizio web. Si tratterà di un client da console denominato httpPost2 e utilizzato come segue:


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>

Il client viene chiamato passando l'URL del servizio web:

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

Successivamente, il client legge i comandi digitati sulla tastiera e li esegue. Questi hanno il seguente formato:

fonction a b

dove function è la funzione del servizio web che viene chiamata (add, subtract, multiply, divide) e a e b sono i valori su cui questa funzione opererà. Ad esempio:

ajouter 6 7

A questo punto, il client invierà la richiesta HTTP necessaria al server web e riceverà una risposta. Gli scambi client-server vengono visualizzati sullo schermo per aiutarti a comprendere meglio il processo:

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]

Lo scambio mostrato sopra è lo stesso che abbiamo visto con il client TCP generico, con una differenza: l'intestazione HTTP **Connection: Keep-Alive indica al server di non chiudere la connessione. La connessione rimane quindi aperta per l'operazione successiva del client, che non ha quindi bisogno di riconnettersi al server. Tuttavia, ciò richiede che il client utilizzi un metodo diverso da ReadLine() per leggere la risposta del server, poiché sappiamo che la risposta è costituita da una sequenza di righe, l'ultima delle quali non è terminata da un carattere di nuova riga. Una volta ricevuta l'intera risposta del server, il client la analizza per trovare il risultato dell'operazione richiesta e visualizzarlo:

[résultat=13]

Esaminiamo il codice del nostro client:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web
 
' web operations client
Public Module clientPOST
 
    Public Sub Main(ByVal args() As String)
        ' syntax
        Const syntaxe As String = "pg URI"
        Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
 
        ' number of arguments
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' note the URI required
        Dim URIstring As String = args(0)
 
        ' connect to the server
        Dim uri As Uri = Nothing        ' the URI of the web service
        Dim client As TcpClient = Nothing        ' the client's tcp link with the server
        Dim [IN] As StreamReader = Nothing        ' the customer's reading flow
        Dim OUT As StreamWriter = Nothing        ' the customer's writing flow
        Try
            ' server connection
            uri = New Uri(URIstring)
            client = New TcpClient(uri.Host, uri.Port)
            ' create customer input/output flows TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
        Catch ex As Exception
            ' URI incorrect or other problem
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try
 
        ' creation of a dictionary of web service functions
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
 
        ' user requests are typed on the keyboard
        ' as function a b
        ' they are terminated with the command fin
        Dim commande As String = Nothing        ' keyboard command
        Dim champs As String() = Nothing        ' command line fields
        Dim fonction As String = Nothing        ' name of a web service function
        Dim a, b As String        ' web service function arguments
 
        ' invites the user
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")
 
        ' error management
        Dim erreurCommande As Boolean
        Try
            ' keyboard command input loop
            While True
                ' no error at start
                erreurCommande = False
                ' read command
                commande = Console.In.ReadLine().Trim().ToLower()
                ' finished?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' breaking down the order into fields
                champs = Regex.Split(commande, "\s+")
                Try
                    ' three fields are required
                    If champs.Length <> 3 Then
                        Throw New Exception
                    End If
                    ' field 0 must be a recognized function
                    fonction = champs(0)
                    If Not dicoFonctions.ContainsKey(fonction) Then
                        Throw New Exception
                    End If
                    ' field 1 must be a valid number
                    a = champs(1)
                    Double.Parse(a)
                    ' field 2 must be a valid number
                    b = champs(2)
                    Double.Parse(b)
                Catch
                    ' invalid order
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    erreurCommande = True
                End Try
                ' request the web service
                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
        ' end of client-server link
        Try
            [IN].Close()
            OUT.Close()
            client.Close()
        Catch
        End Try
    End Sub
...........
    ' error display
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Module

Abbiamo già visto questi elementi diverse volte in precedenza e non richiedono particolari commenti. Esaminiamo ora il codice del metodo executeFonction, dove si trovano i nuovi elementi:


    ' 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)
        ' executes function(a,b) on the URI uri web service
        ' client-server exchanges take place via IN and OUT flows
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server
 
        ' query chain construction
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length
 
        ' construction of header table HTTP to be sent
        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) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i
 
        ' we read the 1st web server response HTTP/1.1 100
        Dim ligne As String = Nothing
        ' a line in the read stream
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' next line
            ligne = [IN].ReadLine()
        End While
        'last line echo
        Console.Out.WriteLine(("<-- " + ligne))
 
        ' send request parameters
        OUT.Write(requête)
        ' echo
        Console.Out.WriteLine(("--> " + requête))
 
        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        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
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length ?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While
        ' last line echo
        Console.Out.WriteLine("<--")
 
        ' build the regular expression to retrieve the result
        ' in the web server response stream
        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
 
        ' we read the rest of the web server response
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)
 
        ' the answer is broken down into lines of text
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
 
        ' scroll through the lines of text looking for the result
        Dim strRésultat As String = "?"        ' function result
        For i = 0 To lignes.Length - 1
            ' follow-up
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' compare current line to model
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' have we found?
            If MatchRésultat.Success Then
                ' we note the result
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' the result is displayed
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

In primo luogo, il client HTTP-POST invia la sua richiesta in formato POST:


        ' query chain construction
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length
 
        ' construction of header table HTTP to be sent
        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) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

Nell'intestazione

--> Content-Length: 7

dobbiamo specificare la dimensione dei parametri che saranno inviati dal client dietro le intestazioni HTTP:

--> a=6&b=7

Per farlo, utilizzare il seguente codice:


        ' query chain construction
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

Il metodo HttpUtility.UrlEncode(string string) converte determinati caratteri nella stringa in %n1n2, dove n1n2 è il codice ASCII del carattere convertito. I caratteri interessati da questa conversione sono tutti quelli che hanno un significato specifico in una richiesta POST (spazio, =, &, ecc.). In questo caso, il metodo HttpUtility.UrlEncode non è normalmente necessario poiché a e b sono numeri che non contengono nessuno di questi caratteri speciali. Viene utilizzato qui a titolo di esempio. Richiede il namespace System.Web. Una volta che il client ha inviato le sue intestazioni 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
-->

Il server risponde con l'intestazione 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
<--

Il codice si limita a leggere e visualizzare questa prima risposta sullo schermo:


        ' we read the 1st web server response HTTP/1.1 100
        Dim ligne As String = Nothing
        ' a line in the read stream
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' next line
            ligne = [IN].ReadLine()
        End While
        'last line echo
        Console.Out.WriteLine(("<-- " + ligne))

Una volta letta questa risposta iniziale, il client deve inviare i propri parametri:

--> a=6&b=7

Lo fa con il seguente codice:


        ' envoi paramètres de la requête
        OUT.Write(requête)
        ' echo
        Console.Out.WriteLine(("--> " + requête))

Il server invierà quindi la sua risposta. Questa risposta è composta da due parti:

  1. intestazioni HTTP seguite da una riga vuota
  2. la risposta in formato 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>

In primo luogo, il client legge le intestazioni HTTP per individuare la riga Content-Length e recuperare la dimensione della risposta XML (in questo caso, 90). Questa viene recuperata utilizzando un'espressione regolare. Avremmo potuto farlo in modo diverso e probabilmente più efficiente.


        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        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
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length ?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While
        ' last line echo
        Console.Out.WriteLine("<--")

Una volta ottenuta la lunghezza N della risposta XML, dobbiamo semplicemente leggere N caratteri dallo stream IN della risposta del server. Questa stringa di N caratteri viene suddivisa in righe di testo ai fini del monitoraggio sullo schermo. Tra queste righe, cerchiamo quella contenente il risultato:

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

utilizzando nuovamente un'espressione regolare. Una volta trovato il risultato, questo viene visualizzato.

[résultat=13]

La parte finale del codice client è la seguente:


        ' build the regular expression to retrieve the result
        ' in the web server response stream
        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
 
        ' we read the rest of the web server response
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)
 
        ' the answer is broken down into lines of text
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
 
        ' scroll through the lines of text looking for the result
        Dim strRésultat As String = "?"        ' function result
        For i = 0 To lignes.Length - 1
            ' follow-up
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' compare current line to model
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' have we found?
            If MatchRésultat.Success Then
                ' we note the result
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' the result is displayed
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.6. Un client SOAP

Qui esaminiamo un secondo client che utilizzerà un dialogo client-server SOAP (Simple Object Access Protocol). Viene presentato un esempio di tale dialogo per la funzione add:

Image

Image

La richiesta del client è una richiesta POST. Vedremo quindi alcuni degli stessi meccanismi del client precedente. La differenza principale è che mentre il client HTTP-POST inviava i parametri a e b nel modulo

    a=A&b=B

il client SOAP li invia in un formato XML più complesso:

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>

Riceve in cambio una risposta XML che è anche più complessa delle risposte viste in precedenza:

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>

Sebbene la richiesta e la risposta siano più complesse, si tratta effettivamente dello stesso meccanismo HTTP utilizzato dal client HTTP-POST. Il codice del client SOAP può quindi essere modellato su quello del client HTTP-POST. Ecco un esempio di esecuzione:

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]

Cambia solo il metodo executeFonction. Il client SOAP invia le intestazioni HTTP per la sua richiesta. Sono semplicemente un po' più complesse di quelle di 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"
-->

Il codice che li genera:


    ' 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)
        ' executes function(a,b) on the URI uri web service
        ' client-server exchanges take place via IN and OUT flows
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server
        ' construction of query string 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 of header table HTTP to be sent
        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) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

Una volta ricevuta questa richiesta, il server invia la sua prima risposta, che il client visualizza:

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Thu, 04 Mar 2004 07:28:29 GMT
<-- X-Powered-By: ASP.NET
<--

Il codice per leggere questa prima risposta è il seguente:


        ' we read the 1st web server response HTTP/1.1 100
        Dim ligne As String = Nothing
        ' a line in the read stream
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' next line
            ligne = [IN].ReadLine()
        End While        'while
        'last line echo
        Console.Out.WriteLine(("<-- " + ligne))

Il client invierà ora i propri parametri in formato XML in quello che viene chiamato un involucro 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>

Il codice:


        ' envoi paramètres de la requête
        OUT.Write(requêteSOAP)
        ' echo
        Console.Out.WriteLine(("--> " + requêteSOAP))

Il server invierà quindi la sua risposta finale:

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

Il client visualizza sullo schermo le intestazioni HTTP ricevute mentre cerca la riga Content-Length:


        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        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
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length ?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While        'while
        ' last line echo
        Console.Out.WriteLine("<--")

Una volta nota la dimensione N della risposta XML, il client legge N caratteri dal flusso di risposta del server, suddivide la stringa recuperata in righe di testo per visualizzarle sullo schermo, cerca il tag XML del risultato: <ajouterResult>7</ajouterResult> e lo visualizza:


        ' build the regular expression to retrieve the result
        ' in the web server response stream
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing
 
        ' we read the rest of the web server response
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)
 
        ' the answer is broken down into lines of text
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
 
        ' scroll through the lines of text looking for the result
        Dim strRésultat As String = "?"        ' function result
        For i = 0 To lignes.Length - 1
            ' follow-up
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' compare current line to model
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' have we found?
            If MatchRésultat.Success Then
                ' we note the result
                strRésultat = MatchRésultat.Groups(1).Value
            End If
            'next line
        Next i
        ' the result is displayed
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.7. Incapsulamento della comunicazione client-server

Immaginiamo che le operazioni del nostro servizio web siano utilizzate da varie applicazioni. Sarebbe utile fornire a queste applicazioni una classe che funga da interfaccia tra l'applicazione client e il servizio web, nascondendo la maggior parte della comunicazione di rete, che non è banale per la maggior parte degli sviluppatori. Ciò porterebbe alla seguente architettura:

L'applicazione client si rivolgerebbe all'interfaccia client-server per effettuare le sue richieste al servizio web. L'interfaccia gestirebbe tutte le comunicazioni di rete necessarie con il server e restituirebbe il risultato all'applicazione client. L'applicazione client non dovrebbe più occuparsi della comunicazione con il server, il che semplificherebbe notevolmente il suo sviluppo.

10.7.1. La classe di incapsulamento

Sulla base di quanto trattato nelle sezioni precedenti, ora abbiamo una buona comprensione della comunicazione di rete tra il client e il server. Abbiamo anche esaminato tre metodi. Abbiamo scelto di incapsulare il metodo SOAP. La classe è la seguente:


' namespaces
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic
 
' clientSOAP of the Web operations service
Public Class clientSOAP
 
    ' instance variables
    Private uri As uri = Nothing    ' the URI of the web service
    Private client As TcpClient = Nothing    ' the client's tcp link with the server
    Private [IN] As StreamReader = Nothing    ' the customer's reading flow
    Private OUT As StreamWriter = Nothing    ' the customer's writing flow
    ' function dictionary
    Private dicoFonctions As New Hashtable
    ' function list
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' verbose
    Private verbose As Boolean = False    ' to true, displays client-server exchanges on screen
 
    ' manufacturer
    Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)
 
        ' we note verbose
        Me.verbose = verbose
 
        ' server connection
        uri = New Uri(uriString)
        client = New TcpClient(uri.Host, uri.Port)
 
        ' create customer input/output flows TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True
 
        ' create a dictionary of web service functions
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
    End Sub
 
    ' close server connection
    Public Sub Close()
        ' end of client-server link
        [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
        ' executes function(a,b) on the URI uri web service
        ' client-server exchanges take place via IN and OUT flows
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server
 
        ' valid function?
        fonction = fonction.Trim().ToLower()
        If Not dicoFonctions.ContainsKey(fonction) Then
            Return "[fonction [" + fonction + "] indisponible : (ajouter, soustraire,multiplier,diviser)]"
        End If
 
        ' valid arguments a and b?
        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 by zero?
        If fonction = "diviser" And doubleB = 0 Then
            Return "[division par zéro]"
        End If
 
        ' construction of query string 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 of header table HTTP to be sent
        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) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            If verbose Then
                Console.Out.WriteLine(("--> " + entetes(i)))
            End If
        Next i
 
        ' we read the 1st web server response HTTP/1.1 100
        Dim ligne As String = Nothing
        ' a line in the read stream
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While
        'last line echo
        If verbose Then
            Console.Out.WriteLine(("<-- " + ligne))
        End If
        ' send request parameters
        OUT.Write(requêteSOAP)
        ' echo
        If verbose Then
            Console.Out.WriteLine(("--> " + requêteSOAP))
        End If
 
        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        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
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            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
            ' next line
            ligne = [IN].ReadLine()
        End While
 
        ' last line echo
        If verbose Then
            Console.Out.WriteLine("<--")
        End If
 
        ' build the regular expression to retrieve the result
        ' in the web server response stream
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing
 
        ' we read the rest of the web server response
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)
 
        ' the answer is broken down into lines of text
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)
 
        ' scroll through the lines of text looking for the result
        Dim strRésultat As String = "?"        ' function result
        For i = 0 To lignes.Length - 1
            ' follow-up
            If verbose Then
                Console.Out.WriteLine(("<-- " + lignes(i)))
            End If            ' compare current line to model
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' have we found?
            If MatchRésultat.Success Then
                ' we note the result
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
 
        ' return the result
        Return strRésultat
    End Function
End Class

Non c'è nulla di nuovo qui rispetto a ciò che abbiamo già visto. Abbiamo semplicemente preso il codice dal client SOAP che abbiamo studiato e lo abbiamo leggermente riorganizzato per trasformarlo in una classe. Questa classe ha un costruttore e due metodi:


    ' manufacturer
    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
 

    ' close server connection
    Public Sub Close()

e presenta i seguenti attributi:


    ' 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

Passiamo due parametri al costruttore:

  1. l'URI del servizio web a cui deve connettersi
  2. un parametro booleano verbose che, se vero, richiede che gli scambi di rete vengano visualizzati sullo schermo; in caso contrario, non verranno visualizzati.

Durante la costruzione vengono creati lo stream IN per la lettura di rete, lo stream OUT per la scrittura di rete e il dizionario delle funzioni gestite dal servizio. Una volta costruito l'oggetto, la connessione client-server è aperta e i suoi stream IN e OUT sono pronti per l'uso.

Il metodo Close chiude la connessione al server.

Il metodo ExecuteFonction è quello che abbiamo scritto per il client SOAP che abbiamo studiato, con alcune piccole differenze:

  1. I parametri `uri`, `IN` e `OUT`, che in precedenza venivano passati come parametri al metodo, non devono più essere passati, poiché ora sono attributi dell'istanza accessibili a tutti i metodi dell'istanza
  2. Il metodo ExecuteFonction, che in precedenza restituiva un tipo void e visualizzava il risultato della funzione sullo schermo, ora restituisce quel risultato, e quindi un tipo stringa.

In genere, un client utilizzerà la classe clientSOAP come segue:

  1. creando un oggetto clientSOAP che stabilirà la connessione al servizio web
  2. chiamando ripetutamente il metodo executeFonction
  3. chiudendo la connessione al servizio web utilizzando il metodo Close.

Esaminiamo un primo client.

10.7.2. Un client da console

Qui riprendiamo il client SOAP che abbiamo studiato quando la classe clientSOAP non esisteva ancora, e lo riprogettiamo in modo che ora utilizzi questa classe:


' namespaces
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic
 
Public Module testClientSoap
 
    ' requests the URI of the operations web service
    ' interactively executes keyboard commands
    Public Sub Main(ByVal args() As String)
        ' syntax
        Const syntaxe As String = "pg URI [verbose]"
 
        ' number of 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
        ' connect to the web service 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' connection error
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try
 
        ' user requests are typed on the keyboard
        ' in the form function a b - they end with the command fin
        Dim commande As String = Nothing        ' keyboard command
        Dim champs As String() = Nothing        ' command line fields
 
        ' invites the user
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b" + ControlChars.Lf)
 
        ' error management
        Dim erreurCommande As Boolean
        Try
            ' keyboard command input loop
            While True
                ' initially no error
                erreurCommande = False
                ' read command
                commande = Console.In.ReadLine().Trim().ToLower()
                ' finished?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' breaking down the order into fields
                champs = Regex.Split(commande, "\s+")
                ' three fields are required
                If champs.Length <> 3 Then
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    ' we note the error
                    erreurCommande = True
                End If
                ' make a request to the web service
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
                ' following request
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' end of client-server link
        Try
            client.Close()
        Catch
        End Try
    End Sub
 
    ' error display
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Module

Il client è ora notevolmente più semplice e non prevede alcuna comunicazione di rete. Il client accetta due parametri:

  1. l'URI delle operazioni del servizio web
  2. la parola chiave opzionale verbose. Se presente, gli scambi di rete verranno visualizzati sullo schermo.

Questi due parametri vengono utilizzati per costruire un oggetto clientSOAP che gestirà la comunicazione con il servizio web.


        ' connect to the web service 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' connection error
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

Una volta stabilita la connessione al servizio web, il client può inviare le proprie richieste. Queste vengono digitate sulla tastiera, analizzate e quindi inviate al server chiamando il metodo executeFonction dell'oggetto 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 viene compilata in un "assembly":

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'applicazione client testClientSoap viene quindi compilata utilizzando:

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

Ecco un esempio di esecuzione non dettagliata:

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

È possibile monitorare il traffico di rete richiedendo un'esecuzione "verbosa":

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

Ora creiamo un client grafico.

10.7.3. Un client grafico per Windows

Ora interrogeremo il nostro servizio web utilizzando un client grafico che utilizzerà anch'esso la classe clientSOAP. L'interfaccia grafica sarà la seguente:

I controlli sono i seguenti:

N.
Tipo
nome
ruolo
1
Casella di testo
txtURI
l'URI delle operazioni del servizio web
2
Pulsante
btnOpen
apre la connessione al servizio web
3
Pulsante
btnClose
chiude la connessione al servizio web
4
Casella combinata
cmbFunzioni
l'elenco delle funzioni (addizione, sottrazione, moltiplicazione, divisione)
5
Casella di testo
txtA
l'argomento delle funzioni
6
Casella di testo
txtB
argomento b delle funzioni
7
Casella di testo
txtResult
il risultato della funzione(a,b)
8
Pulsante
btnCalcola
avvia il calcolo della funzione(a,b)
9
Casella di testo
txtError
visualizza un messaggio relativo allo stato della connessione

Esistono alcuni vincoli operativi:

  • Il pulsante btnOpen è attivo solo se il campo txtURI non è vuoto e non è già aperta una connessione
  • Il pulsante btnClose è attivo solo quando è stata aperta una connessione al servizio web
  • Il pulsante btnCalculate è attivo solo quando è aperta una connessione e i campi txtA e txtB non sono vuoti
  • I campi txtResult e txtError hanno l'attributo ReadOnly impostato su true

Il client inizia aprendo la connessione al servizio web utilizzando il pulsante [Open]:

Image

Successivamente, l'utente può selezionare una funzione e i valori per a e b:

Image

Image

Image

Image

Image

Di seguito è riportato il codice dell'applicazione. Abbiamo omesso il codice del modulo, che in questo contesto non è rilevante.


'namespaces
Imports System
Imports System.Windows.Forms
 
' the form class
Public Class FormClientSOAP
    Inherits System.Windows.Forms.Form
 
    ' instance attributes
    Dim client As clientSOAP    ' client SOAP of the web operations service
 
#Region " Code généré par le Concepteur Windows Form "
 
    Public Sub New()
        MyBase.New()
        'This call is required by the Windows Form Designer.
        InitializeComponent()
        ' other initializations
        myInit()
    End Sub
 
    'The substituted method Disposes of the form to clean up the list of components.
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
    End Sub
 
...
 
    Private Sub InitializeComponent()
....
    End Sub
 
#End Region
 
 
    Private Sub myInit()
        ' init form
        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
        ' the content of the input field has changed - set the state of the open button
        btnOuvrir.Enabled = txtURI.Text.Trim <> ""
    End Sub
 
    Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
        ' request to open a connection with the web service
        Try
            ' creation of an object of type [clientSOAP]
            client = New clientSOAP(txtURI.Text.Trim, False)
            ' button status
            btnOuvrir.Enabled = False
            btnFermer.Enabled = True
            ' the URI can no longer be modified
            txtURI.ReadOnly = True
            ' customer status
            txtErreur.Text = "Liaison au service web ouverte"
        Catch ex As Exception
            ' there has been an error - it is displayed
            txtErreur.Text = ex.Message
        End Try
    End Sub
 
    Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
        ' closing the web service connection
        client.Close()
        ' button states
        btnOuvrir.Enabled = True
        btnFermer.Enabled = False
        ' URI
        txtURI.ReadOnly = False
        ' customer status
        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
        ' calculating a function f(a,b)
        ' delete the previous result
        txtRésultat.Text = ""
        Try
            txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
        Catch ex As Exception
            ' there has been a network error
            txtErreur.Text = ex.Message
            ' we close the link
            btnFermer_Click(Nothing, Nothing)
        End Try
    End Sub
 
    Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
        ' change in the value of 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
        ' change in the value of B
        txtA_TextChanged(Nothing, Nothing)
    End Sub
 
    ' main method
    Public Shared Sub main()
        Application.Run(New FormClientSOAP)
    End Sub
End Class

Ancora una volta, la classe clientSOAP nasconde tutti gli aspetti dell'applicazione relativi alla rete. L'applicazione è stata realizzata come segue:

  • L'assembly clientSOAP.dll contenente la classe clientSOAP è stato inserito nella cartella del progetto
  • L'interfaccia grafica clientsoapgui.vb è stata realizzata con VS.NET e poi compilata in una finestra 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'interfaccia grafica è stata quindi avviata tramite:

dos>clientsoapgui

10.8. Un client proxy

Ricapitoliamo ciò che abbiamo appena fatto. Abbiamo creato una classe intermedia che incapsula gli scambi di rete tra un client e un servizio web secondo lo schema riportato di seguito:

La piattaforma .NET porta questa logica un passo avanti. Una volta individuato il servizio Web a cui accedere, possiamo generare automaticamente la classe che fungerà da intermediario per accedere alle funzioni del servizio Web e nascondere l'intero livello di rete. Questa classe è denominata proxy per il servizio Web per il quale è stata generata.

Come si genera una classe proxy per un servizio Web? Un servizio Web è sempre accompagnato da un file di descrizione in formato XML. Se l'URI delle operazioni del nostro servizio Web è http://localhost/operations/operations.asmx, il suo file di descrizione è disponibile all'URL http://localhost/operations/operations.asmx?wsdl, come mostrato nella schermata seguente:

Image

Si tratta di un file XML che descrive in modo dettagliato tutte le funzioni del servizio web, indicando per ciascuna di esse il tipo e il numero di parametri, nonché il tipo del risultato. Questo file è denominato file WSDL del servizio poiché utilizza il linguaggio WSDL (Web Services Description Language). Da questo file è possibile generare una classe proxy utilizzando lo strumento 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

Lo strumento wsdl genera un file sorgente VB.NET (opzione /language=vb) che prende il nome dalla classe che implementa il servizio web, in questo caso operations. Esaminiamo una parte del codice generato:

'------------------------------------------------------------------------------
' <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

'
'This source code has been té automatically généré by 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
....

A prima vista questo codice può sembrare un po' complesso. Non è necessario comprenderne i dettagli per poterlo utilizzare. Esaminiamo innanzitutto la dichiarazione della classe:

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

La classe porta il nome delle operazioni del servizio Web per cui è stata creata. Deriva dalla classe SoapHttpClientProtocol:

Image

La nostra classe proxy ha un unico costruttore:

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

Il costruttore assegna l'URL del servizio web associato al proxy all'attributo url. La classe delle operazioni sopra riportata non definisce l'attributo url in sé. Esso viene ereditato dalla classe da cui deriva il proxy: System.Web.Services.Protocols.SoapHttpClientProtocol. Esaminiamo ora ciò che riguarda il metodo add:

    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

Possiamo notare che presenta la stessa firma del servizio Web delle operazioni, dove era definito come segue:

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

Il modo in cui questa classe comunica con il servizio web non è mostrato qui. Questa comunicazione è interamente gestita dalla classe padre System.Web.Services.Protocols.SoapHttpClientProtocol. Il proxy contiene solo ciò che lo distingue dagli altri proxy:

  • l'URL del servizio web associato
  • la definizione dei metodi del servizio associato.

Per utilizzare i metodi del servizio web delle operazioni, un client ha bisogno solo della classe proxy delle operazioni generata in precedenza. Compiliamo questa classe in un file di 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

Ora scriviamo un client da console. Viene chiamato senza parametri ed esegue le richieste digitate sulla tastiera:

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

Il codice del cliente è il seguente:


' namespaces
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
 
Public Module testClientProxy
 
    ' interactively executes keyboard commands
    ' and sends them to the web operations service
    Public Sub Main()
        ' there are no more arguments - the web service's URL is hard-coded in the proxy
 
        ' creation of a dictionary of web service functions
        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
 
        ' create a proxy operations object 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' connection error
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try
 
        ' user requests are typed on the keyboard
        ' in the form function a b - they end with the command fin
        Dim commande As String = Nothing        ' keyboard command
        Dim champs As String() = Nothing        ' command line fields
 
        ' invites the user
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)
 
        ' some local data
        Dim erreurCommande As Boolean
        Dim fonction As String
        Dim a, b As Double
        ' keyboard command input loop
        While True
            ' initially no error
            erreurCommande = False
            ' read command
            commande = Console.In.ReadLine().Trim().ToLower()
            ' finished?
            If commande Is Nothing Or commande = "fin" Then
                Exit While
            End If
            ' breaking down the order into fields
            champs = Regex.Split(commande, "\s+")
            Try
                ' three fields are required
                If champs.Length <> 3 Then
                    Throw New Exception
                End If
                ' field 0 must be a recognized function
                fonction = champs(0)
                If Not dicoFonctions.ContainsKey(fonction) Then
                    Throw New Exception
                End If
                ' field 1 must be a valid number
                a = Double.Parse(champs(1))
                ' field 2 must be a valid number
                b = Double.Parse(champs(2))
            Catch
                ' invalid order
                Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                erreurCommande = True
            End Try
            ' make a request to the web service
            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
 
    ' error display
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' error display
        System.Console.Error.WriteLine(msg)
        ' stop with error
        Environment.Exit(exitCode)
    End Sub
End Module

Stiamo esaminando solo il codice specifico per l'utilizzo della classe proxy. Innanzitutto, viene creato un oggetto operazioni proxy:


        ' create a proxy operations object 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' connection error
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

Le righe a e b vengono digitate sulla tastiera. Sulla base di queste informazioni, vengono chiamati i metodi proxy appropriati:


            ' make a request to the web service
            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

Qui ci troviamo per la prima volta di fronte all'operazione "all-in-one" che esegue tutte e quattro le operazioni. Finora era stata ignorata perché restituisce un array di numeri racchiuso in un wrapper XML, più complicato da gestire rispetto alle semplici risposte XML delle altre funzioni, che restituiscono un solo risultato. Qui, con la classe proxy, vediamo che l'uso del metodo all-in-one non è più complicato dell'uso degli altri metodi. L'applicazione è stata compilata in una finestra DOS come segue:

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. Configurazione di un servizio Web

Un servizio Web potrebbe richiedere informazioni di configurazione per l'inizializzazione corretta. Con IIS, queste informazioni possono essere inserite in un file denominato web.config, situato nella stessa cartella del servizio Web. Supponiamo di voler creare un servizio Web che richieda due informazioni per l'inizializzazione: un nome e un'età. Queste due informazioni possono essere inserite nel file web.config nel formato seguente:

<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
  <appSettings>
    <add key="nom" value="tintin"/>
    <add key="age" value="27"/>
  </appSettings>   
</configuration>

Le impostazioni di inizializzazione sono inserite in un contenitore XML:

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

Un parametro di inizializzazione denominato P con valore V verrà dichiarato con la riga:


        <add key="P" value="V"/>

In che modo il servizio web recupera queste informazioni? Quando IIS carica un servizio web, verifica se nella stessa cartella è presente un file web.config. In caso affermativo, lo legge. Il valore V di un parametro P viene ottenuto utilizzando l'istruzione:

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

dove ConfigurationSettings è una classe nel namespace System.Configuration.

Proviamo questa tecnica sul seguente servizio web:


<%@ WebService language="VB" class=personne %>
 
Imports System.Web.Services
imports System.Configuration
 
<WebService([Namespace] := "st.istia.univ-angers.fr")> _
Public Class personne
   Inherits WebService
 
   ' attributes
   Private nom As String
   Private age As Integer
 
   ' manufacturer
   Public Sub New()
      ' init attributes
      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 

Il servizio Web Person ha due attributi, name e age, che vengono inizializzati nel suo costruttore senza parametri utilizzando i valori letti dal file di configurazione web.config del servizio Person. Il file è il seguente:


<configuration>
    <appSettings>
        <add key="nom" value="tintin"/>
        <add key="age" value="27"/>
    </appSettings>
</configuration>

Il servizio web dispone anche di un <WebMethod> senza parametri che restituisce semplicemente gli attributi nome ed età. Il servizio è registrato nel file sorgente personne.asmx, che si trova insieme al suo file di configurazione nella cartella c:\inetpub\wwwroot\st\personne:

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

Associamo una cartella IIS virtuale denominata /config alla cartella fisica sopra indicata. Avviamo IIS, quindi utilizziamo un browser per richiedere l'URL http://localhost/config/personne.asmx per il servizio person:

Image

Segui il link per il metodo single id:

Image

Il metodo id non ha parametri. Utilizziamo il pulsante Call:

Image

Abbiamo recuperato con successo le informazioni memorizzate nel file web.config del servizio.

10.10. Il servizio Web per il calcolo delle imposte

Torneremo all'applicazione IMPOTS, ormai familiare. L'ultima volta che l'abbiamo utilizzata, l'abbiamo trasformata in un server remoto accessibile tramite Internet. Ora la trasformeremo in un servizio Web.

10.10.1. Il servizio web

Inizieremo con la classe impôt creata nel capitolo sui database, che è costruita a partire dalle informazioni contenute in un database ODBC:


' options
Option Strict On
Option Explicit On 
 
' namespaces
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
 
Public Class impôt
    ' data required for tax calculation
    ' come from an external source
    Private limites(), coeffR(), coeffN() As Decimal
 
    ' manufacturer
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' we check that the 3 tablaeux are the same size
        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
        ' it's good
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub
 
    ' builder 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception
        Dim connectString As String = "DSN=" + DSNimpots + ";"        ' base connection chain
        Dim impotsConn As OdbcConnection = Nothing        ' the connection
        Dim sqlCommand As OdbcCommand = Nothing        ' the SQL command
        ' the SELECT query
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' tables to retrieve data
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList
 
        ' attempt to access the database
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' create a command object
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' execute the query
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' Using the recovered table
        While myReader.Read()
            ' the data of the current line are put in the tables
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' freeing up resources
        myReader.Close()
        impotsConn.Close()
 
        ' dynamic tables are placed in static tables
        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
 
    ' tAX CALCULATION
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' calculating the number of shares
        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
        ' calculation of taxable income & family quota
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' tAX CALCULATION
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' return result
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

Nel servizio web è possibile utilizzare solo un costruttore senza parametri. Pertanto, il costruttore della classe sarà il seguente:


    ' manufacturer
    Public Sub New()
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception
 
        ' retrieve the service configuration parameters
        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")
 
        ' database operation
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' base connection chain

I cinque parametri del costruttore della classe precedente vengono ora letti dal file web.config del servizio. Il codice nel file sorgente impots.asmx è il seguente. Include la maggior parte del codice precedente. Abbiamo semplicemente racchiuso le parti di codice specifiche del servizio web:


<%@ WebService language="VB" class=impots %>
 
' creation of a tax web service
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
 
    ' data required for tax calculation
    ' come from an external source
    Private limites(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""
 
 
    ' manufacturer
    Public Sub New()
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception
 
        ' retrieve the service configuration parameters
        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")
 
        ' database operation
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' base connection chain
        Dim impotsConn As OdbcConnection = Nothing     ' the connection
        Dim sqlCommand As OdbcCommand = Nothing     ' the SQL command
        Dim myReader As OdbcDataReader     ' odbc data reader
 
        ' the SELECT query
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
 
        ' tables to retrieve data
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList
 
        ' attempt to access the database
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' create a command object
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' execute the query
            myReader = sqlCommand.ExecuteReader()
            ' Using the recovered table
            While myReader.Read()
                ' the data of the current line are put in the tables
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' freeing up resources
            myReader.Close()
            impotsConn.Close()
 
            ' dynamic tables are placed in static tables
            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
            ' it's good
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' error
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub
 
    ' tAX CALCULATION
    <WebMethod()> _
    Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' calculating the number of shares
        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
        ' calculation of taxable income & family quota
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' tAX CALCULATION
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' return result
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
 
    ' id
    <WebMethod()> _
    Function id() As String
        ' to see if everything is OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

Spieghiamo le poche modifiche apportate alla classe impots oltre a quelle necessarie per trasformarla in un servizio web:

  • La lettura del database nel costruttore potrebbe fallire. Abbiamo quindi aggiunto due attributi alla nostra classe e un metodo:
    • il booleano `OK` è vero se il database è stato letto, falso in caso contrario
    • La stringa `errMessage` contiene un messaggio di errore se non è stato possibile leggere il database.
    • Il metodo id senza parametri recupera i valori di questi due attributi.
  • Per gestire eventuali errori di accesso al database, la parte del codice del costruttore relativa a tale accesso è stata racchiusa in un blocco try-catch.

Il file web.config per la configurazione del servizio è il seguente:


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

Al primo tentativo di caricare il servizio impots, il compilatore ha segnalato di non riuscire a trovare lo spazio dei nomi Microsoft.Data.Odbc utilizzato nella direttiva:

Imports Microsoft.Data.Odbc

Dopo aver consultato la documentazione

  • è stata aggiunta una direttiva di compilazione al file web.config per specificare che deve essere utilizzato l'assembly Microsoft.Data.odbc
  • è stata inserita una copia del file microsoft.data.odbc.dll nella cartella bin del progetto. Questa cartella viene sistematicamente esaminata dal compilatore del servizio web quando cerca un “assembly”.

Altre soluzioni sembrano possibili ma non sono state prese in considerazione in questa sede. Il file di configurazione è quindi diventato:


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

Il contenuto della cartella impots\bin:

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

Il servizio e il relativo file di configurazione sono stati inseriti in impots:

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

La cartella fisica del servizio web è stata mappata alla cartella virtuale /impots in IIS. La pagina del servizio è quindi la seguente:

Image

Se si segue il link id:

Image

Se utilizzi il pulsante Chiama:

Image

Il risultato precedente mostra i valori degli attributi OK (true) ed errMessage (""). In questo esempio, il database è stato caricato correttamente. Non è sempre stato così, ed è per questo che abbiamo aggiunto il metodo id per accedere al messaggio di errore. L'errore era che il nome DSN del database era stato definito come DSN utente quando avrebbe dovuto essere definito come DSN di sistema. Questa distinzione viene effettuata nel Gestore sorgenti ODBC a 32 bit:

Torniamo alla pagina dei servizi:

Image

Clicchiamo sul link “Calcola”:

Image

Definiamo i parametri della chiamata ed eseguiamo la chiamata:

Image

Il risultato è corretto.

10.10.2. Generiamo il proxy per il servizio impots

Ora che disponiamo di un servizio web impots funzionante, possiamo generare la relativa classe proxy. Ricordiamo che questa verrà utilizzata dalle applicazioni client per accedere al servizio web impots in modo trasparente. Innanzitutto, utilizziamo l'utilità wsdl per generare il file sorgente della classe proxy, che verrà poi compilato in una 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. Utilizzo del proxy con un client

Nel capitolo sui database, abbiamo creato un'applicazione console per calcolare le imposte:

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

Il programma testimpots utilizzava quindi la classe fiscale standard contenuta nel file impots.dll. Il codice del programma testimpots.vb era il seguente:


Option Explicit On 
Option Strict On
 
' namespaces
Imports System
Imports Microsoft.VisualBasic
 
' test pg
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' interactive tax calculator
        ' the user enters three data points on the keyboard: married nbEnfants salary
        ' the program then displays the tax payable
        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"
 
        ' checking program parameters
        If arguments.Length <> 5 Then
            ' error msg
            Console.Error.WriteLine(syntaxe1)
            ' end
            Environment.Exit(1)
        End If        'if
        ' retrieve the 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)
 
        ' tax object creation
        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
 
        ' infinite loop
        While True
            ' initially no errors
            Dim erreur As Boolean = False
 
            ' tax calculation parameters are requested
            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()
 
            ' anything to do?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
 
            ' check the number of arguments in the input line
            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
                ' checking the validity of parameters
                ' married
                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
                ' salary
                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
                ' parameters are correct - tax is calculated
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

Useremo lo stesso programma per farlo ora utilizzare il servizio web impots tramite la classe proxy impots creata in precedenza. Dobbiamo modificare leggermente il codice:

  • mentre la classe tax originale aveva un costruttore con cinque argomenti, la classe proxy tax ha un costruttore senza parametri. Come abbiamo visto, i cinque parametri sono ora impostati nel file di configurazione del servizio web.
  • Pertanto, non è più necessario passare questi cinque parametri come argomenti al programma di test

Il nuovo codice è il seguente:


Imports System
Imports Microsoft.VisualBasic
 
' test pg
Module testimpots
 
    Public Sub Main(ByVal arguments() As String)
        ' interactive tax calculator
        ' the user enters three data points on the keyboard: married nbEnfants salary
        ' the program then displays the tax payable
        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"
 
        ' tax object creation
        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
 
        ' infinite loop
        Dim erreur As Boolean
        While True
            ' initially no error
            erreur = False
            ' tax calculation parameters are requested
            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()
            ' anything to do?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' check the number of arguments in the input line
            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
                ' checking parameter validity
                ' married
                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
                ' salary
                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
                ' if the parameters are correct - the tax is calculated
                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

Abbiamo il file proxy impots.dll e il codice sorgente di testimpots nella stessa cartella.

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

Compiliamo il file sorgente 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

quindi eseguirlo:

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

Otteniamo il risultato previsto.