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:

mentre Netscape Navigator visualizzerà:

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

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

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:

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:

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:

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:

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

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

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

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:

Apriamolo con un editor di testo (Blocco note o altro). Otteniamo il seguente contenuto:
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:

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

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:

Abbiamo intenzionalmente troncato la pagina risultante per mantenere concisa la nostra dimostrazione. Notate nuovamente l'URL:
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:

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):
Ora creiamo il file demo2.asmx. Questo è il file che verrà richiamato dai client web. Il suo contenuto è il seguente:
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:

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

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

Utilizziamo il pulsante [Call] in alto:

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:
Possiamo notare che il codice sorgente del servizio si trova ora direttamente nel file sorgente demo3.asmx. La direttiva
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:

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

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

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:

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:

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

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:

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

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:
- add(a,b), che restituisce a+b
- subtract(a,b), che restituisce a-b
- multiply(a,b), che restituisce a*b
- divide(a,b), che restituisce a/b
- 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:

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

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

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

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

Ripetiamo la procedura per il metodo [toutfaire]:

Otteniamo la seguente pagina:

Utilizziamo il pulsante [Chiama] in alto:

In tutti i casi, la risposta del server ha il seguente formato:
- 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:

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



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:

Analizziamo ciò che è scritto. Innanzitutto, il client web deve inviare le seguenti intestazioni HTTP:
Il client web effettua una richiesta POST all'URL /operations/operations.asmx/add secondo il protocollo HTTP versione 1.1 | |
Specifichiamo la macchina di destinazione della richiesta. In questo caso, localhost. Questa intestazione è stata resa obbligatoria dalla versione 1.1 del protocollo HTTP | |
Questo specifica che, dopo le intestazioni HTTP, verranno inviati parametri aggiuntivi in formato urlencoded. Questo formato sostituisce determinati caratteri con i loro codici esadecimali. | |
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:
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:
Successivamente, il client legge i comandi digitati sulla tastiera e li esegue. Questi hanno il seguente formato:
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:
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:
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
dobbiamo specificare la dimensione dei parametri che saranno inviati dal client dietro le intestazioni HTTP:
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:
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:
- intestazioni HTTP seguite da una riga vuota
- 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:
utilizzando nuovamente un'espressione regolare. Una volta trovato il risultato, questo viene visualizzato.
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:


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
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:
- l'URI del servizio web a cui deve connettersi
- 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:
- 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
- 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:
- creando un oggetto clientSOAP che stabilirà la connessione al servizio web
- chiamando ripetutamente il metodo executeFonction
- 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:
- l'URI delle operazioni del servizio web
- 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 |
Casella di testo | txtURI | l'URI delle operazioni del servizio web | |
Pulsante | btnOpen | apre la connessione al servizio web | |
Pulsante | btnClose | chiude la connessione al servizio web | |
Casella combinata | cmbFunzioni | l'elenco delle funzioni (addizione, sottrazione, moltiplicazione, divisione) | |
Casella di testo | txtA | l'argomento delle funzioni | |
Casella di testo | txtB | argomento b delle funzioni | |
Casella di testo | txtResult | il risultato della funzione(a,b) | |
Pulsante | btnCalcola | avvia il calcolo della funzione(a,b) | |
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]:

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





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

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:
La classe porta il nome delle operazioni del servizio Web per cui è stata creata. Deriva dalla classe SoapHttpClientProtocol:

La nostra classe proxy ha un unico costruttore:
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:
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:
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:
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:
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:
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:

Segui il link per il metodo single id:

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

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:
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:
Il servizio e il relativo file di configurazione sono stati inseriti in impots:
La cartella fisica del servizio web è stata mappata alla cartella virtuale /impots in IIS. La pagina del servizio è quindi la seguente:

Se si segue il link id:

Se utilizzi il pulsante Chiama:

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:

Clicchiamo sul link “Calcola”:

Definiamo i parametri della chiamata ed eseguiamo la chiamata:

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.





