Skip to content

10. Webservices

10.1. Inleiding

In het vorige hoofdstuk hebben we verschillende TCP/IP-client-servertoepassingen besproken. Aangezien de clients en de server tekstregels uitwisselen, kunnen ze in elke willekeurige programmeertaal worden geschreven. De client moet alleen het communicatieprotocol kennen dat de server verwacht. Webservices zijn TCP/IP-servertoepassingen met de volgende kenmerken:

  • Ze worden gehost door webservers en het protocol voor de communicatie tussen client en server is daarom HTTP (HyperText Transport Protocol), een protocol dat bovenop TCP-IP is gebouwd.
  • De webservice heeft een standaardcommunicatieprotocol, ongeacht de aangeboden dienst. Een webservice biedt diverse diensten aan: S1, S2, …, Sn. Elk daarvan verwacht parameters die door de client worden aangeleverd en levert een resultaat terug aan de client. Voor elke dienst moet de client het volgende weten:
    • de exacte naam van de dienst, indien
    • de lijst met parameters die moeten worden doorgegeven en het type daarvan
    • het type resultaat dat door de service wordt teruggestuurd

Zodra deze elementen bekend zijn, verloopt de dialoog tussen client en server volgens hetzelfde patroon, ongeacht de opgevraagde webservice. Het schrijven van de clients is hierdoor gestandaardiseerd.

  • Om veiligheidsredenen tegen aanvallen via het internet beschikken veel organisaties over privénetwerken en stellen ze slechts bepaalde poorten van hun servers open voor het internet: voornamelijk poort 80 van de webservice. Alle andere poorten zijn geblokkeerd. Daarom worden client-servertoepassingen zoals beschreven in het vorige hoofdstuk binnen het privénetwerk (intranet) gebouwd en zijn ze over het algemeen niet van buitenaf toegankelijk. Door een dienst op een webserver te hosten, wordt deze toegankelijk voor de hele internetgemeenschap.
  • De webservice kan worden gemodelleerd als een object op afstand. De aangeboden diensten worden dan methoden van dit object. Een client kan toegang krijgen tot dit object op afstand alsof het lokaal is. Hierdoor wordt het gehele netwerkcommunicatiegedeelte verborgen en kan een client worden gebouwd die onafhankelijk is van deze laag. Als deze laag verandert, hoeft de client niet te worden aangepast. Dit is een enorm voordeel en waarschijnlijk het belangrijkste pluspunt van webservices.
  • Net als bij de TCP/IP-client-servertoepassingen die in het vorige hoofdstuk zijn besproken, kunnen de client en de server in elke willekeurige programmeertaal worden geschreven. Ze wisselen tekstregels uit. Deze bestaan uit twee delen:
    • de headers die nodig zijn voor het protocol HTTP
    • de hoofdtekst van het bericht. Bij een antwoord van de server aan de client heeft dit de indeling XML (eXtensible Markup Language). Bij een verzoek van de client aan de server kan de hoofdtekst van het bericht verschillende vormen aannemen, waaronder XML. Het verzoek XML van de client kan een specifieke indeling hebben, namelijk SOAP (Simple Object Access Protocol). In dat geval volgt het antwoord van de server eveneens de indeling SOAP.

10.2. Browsers en XML

Webservices sturen XML naar hun klanten. Browsers kunnen verschillend reageren op de ontvangst van deze XML-stream. Internet Explorer heeft een vooraf gedefinieerd stylesheet waarmee dit kan worden weergegeven. Netscape Communicator beschikt niet over dit stylesheet en geeft de ontvangen XML-code niet weer. Je moet de broncode van de ontvangen pagina bekijken om toegang te krijgen tot de XML-code. Hier volgt een voorbeeld voor de volgende XML-code:

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

Internet Explorer geeft de volgende pagina weer:

Image

terwijl Netscape Navigator het volgende weergeeft:

Image

Als we de broncode van de door Netscape ontvangen pagina bekijken, krijgen we:

Image

Netscape heeft wel degelijk hetzelfde ontvangen als Internet Explorer, maar heeft het anders weergegeven. In het vervolg zullen we Internet Explorer gebruiken voor de schermafbeeldingen.

10.3. Een eerste webservice

We gaan webservices ontdekken aan de hand van een heel eenvoudig voorbeeld dat in drie versies wordt uitgewerkt.

10.3.1. Versie 1

Voor deze eerste versie gebruiken we VS.NET, dat het voordeel biedt dat het een direct werkend raamwerk voor een webservice kan genereren. Zodra we deze architectuur begrijpen, kunnen we op eigen kracht verdergaan. Dat is het doel van de volgende versies.

Laten we met VS.NET een nieuw project opzetten met de optie [Fichier/Nouveau/Projet]:

Image

Let op de volgende punten:

  • het projecttype is Visual Basic (linkervenster)
  • het projectmodel is Webservice ASP.NET (rechtervenster)
  • de locatie is vrij te kiezen. In dit geval wordt de webservice gehost door een lokale webserver IIS. Het adres wordt dan http://localhost/[chemin], waarbij [chemin] nog moet worden gedefinieerd. Hier kiezen we het pad http://localhost/polyvbnet/demo. VS.NET maakt dan een map aan voor dit project. Waar? De server IIS heeft een root voor de boomstructuur van de webdocumenten die hij levert. Laten we deze root <IISroot> noemen. Deze komt overeen met de URL http://localhost. Hieruit volgt dat de http://localhost/polyvbnet/demo van URL gekoppeld zal worden aan de map <IISroot>/polyvbnet/demo. <IISroot> is normaal gesproken de map \inetpub\wwwroot op de schijf waarop IIS is geïnstalleerd. In ons voorbeeld is dat schijf E. De map die door VS.NET is aangemaakt, is dus de map e:\inetpub\wwwroot\polyvbnet\demo:

Image

Zoals altijd zijn er weer veel te veel mappen aangemaakt. Deze zijn niet altijd even nuttig. We zullen alleen die mappen toelichten die we op een bepaald moment nodig hebben. VS.NET heeft een project aangemaakt:

Image

We vinden hier enkele bestanden terug die ook in de fysieke projectmap aanwezig zijn. Het meest interessant voor ons is het bestand met de extensie asmx. Dit is de extensie van webservices. Een webservice wordt door VS.NET beheerd als een Windows-toepassing, c.a.d: een toepassing met een grafische gebruikersinterface en code om deze te beheren. Daarom hebben we een ontwerpvenster:

Image

Een webservice heeft normaal gesproken geen grafische interface. Het is een object dat op afstand kan worden aangeroepen. Het beschikt over methoden die door applicaties worden aangeroepen. We zullen het dus beschouwen als een klassiek object, met als bijzonderheid dat het op afstand via het netwerk kan worden geïnstantieerd. Daarom zullen we het ontwerpvenster dat wordt weergegeven door VS.NET niet gebruiken. Laten we ons in plaats daarvan richten op de code van de service met behulp van de optie Weergave/Code:

Image

Er zijn een aantal punten die de aandacht verdienen:

  • het bestand heet Service1.asmx.vb en niet Service1.asmx. We komen straks terug op de inhoud van het bestand Service1.asmx.
  • we zien een codevenster dat vergelijkbaar is met het venster dat we hadden toen we Windows-toepassingen bouwden met VS.NET

De door VS.NET gegenereerde code is als volgt:


Imports System.Web.Services

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

#Regio "Code gegenereerd door de Web Services Designer"

    Public Sub New()
        MyBase.New()

        'Deze aanroep is vereist door de Web Services Designer.
        InitializeComponent()

        'Voeg uw initialisatiecode toe na de aanroep InitializeComponent()

    End Sub

    'Vereist door de Web Services Designer
    Private components As System.ComponentModel.IContainer

    'REMARQUE: de volgende procedure is vereist door de Web Services Designer
    'Deze kan worden gewijzigd met behulp van de Web Services Designer.  
    'Wijzig deze niet met behulp van de 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: deze procedure is vereist door de Web Services Designer
        'Wijzig dit niet met behulp van de code-editor.
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

#Einde regio

    ' EXEMPLE DE SERVICE WEB
    ' Het voorbeeld van de service HelloWorld() retourneert de tekenreeks Hello World.
    ' Om het project te genereren, mag u de volgende regels niet uitcommentariëren; sla het project vervolgens op en genereer het.
    ' Om deze webservice te testen, moet je ervoor zorgen dat het .asmx-bestand de startpagina is
    ' en druk op F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' End Function

End Class

Allereerst merken we op dat we hier een klasse hebben, de klasse Service1, die is afgeleid van de klasse WebService:

Public Class Service1
    Inherits System.Web.Services.WebService

Dit betekent dat we de naamruimte System.Web.Services moeten importeren:


Imports System.Web.Services

De declaratie van de klasse wordt voorafgegaan door een compilatieattribuut:


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

Het attribuut System.Web.Services.WebService() geeft aan dat de daaropvolgende klasse een webservice is. Dit attribuut ondersteunt verschillende parameters, waaronder een parameter met de naam NameSpace. Deze dient om de webservice in een naamruimte te plaatsen. Het is namelijk denkbaar dat er wereldwijd meerdere webservices met de naam meteo bestaan. We hebben een manier nodig om ze van elkaar te onderscheiden. Dat is mogelijk dankzij de naamruimte. De ene kan bijvoorbeeld [espacenom1].meteo heten en de andere [espacenom2].meteo. Dit is een concept dat vergelijkbaar is met de naamruimten van klassen. VS.NET heeft automatisch code gegenereerd en deze in een deel van de broncode geplaatst:

#Regio " Code gegenereerd door de Web Services Designer "

Als we deze code bekijken, zien we dezelfde code die de ontwerper genereerde bij het bouwen van Windows-toepassingen. Dit is code die we simpelweg kunnen verwijderen als we geen grafische interface hebben, wat bij webservices het geval is.

De les wordt afgesloten met een voorbeeld van hoe een webservice eruit zou kunnen zien:


#Einde Regio

    ' EXEMPLE DE SERVICE WEB
    ' Het voorbeeld van de service HelloWorld() retourneert de tekenreeks Hello World.
    ' Om het project te genereren, mag u de volgende regels niet uitcommentariëren; sla het project vervolgens op en genereer het.
    ' Om deze webservice te testen, moet je ervoor zorgen dat het .asmx-bestand de startpagina is
    ' en druk op F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' End Function

Op basis van het bovenstaande maken we de code opschoner, zodat deze er als volgt uitziet:


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

Nu wordt het wat duidelijker.

  • Een webservice is een klasse die afstamt van de klasse WebService
  • De klasse wordt gekwalificeerd door het attribuut <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. We plaatsen onze service dus in de naamruimte st.istia.univ-angers.fr.
  • De methoden van de klasse worden gekwalificeerd door een attribuut <WebMethod()>, wat aangeeft dat het gaat om een methode die op afstand via het netwerk kan worden aangeroepen

De klasse die onze webservice verzorgt, heet dus Bonjour en heeft één enkele methode die ook Bonjour heet en een tekenreeks retourneert. We zijn klaar voor een eerste test.

  • Laten we de webserver IIS starten, als dat nog niet is gebeurd
  • gebruiken we de optie Debuggen/Uitvoeren zonder debuggen. VS.NET

VS.NET zal vervolgens de gehele applicatie compileren, een browser starten (vaak Internet Explorer, indien aanwezig) en de URL http://localhost/polyvbnet/demo/Service1.asmx weergeven:

Image

Waarom de URL http://localhost/polyvbnet/demo/Service1.asmx? Omdat dit het enige .asmx-bestand in het project was:

Image

Als er meerdere .asmx-bestanden waren geweest, hadden we moeten aangeven welk bestand als eerste moest worden uitgevoerd. Dit doe je door met de rechtermuisknop op het betreffende .asmx-bestand te klikken en de optie [Définir comme page de démarrage] te selecteren.

Image

Misschien is het interessant om te weten wat er in het bestand service1.asmx staat. Met VS.NET hebben we namelijk aan het bestand service1.asmx.vb gewerkt en niet aan het bestand service1.asmx. Dit bestand bevindt zich in de projectmap:

Image

Laten we het openen met een teksteditor (Kladblok of iets dergelijks). We krijgen dan de volgende inhoud te zien:

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

Het bestand bevat een eenvoudige instructie voor de server IIS, waarin staat:

  • dat het om een webservice gaat (trefwoord WebService)
  • dat de programmeertaal van deze webservice Visual Basic is (Language="vb")
  • dat de broncode van deze klasse te vinden is in het bestand Service1.asmx.vb (Codebehind="Service1.asmx.vb")
  • dat de klasse die de service implementeert demo.Bonjour heet (Class="demo.Bonjour"). Merk op dat VS.NET de klasse Bonjour in de naamruimte demo heeft geplaatst, wat ook de naam van het project is.

Laten we teruggaan naar de pagina die te vinden is via de URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

Wie heeft de code HTML van de bovenstaande pagina geschreven? Wij niet, dat weten we zeker. Het is IIS, dat de webservices op een standaardmanier presenteert. Deze pagina biedt ons twee links. Laten we de eerste volgen, [Description du service]:

Image

Oeps... dat is nogal cryptisch XML. Laten we toch even letten op de URL

http://localhost/polyvbnet/demo/Service1.asmx?WSDL. Open een browser en typ deze URL rechtstreeks in. Je krijgt hetzelfde resultaat als eerder. We onthouden dus dat de URL http://serviceweb?WSDL toegang geeft tot de beschrijving XML van de webservice. Laten we teruggaan naar de startpagina en de link [Bonjour] volgen. Laten we niet vergeten dat Bonjour een methode van de webservice is. Als er meerdere methoden waren geweest, zouden ze hier allemaal zijn weergegeven. We krijgen de volgende nieuwe pagina te zien:

Image

We hebben de verkregen pagina bewust ingekort om onze demonstratie overzichtelijk te houden. Laten we nogmaals kijken naar de verkregen URL:

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

Als we deze URL rechtstreeks in een browser invoeren, krijgen we hetzelfde resultaat als hierboven. We worden aangemoedigd om de knop [Appeler] te gebruiken. Laten we dat doen. We krijgen een nieuwe pagina te zien:

Image

Dit is weer XML. We vinden hier twee gegevens terug die in onze webservice aanwezig waren:

  • de naamruimte st.istia.univ-angers.fr van onze service

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
  • de waarde die door de Bonjour-methode wordt geretourneerd:

        Return "bonjour !"

Wat hebben we geleerd?

  • hoe je een S-webservice schrijft
  • hoe we deze kunnen aanroepen

We gaan nu kijken hoe we een webservice kunnen schrijven zonder gebruik te maken van VS.NET.

10.3.2. Versie 2

In het vorige voorbeeld heeft VS.NET veel dingen helemaal zelf gedaan. Is het mogelijk om een webservice te bouwen zonder deze tool? Het antwoord is ja, en dat laten we nu zien. Met een teksteditor bouwen we de volgende webservice:


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

De klasse heet Bonjour2 en heeft een methode die getBonjour heet. Deze is geplaatst in het bestand demo2.vb, dat zich op zijn beurt in de boomstructuur van de server IIS bevindt, in de map E:\Inetpub\wwwroot\polyvbnet\demo2. Het is een klassieke VB.NET-klasse die dus gecompileerd kan worden:

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

We plaatsen de assemblage demo2.dll in een map met de naam 'bin' (deze naam is verplicht):

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

We maken nu het bestand demo2.asmx aan. Dit bestand wordt door de webclients aangeroepen. De inhoud ervan is als volgt:

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

We zijn deze richtlijn al eens tegengekomen. Deze geeft aan dat:

  • de klasse van de webservice Bonjour2 heet en zich in de assembly demo2.dll bevindt. IIS zal op verschillende locaties naar deze assembly zoeken, met name in de map bin van de webservice. Daarom hebben we de assembly demo2.dll daar geplaatst.

Nu kunnen we verschillende tests uitvoeren. We controleren of IIS actief is en roepen met een browser de URL http://localhost/polyvbnet/demo2/demo2.asmx op:

Image

Vervolgens de URL http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Vervolgens de URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, waarbij getBonjour de naam is van de enige methode van onze webservice:

Image

We gebruiken de bovenstaande knop [Appeler]:

Image

We krijgen inderdaad het resultaat van de aanroep van de methode getBonjour van de webservice. We weten nu hoe we een webservice kunnen bouwen zonder vs.net. We zullen voortaan abstractie maken van de manier waarop de webservice is opgebouwd en ons alleen richten op de fundamentele bestanden.

10.3.3. Versie 3

De twee voorgaande versies van de webservice [Bonjour] maakten gebruik van twee bestanden:

  • een .asmx-bestand, het toegangspunt van de webservice
  • een .vb-bestand, de broncode van de webservice

We laten hier zien dat het .asmx-bestand op zichzelf volstaat. De code van de service demo3.asmx is als volgt:

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

Imports System.Web.Services

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

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

We zien dat de broncode van de webservice nu rechtstreeks in het bronbestand van demo3.asmx staat. De instructie

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

verwijst niet langer naar een klasse in een externe assembly, maar naar een klasse die zich in hetzelfde bronbestand bevindt. Laten we dit bestand in de map <IISroot>\polyvbnet\demo3 plaatsen:

Image

Laten we IIS starten en de URL http://localhost/polyvbnet/demo3/demo3.asmx opvragen:

Image

We zien een belangrijk verschil ten opzichte van de vorige versie: we hoefden de code VB van de service niet te compileren. IIS heeft deze compilatie zelf uitgevoerd via de compiler VB.NET die op dezelfde machine is geïnstalleerd. Vervolgens heeft het de pagina weergegeven. Als er een compilatiefout optreedt, wordt deze gemeld door IIS:

Image

10.3.4. Versie 4

We richten ons hier op de configuratie van de server IIS. Tot nu toe hebben we onze webservices altijd in de hoofdmap <IISroot> van de server IIS geplaatst, in dit geval [e:\inetpub\wwwroot]. We laten hier zien dat we de webservice overal kunnen plaatsen. Dit doen we met behulp van de virtuele mappen van IIS. Laten we onze service in de volgende map plaatsen:

Image

De map [D:\data\devel\vbnet\poly\chap9\demo3] bevindt zich niet in de mappenstructuur van de server IIS. We moeten dit aan de server doorgeven door een virtuele map IIS aan te maken. Laten we IIS starten en de onderstaande optie [Avancé] selecteren:

Image

Er wordt een lijst met virtuele mappen weergegeven. We zullen hier niet verder op ingaan. We maken een nieuwe virtuele map aan met de knop [Ajouter] hierboven:

Image

Met behulp van de knop [Parcourir] wijzen we de fysieke map aan die de webservice bevat, in dit geval de map [D:\data\devel\vbnet\poly\chap9\demo3]. We geven deze map een logische (virtuele) naam: [virdemo3]. Dit betekent dat de documenten in de fysieke map [D:\data\devel\vbnet\poly\chap9\demo3] via de URL [http://<machine>/virdemo3] op het netwerk toegankelijk zullen zijn. Het bovenstaande dialoogvenster bevat nog andere instellingen die we ongewijzigd laten. We bevestigen het dialoogvenster. De nieuwe virtuele map verschijnt in de lijst met virtuele mappen van IIS:

Image

Nu openen we een browser en roepen we de URL [http://localhost/virdemo3/demo3.asmx] op. We krijgen hetzelfde resultaat als eerder:

Image

10.3.5. Conclusie

We hebben verschillende manieren laten zien om een webservice te maken. Vervolgens zullen we de methode uit versie 3 gebruiken voor het aanmaken van de service en methode 4 voor de lokalisatie ervan. We hebben VS.NET dus niet nodig. Toch is het de moeite waard om VS.NET te gebruiken vanwege de hulp die het biedt bij het debuggen. Er bestaan gratis tools voor het ontwikkelen van webapplicaties, met name het door Microsoft gesponsorde product WebMatrix, dat te vinden is op URL [http://www.asp.net/webmatrix]. Dit is een uitstekende tool om zonder voorafgaande investering aan de slag te gaan met webprogrammering.

10.4. Een webservice voor bewerkingen

We bekijken een webservice die vijf functies biedt:

  1. toevoegen(a,b), die a+b oplevert
  2. aftrekken(a,b), wat a-b oplevert
  3. vermenigvuldigen(a,b), wat a*b oplevert
  4. delen(a,b), wat a/b oplevert
  5. allesdoen(a,b), wat de array [a+b,a-b,a*b,a/b] oplevert

De code VB.NET van deze dienst is als volgt:


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

We herhalen hier enkele uitleg die al is gegeven, maar die het waard is om nog eens te worden vermeld of aangevuld. De klasse operations lijkt op een klasse VB.NET, maar er zijn enkele punten waar je op moet letten:

  • de methoden worden voorafgegaan door een attribuut <WebMethod()> dat de compiler aangeeft welke methoden moeten worden "gepubliceerd" c.a.d. en beschikbaar moeten worden gesteld aan de client. Een methode die niet wordt voorafgegaan door dit attribuut, zou onzichtbaar zijn voor externe clients. Dit zou een interne methode kunnen zijn die door andere methoden wordt gebruikt, maar niet bedoeld is om te worden gepubliceerd.
  • De klasse is afgeleid van de klasse WebService, gedefinieerd in de naamruimte System.Web.Services. Deze overerving is niet altijd verplicht. In dit voorbeeld zou men er bijvoorbeeld zonder kunnen.
  • De klasse zelf wordt voorafgegaan door een attribuut <WebService(Namespace="st.istia.univ-angers.fr")> dat bedoeld is om de webservice een naamruimte te geven. Een leverancier van klassen kent een naamruimte toe aan zijn klassen om ze een unieke naam te geven en zo conflicten te voorkomen met klassen van andere leveranciers die dezelfde naam zouden kunnen dragen. Voor webservices geldt hetzelfde. Elke webservice moet kunnen worden geïdentificeerd aan de hand van een unieke naam, in dit geval st.istia.univ-angers.fr.
  • We hebben geen constructor gedefinieerd. Daarom wordt impliciet de constructor van de bovenliggende klasse gebruikt.

De bovenstaande broncode is niet rechtstreeks bedoeld voor de compiler VB.NET, maar voor de webserver IIS. De code moet het achtervoegsel .asmx dragen en worden opgeslagen in de mapstructuur van de webserver. Hier slaan we het op onder de naam operations.asmx in de map <IISroot>\polyvbnet\operations:

Image

Aan deze fysieke map koppelen we de virtuele map IIS [operations]:

Laten we de dienst openen met een browser. Het op te vragen URl is [http://localhost/operations/operations.asmx]:

Image

We krijgen een webpagina te zien met een link voor elk van de methoden die zijn gedefinieerd in de webservice operations. Laten we de link ajouter volgen:

Image

Op de pagina die we te zien krijgen, wordt ons voorgesteld om de methode ajouter te testen door de twee benodigde argumenten a en b in te voeren. Laten we de definitie van de methode ajouter nog eens bekijken:

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

Merk op dat de pagina de namen van de argumenten a en b heeft overgenomen die in de definitie van de methode worden gebruikt. We klikken op de knop Appeler en krijgen het volgende antwoord in een apart browservenster:

Image

Als we hierboven [Affichage/Source] invoeren, krijgen we de volgende code:

Image

Laten we de handeling herhalen voor de methode [toutfaire]:

Image

We krijgen de volgende pagina:

Image

Laten we de bovenstaande knop [Appeler] gebruiken:

Image

In alle gevallen heeft het antwoord van de server de volgende vorm:

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • het antwoord heeft de indeling XML
  • regel 1 is standaard en komt altijd voor in het antwoord
  • de volgende regels zijn afhankelijk van het type resultaat (double, ArrayOfDouble), het aantal resultaten en de naamruimte van de webservice (hier st.istia.univ-angers.fr).

Er zijn verschillende manieren om een webservice te benaderen en een antwoord te ontvangen. Laten we teruggaan naar de URL van de service:

Image

en volgen we de link [ajouter]. Op de weergegeven pagina worden twee methoden beschreven om de functie [ajouter] van de webservice op te roepen:

Image

Image

Image

Deze twee methoden voor toegang tot de functies van een webservice heten respectievelijk: HTTP-POST en SOAP. We zullen ze nu een voor een bekijken.

Opmerking: in de eerste versies van VS.NET bestond er een derde methode, genaamd HTTP-GET. Op het moment van schrijven van dit document (maart 2004) lijkt deze methode niet meer beschikbaar te zijn. Dit betekent dat de door VS.NET gegenereerde webservice geen GET-verzoeken accepteert. Dit betekent niet dat er geen webservices kunnen worden geschreven die GET-verzoeken accepteren, met name met andere tools dan VS.NET of gewoon met de hand.

10.5. Een HTTP-POST-client

We volgen de door de webservice voorgestelde methode:

Image

Laten we het beschreven proces toelichten. Allereerst moet de webclient de volgende HTTP-headers verzenden:

POST /operations/operations.asmx/ajouter HTTP/1.1
De webclient doet een verzoek POST aan de URL /operations/operations.asmx/toevoegen volgens het protocol HTTP versie 1.1
HOST: localhost
Hier wordt de doelmachine van het verzoek gespecificeerd. In dit geval is dat localhost. Deze header is verplicht gesteld door versie 1.1 van het protocol HTTP
Content-Type: application/x-www-form-urlencoded
Hier wordt aangegeven dat er na de headers HTTP extra parameters in urlencoded-formaat worden verzonden. In dit formaat worden bepaalde tekens vervangen door hun hexadecimale code.
Content-length: 7
Dit is het aantal tekens van de parameterreeks die na de headers HTTP wordt verzonden.

De kopteksten HTTP worden gevolgd door een lege regel en vervolgens door de parameterreeks van POST, bestaande uit [Content-Length] tekens in de vorm a=XX&b=YY, waarbij XX en YY de "url-gecodeerde" tekenreeksen zijn van de waarden van de parameters a en b. We weten nu genoeg om het bovenstaande na te bootsen met onze generieke TCP-client die we al hebben gebruikt in het hoofdstuk over TCP/IP-programmering:

  • we starten IIS
  • de service is beschikbaar via de URL [http://localhost/operations/operations.asmx]
  • we gebruiken de generieke TCP-client in een venster 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]

Laten we allereerst opmerken dat we de header [Connection: close] hebben toegevoegd om de server te vragen de verbinding te verbreken na het verzenden van het antwoord. Dit is hier noodzakelijk. Als we dit niet aangeven, houdt de server de verbinding standaard open. Het antwoord van de server bestaat echter uit een reeks tekstregels waarvan de laatste niet wordt afgesloten met een regeleinde. Onze generieke client TCP leest tekstregels die worden afgesloten met een regeleinde met behulp van de methode ReadLine. Als de server de verbinding niet verbreekt na het verzenden van de laatste regel, loopt de client vast omdat hij wacht op een regeleinde-teken dat niet komt. Als de server de verbinding verbreekt, wordt de methode ReadLine van de client beëindigd en loopt de client niet vast.

Onmiddellijk na ontvangst van de lege regel die het einde van de headers aangeeft (HTTP), verstuurt de server (IIS) een eerste antwoord:

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

Dit antwoord, dat uitsluitend bestaat uit de headers HTTP, geeft de klant aan dat hij de 7 tekens mag verzenden die hij zei te willen verzenden. Wat we doen:

a=2&b=3

Hier moet worden opgemerkt dat onze TCP-client meer dan 7 tekens verstuurt, aangezien hij deze samen met een regeleinde (WriteLine) verstuurt. Dit vormt geen probleem voor de server, die van de ontvangen tekens alleen de eerste 7 zal overnemen en omdat de verbinding daarna wordt gesloten (Connection: close). Deze overtollige tekens zouden wel een probleem zijn geweest als de verbinding open was gebleven, omdat ze dan zouden zijn geïnterpreteerd als afkomstig van het volgende commando van de client. Zodra de parameters zijn ontvangen, stuurt de server zijn antwoord:

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

We beschikken nu over de benodigde elementen om een client te schrijven die is geprogrammeerd voor onze webservice. Dit wordt een console-client met de naam httpPost2, die als volgt wordt gebruikt:

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>

De client wordt aangeroepen door de URL van de webservice door te geven:

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

Vervolgens leest de client de via het toetsenbord ingevoerde opdrachten en voert deze uit. Deze hebben de volgende indeling:

fonction a b

waarbij ‘functie’ de aangeroepen webservicefunctie is (optellen, aftrekken, vermenigvuldigen, delen) en ‘a’ en ‘b’ de waarden zijn waarop deze functie wordt toegepast. Bijvoorbeeld:

ajouter 6 7

Vanaf dat moment stuurt de client het benodigde verzoek HTTP naar de webserver en ontvangt een antwoord. De communicatie tussen client en server wordt op het scherm weergegeven om het proces beter te begrijpen:

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]

Hierboven zien we dezelfde uitwisseling als bij de generieke TCP-client, met één verschil: de header HTTP Connection: Keep-Alive vraagt de server om de verbinding niet te verbreken. De verbinding blijft dus open voor de volgende bewerking van de client, die daardoor niet opnieuw verbinding hoeft te maken met de server. Dit dwingt de client echter wel om een andere methode dan ReadLine() te gebruiken om het antwoord van de server te lezen, aangezien we weten dat dit een reeks regels is waarvan de laatste niet wordt afgesloten met een regeleinde. Zodra het volledige antwoord van de server is ontvangen, analyseert de client dit om het resultaat van de gevraagde bewerking te vinden en weer te geven:

[résultat=13]

Laten we de code van onze client eens bekijken:


' naamruimten
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web

' client van een webservice operations
Public Module clientPOST

    Public Sub Main(ByVal args() As String)
        ' syntaxis
        Const syntaxe As String = "pg URI"
        Dim fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}

        ' aantal argumenten
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' let op de gevraagde URI
        Dim URIstring As String = args(0)

        ' er wordt verbinding gemaakt met de server
        Dim uri As Uri = Nothing        ' l'URI du service web
        Dim client As TcpClient = Nothing        ' la liaison tcp du client avec le serveur
        Dim [IN] As StreamReader = Nothing        ' le flux de lecture du client
        Dim OUT As StreamWriter = Nothing        ' le flux d'écriture du client
        Try
            ' verbinding met de server
            uri = New Uri(URIstring)
            client = New TcpClient(uri.Host, uri.Port)
            ' de in- en uitgaande stromen van de client TCP worden aangemaakt
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
        Catch ex As Exception
            ' URI onjuist of ander probleem
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

        ' een woordenboek met de functies van de webservice aanmaken
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i

        ' de verzoeken van de gebruiker worden via het toetsenbord ingevoerd
        ' in de vorm functie a b
        ' ze eindigen met het commando 'fin'
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande
        Dim fonction As String = Nothing        ' nom d'une fonction du service web
        Dim a, b As String        ' les arguments des fonctions du service web

        ' vraagt de gebruiker
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")

        ' foutbeheer
        Dim erreurCommande As Boolean
        Try
            ' lus voor het invoeren van via het toetsenbord ingevoerde opdrachten
            While True
                ' geen fout bij het begin
                erreurCommande = False
                ' opdracht lezen
                commande = Console.In.ReadLine().Trim().ToLower()
                ' klaar?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' opdeling van de opdracht in velden
                champs = Regex.Split(commande, "\s+")
                Try
                    ' er zijn drie velden nodig
                    If champs.Length <> 3 Then
                        Throw New Exception
                    End If
                    ' veld 0 moet een erkende functie zijn
                    fonction = champs(0)
                    If Not dicoFonctions.ContainsKey(fonction) Then
                        Throw New Exception
                    End If
                    ' veld 1 moet een geldig getal zijn
                    a = champs(1)
                    Double.Parse(a)
                    ' veld 2 moet een geldig getal zijn
                    b = champs(2)
                    Double.Parse(b)
                Catch
                    ' ongeldig commando
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    erreurCommande = True
                End Try
                ' de aanvraag wordt naar de webservice verzonden
                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
        ' einde van de verbinding tussen client en server
        Try
            [IN].Close()
            OUT.Close()
            client.Close()
        Catch
        End Try
    End Sub
...........
    ' foutmeldingen weergeven
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' fout weergeven
        System.Console.Error.WriteLine(msg)
        ' afgesloten met fout
        Environment.Exit(exitCode)
    End Sub
End Module

Dit zijn zaken die we al meerdere keren zijn tegengekomen en die geen speciale toelichting behoeven. Laten we nu de code van de methode executeFonction bekijken, waar de nieuwe elementen zitten:


    ' 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)
        ' voert functie(a,b) uit op de webservice met URI URI
        ' de communicatie tussen client en server verloopt via de stromen IN en OUT
        ' het resultaat van de functie staat in de regel
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' verzonden door de server

        ' opbouw van de queryreeks
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' opbouw van de tabel met te verzenden headers HTTP
        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) = ""

        ' de headers HTTP worden naar de server verzonden
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' verzending naar de server
            OUT.WriteLine(entetes(i))
            ' schermweergave
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

        ' het eerste antwoord van de webserver wordt gelezen: HTTP/1.1 100
        Dim ligne As String = Nothing
        ' een regel uit de leesstroom
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' volgende regel
            ligne = [IN].ReadLine()
        End While
        'echo laatste regel
        Console.Out.WriteLine(("<-- " + ligne))

        ' verzoekparameters verzenden
        OUT.Write(requête)
        ' echo
        Console.Out.WriteLine(("--> " + requête))

        ' opstellen van de reguliere expressie om de grootte van het antwoord te bepalen XML
        ' in de responsstroom van de webserver
        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

        ' het tweede antwoord van de webserver lezen na het verzenden van het verzoek
        ' de waarde van de regel Content-Length wordt opgeslagen
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' schermuitvoer
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' volgende regel
            ligne = [IN].ReadLine()
        End While
        ' laatste regel weergeven
        Console.Out.WriteLine("<--")

        ' opstellen van de reguliere expressie om het resultaat te vinden
        ' in de responsstroom van de webserver
        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

        ' de rest van het antwoord van de webserver wordt gelezen
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' het antwoord wordt opgesplitst in tekstregels
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' de tekstregels worden doorlopen op zoek naar het resultaat
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' vervolg
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' de huidige regel wordt vergeleken met het sjabloon
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' is er iets gevonden?
            If MatchRésultat.Success Then
                ' het resultaat wordt genoteerd
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' het resultaat wordt weergegeven
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

Allereerst stuurt de client HTTP-POST zijn verzoek in het formaat POST:


        ' opbouw van de zoekopdracht
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' opbouw van de tabel met headers HTTP die moeten worden verzonden
        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) = ""

        ' de headers HTTP worden naar de server verzonden
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' verzending naar de server
            OUT.WriteLine(entetes(i))
            ' schermuitvoer
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

In de header

--> Content-Length: 7

moet de grootte worden aangegeven van de parameters die door de client achter de headers HTTP worden verzonden:

--> a=6&b=7

Hiervoor wordt de volgende code gebruikt:


        ' opbouw van de queryreeks
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

De methode HttpUtility.UrlEncode(string reeks) zet bepaalde tekens van chaîne om in %n1n2, waarbij n1n2 de code ASCII is van het omgezette teken. De tekens waarop deze transformatie betrekking heeft, zijn alle tekens die een specifieke betekenis hebben in een zoekopdracht POST (de spatie, het =-teken, het &-teken, ...). In dit geval is de methode HttpUtility.UrlEncode normaal gesproken overbodig, aangezien a en b getallen zijn die geen van deze speciale tekens bevatten. Ze wordt hier ter illustratie gebruikt. Deze methode heeft de naamruimte System.Web nodig. Zodra de client zijn headers HTTP heeft verzonden:

--> POST /operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->

de server antwoordt met de header 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
<--

De code leest alleen dit eerste antwoord en geeft het weer op het scherm:


        ' het eerste antwoord van de webserver wordt gelezen HTTP/1.1 100
        Dim ligne As String = Nothing
        ' een regel uit de leesstroom
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' volgende regel
            ligne = [IN].ReadLine()
        End While
        'echo laatste regel
        Console.Out.WriteLine(("<-- " + ligne))

Nadat deze eerste reactie is gelezen, moet de klant zijn parameters versturen:

--> a=6&b=7

Dat doet hij met de volgende code:

        ' verzoekparameters verzenden
        OUT.Write(requête)
        ' echo
        Console.Out.WriteLine(("--> " + requête))

De server stuurt vervolgens zijn antwoord. Dit bestaat uit twee delen:

  1. de headers HTTP, afgesloten met een lege regel
  2. het antwoord in het formaat 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 eerste instantie leest de client de headers HTTP om daar de regel Content-Length te vinden en de grootte van het antwoord XML (hier 90) op te halen. Deze wordt opgehaald met behulp van een reguliere expressie. Dit had ook anders gekund, en waarschijnlijk op een efficiëntere manier.


        ' opstellen van de reguliere expressie om de grootte van het antwoord te bepalen XML
        ' in de responsstroom van de webserver
        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

        ' het tweede antwoord van de webserver lezen na het verzenden van het verzoek
        ' de waarde van de regel Content-Length wordt opgeslagen
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' schermuitvoer
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' volgende regel
            ligne = [IN].ReadLine()
        End While
        ' laatste regel weergeven
        Console.Out.WriteLine("<--")

Zodra we de lengte N van het antwoord XML hebben, hoeven we alleen nog maar N tekens te lezen uit de stream IN van het serverantwoord. Deze reeks van N tekens wordt voor de schermmonitoring opgesplitst in tekstregels. Onder deze regels zoeken we de regel met het resultaat:

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

, opnieuw met behulp van een reguliere expressie. Zodra het resultaat is gevonden, wordt het weergegeven.

[résultat=13]

Het einde van de clientcode is als volgt:


        ' opstellen van de reguliere expressie om het resultaat te vinden
        ' in de responsstroom van de webserver
        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

        ' de rest van het antwoord van de webserver wordt gelezen
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' het antwoord wordt opgesplitst in tekstregels
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' de tekstregels worden doorlopen op zoek naar het resultaat
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' vervolg
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' de huidige regel wordt vergeleken met het sjabloon
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' is er iets gevonden?
            If MatchRésultat.Success Then
                ' het resultaat wordt genoteerd
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' het resultaat wordt weergegeven
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.6. Een client SOAP

We bekijken hier een tweede client die gebruikmaakt van een client-serverdialoog van het type SOAP (Simple Object Access Protocol). Er wordt een voorbeeld van een dialoog getoond voor de functie ajouter:

Image

Image

Het verzoek van de client is een POST-verzoek. We zullen dus enkele mechanismen van de vorige client terugzien. Het belangrijkste verschil is dat, terwijl de client HTTP-POST de parameters a en b verstuurde in de vorm

    a=A&b=B

de client SOAP deze in een complexer formaat XML verstuurt:

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>

Hij ontvangt als antwoord een XML, dat eveneens complexer is dan de eerder bekeken antwoorden:

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>

Hoewel het verzoek en het antwoord complexer zijn, gaat het wel degelijk om hetzelfde mechanisme HTTP als voor de client HTTP-POST. De code voor de client SOAP kan dus worden gekopieerd van die voor de client HTTP-POST. Hier volgt een uitvoervoorbeeld:

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]

Alleen de methode executeFonction verandert. De client SOAP verstuurt de headers HTTP van zijn verzoek. Deze zijn slechts iets complexer dan die van 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"
-->

De code waarmee ze worden gegenereerd:


    ' 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)
        ' voert de functie(a,b) uit op de webservice met URI URI
        ' de communicatie tussen client en server verloopt via de stromen IN en OUT
        ' het resultaat van de functie staat in de regel
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' verzonden door de server
        ' opbouw van de queryreeks 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

        ' opbouw van de tabel met te verzenden headers HTTP
        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) = ""

        ' de headers HTTP worden naar de server verzonden
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' verzending naar de server
            OUT.WriteLine(entetes(i))
            ' schermweergave
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

Bij ontvangst van dit verzoek stuurt de server zijn eerste antwoord, dat door de client wordt weergegeven:

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

De leescode van dit eerste antwoord is als volgt:


        ' het eerste antwoord van de webserver wordt gelezen: HTTP/1.1 100
        Dim ligne As String = Nothing
        ' een regel uit de leesstroom
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' volgende regel
            ligne = [IN].ReadLine()
        End While        'while
        'echo laatste regel
        Console.Out.WriteLine(("<-- " + ligne))

De client zal nu zijn parameters in het formaat XML verzenden in wat een SOAP-envelop wordt genoemd:

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

De code:

1
2
3
4
        ' verzoekparameters verzenden
        OUT.Write(requêteSOAP)
        ' echo
        Console.Out.WriteLine(("--> " + requêteSOAP))

De server stuurt vervolgens zijn definitieve antwoord:

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

De klant geeft de ontvangen kopteksten HTTP op het scherm weer en zoekt daarbij naar de regel Content-Length:


        ' opstellen van de reguliere expressie waarmee de grootte van het antwoord kan worden bepaald XML
        ' in de responsstroom van de webserver
        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

        ' uitlezing van het tweede antwoord van de webserver na het verzenden van het verzoek
        ' de waarde van de regel Content-Length wordt opgeslagen
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' schermuitvoer
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' volgende regel
            ligne = [IN].ReadLine()
        End While        'while
        ' laatste regel weergeven
        Console.Out.WriteLine("<--")

Zodra de grootte N van het antwoord XML bekend is, leest de client N tekens uit de responsstroom van de server, splitst de opgehaalde tekenreeks op in tekstregels om deze op het scherm weer te geven en zoekt daarin naar de tag XML van het resultaat: <ajouterResult>7</ajouterResult> en deze weer te geven:


        ' opstellen van de reguliere expressie om het resultaat te vinden
        ' in de responsstroom van de webserver
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' de rest van het antwoord van de webserver wordt gelezen
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' het antwoord wordt opgesplitst in tekstregels
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' de tekstregels worden doorlopen op zoek naar het resultaat
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' vervolg
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' de huidige regel wordt vergeleken met het sjabloon
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' is er iets gevonden?
            If MatchRésultat.Success Then
                ' het resultaat wordt genoteerd
                strRésultat = MatchRésultat.Groups(1).Value
            End If
            'volgende regel
        Next i
        ' het resultaat wordt weergegeven
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.7. Inkapseling van client-server-uitwisselingen

Stel dat onze webservice operations door verschillende applicaties wordt gebruikt. Het zou interessant zijn om voor deze applicaties een klasse beschikbaar te stellen die als interface fungeert tussen de clientapplicatie en de webservice en die het grootste deel van de netwerkcommunicatie verbergt, wat voor de meeste ontwikkelaars geen eenvoudige zaak is. We zouden dan het volgende schema hebben:

De clienttoepassing zou zich tot de client-server-interface wenden om verzoeken aan de webservice te doen. Deze zou alle benodigde netwerkcommunicatie met de server afhandelen en het verkregen resultaat teruggeven aan de clienttoepassing. De clienttoepassing zou zich dan niet meer bezig hoeven te houden met de communicatie met de server, wat het schrijven ervan aanzienlijk zou vergemakkelijken.

10.7.1. De inkapselingsklasse

Na wat we in de voorgaande paragrafen hebben gezien, zijn we nu goed op de hoogte van de netwerkcommunicatie tussen de client en de server. We hebben zelfs drie methoden bekeken. We kiezen ervoor om de methode SOAP te inkapselen. De klasse ziet er als volgt uit:


' naamruimten
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic

' clientSOAP van de webservice operations
Public Class clientSOAP

    ' instantievariabelen
    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
    ' functiewoordenboek
    Private dicoFonctions As New Hashtable
    ' lijst met functies
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' uitgebreide weergave
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

    ' constructor
    Public Sub New(ByVal uriString As String, ByVal verbose As Boolean)

        ' uitgebreide uitvoer
        Me.verbose = verbose

        ' verbinding met de server
        uri = New Uri(uriString)
        client = New TcpClient(uri.Host, uri.Port)

        ' invoer- en uitvoerstromen van de client aanmaken TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' het functiewordboek van de webservice wordt aangemaakt
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
    End Sub

    ' verbinding met de server wordt verbroken
    Public Sub Close()
        ' einde van de verbinding tussen client en server
        [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
        ' voert functie(a,b) uit op de webservice met URI URI
        ' de communicatie tussen client en server verloopt via de stromen IN en OUT
        ' het resultaat van de functie staat in de regel
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' verzonden door de server

        ' geldige functie?
        fonction = fonction.Trim().ToLower()
        If Not dicoFonctions.ContainsKey(fonction) Then
            Return "[fonction [" + fonction + "] indisponible : (ajouter, soustraire,multiplier,diviser)]"
        End If

        ' geldige argumenten a en 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

        ' delen door nul?
        If fonction = "diviser" And doubleB = 0 Then
            Return "[division par zéro]"
        End If

        ' opbouw van de queryreeks 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

        ' opbouw van de tabel met te verzenden headers HTTP
        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) = ""

        ' de headers HTTP worden naar de server verzonden
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' verzending naar de server
            OUT.WriteLine(entetes(i))
            ' schermweergave
            If verbose Then
                Console.Out.WriteLine(("--> " + entetes(i)))
            End If
        Next i

        ' het eerste antwoord van de webserver wordt gelezen: HTTP/1.1 100
        Dim ligne As String = Nothing
        ' een regel uit de leesstroom
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' volgende regel
            ligne = [IN].ReadLine()
        End While
        'echo laatste regel
        If verbose Then
            Console.Out.WriteLine(("<-- " + ligne))
        End If
        ' verzoekparameters verzenden
        OUT.Write(requêteSOAP)
        ' echo
        If verbose Then
            Console.Out.WriteLine(("--> " + requêteSOAP))
        End If

        ' opstellen van de reguliere expressie om de grootte van het antwoord te bepalen XML
        ' in de responsstroom van de webserver
        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

        ' het tweede antwoord van de webserver lezen na het verzenden van de verzoek
        ' de waarde van de regel Content-Length wordt opgeslagen
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' schermuitvoer
            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
            ' volgende regel
            ligne = [IN].ReadLine()
        End While

        ' uitvoer van de laatste regel
        If verbose Then
            Console.Out.WriteLine("<--")
        End If

        ' opstellen van de reguliere expressie om het resultaat te vinden
        ' in de responsstroom van de webserver
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' de rest van het antwoord van de webserver wordt gelezen
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' het antwoord wordt opgesplitst in tekstregels
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' de tekstregels worden doorlopen op zoek naar het resultaat
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' vervolg
            If verbose Then
                Console.Out.WriteLine(("<-- " + lignes(i)))
            End If            ' comparaison ligne courante au modèle
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' is het gevonden?
            If MatchRésultat.Success Then
                ' het resultaat wordt genoteerd
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i

        ' het resultaat wordt teruggestuurd
        Return strRésultat
    End Function
End Class

We zien hier niets nieuws ten opzichte van wat we al hebben gezien. We hebben simpelweg de code van de client SOAP die we hebben bestudeerd, overgenomen en enigszins aangepast om er een klasse van te maken. Deze heeft een constructor en twee methoden:


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


    ' de verbinding met de server wordt verbroken
    Public Sub Close()

en heeft de volgende kenmerken:


     ' instantievariabelen
    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
     ' functie-woordenboek
    Private dicoFonctions As New Hashtable
     ' lijst met functies
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
     ' uitgebreide weergave
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

Er worden twee parameters doorgegeven aan de constructor:

  1. de URI van de webservice waarmee hij verbinding moet maken
  2. een booleaanse waarde verbose die, als deze op 'waar' staat, aangeeft dat de netwerkverkeer op het scherm moet worden weergegeven; anders gebeurt dit niet.

Tijdens het aanmaken worden de stromen IN voor het lezen van het netwerk, OUT voor het schrijven naar het netwerk, evenals het woordenboek met de door de service beheerde functies aangemaakt. Zodra het object is aangemaakt, wordt de client-serververbinding geopend en zijn de stromen IN en OUT bruikbaar.

Met de methode Close kan de verbinding met de server worden verbroken.

De methode ExecuteFonction is dezelfde die we hebben geschreven voor de besproken client SOAP, op enkele details na:

  1. De parameters `uri`, `IN` en `OUT`, die voorheen als parameters aan de methode werden doorgegeven, hoeven dat nu niet meer te zijn, aangezien het nu instantie-attributen zijn die toegankelijk zijn voor alle methoden van de instantie
  2. De methode ExecuteFonction, die voorheen een type void retourneerde en het resultaat van de functie op het scherm weergeeft, retourneert nu dit resultaat en dus een type string.

Doorgaans zal een klant de klasse clientSOAP als volgt gebruiken:

  1. een object van het type clientSOAP aanmaken, dat de verbinding met de webservice tot stand brengt
  2. herhaaldelijk gebruik van de methode executeFonction
  3. het verbreken van de verbinding met de webservice via de methode Close.

Laten we een eerste client bekijken.

10.7.2. Een console-client

We nemen hier de client SOAP die we eerder hebben bekeken toen de klasse clientSOAP nog niet bestond, en passen deze aan zodat hij nu gebruikmaakt van deze klasse:


' naamruimten
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

Public Module testClientSoap

    ' vraagt de URI van de webservice operations op
    ' voert de via het toetsenbord ingevoerde opdrachten interactief uit
    Public Sub Main(ByVal args() As String)
        ' syntaxis
        Const syntaxe As String = "pg URI [verbose]"

        ' aantal argumenten
        If args.Length <> 1 And args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' uitgebreide uitvoer?
        Dim verbose As Boolean = False
        If args.Length = 2 Then
            verbose = args(1).ToLower() = "verbose"
        End If
        ' verbinding maken met de webservice 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' verbindingsfout
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

        ' de verzoeken van de gebruiker worden via het toetsenbord ingevoerd
        ' in de vorm functie a b – ze worden afgesloten met het commando 'einde'
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

        ' prompt voor de gebruiker
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b" + ControlChars.Lf)

        ' foutbeheer
        Dim erreurCommande As Boolean
        Try
            ' lus voor het invoeren van via het toetsenbord ingevoerde opdrachten
            While True
                ' in eerste instantie geen fout
                erreurCommande = False
                ' opdracht lezen
                commande = Console.In.ReadLine().Trim().ToLower()
                ' klaar?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' opdeling van de opdracht in velden
                champs = Regex.Split(commande, "\s+")
                ' er zijn drie velden nodig
                If champs.Length <> 3 Then
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    ' de fout wordt genoteerd
                    erreurCommande = True
                End If
                ' verzoek wordt naar de webservice gestuurd
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
                ' volgende verzoek
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' einde van de verbinding tussen client en server
        Try
            client.Close()
        Catch
        End Try
    End Sub

    ' fouten weergeven
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' fout weergeven
        System.Console.Error.WriteLine(msg)
        ' afsluiten met fout
        Environment.Exit(exitCode)
    End Sub
End Module

De client is nu aanzienlijk eenvoudiger en bevat geen netwerkcommunicatie meer. De client accepteert twee parameters:

  1. de URI van de webservice operations
  2. het optionele trefwoord verbose. Als dit aanwezig is, wordt de netwerkcommunicatie op het scherm weergegeven.

Deze twee parameters worden gebruikt om een object clientSOAP te construeren dat de communicatie met de webservice verzorgt.


         ' verbinding maken met de webservice 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
             ' verbindingsfout
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

Zodra de verbinding met de webservice tot stand is gebracht, kan de client zijn verzoeken verzenden. Deze worden via het toetsenbord ingevoerd, geanalyseerd en vervolgens naar de server verzonden door de methode executeFonction van het object clientSOAP aan te roepen.


                 ' verzoek wordt verzonden naar de webservice
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))

De klasse clientSOAP wordt gecompileerd in een „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

De clienttoepassing testClientSoap wordt vervolgens gecompileerd door:

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

Hier volgt een voorbeeld van een uitvoering zonder logboekuitvoer:

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

Je kunt de netwerkverkeer volgen door een "uitgebreide" uitvoering aan te vragen:

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

Laten we nu een grafische client bouwen.

10.7.3. Een grafische Windows-client

We gaan nu onze webservice opvragen met een grafische client die ook gebruikmaakt van de klasse clientSOAP. De grafische interface ziet er als volgt uit:

De besturingselementen zijn als volgt:

nr.
type
naam
rol
1
TextBox
txtURI
de URI van de afdeling Web Operations
2
Knop
btnOuvrir
opent de verbinding met de webservice
3
Knop
btnFermer
sluit de verbinding met de webservice
4
ComboBox
cmbFonctions
de lijst met bewerkingen (optellen, aftrekken, vermenigvuldigen, delen)
5
TextBox
txtA
de argumenten van de bewerkingen
6
TextBox
txtB
argument b van de functies
7
TextBox
txtRésultat
het resultaat van de functie (a, b)
8
Knop
btnCalculer
start de berekening van de functie(a,b)
9
TextBox
txtErreur
geeft een statusbericht van de verbinding weer

Er zijn enkele gebruiksbeperkingen:

  • de knop btnOuvrir is alleen actief als het veld txtURI niet leeg is en er nog geen verbinding is geopend
  • de knop btnFermer is alleen actief wanneer er een verbinding met de webservice is geopend
  • de knop btnCalculer is alleen actief wanneer er een verbinding is geopend en de velden txtA en txtB niet leeg zijn
  • de velden txtRésultat en txtErreur hebben het attribuut ReadOnly ingesteld op ‘waar’

De client begint met het openen van de verbinding met de webservice met behulp van de knop [Ouvrir]:

Image

Vervolgens kan de gebruiker een functie en waarden voor a en b kiezen:

Image

Image

Image

Image

Image

Hieronder volgt de code van de applicatie. We hebben de code van het formulier weggelaten, aangezien die hier niet van belang is.


'naamruimten
Imports System
Imports System.Windows.Forms

' de formulierklasse
Public Class FormClientSOAP
    Inherits System.Windows.Forms.Form

    ' instantie-attributen
    Dim client As clientSOAP    ' client SOAP du service web operations

#Regio "Code gegenereerd door de Windows Form Designer"

    Public Sub New()
        MyBase.New()
        'Deze aanroep is vereist door de Windows Form Designer.
        InitializeComponent()
        ' overige initialisaties
        myInit()
    End Sub

    'De overschreven methode Dispose van het formulier om de lijst met componenten op te schonen.
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
    End Sub

...

    Private Sub InitializeComponent()
....
    End Sub

#Einde regio


    Private Sub myInit()
        ' formulier initialiseren
        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
        ' de inhoud van het invoerveld is gewijzigd – de status van de knop ‘Openen’ wordt ingesteld
        btnOuvrir.Enabled = txtURI.Text.Trim <> ""
    End Sub

    Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
        ' verzoek om een verbinding met de webservice tot stand te brengen
        Try
            ' aanmaken van een object van het type [clientSOAP]
            client = New clientSOAP(txtURI.Text.Trim, False)
            ' status van de knoppen
            btnOuvrir.Enabled = False
            btnFermer.Enabled = True
            ' URI kan niet meer worden gewijzigd
            txtURI.ReadOnly = True
            ' klantstatus
            txtErreur.Text = "Liaison au service web ouverte"
        Catch ex As Exception
            ' er is een fout opgetreden – deze wordt weergegeven
            txtErreur.Text = ex.Message
        End Try
    End Sub

    Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
        ' de verbinding met de webservice wordt verbroken
        client.Close()
        ' status van de knoppen
        btnOuvrir.Enabled = True
        btnFermer.Enabled = False
        ' URI
        txtURI.ReadOnly = False
        ' status van de klant
        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
        ' berekening van een functie f(a,b)
        ' het vorige resultaat wordt gewist
        txtRésultat.Text = ""
        Try
            txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
        Catch ex As Exception
            ' er is een netwerkfout opgetreden
            txtErreur.Text = ex.Message
            ' de verbinding wordt verbroken
            btnFermer_Click(Nothing, Nothing)
        End Try
    End Sub

    Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
        ' de waarde van A wordt gewijzigd
        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
        ' de waarde van B is gewijzigd
        txtA_TextChanged(Nothing, Nothing)
    End Sub

    ' hoofdmethode
    Public Shared Sub main()
        Application.Run(New FormClientSOAP)
    End Sub
End Class

Ook hier verbergt de klasse clientSOAP het volledige netwerkaspect van de applicatie. De applicatie is als volgt opgebouwd:

  • het assembly-bestand clientSOAP.dll, dat de klasse clientSOAP bevat, is in de projectmap geplaatst
  • de grafische interface clientsoapgui.vb is gebouwd met VS.NET en vervolgens gecompileerd in een DOS-venster:
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

De grafische interface werd vervolgens gestart met:

dos>clientsoapgui

10.8. Een proxy-client

Laten we even terugblikken op wat er zojuist is gedaan. We hebben een tussenklasse gemaakt die de netwerkcommunicatie tussen een client en een webservice inkapselen volgens het onderstaande schema:

Het .NET-platform gaat nog een stap verder met deze logica. Zodra bekend is welke webservice moet worden benaderd, kunnen we automatisch de klasse genereren die als tussenlaag zal dienen om toegang te krijgen tot de functies van de webservice en die het gehele netwerkgedeelte verbergt. Deze klasse wordt een proxy genoemd voor de webservice waarvoor deze is gegenereerd.

Hoe genereer je de proxyklasse van een webservice? Een webservice gaat altijd vergezeld van een beschrijvingsbestand in het formaat XML. Als het URI-bestand van onze webservice ‘operations’ http://localhost/operations/operations.asmx is, dan is het beschrijvingsbestand beschikbaar op http://localhost/operations/operations.asmx?wsdl, zoals te zien is op de volgende schermafbeelding:

Image

Dit is een XML-bestand waarin alle functies van de webservice nauwkeurig worden beschreven, met voor elke functie het type en het aantal parameters, en het type van het resultaat. Dit bestand wordt het WSDL-bestand van de service genoemd, omdat het gebruikmaakt van de taal WSDL (Web Services Description Language). Op basis van dit bestand kan met behulp van de tool wsdl een proxyklasse worden gegenereerd:

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

De tool wsdl genereert een bronbestand VB.NET (optie /language=vb) met de naam van de klasse die de webservice implementeert, in dit geval operations. Laten we een deel van de gegenereerde code bekijken:

'------------------------------------------------------------------------------
' <automatisch gegenereerd>
'      Deze code is gegenereerd door een tool.
'      Runtime-versie: 1.1.4322.573
'
'      Wijzigingen aan dit bestand kunnen tot onjuist gedrag leiden en gaan verloren als 
'      de code opnieuw wordt gegenereerd.
' </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

'
'Deze broncode is automatisch gegenereerd door wsdl, Versie=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

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

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

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

     '<opmerkingen/>
    Public Function Endajouter(ByVal asyncResult As System.IAsyncResult) As Double
        Dim results() As Object = Me.EndInvoke(asyncResult)
        Return CType(results(0),Double)
    End Function
....

Deze code lijkt op het eerste gezicht wat ingewikkeld. We hoeven de details ervan niet te begrijpen om ermee te kunnen werken. Laten we eerst eens kijken naar de klasseverklaring:

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

De klasse draagt de naam operations van de webservice waarvoor deze is ontwikkeld. Deze is afgeleid van de klasse SoapHttpClientProtocol:

Image

Onze proxyklasse heeft één constructor:

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

De constructor wijst aan het attribuut url de waarde URL toe van de webservice die aan de proxy is gekoppeld. De hierboven genoemde klasse operations definieert het attribuut url zelf niet. Dit wordt overgenomen van de klasse waarvan de proxy is afgeleid: System.Web.Services.Protocols.SoapHttpClientProtocol. Laten we nu eens kijken wat er betrekking heeft op de methode ajouter:

    Public Function ajouter(ByVal a As Double, ByVal b As Double) As Double
        Dim results() As Object = Me.Invoke("ajouter", New Object() {a, b})
        Return CType(results(0),Double)
    End Function

We zien dat deze dezelfde handtekening heeft als in de webservice operations, waar deze als volgt was gedefinieerd:

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

De manier waarop deze klasse met de webservice communiceert, wordt hier niet weergegeven. Deze communicatie wordt volledig afgehandeld door de bovenliggende klasse System.Web.Services.Protocols.SoapHttpClientProtocol. In de proxy staat alleen wat deze onderscheidt van andere proxy’s:

  • de URL van de bijbehorende webservice
  • de definitie van de methoden van de bijbehorende service.

Om de methoden van de webservice operations te gebruiken, heeft een client alleen de eerder gegenereerde proxyklasse operations nodig. Laten we deze klasse compileren in een bestand assembly:

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

Laten we nu een console-client schrijven. Deze wordt zonder parameters aangeroepen en voert de via het toetsenbord ingevoerde verzoeken uit:

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

De code van de klant is als volgt:


' naamruimten
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic

Public Module testClientProxy

    ' voert de via het toetsenbord ingevoerde opdrachten interactief uit
    ' en stuurt deze naar de webservice operations
    Public Sub Main()
        ' er zijn geen argumenten meer – de URL van de webservice is hard gecodeerd in de proxy

        ' aanmaken van een woordenboek met de functies van de webservice
        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

        ' er wordt een proxy-object ‘operations’ aangemaakt 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' verbindingsfout
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

        ' de verzoeken van de gebruiker worden via het toetsenbord ingevoerd
        ' in de vorm functie a b – ze eindigen met het commando 'fin'
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

        ' prompt voor de gebruiker
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)

        ' enkele lokale gegevens
        Dim erreurCommande As Boolean
        Dim fonction As String
        Dim a, b As Double
        ' lus voor het invoeren van via het toetsenbord ingevoerde opdrachten
        While True
            ' in het begin geen fout
            erreurCommande = False
            ' opdracht lezen
            commande = Console.In.ReadLine().Trim().ToLower()
            ' klaar?
            If commande Is Nothing Or commande = "fin" Then
                Exit While
            End If
            ' opdeling van de opdracht in velden
            champs = Regex.Split(commande, "\s+")
            Try
                ' er zijn drie velden nodig
                If champs.Length <> 3 Then
                    Throw New Exception
                End If
                ' veld 0 moet een erkende functie zijn
                fonction = champs(0)
                If Not dicoFonctions.ContainsKey(fonction) Then
                    Throw New Exception
                End If
                ' veld 1 moet een geldig getal zijn
                a = Double.Parse(champs(1))
                ' veld 2 moet een geldig getal zijn
                b = Double.Parse(champs(2))
            Catch
                ' ongeldig commando
                Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                erreurCommande = True
            End Try
            ' de aanvraag wordt naar de webservice verzonden
            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

    ' foutmeldingen weergeven
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' foutmelding
        System.Console.Error.WriteLine(msg)
        ' afgebroken met fout
        Environment.Exit(exitCode)
    End Sub
End Module

We bekijken alleen de code die specifiek betrekking heeft op het gebruik van de proxyklasse. Eerst wordt een proxyobject operations aangemaakt:


        ' een proxy-object voor bewerkingen wordt aangemaakt 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' verbindingsfout
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

De regels van functie a en b worden via het toetsenbord ingevoerd. Op basis van deze informatie worden de juiste methoden van de proxy aangeroepen:


            ' verzoek wordt verzonden naar de webservice
            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

Hier wordt voor het eerst de bewerking toutfaire verwerkt, die de vier bewerkingen uitvoert. Deze was tot nu toe genegeerd omdat deze een array met getallen verstuurt, ingekapseld in een XML-envelop, die ingewikkelder is te verwerken dan de eenvoudige XML-antwoorden van de andere functies, die slechts één enkel resultaat opleveren. We zien dat het gebruik van de methode toutfaire hier, met de proxyklasse, niet ingewikkelder is dan het gebruik van de andere methoden. De toepassing is als volgt in een DOS-venster gecompileerd:

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. Een webservice configureren

Een webservice heeft mogelijk configuratiegegevens nodig om correct te kunnen opstarten. Bij de server IIS kunnen deze gegevens worden opgeslagen in een bestand met de naam web.config, dat zich in dezelfde map bevindt als de webservice. Stel dat we een webservice willen maken die twee gegevens nodig heeft om te worden geïnitialiseerd: een naam en een leeftijd. Deze twee gegevens kunnen in het bestand web.config in de volgende vorm worden geplaatst:

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

De initialisatieparameters worden in een envelop XML geplaatst:

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

Een initialisatieparameter met de naam P en de waarde V wordt gedeclareerd met de regel:


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

Hoe haalt de webservice deze informatie op? Wanneer IIS een webservice laadt, controleert het of er in dezelfde map een bestand met de naam web.config aanwezig is. Zo ja, dan leest het dit bestand. De waarde V van een parameter P wordt verkregen via de instructie:

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

waarbij ConfigurationSettings een klasse is in de naamruimte System.Configuration.

Laten we deze techniek eens testen op de volgende webservice:


<%@ WebService language="VB" class=personne %>

Imports System.Web.Services
imports System.Configuration

<WebService([Namespace] := "st.istia.univ-angers.fr")> _
Public Class personne
   Inherits WebService

   ' attributen
   Private nom As String
   Private age As Integer

   ' constructor
   Public Sub New()
      ' attributen initialiseren
      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 

De webservice personne heeft twee attributen, nom en age, die in de constructor zonder parameters worden geïnitialiseerd op basis van de waarden die worden gelezen uit het configuratiebestand web.config van de service personne. Dit bestand ziet er als volgt uit:


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

De webservice heeft bovendien een <WebMethod>-id zonder parameters, die zich beperkt tot het weergeven van de attributen nom en age. De service is opgeslagen in het bronbestand personne.asmx, dat samen met het configuratiebestand in de map c:\inetpub\wwwroot\st\personne staat:

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

Laten we een virtuele map IIS/config koppelen aan de voorgaande fysieke map. Laten we IIS starten en vervolgens met een browser de URL http://localhost/config/personne.asmx van de service personne opvragen:

Image

Laten we de link van de enige methode-id volgen:

Image

De methode id heeft geen parameters. Laten we de knop Appeler gebruiken:

Image

We hebben de informatie uit het bestand web.config van de service inderdaad opgehaald.

10.10. De webservice IMPOTS

We pakken de inmiddels bekende applicatie IMPOTS weer op. De laatste keer dat we ermee werkten, hadden we er een externe server van gemaakt die via internet kon worden aangeroepen. Nu maken we er een webservice van.

10.10.1. De webservice

We gaan uit van de klasse impôt die in het hoofdstuk over databases is aangemaakt en die is opgebouwd op basis van de informatie in een database ODBC:


' opties
Option Strict On
Option Explicit On 

' naamruimten
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections

Public Class impôt
    ' de gegevens die nodig zijn voor de berekening van de belasting
    ' is afkomstig van een externe bron
    Private limites(), coeffR(), coeffN() As Decimal

    ' maker
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' er wordt gecontroleerd of de drie tabellen dezelfde grootte hebben
        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
        ' alles is in orde
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub

    ' constructor 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' initialiseert de drie grensarrays, coeffR, coeffN, op basis van
        ' de inhoud van de tabel Timpots uit de database ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN zijn de drie kolommen van deze tabel
        ' kan een uitzondering veroorzaken
        Dim connectString As String = "DSN=" + DSNimpots + ";"        ' chaîne de connexion à la base
        Dim impotsConn As OdbcConnection = Nothing        ' la connexion
        Dim sqlCommand As OdbcCommand = Nothing        ' la commande SQL
        ' de query SELECT
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' tabellen om de gegevens op te halen
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' er wordt geprobeerd toegang te krijgen tot de database
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' er wordt een command-object aangemaakt
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' de query wordt uitgevoerd
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' De opgehaalde tabel wordt verwerkt
        While myReader.Read()
            ' de gegevens van de huidige rij worden in de tabellen geplaatst
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' vrijgeven van de bronnen
        myReader.Close()
        impotsConn.Close()

        ' de dynamische tabellen worden omgezet in statische tabellen
        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

    ' Berekening van de belasting
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' berekening van het aantal aandelen
        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
        ' berekening van het belastbaar inkomen en de gezinsquotiënt
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' berekening van de belasting
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' weergave van het resultaat
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

In de webservice mag alleen een constructor zonder parameters worden gebruikt. Daarom wordt de constructor van de klasse als volgt:


    ' constructor
    Public Sub New()
        ' initialiseert de drie limiettabellen, coeffR, coeffN, op basis van
        ' de inhoud van de tabel Timpots uit de database ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN zijn de drie kolommen van deze tabel
        ' kan een uitzondering veroorzaken

        ' hiermee worden de configuratieparameters van de service opgehaald
        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")

        ' de database wordt geraadpleegd
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' chaîne de connexion à la base

De vijf parameters van de constructor van de vorige klasse worden nu ingelezen uit het bestand web.config van de webservice. De code van het bronbestand impots.asmx is als volgt. Deze bevat het grootste deel van de vorige code. We hebben alleen de codegedeelten die specifiek zijn voor de webservice gemarkeerd:


<%@ WebService language="VB" class=impots %>

' aanmaken van een webdienst voor belastingen
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

    ' de gegevens die nodig zijn voor de berekening van de belasting
    ' zijn afkomstig van een externe bron
    Private limites(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""


    ' constructor
    Public Sub New()
        ' initialiseert de drie limiettabellen, coeffR, coeffN, op basis van
        ' de inhoud van de tabel Timpots uit de database ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN zijn de drie kolommen van deze tabel
        ' kan een uitzondering veroorzaken

        ' hiermee worden de configuratieparameters van de service opgehaald
        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")

        ' de database wordt geraadpleegd
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' chaîne de connexion à la base
        Dim impotsConn As OdbcConnection = Nothing     ' la connexion
        Dim sqlCommand As OdbcCommand = Nothing     ' la commande SQL
        Dim myReader As OdbcDataReader     ' lecteur de données Odbc

        ' de query SELECT
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots

        ' tabellen om de gegevens op te halen
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' er wordt geprobeerd toegang te krijgen tot de database
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' er wordt een command-object aangemaakt
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' de query wordt uitgevoerd
            myReader = sqlCommand.ExecuteReader()
            ' De opgehaalde tabel wordt verwerkt
            While myReader.Read()
                ' de gegevens van de huidige rij worden in de tabellen geplaatst
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' vrijgeven van de bronnen
            myReader.Close()
            impotsConn.Close()

            ' de dynamische tabellen worden in statische tabellen geplaatst
            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
            ' Klaar
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' fout
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub

    ' berekening van de belasting
    <WebMethod()> _
    Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' berekening van het aantal aandelen
        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
        ' berekening van het belastbaar inkomen en het gezinsquotiënt
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' berekening van de belasting
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' resultaat weergeven
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function

    ' ID
    <WebMethod()> _
    Function id() As String
        ' om te controleren of alles in orde is OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

Laten we de enkele wijzigingen toelichten die in de klasse impots zijn aangebracht, afgezien van de wijzigingen die nodig waren om er een webservice van te maken:

  • het inlezen van de database in de constructor kan mislukken. Daarom hebben we twee attributen en een methode aan onze klasse toegevoegd:
    • de booleaanse waarde OK is gelijk aan vrai als de database kon worden gelezen, en aan faux in het tegenovergestelde geval
    • de tekenreeks errMessage bevat een foutmelding als de database niet kon worden gelezen.
    • Met de methode id zonder parameters kun je de waarde van deze twee attributen opvragen.
  • Om een eventuele fout bij de toegang tot de database af te vangen, is het deel van de code van de constructor dat betrekking heeft op deze toegang omgeven door een try-catch.

Het configuratiebestand web.config van de service ziet er als volgt uit:


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

Bij een eerste poging om de service impots te laden, meldde de compiler dat hij de naamruimte Microsoft.Data.Odbc, die in de richtlijn wordt gebruikt, niet kon vinden:

Imports Microsoft.Data.Odbc

Na raadpleging van de documentatie

  • werd er een compilatierichtlijn toegevoegd aan web.config om aan te geven dat de assembly Microsoft.Data.odbc moest worden gebruikt
  • is een kopie van het bestand microsoft.data.odbc.dll in de map bin van het project geplaatst. Deze map wordt systematisch doorzocht door de compiler van een webservice wanneer deze op zoek is naar een "assembly".

Andere oplossingen lijken mogelijk, maar zijn hier niet verder onderzocht. Het configuratiebestand is dus als volgt geworden:


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

De inhoud van het dossier impots\bin:

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

De service en het bijbehorende configuratiebestand zijn geplaatst in impots:

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

De fysieke map van de webservice is gekoppeld aan de virtuele map /impots van IIS. De pagina van de service ziet er dan als volgt uit:

Image

Als we de link id volgen:

Image

Als je de knop Appeler gebruikt:

Image

Het vorige resultaat toont de waarden van de attributen OK (true) en errMessage (""). In dit voorbeeld is de database correct geladen. Dat was niet altijd het geval en daarom hebben we de methode id toegevoegd om toegang te krijgen tot de foutmelding. De fout was dat de naam DSN van de database was gedefinieerd als DSN (gebruiker), terwijl deze had moeten worden gedefinieerd als DSN (systeem). Dit onderscheid wordt gemaakt in de 32-bits bronbeheerder ODBC:

Laten we teruggaan naar de servicepagina:

Image

Laten we de link calculer volgen:

Image

We stellen de parameters van de aanroep in en voeren deze uit:

Image

Het resultaat is correct.

10.10.2. De proxy voor de dienst impots genereren

Nu we een operationele webservice impots hebben, kunnen we de bijbehorende proxyklasse genereren. Ter herinnering: deze zal door clienttoepassingen worden gebruikt om op transparante wijze toegang te krijgen tot de webservice impots. Eerst gebruiken we het hulpprogramma wsdl om het bronbestand van de proxyklasse te genereren, waarna dit wordt gecompileerd tot een 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. De proxy gebruiken met een client

In het hoofdstuk over databases hebben we een console-applicatie gemaakt waarmee de belasting kan worden berekend:

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

Het programma testimpots maakte toen gebruik van de klassieke klasse impôt, die in het bestand impots.dll stond. De programmacode van testimpots.vb was als volgt:


Option Explicit On 
Option Strict On

' naamruimten
Imports System
Imports Microsoft.VisualBasic

' testpagina
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' interactief programma voor belastingberekening
        ' de gebruiker voert drie gegevens in via het toetsenbord: getrouwd nbEnfants salaris
        ' het programma geeft vervolgens de te betalen belasting weer
        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"

        ' controle van de programma-instellingen
        If arguments.Length <> 5 Then
            ' foutmelding
            Console.Error.WriteLine(syntaxe1)
            ' einde
            Environment.Exit(1)
        End If        'if
        ' de argumenten worden opgehaald
        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)

        ' een belastingobject aanmaken
        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

        ' oneindige lus
        While True
            ' in het begin geen fouten
            Dim erreur As Boolean = False

            ' de parameters voor de belastingberekening worden opgevraagd
            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()

            ' moet er iets gebeuren?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If

            ' controle van het aantal argumenten in de ingevoerde regel
            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
                ' controle van de geldigheid van de parameters
                ' getrouwd
                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
                ' salaris
                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
                ' de parameters zijn correct – de belasting wordt berekend
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

We nemen hetzelfde programma opnieuw ter hand om het nu de webservice impots te laten gebruiken via de eerder aangemaakte proxyklasse impots. We moeten de code enigszins aanpassen:

  • terwijl de oorspronkelijke klasse impôt een constructor met vijf argumenten had, heeft de proxyklasse impots een constructor zonder parameters. De vijf parameters zijn, zoals we hebben gezien, nu vastgelegd in het configuratiebestand van de webservice.
  • Het is dus niet meer nodig om deze vijf parameters als argumenten door te geven aan het testprogramma

De nieuwe code is als volgt:


Imports System
Imports Microsoft.VisualBasic

' testpagina
Module testimpots

    Public Sub Main(ByVal arguments() As String)
        ' interactief programma voor belastingberekening
        ' de gebruiker voert drie gegevens in via het toetsenbord: gehuwd nbEnfants salaris
        ' het programma geeft vervolgens de te betalen belasting weer
        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"

        ' aanmaken van een belastingobject
        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

        ' oneindige lus
        Dim erreur As Boolean
        While True
            ' in eerste instantie geen fout
            erreur = False
            ' er wordt gevraagd om de parameters voor de belastingberekening
            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()
            ' moet er iets gebeuren?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' controle van het aantal argumenten in de ingevoerde regel
            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
                ' controle van de geldigheid van de parameters
                ' getrouwd
                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
                ' salaris
                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
                ' als de parameters correct zijn, wordt de belasting berekend
                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

We hebben de proxy impots.dll en de broncode testimpots in dezelfde map.

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

We compileren de broncode 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

en voeren we het vervolgens uit:

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

We krijgen inderdaad het verwachte resultaat.