Skip to content

10. Webdienste

10.1. Einführung

Im vorigen Kapitel haben wir mehrere TCP/IP-Client-Server-Anwendungen vorgestellt. Da Clients und Server Textzeilen austauschen, können sie in jeder beliebigen Sprache geschrieben werden. Der Client muss lediglich das vom Server erwartete Kommunikationsprotokoll kennen. Webdienste sind TCP/IP-Serveranwendungen mit den folgenden Merkmalen:

  • Sie werden von Webservern gehostet, und das Client-Server-Kommunikationsprotokoll ist daher HTTP (HyperText Transfer Protocol), ein Protokoll, das über TCP/IP läuft.
  • Der Webdienst verfügt über ein Standard-Kommunikationsprotokoll, unabhängig vom angebotenen Dienst. Ein Webdienst bietet verschiedene Dienste S1, S2, .., Sn an. Jeder von ihnen erwartet vom Client bereitgestellte Parameter und gibt ein Ergebnis an den Client zurück. Für jeden Dienst muss der Client Folgendes wissen:
    • den genauen Namen des Dienstes Si
    • die Liste der bereitzustellenden Parameter und deren Typen
    • den Typ des vom Dienst zurückgegebenen Ergebnisses

Sobald diese Elemente bekannt sind, folgt die Client-Server-Interaktion dem gleichen Format, unabhängig davon, welcher Webdienst abgefragt wird. Der Client-Code ist somit standardisiert.

  • Aus Sicherheitsgründen im Zusammenhang mit Angriffen aus dem Internet unterhalten viele Organisationen private Netzwerke und öffnen nur bestimmte Ports ihrer Server für das Internet: in erster Linie Port 80 für den Webdienst. Alle anderen Ports sind gesperrt. Folglich werden Client-Server-Anwendungen, wie sie im vorigen Kapitel vorgestellt wurden, innerhalb des privaten Netzwerks (Intranet) aufgebaut und sind von außen in der Regel nicht zugänglich. Das Hosten eines Dienstes auf einem Webserver macht ihn für die gesamte Internet-Community zugänglich.
  • Der Webdienst kann als Remote-Objekt modelliert werden. Die angebotenen Dienste werden dann zu Methoden dieses Objekts. Ein Client kann auf dieses Remote-Objekt zugreifen, als wäre es lokal. Dies verbirgt die gesamte Netzwerkkommunikationsschicht und ermöglicht die Entwicklung eines von dieser Schicht unabhängigen Clients. Wenn sich die Schicht ändert, muss der Client nicht angepasst werden. Dies ist ein enormer Vorteil und wahrscheinlich der Hauptnutzen von Webdiensten.
  • Wie bei den im vorigen Kapitel vorgestellten TCP/IP-Client-Server-Anwendungen können Client und Server in jeder beliebigen Sprache geschrieben werden. Sie tauschen Textzeilen aus. Diese bestehen aus zwei Teilen:
    • die vom HTTP-Protokoll geforderten Header
    • dem Nachrichtentext. Bei einer Serverantwort an den Client liegt der Nachrichtentext im XML-Format (eXtensible Markup Language) vor. Bei einer Clientanfrage an den Server kann der Nachrichtentext verschiedene Formen annehmen, darunter auch XML. Die XML-Anfrage des Clients kann ein spezielles Format namens SOAP (Simple Object Access Protocol) verwenden. In diesem Fall folgt auch die Antwort des Servers dem SOAP-Format.

10.2. Browser und XML

Webdienste senden XML an ihre Clients. Browser können beim Empfang dieses XML-Stroms unterschiedlich reagieren. Internet Explorer verfügt über ein vordefiniertes Stylesheet, das die Anzeige des XML ermöglicht. Netscape Communicator verfügt jedoch nicht über dieses Stylesheet und zeigt den empfangenen XML-Code nicht an. Sie müssen den Quellcode der empfangenen Seite anzeigen, um auf das XML zuzugreifen. Hier ist ein Beispiel. Für den folgenden XML-Code:

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

Internet Explorer zeigt die folgende Seite an:

Image

während Netscape Navigator Folgendes anzeigt:

Image

Wenn wir uns den Quellcode der von Netscape empfangenen Seite ansehen, erhalten wir:

Image

Netscape hat zwar denselben Inhalt wie der Internet Explorer empfangen, ihn jedoch anders dargestellt. Von nun an werden wir für die Screenshots den Internet Explorer verwenden.

10.3. Ein erster Webdienst

Wir werden Webdienste anhand eines sehr einfachen Beispiels untersuchen, das in drei Versionen verfügbar ist.

10.3.1. Version 1

Für diese erste Version verwenden wir VS.NET, was den Vorteil hat, dass damit ein sofort einsatzbereites Webdienst-Grundgerüst generiert werden kann. Sobald wir diese Architektur verstanden haben, können wir selbstständig weiterarbeiten. Das wird der Schwerpunkt der folgenden Versionen sein.

Erstellen wir mit VS.NET über die Option [Datei/Neu/Projekt] ein neues Projekt:

Image

Beachten Sie folgende Punkte:

  • Der Projekttyp ist Visual Basic (linker Bereich)
  • Die Projektvorlage ist „ASP.NET-Webdienst“ (rechter Bereich)
  • Der Speicherort ist flexibel. Hier wird der Webdienst von einem lokalen IIS-Webserver gehostet. Seine URL lautet daher http://localhost/[Pfad], wobei [Pfad] noch definiert werden muss. Hier wählen wir den Pfad http://localhost/polyvbnet/demo. VS.NET erstellt dann einen Ordner für dieses Projekt. Wo? Der IIS-Server verfügt über ein Stammverzeichnis für die von ihm bereitgestellte Webdokumentenstruktur. Nennen wir dieses Stammverzeichnis <IISroot>. Es entspricht der URL http://localhost. Daraus lässt sich ableiten, dass die URL http://localhost/polyvbnet/demo dem Ordner <IISroot>/polyvbnet/demo zugeordnet wird. <IISroot> ist normalerweise der Ordner \inetpub\wwwroot auf dem Laufwerk, auf dem IIS installiert wurde. In unserem Beispiel ist dies Laufwerk E. Der von VS.NET erstellte Ordner ist daher der Ordner e:\inetpub\wwwroot\polyvbnet\demo:

Image

Wie immer wurden zahlreiche Ordner erstellt. Diese sind nicht immer nützlich. Wir werden nur diejenigen erläutern, die wir zu einem bestimmten Zeitpunkt benötigen. VS.NET hat ein Projekt erstellt:

Image

Wir finden einige der Dateien im physischen Ordner des Projekts. Am interessantesten ist für uns die Datei mit der Erweiterung .asmx. Dies ist die Erweiterung für Webdienste. Ein Webdienst wird von VS.NET als Windows-Anwendung verwaltet, d. h. als Anwendung mit einer grafischen Benutzeroberfläche und Code zu deren Verwaltung. Deshalb haben wir ein Entwurfsfenster:

Image

Ein Webdienst verfügt normalerweise nicht über eine grafische Benutzeroberfläche. Er stellt ein Objekt dar, das remote aufgerufen werden kann. Er verfügt über Methoden, und Anwendungen rufen diese Methoden auf. Wir betrachten ihn daher als ein klassisches Objekt mit der Besonderheit, dass es remote über das Netzwerk instanziiert werden kann. Daher werden wir das von VS.NET bereitgestellte Entwurfsfenster nicht verwenden. Konzentrieren wir uns stattdessen mithilfe der Option „Ansicht/Code“ auf den Code des Dienstes:

Image

Einige Punkte sind dabei besonders erwähnenswert:

  • Die Datei heißt Service1.asmx.vb, nicht Service1.asmx. Wir werden etwas später auf den Inhalt der Datei Service1.asmx zurückkommen.
  • Wir sehen ein Codefenster, das dem ähnelt, das wir beim Erstellen von Windows-Anwendungen mit VS.NET hatten

Der von VS.NET generierte Code lautet wie folgt:


Imports System.Web.Services
 
<System.Web.Services.WebService(Namespace := "http://tempuri.org/demo/Service1")> _
Public Class Service1
    Inherits System.Web.Services.WebService
 
#Region " Code généré par le Concepteur des services Web "
 
    Public Sub New()
        MyBase.New()
 
        'This call is required by the Web Services Designer.
        InitializeComponent()
 
        'Add your initialization code after the InitializeComponent() call
 
    End Sub
 
    'Required by the Web Services Designer
    Private components As System.ComponentModel.IContainer
 
    'REMARQUE: the following procedure is required by the Web Services Designer
    'It can be modified using the Web Services Designer.  
    'Do not modify it using the code editor.
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
    End Sub
 
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        'CODEGEN: this procedure is required by the Web Services Designer
        'Do not modify it using the code editor.
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub
 
#End Region
 
    ' EXEMPLE DE SERVICE WEB
    ' The HelloWorld() service example returns the string Hello World.
    ' To generate, leave the following lines uncommented, then save and generate the project.
    ' To test this Web service, make sure that the .asmx file is the start page
    ' and press F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World
    ' End Function
 
End Class

Beachten Sie zunächst, dass wir hier eine Klasse haben, die Klasse „Service1“, die von der Klasse „WebService“ abgeleitet ist:

Public Class Service1
    Inherits System.Web.Services.WebService

Dies führt dazu, dass wir den Namespace „System.Web.Services“ importieren:


Imports System.Web.Services

Der Klassendeklaration geht ein Kompilierungsattribut voraus:


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

Das Attribut System.Web.Services.WebService() gibt an, dass die folgende Klasse ein Webdienst ist. Dieses Attribut akzeptiert verschiedene Parameter, darunter einen namens NameSpace. Er wird verwendet, um den Webdienst in einem Namespace zu platzieren. Tatsächlich kann man sich vorstellen, dass es weltweit mehrere Webdienste mit dem Namen „weather“ gibt. Wir benötigen eine Möglichkeit, diese voneinander zu unterscheiden. Der Namespace macht dies möglich. Einer könnte [namespace1].weather heißen und ein anderer [namespace2].weather. Dies ist ein Konzept, das den Klassennamespaces entspricht. VS.NET hat automatisch Code generiert und ihn in einem Bereich der Quelldatei platziert:


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

Wenn wir uns diesen Code ansehen, handelt es sich um denselben Code, den der Designer generiert hat, als wir Windows-Anwendungen erstellt haben. Diesen Code können wir einfach löschen, wenn wir keine grafische Benutzeroberfläche haben, was bei Webdiensten der Fall sein wird.

Die Lektion endet mit einem Beispiel dafür, wie ein Webdienst aussehen könnte:


#End Region
 
    ' EXEMPLE DE SERVICE WEB
    ' L'exemple de service HelloWorld() retourne la chaîne Hello World.
    ' Pour générer, ne commentez pas les lignes suivantes, puis enregistrez et générez le projet.
    ' Pour tester ce service Web, assurez-vous que le fichier .asmx est la page de démarrage
    ' et appuyez sur F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = "Hello World"
    ' End Function

Auf der Grundlage des eben Gesagten bereinigen wir den Code, sodass er wie folgt aussieht:


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

Jetzt haben wir ein klareres Bild.

  • Ein Webdienst ist eine von der WebService-Klasse abgeleitete Klasse
  • Die Klasse wird durch das Attribut <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> qualifiziert. Wir platzieren unseren Dienst daher im Namespace st.istia.univ-angers.fr.
  • Die Methoden der Klasse werden durch ein <WebMethod()>-Attribut gekennzeichnet, das angibt, dass es sich um eine Methode handelt, die remote über das Netzwerk aufgerufen werden kann

Die Klasse, die unseren Webdienst bereitstellt, heißt daher Bonjour und verfügt über eine einzige Methode, die ebenfalls Bonjour heißt und eine Zeichenkette zurückgibt. Wir sind bereit für einen ersten Test.

  • Starten Sie den IIS-Webserver, falls Sie dies noch nicht getan haben
  • Verwenden Sie die Option „Debug/Run Without Debugging“ (Debuggen/Ohne Debugging ausführen). VS.NET

VS.NET kompiliert dann die gesamte Anwendung, startet einen Browser (oft Internet Explorer, falls verfügbar) und zeigt die URL http://localhost/polyvbnet/demo/Service1.asmx an:

Image

Warum die URL http://localhost/polyvbnet/demo/Service1.asmx? Weil es die einzige .asmx-Datei im Projekt war:

Image

Hätte es mehrere .asmx-Dateien gegeben, hätten wir angeben müssen, welche zuerst ausgeführt werden soll. Dies geschieht durch einen Rechtsklick auf die entsprechende .asmx-Datei und die Auswahl der Option [Als Startseite festlegen].

Image

Vielleicht interessiert es Sie, was die Datei service1.asmx enthält. Tatsächlich haben wir mit VS.NET an der Datei service1.asmx.vb gearbeitet und nicht an der Datei service1.asmx. Diese Datei befindet sich im Projektordner:

Image

Öffnen wir sie mit einem Texteditor (Notepad oder einem anderen). Wir erhalten folgenden Inhalt:

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

Die Datei enthält eine einfache Anweisung für den IIS-Server, die angibt:

  • dass es sich um einen Webdienst handelt (Schlüsselwort „WebService“)
  • dass die Sprache der Klasse dieses Dienstes Visual Basic ist (Language="vb")
  • dass sich der Quellcode für diese Klasse in der Datei Service1.asmx.vb befindet (Codebehind="Service1.asmx.vb")
  • dass die Klasse, die den Dienst implementiert, demo.Bonjour heißt (Class="demo.Bonjour"). Beachten Sie, dass VS.NET die Bonjour-Klasse im Namespace „demo“ abgelegt hat, der auch der Name des Projekts ist.

Kehren wir nun zu der Seite zurück, auf die über die URL http://localhost/polyvbnet/demo/Service1.asmx zugegriffen wird:

Image

Wer hat den HTML-Code für die obige Seite geschrieben? Nicht wir – das wissen wir. Es ist IIS, das Webdienste auf standardisierte Weise präsentiert. Diese Seite bietet uns zwei Links. Folgen wir dem ersten [Service Description]:

Image

Hoppla... das ist ziemlich unverständliches XML. Beachten Sie jedoch die URL

http://localhost/polyvbnet/demo/Service1.asmx?WSDL. Öffnen Sie einen Browser und geben Sie diese URL direkt ein. Sie erhalten dasselbe Ergebnis wie zuvor. Merken Sie sich also, dass die URL http://serviceweb?WSDL Zugriff auf die XML-Beschreibung des Webdienstes bietet. Kehren wir zur Startseite zurück und klicken wir auf den Link [Hello]. Denken Sie daran, dass „Hello“ eine Methode des Webdienstes ist. Hätte es mehrere Methoden gegeben, wären sie alle hier aufgelistet worden. Wir erhalten die folgende neue Seite:

Image

Wir haben die angezeigte Seite absichtlich gekürzt, um unsere Demonstration übersichtlich zu halten. Beachten Sie erneut die URL:

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

Wenn wir diese URL direkt in einen Browser eingeben, erhalten wir das gleiche Ergebnis wie oben. Wir werden aufgefordert, die Schaltfläche [Call] zu verwenden. Machen wir das. Wir erhalten eine neue Seite:

Image

Dies ist wieder XML. Es enthält zwei Informationen, die in unserem Webdienst vorhanden waren:

  • den Namensraum st.istia.univ-angers.fr unseres Dienstes

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
  • der von der Bonjour-Methode zurückgegebene Wert:

        Return "bonjour !"

Was haben wir gelernt?

  • wie man einen S-Webdienst schreibt
  • wie man ihn aufruft

Wir werden uns nun damit befassen, einen Webdienst ohne Verwendung von VS.NET zu schreiben.

10.3.2. Version 2

Im vorherigen Beispiel hat VS.NET vieles selbst erledigt. Ist es möglich, einen Webdienst ohne dieses Tool zu erstellen? Die Antwort lautet ja, und wir zeigen Ihnen jetzt, wie das geht. Mit einem Texteditor erstellen wir den folgenden Webdienst:


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

Die Klasse heißt Bonjour2 und verfügt über eine Methode namens getBonjour. Sie befindet sich in der Datei demo2.vb, die sich wiederum in der IIS-Serververzeichnisstruktur im Ordner E:\Inetpub\wwwroot\polyvbnet\demo2 befindet. Es handelt sich um eine Standard-VB.NET-Klasse, die daher kompiliert werden kann:

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

Wir legen die Assembly „demo2.dll“ in einem Ordner namens „bin“ ab (dieser Name ist erforderlich):

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

Wir erstellen nun die Datei demo2.asmx. Dies ist die Datei, die von Web-Clients aufgerufen wird. Ihr Inhalt lautet wie folgt:

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

Diese Anweisung ist uns bereits begegnet. Sie besagt Folgendes:

  • die Webdienstklasse Bonjour2 heißt und sich in der Assembly demo2.dll befindet. IIS sucht an verschiedenen Orten nach dieser Assembly, darunter auch im bin-Ordner des Webdienstes. Deshalb haben wir die Assembly demo2.dll dort abgelegt.

Nun können wir verschiedene Tests durchführen. Wir stellen sicher, dass IIS läuft, und rufen die URL http://localhost/polyvbnet/demo2/demo2.asmx in einem Browser auf:

Image

Dann die URL http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Anschließend die URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, wobei getBonjour der Name der einzigen Methode in unserem Webdienst ist:

Image

Wir verwenden die Schaltfläche [Call] oben:

Image

Wir erhalten erfolgreich das Ergebnis des Aufrufs der Methode „getBonjour“ des Webdienstes. Wir wissen nun, wie man einen Webdienst ohne Visual Studio .NET erstellt. Von nun an werden wir die Einzelheiten der Erstellung des Webdienstes außer Acht lassen und uns ausschließlich auf die grundlegenden Dateien konzentrieren.

10.3.3. Version 3

Die beiden vorherigen Versionen des [Hello]-Webdienstes verwendeten zwei Dateien:

  • eine .asmx-Datei, den Einstiegspunkt des Webdienstes
  • eine .vb-Datei, den Quellcode des Webdienstes

Hier zeigen wir, dass eine einzige .asmx-Datei ausreicht. Der Code für den Dienst demo3.asmx lautet wie folgt:

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

Wir sehen, dass sich der Quellcode des Dienstes nun direkt in der Quelldatei demo3.asmx befindet. Die Anweisung

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

verweist nicht mehr auf eine Klasse in einer externen Assembly, sondern auf eine Klasse, die sich in derselben Quelldatei befindet. Speichern wir diese Datei im Ordner <IISroot>\polyvbnet\demo3:

Image

Starten wir IIS und rufen wir die URL http://localhost/polyvbnet/demo3/demo3.asmx auf:

Image

Wir stellen einen deutlichen Unterschied zur vorherigen Version fest: Wir mussten den VB-Code des Dienstes nicht kompilieren. IIS führte diese Kompilierung selbst mit dem auf demselben Rechner installierten VB.NET-Compiler durch. Anschließend lieferte es die Seite aus. Sollte ein Kompilierungsfehler auftreten, meldet IIS diesen:

Image

10.3.4. Version 4

Hier konzentrieren wir uns auf die IIS-Serverkonfiguration. Bislang haben wir unsere Webdienste immer im Stammverzeichnis <IISroot> des IIS-Servers abgelegt, hier [e:\inetpub\wwwroot]. Wir zeigen hier, dass wir den Webdienst an einem beliebigen Ort ablegen können. Dies geschieht mithilfe virtueller Verzeichnisse in IIS. Legen wir unseren Dienst im folgenden Verzeichnis ab:

Image

Der Ordner [D:\data\devel\vbnet\poly\chap9\demo3] befindet sich nicht in der Verzeichnisstruktur des IIS-Servers. Wir müssen dies dem IIS mitteilen, indem wir einen virtuellen IIS-Ordner erstellen. Starten wir den IIS und wählen wir unten die Option [Erweitert] aus:

Image

Es wird eine Liste der virtuellen Verzeichnisse angezeigt. Wir werden uns nicht näher mit dieser Liste befassen. Wir erstellen ein neues virtuelles Verzeichnis über die Schaltfläche [Hinzufügen] oben:

Image

Über die Schaltfläche [Durchsuchen] wählen wir den physischen Ordner aus, der den Webdienst enthält, in diesem Fall den Ordner [D:\data\devel\vbnet\poly\chap9\demo3]. Wir geben diesem Ordner einen logischen (virtuellen) Namen: [virdemo3]. Das bedeutet, dass die Dokumente im physischen Ordner [D:\data\devel\vbnet\poly\chap9\demo3] über die URL [http://<machine>/virdemo3] im Netzwerk zugänglich sind. Das obige Dialogfeld enthält weitere Einstellungen, die wir unverändert lassen. Wir klicken auf OK. Der neue virtuelle Ordner erscheint in der Liste der virtuellen Ordner in IIS:

Image

Nun öffnen wir einen Browser und rufen die URL [http://localhost/virdemo3/demo3.asmx] auf. Wir erhalten das gleiche Ergebnis wie zuvor:

Image

10.3.5. Fazit

Wir haben verschiedene Möglichkeiten zur Erstellung eines Webdienstes vorgestellt. Im weiteren Verlauf werden wir die Methode aus Version 3 zur Erstellung des Dienstes und Methode 4 für dessen Bereitstellung verwenden. Auf diese Weise benötigen wir VS.NET nicht. Dennoch sind die Vorteile der Verwendung von VS.NET hinsichtlich der Debugging-Unterstützung erwähnenswert. Es gibt kostenlose Tools für die Entwicklung von Webanwendungen, insbesondere das von Microsoft gesponserte Produkt WebMatrix, das unter der URL [http://www.asp.net/webmatrix] zu finden ist. Es ist ein hervorragendes Tool, um ohne Vorabinvestitionen in die Webprogrammierung einzusteigen.

10.4. Ein Webdienst für Operationen

Betrachten wir einen Webdienst, der fünf Funktionen bietet:

  1. add(a,b), das a+b zurückgibt
  2. subtract(a,b), das a-b zurückgibt
  3. multiply(a,b), der a*b zurückgibt
  4. divide(a,b), das a/b zurückgibt
  5. doAll(a,b), das das Array [a+b, a-b, a*b, a/b] zurückgibt

Der VB.NET-Code für diesen Dienst lautet wie folgt:


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

Wir wiederholen hier einige Erläuterungen, die bereits gegeben wurden, die es jedoch wert sind, noch einmal aufgegriffen oder vertieft zu werden. Die Operationsklasse ähnelt einer VB.NET-Klasse, wobei einige Punkte zu beachten sind:

  • Methoden werden durch ein <WebMethod()>-Attribut eingeleitet, das dem Compiler mitteilt, welche Methoden „veröffentlicht“, d. h. dem Client zur Verfügung gestellt werden sollen. Eine Methode, der dieses Attribut nicht vorangestellt ist, wäre für Remote-Clients unsichtbar. Dies könnte eine interne Methode sein, die von anderen Methoden verwendet wird, aber nicht zur Veröffentlichung bestimmt ist.
  • Die Klasse leitet sich von der WebService-Klasse ab, die im Namespace System.Web.Services definiert ist. Diese Vererbung ist nicht immer zwingend erforderlich. Insbesondere in diesem Beispiel könnten wir darauf verzichten.
  • Der Klasse selbst ist ein <WebService(Namespace="st.istia.univ-angers.fr")>-Attribut vorangestellt, das einen Namespace für den Webdienst bereitstellen soll. Ein Klassenanbieter weist seinen Klassen einen Namespace zu, um ihnen einen eindeutigen Namen zu geben und so Konflikte mit Klassen anderer Anbieter zu vermeiden, die möglicherweise denselben Namen haben. Das Gleiche gilt für Webdienste. Jeder Webdienst muss durch einen eindeutigen Namen identifizierbar sein, in diesem Fall st.istia.univ-angers.fr.
  • Wir haben keinen Konstruktor definiert. Daher wird implizit der Konstruktor der übergeordneten Klasse verwendet.

Der obige Quellcode ist nicht direkt für den VB.NET-Compiler bestimmt, sondern für den IIS-Webserver. Er muss die Erweiterung .asmx haben und in der Verzeichnisstruktur des Webservers gespeichert werden. Hier speichern wir ihn als operations.asmx im Ordner <IISroot>\polyvbnet\operations:

Image

Wir verknüpfen das virtuelle IIS-Verzeichnis [operations] mit diesem physischen Verzeichnis:

Rufen wir den Dienst über einen Browser auf. Die anzufordernde URL lautet [http://localhost/operations/operations.asmx]:

Image

Wir erhalten ein Webdokument mit einem Link für jede der im Operations-Webdienst definierten Methoden. Folgen wir dem Link „Hinzufügen“:

Image

Die angezeigte Seite fordert uns auf, die Methode add zu testen, indem wir die beiden erforderlichen Argumente a und b übergeben. Erinnern wir uns an die Definition der Methode *add*:

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

Beachten Sie, dass die Seite die Argumentnamen a und b aus der Methodendefinition verwendet hat. Klicken Sie auf die Schaltfläche „Aufrufen“, woraufhin die folgende Antwort in einem separaten Browserfenster angezeigt wird:

Image

Wenn Sie oben [Ansicht/Quelltext] auswählen, erhalten Sie den folgenden Code:

Image

Wiederholen wir den Vorgang für die Methode [toutfaire]:

Image

Wir erhalten die folgende Seite:

Image

Klicken wir oben auf die Schaltfläche [Anrufen]:

Image

In allen Fällen hat die Antwort des Servers das folgende Format:

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • Die Antwort liegt im XML-Format vor
  • Zeile 1 ist Standard und immer in der Antwort enthalten
  • Die folgenden Zeilen hängen vom Ergebnistyp (double, ArrayOfDouble), der Anzahl der Ergebnisse und dem Webservice-Namespace (in diesem Fall st.istia.univ-angers.fr) ab.

Es gibt mehrere Methoden, um einen Webdienst abzufragen und dessen Antwort zu erhalten. Kehren wir zur URL des Dienstes zurück:

Image

und folgen Sie dem Link [Add]. Auf der angezeigten Seite werden zwei Methoden zur Abfrage der Funktion [Add] des Webdienstes vorgestellt:

Image

Image

Image

Diese beiden Methoden für den Zugriff auf die Funktionen eines Webdienstes heißen: HTTP-POST und SOAP. Wir werden sie nun nacheinander betrachten.

Hinweis: In früheren Versionen von VS.NET gab es eine dritte Methode namens HTTP-GET. Zum Zeitpunkt der Erstellung dieses Dokuments (März 2004) scheint diese Methode nicht mehr verfügbar zu sein. Das bedeutet, dass der von VS.NET generierte Webdienst keine GET-Anfragen akzeptiert. Das bedeutet jedoch nicht, dass Sie keine Webdienste schreiben können, die GET-Anfragen akzeptieren, insbesondere wenn Sie andere Tools als VS.NET verwenden oder den Code einfach von Hand schreiben.

10.5. Ein HTTP-POST-Client

Wir werden der vom Webdienst vorgeschlagenen Methode folgen:

Image

Schauen wir uns den Text genauer an. Zunächst muss der Web-Client die folgenden HTTP-Header senden:

POST /operations/operations.asmx/add HTTP/1.1
Der Webclient sendet eine POST-Anfrage an die URL /operations/operations.asmx/add gemäß dem HTTP-Protokoll der Version 1.1
HOST: localhost
Wir geben den Zielrechner für die Anfrage an. Hier ist das localhost. Dieser Header wurde durch Version 1.1 des HTTP-Protokolls zur Pflicht gemacht
Content-Type: application/x-www-form-urlencoded
Dies gibt an, dass nach den HTTP-Headern zusätzliche Parameter im URL-kodierten Format gesendet werden. Bei diesem Format werden bestimmte Zeichen durch ihre Hexadezimalcodes ersetzt.
Content-Length: 7
Dies ist die Zeichenanzahl der Parameterzeichenfolge, die nach den HTTP-Headern gesendet wird.

Auf die HTTP-Header folgt eine Leerzeile, dann die POST-Parameterzeichenfolge mit [Content-Length] Zeichen in der Form a=XX&b=YY, wobei XX und YY die „URL-kodierten“ Zeichenfolgen der Werte der Parameter a und b sind. Wir wissen genug, um das Obige mit unserem generischen TCP-Client zu reproduzieren, der bereits im Kapitel über TCP/IP-Programmierung verwendet wurde:

  • Wir starten IIS
  • der Dienst ist unter der URL [http://localhost/operations/operations.asmx] verfügbar
  • wir verwenden den generischen TCP-Client in einem DOS-Fenster
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]

Zunächst ist zu beachten, dass wir den Header [Connection: close] hinzugefügt haben, um den Server anzuweisen, die Verbindung nach dem Senden der Antwort zu schließen. Dies ist hier notwendig. Wenn wir dies nicht angeben, hält der Server die Verbindung standardmäßig offen. Seine Antwort besteht jedoch aus einer Folge von Textzeilen, von denen die letzte nicht durch ein Zeilenendezeichen beendet wird. Es stellt sich heraus, dass unser generischer TCP-Client Textzeilen, die durch ein Zeilenendezeichen beendet werden, mit der ReadLine-Methode liest. Wenn der Server die Verbindung nach dem Senden der letzten Zeile nicht schließt, wird der Client blockiert, da er auf ein Zeilenendezeichen wartet, das niemals eintrifft. Wenn der Server die Verbindung schließt, wird die ReadLine-Methode des Clients abgeschlossen, und der Client wird nicht blockiert.

Unmittelbar nach dem Empfang der Leerzeile, die das Ende der HTTP-Header signalisiert, sendet der IIS-Server eine erste Antwort:

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

Diese Antwort, die ausschließlich aus HTTP-Headern besteht, teilt dem Client mit, dass er die 7 Zeichen senden kann, die er senden wollte. Was wir tun:

a=2&b=3

Beachten Sie, dass unser TCP-Client hier mehr als 7 Zeichen sendet, da er diese mit einem Zeilenende-Marker (WriteLine) versendet. Dies hat keine Auswirkungen auf den Server, der nur die ersten 7 der empfangenen Zeichen übernimmt, da die Verbindung anschließend geschlossen wird (Connection: close). Diese zusätzlichen Zeichen wären problematisch gewesen, wenn die Verbindung offen geblieben wäre, da sie dann als Teil des nächsten Befehls des Clients interpretiert worden wären. Sobald die Parameter empfangen wurden, sendet der Server seine Antwort:

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

Wir verfügen nun über die Elemente, um ein Client-Programm für unseren Webdienst zu schreiben. Es handelt sich um einen Konsolen-Client namens httpPost2, der wie folgt verwendet wird:


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>

Der Client wird aufgerufen, indem ihm die URL des Webdienstes übergeben wird:

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

Anschließend liest der Client die über die Tastatur eingegebenen Befehle ein und führt sie aus. Diese haben das folgende Format:

fonction a b

wobei „function“ die aufgerufene Webservice-Funktion ist (add, subtract, multiply, divide) und „a“ und „b“ die Werte sind, auf die diese Funktion angewendet wird. Zum Beispiel:

ajouter 6 7

Von dort aus sendet der Client die erforderliche HTTP-Anfrage an den Webserver und erhält eine Antwort. Der Austausch zwischen Client und Server wird auf dem Bildschirm angezeigt, damit Sie den Vorgang besser nachvollziehen können:

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]

Der oben gezeigte Austausch entspricht dem, den wir beim generischen TCP-Client gesehen haben, mit einem Unterschied: Der HTTP-Header **Connection: Keep-Alive weist den Server an, die Verbindung nicht zu schließen. Die Verbindung bleibt daher für den nächsten Vorgang des Clients offen, sodass der Client keine neue Verbindung zum Server herstellen muss. Dies erfordert jedoch, dass der Client eine andere Methode als ReadLine() verwendet, um die Antwort des Servers zu lesen, da wir wissen, dass die Antwort aus einer Folge von Zeilen besteht, von denen die letzte nicht durch ein Zeilenumbruchzeichen beendet wird. Sobald die gesamte Serverantwort empfangen wurde, analysiert der Client sie, um das Ergebnis der angeforderten Operation zu finden und anzuzeigen:

[résultat=13]

Sehen wir uns den Code unseres Clients an:


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

Diese Elemente haben wir bereits mehrfach gesehen, und sie bedürfen keiner besonderen Erläuterung. Sehen wir uns nun den Code für die Methode executeFonction an, in der sich die neuen Elemente befinden:


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

Zunächst sendet der HTTP-POST-Client seine Anfrage im POST-Format:


        ' query chain construction
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length
 
        ' construction of header table HTTP to be sent
        Dim entetes(5) As String
        entetes(0) = "POST " + uri.AbsolutePath + "/" + fonction + " HTTP/1.1"
        entetes(1) = "Host: " & uri.Host & ":" & uri.Port
        entetes(2) = "Content-Type: application/x-www-form-urlencoded"
        entetes(3) = "Content-Length: " & nbChars
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

In der Kopfzeile

--> Content-Length: 7

Wir müssen die Größe der Parameter angeben, die vom Client hinter den HTTP-Headern gesendet werden:

--> a=6&b=7

Verwenden Sie dazu den folgenden Code:


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

Die Methode HttpUtility.UrlEncode(string string) wandelt bestimmte Zeichen in der Zeichenfolge in %n1n2 um, wobei n1n2 der ASCII-Code des umgewandelten Zeichens ist. Die von dieser Konvertierung betroffenen Zeichen sind alle Zeichen, die in einer POST-Anfrage eine bestimmte Bedeutung haben (Leerzeichen, =, & usw.). Hier ist die Methode HttpUtility.UrlEncode normalerweise nicht erforderlich, da a und b Zahlen sind, die keine dieser Sonderzeichen enthalten. Sie wird hier als Beispiel verwendet. Sie erfordert den Namespace System.Web. Sobald der Client seine HTTP-Header gesendet hat:

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

Der Server antwortet mit dem HTTP-Header „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
<--

Der Code liest einfach diese erste Antwort ein und zeigt sie auf dem Bildschirm an:


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

Sobald diese erste Antwort gelesen wurde, muss der Client seine Parameter senden:

--> a=6&b=7

Dies geschieht mit dem folgenden Code:


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

Der Server sendet daraufhin seine Antwort. Diese Antwort besteht aus zwei Teilen:

  1. HTTP-Header, gefolgt von einer Leerzeile
  2. die Antwort im XML-Format
<-- 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>

Zunächst liest der Client die HTTP-Header, um die Zeile „Content-Length“ zu finden und die Größe der XML-Antwort abzurufen (hier 90). Dies geschieht mithilfe eines regulären Ausdrucks. Wir hätten dies auch anders und wahrscheinlich effizienter lösen können.


        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length ?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While
        ' last line echo
        Console.Out.WriteLine("<--")

Sobald wir die Länge N der XML-Antwort haben, müssen wir lediglich N Zeichen aus dem IN-Stream der Serverantwort lesen. Diese Zeichenfolge aus N Zeichen wird für die Bildschirmüberwachung in Textzeilen aufgeteilt. Unter diesen Zeilen suchen wir nach der Zeile, die das Ergebnis enthält:

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

wiederum unter Verwendung eines regulären Ausdrucks. Sobald das Ergebnis gefunden wurde, wird es angezeigt.

[résultat=13]

Der Client-Code endet wie folgt:


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

10.6. Ein SOAP-Client

Hier betrachten wir einen zweiten Client, der einen SOAP-Client-Server-Dialog (Simple Object Access Protocol) verwendet. Ein Beispiel für einen solchen Dialog wird für die Funktion „add“ vorgestellt:

Image

Image

Die Anfrage des Clients ist eine POST-Anfrage. Wir werden daher einige der gleichen Mechanismen wie beim vorherigen Client sehen. Der Hauptunterschied besteht darin, dass der HTTP-POST-Client die Parameter a und b im Formular gesendet hat

    a=A&b=B

, sendet der SOAP-Client diese in einem komplexeren XML-Format:

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>

Als Antwort erhält es eine XML-Antwort, die ebenfalls komplexer ist als die zuvor gezeigten Antworten:

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>

Auch wenn die Anfrage und die Antwort komplexer sind, handelt es sich hier tatsächlich um denselben HTTP-Mechanismus wie beim HTTP-POST-Client. Der SOAP-Client-Code kann daher nach dem Vorbild des HTTP-POST-Clients gestaltet werden. Hier ein Ausführungsbeispiel:

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]

Nur die Methode executeFonction ändert sich. Der SOAP-Client sendet die HTTP-Header für seine Anfrage. Diese sind lediglich etwas komplexer als die von 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"
-->

Der Code, der sie generiert:


    ' executeFonction
    Public Sub executeFonction(ByVal [IN] As StreamReader, ByVal OUT As StreamWriter, ByVal uri As Uri, ByVal fonction As String, ByVal a As String, ByVal b As String)
        ' executes function(a,b) on the URI uri web service
        ' client-server exchanges take place via IN and OUT flows
        ' the result of the function is in the line
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' sent by the server
        ' construction of query string SOAP
 
        Dim requêteSOAP As String = "<?xml version=" + """1.0"" encoding=""utf-8""?>" + ControlChars.Lf
        requêteSOAP += "<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">" + ControlChars.Lf
        requêteSOAP += "<soap:Body>" + ControlChars.Lf
        requêteSOAP += "<" + fonction + " xmlns=""st.istia.univ-angers.fr"">" + ControlChars.Lf
        requêteSOAP += "<a>" + a + "</a>" + ControlChars.Lf
        requêteSOAP += "<b>" + b + "</b>" + ControlChars.Lf
        requêteSOAP += "</" + fonction + ">" + ControlChars.Lf
        requêteSOAP += "</soap:Body>" + ControlChars.Lf
        requêteSOAP += "</soap:Envelope>"
        Dim nbCharsSOAP As Integer = requêteSOAP.Length
 
        ' construction of header table HTTP to be sent
        Dim entetes(6) As String
        entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
        entetes(1) = "Host: " & uri.Host & ":" & uri.Port
        entetes(2) = "Content-Type: text/xml; charset=utf-8"
        entetes(3) = "Content-Length: " & nbCharsSOAP
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
        entetes(6) = ""
 
        ' send HTTP headers to the server
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' send to server
            OUT.WriteLine(entetes(i))
            ' screen echo
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

Nach Erhalt dieser Anfrage sendet der Server seine erste Antwort, die der Client anzeigt:

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

Der Code zum Auslesen dieser ersten Antwort lautet wie folgt:


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

Der Client sendet nun seine Parameter im XML-Format in einem sogenannten SOAP-Envelope:

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

Der Code:


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

Der Server sendet dann seine endgültige Antwort:

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

Der Client zeigt die empfangenen HTTP-Header auf dem Bildschirm an und sucht dabei nach der Zeile „Content-Length“:


        ' construction of the regular expression to find the response size XML
        ' in the web server response stream
        Dim modèleLength As String = "^Content-Length: (.+?)\s*$"
        Dim RegexLength As New Regex(modèleLength)        '
        Dim MatchLength As Match = Nothing
        Dim longueur As Integer = 0
 
        ' read the second response from the web server after sending the request
        ' the value of the Content-Length line is stored
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' screen echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length ?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' next line
            ligne = [IN].ReadLine()
        End While        'while
        ' last line echo
        Console.Out.WriteLine("<--")

Sobald die Größe N der XML-Antwort bekannt ist, liest der Client N Zeichen aus dem Antwortstrom des Servers, teilt die abgerufene Zeichenfolge in Textzeilen auf, um sie auf dem Bildschirm anzuzeigen, und sucht nach dem XML-Tag des Ergebnisses: <ajouterResult>7</ajouterResult> und zeigt es an:


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

10.7. Kapselung der Client-Server-Kommunikation

Stellen wir uns vor, dass unsere Webdienst-Operationen von verschiedenen Anwendungen genutzt werden. Es wäre sinnvoll, diesen Anwendungen eine Klasse zur Verfügung zu stellen, die als Schnittstelle zwischen der Client-Anwendung und dem Webdienst fungiert und den Großteil der Netzwerkkommunikation verbirgt, was für die meisten Entwickler nicht trivial ist. Dies würde zu folgender Architektur führen:

Die Client-Anwendung würde sich an die Client-Server-Schnittstelle wenden, um ihre Anfragen an den Webdienst zu stellen. Die Schnittstelle würde die gesamte erforderliche Netzwerkkommunikation mit dem Server übernehmen und das Ergebnis an die Client-Anwendung zurückgeben. Die Client-Anwendung müsste sich nicht mehr um die Kommunikation mit dem Server kümmern, was ihre Entwicklung erheblich vereinfachen würde.

10.7.1. Die Kapselungsklasse

Auf der Grundlage dessen, was wir in den vorangegangenen Abschnitten behandelt haben, verfügen wir nun über ein gutes Verständnis der Netzwerkkommunikation zwischen Client und Server. Wir haben uns sogar drei Methoden angesehen. Wir haben uns dafür entschieden, die SOAP-Methode zu kapseln. Die Klasse sieht wie folgt aus:


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

Hier gibt es nichts Neues im Vergleich zu dem, was wir bereits gesehen haben. Wir haben einfach den Code aus dem SOAP-Client, den wir untersucht haben, übernommen und ihn leicht umgestaltet, um daraus eine Klasse zu machen. Diese Klasse verfügt über einen Konstruktor und zwei Methoden:


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

    ' executeFonction
    Public Function executeFonction(ByVal fonction As String, ByVal a As String, ByVal b As String) As String
 

    ' close server connection
    Public Sub Close()

und verfügt über die folgenden Attribute:


    ' variables d'instance
    Private uri As Uri = Nothing    ' l'URI du service web
    Private client As TcpClient = Nothing    ' la liaison tcp du client avec le serveur
    Private [IN] As StreamReader = Nothing    ' le flux de lecture du client
    Private OUT As StreamWriter = Nothing    ' le flux d'écriture du client
    ' dictionnaire des fonctions
    Private dicoFonctions As New Hashtable
    ' liste des fonctions
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' verbose
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

Wir übergeben zwei Parameter an den Konstruktor:

  1. die URI des Webdienstes, mit dem eine Verbindung hergestellt werden soll
  2. einen booleschen Parameter „verbose“, der, wenn er auf „true“ gesetzt ist, die Anzeige des Netzwerkverkehrs auf dem Bildschirm anfordert; andernfalls wird dieser nicht angezeigt.

Während der Konstruktion werden der IN-Stream für das Lesen aus dem Netzwerk, der OUT-Stream für das Schreiben ins Netzwerk und das vom Dienst verwaltete Funktionswörterbuch erstellt. Sobald das Objekt konstruiert ist, ist die Client-Server-Verbindung geöffnet und seine IN- und OUT-Streams sind einsatzbereit.

Die Methode „Close“ schließt die Verbindung zum Server.

Die Methode ExecuteFonction ist diejenige, die wir für den untersuchten SOAP-Client geschrieben haben, mit einigen geringfügigen Unterschieden:

  1. Die Parameter `uri`, `IN` und `OUT`, die zuvor als Parameter an die Methode übergeben wurden, müssen nicht mehr übergeben werden, da sie nun Instanzattribute sind, auf die alle Methoden der Instanz zugreifen können
  2. Die Methode `ExecuteFonction`, die zuvor den Typ `void` zurückgab und das Ergebnis der Funktion auf dem Bildschirm anzeigte, gibt nun dieses Ergebnis zurück – und somit den Typ `string`.

In der Regel verwendet ein Client die Klasse clientSOAP wie folgt:

  1. Erstellen eines clientSOAP-Objekts, das die Verbindung zum Webdienst herstellt
  2. wiederholtes Aufrufen der Methode „executeFonction“
  3. Schließen der Verbindung zum Webdienst mithilfe der Methode „Close“.

Schauen wir uns einen ersten Client an.

10.7.2. Ein Konsolen-Client

Hier greifen wir den SOAP-Client wieder auf, den wir untersucht haben, als die Klasse clientSOAP noch nicht existierte, und gestalten ihn so um, dass er nun diese Klasse verwendet:


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

Der Client ist nun wesentlich einfacher und enthält keine Netzwerkkommunikation. Der Client akzeptiert zwei Parameter:

  1. die URI der Webdienst-Operationen
  2. das optionale Schlüsselwort „verbose“. Ist dieses vorhanden, werden die Netzwerkkommunikationen auf dem Bildschirm angezeigt.

Diese beiden Parameter werden verwendet, um ein clientSOAP-Objekt zu erstellen, das die Kommunikation mit dem Webdienst übernimmt.


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

Sobald die Verbindung zum Webdienst hergestellt ist, kann der Client seine Anfragen senden. Diese werden über die Tastatur eingegeben, analysiert und dann durch Aufruf der Methode executeFonction des clientSOAP-Objekts an den Server gesendet.


                ' on fait la demande au service web
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))

Die Klasse clientSOAP wird zu einer „Assembly“ kompiliert:

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

Die Client-Anwendung „testClientSoap“ wird dann wie folgt kompiliert:

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 ist ein Beispiel für eine Ausführung ohne ausführliche Ausgabe:

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

Sie können den Netzwerkverkehr überwachen, indem Sie eine „ausführliche“ Ausführung anfordern:

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

Erstellen wir nun einen grafischen Client.

10.7.3. Ein grafischer Windows-Client

Wir werden nun unseren Webdienst über einen grafischen Client abfragen, der ebenfalls die Klasse clientSOAP verwendet. Die grafische Oberfläche sieht wie folgt aus:

Die Steuerelemente sind wie folgt:

Nr.
Typ
Name
Rolle
1
TextBox
txtURI
die URI der Webdienst-Operationen
2
Schaltfläche
btnOpen
öffnet die Verbindung zum Webdienst
3
Schaltfläche
btnClose
schließt die Verbindung zum Webdienst
4
ComboBox
cmbFunctions
die Liste der Funktionen (addieren, subtrahieren, multiplizieren, dividieren)
5
Textfeld
txtA
das Argument für Funktionen
6
Textfeld
txtB
Argument b der Funktionen
7
TextBox
txtResult
das Ergebnis von function(a,b)
8
Schaltfläche
btnCalculate
startet die Berechnung von Funktion(a,b)
9
Textfeld
txtError
zeigt eine Meldung zum Verbindungsstatus an

Es gibt einige betriebliche Einschränkungen:

  • Die Schaltfläche „btnOpen“ ist nur aktiv, wenn das Feld „txtURI“ nicht leer ist und noch keine Verbindung besteht
  • Die Schaltfläche „btnClose“ ist nur aktiv, wenn eine Verbindung zum Webdienst geöffnet wurde
  • Die Schaltfläche „btnCalculate“ ist nur aktiv, wenn eine Verbindung besteht und die Felder „txtA“ und „txtB“ nicht leer sind
  • Für die Felder „txtResult“ und „txtError“ ist das Attribut „ReadOnly“ auf „true“ gesetzt

Der Client beginnt damit, die Verbindung zum Webdienst über die Schaltfläche [Open] zu öffnen:

Image

Anschließend kann der Benutzer eine Funktion sowie Werte für a und b auswählen:

Image

Image

Image

Image

Image

Es folgt der Anwendungscode. Den Formularcode haben wir weggelassen, da er hier nicht relevant ist.


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

    Private Sub txtB_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtB.TextChanged
        ' change in the value of B
        txtA_TextChanged(Nothing, Nothing)
    End Sub
 
    ' main method
    Public Shared Sub main()
        Application.Run(New FormClientSOAP)
    End Sub
End Class

Auch hier verbirgt die clientSOAP-Klasse alle netzwerkbezogenen Aspekte der Anwendung. Die Anwendung wurde wie folgt erstellt:

  • Die Assembly clientSOAP.dll, die die Klasse clientSOAP enthält, wurde im Projektordner abgelegt
  • Die GUI clientsoapgui.vb wurde mit VS.NET erstellt und anschließend in einem DOS-Fenster kompiliert:
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

Die grafische Benutzeroberfläche wurde dann gestartet mit:

dos>clientsoapgui

10.8. Ein Proxy-Client

Fassen wir zusammen, was wir gerade gemacht haben. Wir haben eine Zwischenklasse erstellt, die den Datenaustausch zwischen einem Client und einem Webdienst gemäß dem folgenden Diagramm kapseln:

Die .NET-Plattform geht bei dieser Logik noch einen Schritt weiter. Sobald wir wissen, auf welchen Webdienst wir zugreifen wollen, können wir automatisch die Klasse generieren, die als Vermittler fungiert, um auf die Funktionen des Webdienstes zuzugreifen und die gesamte Netzwerkschicht zu verbergen. Diese Klasse wird als Proxy für den Webdienst bezeichnet, für den sie generiert wurde.

Wie generiert man eine Webdienst-Proxy-Klasse? Ein Webdienst wird immer von einer Beschreibungsdatei im XML-Format begleitet. Wenn die URI unserer Webdienstoperationen http://localhost/operations/operations.asmx lautet, ist die zugehörige Beschreibungsdatei unter der URL http://localhost/operations/operations.asmx?wsdl verfügbar, wie im folgenden Screenshot zu sehen ist:

Image

Dies ist eine XML-Datei, die alle Funktionen des Webdienstes genau beschreibt, einschließlich des Typs und der Anzahl der Parameter sowie des Typs des Ergebnisses für jede einzelne Funktion. Diese Datei wird als WSDL-Datei des Dienstes bezeichnet, da sie die WSDL (Web Services Description Language) verwendet. Aus dieser Datei kann mit dem Tool „wsdl“ eine Proxy-Klasse generiert werden:


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

Das WSDL-Tool generiert eine VB.NET-Quelldatei (Option /language=vb), die nach der Klasse benannt ist, die den Webdienst implementiert, in diesem Fall operations. Sehen wir uns einen Teil des generierten Codes an:

'------------------------------------------------------------------------------
' <autogenerated>
'     This code was generated by a tool.
'     Runtime Version: 1.1.4322.573
'
'     Changes to this file may cause incorrect behavior and will be lost if 
'     the code is regenerated.
' </autogenerated>
'------------------------------------------------------------------------------

Option Strict Off
Option Explicit On

Imports System
Imports System.ComponentModel
Imports System.Diagnostics
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.Xml.Serialization

'
'This source code has been té automatically généré by wsdl, Version=1.1.4322.573.
'

'<remarks/>
<System.Diagnostics.DebuggerStepThroughAttribute(),  _
 System.ComponentModel.DesignerCategoryAttribute("code"),  _
 System.Web.Services.WebServiceBindingAttribute(Name:="operationsSoap", [Namespace]:="st.istia.univ-angers.fr")>  _
Public Class operations
    Inherits System.Web.Services.Protocols.SoapHttpClientProtocol

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

    '<remarks/>
    <System.Web.Services.Protocols.SoapDocumentMethodAttribute("st.istia.univ-angers.fr/ajouter", RequestNamespace:="st.istia.univ-angers.fr", ResponseNamespace:="st.istia.univ-angers.fr", Use:=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle:=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)>  _

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

    '<remarks/>
    Public Function Beginajouter(ByVal a As Double, ByVal b As Double, ByVal callback As System.AsyncCallback, ByVal asyncState As Object) As System.IAsyncResult
        Return Me.BeginInvoke("ajouter", New Object() {a, b}, callback, asyncState)
    End Function

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

Dieser Code mag auf den ersten Blick etwas komplex erscheinen. Wir müssen die Details jedoch nicht verstehen, um ihn nutzen zu können. Betrachten wir zunächst die Klassendeklaration:

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

Die Klasse trägt den Namen der Operationen des Webdienstes, für den sie erstellt wurde. Sie leitet sich von der Klasse „SoapHttpClientProtocol“ ab:

Image

Unsere Proxy-Klasse verfügt über einen einzigen Konstruktor:

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

Der Konstruktor weist dem Attribut „url“ die URL des mit dem Proxy verbundenen Webdienstes zu. Die oben gezeigte Operationsklasse definiert das Attribut „url“ nicht selbst. Es wird von der Klasse geerbt, von der der Proxy abgeleitet ist: System.Web.Services.Protocols.SoapHttpClientProtocol. Sehen wir uns nun an, was mit der Methode „add“ zusammenhängt:

    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

Wir sehen, dass sie dieselbe Signatur wie im Operations-Webdienst hat, wo sie wie folgt definiert wurde:

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

Die Art und Weise, wie diese Klasse mit dem Webdienst kommuniziert, wird hier nicht gezeigt. Diese Kommunikation wird vollständig von der übergeordneten Klasse System.Web.Services.Protocols.SoapHttpClientProtocol abgewickelt. Der Proxy enthält nur das, was ihn von anderen Proxys unterscheidet:

  • die URL des zugehörigen Webdienstes
  • die Definition der Methoden des zugehörigen Dienstes.

Um die Methoden des Operations-Webdienstes zu nutzen, benötigt ein Client lediglich die zuvor generierte Operations-Proxy-Klasse. Kompilieren wir diese Klasse in eine Assembly-Datei:

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

Schreiben wir nun einen Konsolen-Client. Er wird ohne Parameter aufgerufen und führt die über die Tastatur eingegebenen Befehle aus:

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

Der Code des Kunden lautet wie folgt:


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

Wir betrachten hier nur den Code, der speziell für die Verwendung der Proxy-Klasse relevant ist. Zunächst wird ein Proxy-Operations-Objekt erstellt:


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

Die Zeilen a und b werden über die Tastatur eingegeben. Auf Grundlage dieser Informationen werden die entsprechenden Proxy-Methoden aufgerufen:


            ' make a request to the web service
            If Not erreurCommande Then
                Try
                    Dim résultat As Double
                    Dim résultats() As Double
                    If fonction = "ajouter" Then
                        résultat = myOperations.ajouter(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "soustraire" Then
                        résultat = myOperations.soustraire(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "multiplier" Then
                        résultat = myOperations.multiplier(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "diviser" Then
                        résultat = myOperations.diviser(a, b)
                        Console.Out.WriteLine(("résultat=" + résultat.ToString))
                    End If
                    If fonction = "toutfaire" Then
                        résultats = myOperations.toutfaire(a, b)
                        Console.Out.WriteLine(("résultats=[" + résultats(0).ToString + "," + résultats(1).ToString + "," + _
                        résultats(2).ToString + "," + résultats(3).ToString + "]"))
                    End If
                Catch e As Exception
                    Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
                End Try

Hier beschäftigen wir uns zum ersten Mal mit der All-in-One-Funktion, die alle vier Operationen ausführt. Sie wurde bisher ignoriert, da sie ein Array von Zahlen zurückgibt, das in einem XML-Wrapper gekapselt ist, dessen Handhabung komplizierter ist als die einfachen XML-Antworten der anderen Funktionen, die nur ein einziges Ergebnis zurückgeben. Hier sehen wir anhand der Proxy-Klasse, dass die Verwendung der All-in-One-Methode nicht komplizierter ist als die Verwendung der anderen Methoden. Die Anwendung wurde in einem DOS-Fenster wie folgt kompiliert:

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. Konfigurieren eines Webdienstes

Ein Webdienst benötigt möglicherweise Konfigurationsinformationen, um korrekt initialisiert zu werden. Bei IIS können diese Informationen in einer Datei namens web.config abgelegt werden, die sich im selben Ordner wie der Webdienst befindet. Angenommen, wir möchten einen Webdienst erstellen, der zur Initialisierung zwei Informationen benötigt: einen Namen und ein Alter. Diese beiden Informationen können im folgenden Format in die Datei web.config eingefügt werden:

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

Die Initialisierungseinstellungen werden in einem XML-Container abgelegt:

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

Ein Initialisierungsparameter namens P mit dem Wert V wird mit der folgenden Zeile deklariert:


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

Wie ruft der Webdienst diese Informationen ab? Wenn IIS einen Webdienst lädt, prüft es, ob sich im selben Ordner eine web.config-Datei befindet. Ist dies der Fall, liest es diese. Der Wert V eines Parameters P wird mithilfe der Anweisung abgerufen:

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

wobei ConfigurationSettings eine Klasse im Namespace System.Configuration ist.

Testen wir diese Technik anhand des folgenden Webdienstes:


<%@ WebService language="VB" class=personne %>
 
Imports System.Web.Services
imports System.Configuration
 
<WebService([Namespace] := "st.istia.univ-angers.fr")> _
Public Class personne
   Inherits WebService
 
   ' attributes
   Private nom As String
   Private age As Integer
 
   ' manufacturer
   Public Sub New()
      ' init attributes
      nom = ConfigurationSettings.AppSettings("nom")
      age = Integer.Parse(ConfigurationSettings.AppSettings("age"))
   End Sub
 
   <WebMethod>  _
   Function id() As String
      Return "[" + nom + "," + age.ToString + "]"
   End Function 
 
End Class 

Der Webdienst „Person“ verfügt über zwei Attribute, „name“ und „age“, die in seinem parameterlosen Konstruktor mit Werten initialisiert werden, die aus der Konfigurationsdatei „web.config“ des Webdienstes „Person“ gelesen werden. Diese Datei sieht wie folgt aus:


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

Der Webdienst verfügt außerdem über eine <WebMethod> ohne Parameter, die lediglich die Attribute „name“ und „age“ zurückgibt. Der Dienst ist in der Quelldatei „personne.asmx“ registriert, die sich zusammen mit ihrer Konfigurationsdatei im Ordner „c:\inetpub\wwwroot\st\personne“ befindet:

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

Verknüpfen wir den virtuellen IIS-Ordner /config mit dem oben genannten physischen Ordner. Starten Sie IIS und rufen Sie dann über einen Browser die URL http://localhost/config/personne.asmx für den Person-Dienst auf:

Image

Folgen Sie dem Link für die Single-ID-Methode:

Image

Die id-Methode hat keine Parameter. Verwenden wir die Schaltfläche „Aufrufen“:

Image

Wir haben die in der Datei „web.config“ des Dienstes gespeicherten Informationen erfolgreich abgerufen.

10.10. Der Webdienst zur Steuerberechnung

Wir kehren zur mittlerweile vertrauten IMPOTS-Anwendung zurück. Als wir das letzte Mal damit gearbeitet haben, haben wir sie in einen Remote-Server umgewandelt, auf den über das Internet zugegriffen werden kann. Nun werden wir sie in einen Webdienst umwandeln.

10.10.1. Der Webdienst

Wir beginnen mit der Klasse impôt, die im Kapitel über Datenbanken erstellt wurde und auf Informationen aus einer ODBC-Datenbank basiert:


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

Im Webdienst kann nur ein parameterloser Konstruktor verwendet werden. Daher sieht der Klassenkonstruktor wie folgt aus:


    ' manufacturer
    Public Sub New()
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception
 
        ' retrieve the service configuration parameters
        Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
        Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
        Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
        Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
        Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")
 
        ' database operation
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' base connection chain

Die fünf Parameter des Konstruktors aus der vorherigen Klasse werden nun aus der web.config-Datei des Dienstes gelesen. Der Code in der Quelldatei impots.asmx lautet wie folgt. Er enthält den Großteil des vorherigen Codes. Wir haben lediglich die für den Webdienst spezifischen Codeabschnitte umschlossen:


<%@ WebService language="VB" class=impots %>
 
' creation of a tax web service
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections
Imports System.Configuration
Imports System.Web.Services
 
<WebService([Namespace]:="st.istia.univ-angers.fr")> _
Public Class impôt
    Inherits WebService
 
    ' data required for tax calculation
    ' come from an external source
    Private limites(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""
 
 
    ' manufacturer
    Public Sub New()
        ' initializes the three limit arrays, coeffR, coeffN from
        ' the contents of the Timpots table in the ODBC DSNimpots database
        ' colLimites, colCoeffR, colCoeffN are the three columns of this table
        ' can throw an exception
 
        ' retrieve the service configuration parameters
        Dim DSNimpots As String = ConfigurationSettings.AppSettings("DSN")
        Dim Timpots As String = ConfigurationSettings.AppSettings("TABLE")
        Dim colLimites As String = ConfigurationSettings.AppSettings("COL_LIMITES")
        Dim colCoeffR As String = ConfigurationSettings.AppSettings("COL_COEFFR")
        Dim colCoeffN As String = ConfigurationSettings.AppSettings("COL_COEFFN")
 
        ' database operation
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' base connection chain
        Dim impotsConn As OdbcConnection = Nothing     ' the connection
        Dim sqlCommand As OdbcCommand = Nothing     ' the SQL command
        Dim myReader As OdbcDataReader     ' odbc data reader
 
        ' the SELECT query
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
 
        ' tables to retrieve data
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList
 
        ' attempt to access the database
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' create a command object
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' execute the query
            myReader = sqlCommand.ExecuteReader()
            ' Using the recovered table
            While myReader.Read()
                ' the data of the current line are put in the tables
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' freeing up resources
            myReader.Close()
            impotsConn.Close()
 
            ' dynamic tables are placed in static tables
            Me.limites = New Decimal(tLimites.Count) {}
            Me.coeffR = New Decimal(tLimites.Count) {}
            Me.coeffN = New Decimal(tLimites.Count) {}
            Dim i As Integer
            For i = 0 To tLimites.Count - 1
                limites(i) = Decimal.Parse(tLimites(i).ToString())
                coeffR(i) = Decimal.Parse(tCoeffR(i).ToString())
                coeffN(i) = Decimal.Parse(tCoeffN(i).ToString())
            Next i
            ' it's good
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' error
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub
 
    ' tAX CALCULATION
    <WebMethod()> _
    Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' calculating the number of shares
        Dim nbParts As Decimal
        If marié Then
            nbParts = CDec(nbEnfants) / 2 + 2
        Else
            nbParts = CDec(nbEnfants) / 2 + 1
        End If
        If nbEnfants >= 3 Then
            nbParts += 0.5D
        End If
        ' calculation of taxable income & family quota
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' tAX CALCULATION
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' return result
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
 
    ' id
    <WebMethod()> _
    Function id() As String
        ' to see if everything is OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

Lassen Sie uns die wenigen Änderungen erläutern, die an der Klasse „imports“ vorgenommen wurden und über das hinausgehen, was notwendig ist, um sie in einen Webdienst umzuwandeln:

  • Das Einlesen der Datenbank im Konstruktor kann fehlschlagen. Deshalb haben wir unserer Klasse zwei Attribute und eine Methode hinzugefügt:
    • Das boolesche Attribut `OK` ist wahr, wenn die Datenbank gelesen werden konnte, andernfalls falsch
    • Die Zeichenkette `errMessage` enthält eine Fehlermeldung, falls die Datenbank nicht gelesen werden konnte.
    • Die parameterlose Methode `id` ruft die Werte dieser beiden Attribute ab.
  • Um mögliche Fehler beim Datenbankzugriff zu behandeln, wurde der Teil des Konstruktor-Codes, der sich auf diesen Zugriff bezieht, in einen try-catch-Block eingeschlossen.

Die Datei web.config für die Dienstkonfiguration sieht wie folgt aus:


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

Beim ersten Versuch, den impots-Dienst zu laden, meldete der Compiler, dass er den in der Anweisung verwendeten Namespace „Microsoft.Data.Odbc“ nicht finden konnte:

Imports Microsoft.Data.Odbc

Nach Durchsicht der Dokumentation

  • wurde eine Kompilierungsanweisung zur Datei web.config hinzugefügt, um festzulegen, dass die Microsoft.Data.odbc-Assembly verwendet werden soll
  • wurde eine Kopie der Datei microsoft.data.odbc.dll im bin-Ordner des Projekts abgelegt. Dieser Ordner wird vom Webdienst-Compiler systematisch durchsucht, wenn er nach einer „Assembly“ sucht.

Andere Lösungen scheinen möglich, wurden hier jedoch nicht untersucht. Die Konfigurationsdatei sieht nun wie folgt aus:


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

Der Inhalt des Ordners „imports\bin“:

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

Der Dienst und seine Konfigurationsdatei wurden in „impots“ abgelegt:

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

Der physische Ordner für den Webdienst wurde im IIS dem virtuellen Ordner /impots zugeordnet. Die Dienstseite sieht dann wie folgt aus:

Image

Wenn Sie dem id-Link folgen:

Image

Wenn Sie die Schaltfläche „Anrufen“ verwenden:

Image

Das vorherige Ergebnis zeigt die Werte der Attribute „OK“ (true) und „errMessage“ („“) an. In diesem Beispiel wurde die Datenbank erfolgreich geladen. Dies war jedoch nicht immer der Fall, weshalb wir die Methode „id“ hinzugefügt haben, um auf die Fehlermeldung zuzugreifen. Der Fehler bestand darin, dass der DSN-Name der Datenbank als Benutzer-DSN definiert worden war, obwohl er als System-DSN hätte definiert werden müssen. Diese Unterscheidung wird im 32-Bit-ODBC-Source-Manager getroffen:

Kehren wir zur Serviceseite zurück:

Image

Klicken wir auf den Link „Berechnen“:

Image

Wir definieren die Aufrufparameter und führen den Aufruf aus:

Image

Das Ergebnis ist korrekt.

10.10.2. Generieren Sie den Proxy für den impots-Dienst

Da wir nun über einen funktionierenden „impots“-Webdienst verfügen, können wir dessen Proxy-Klasse generieren. Denken Sie daran, dass diese von Client-Anwendungen verwendet wird, um transparent auf den „impots“-Webdienst zuzugreifen. Zunächst verwenden wir das Dienstprogramm „wsdl“, um die Quelldatei für die Proxy-Klasse zu generieren, die anschließend zu einer DLL kompiliert wird.

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. Verwendung des Proxys mit einem Client

Im Kapitel über Datenbanken haben wir eine Konsolenanwendung zur Berechnung von Steuern erstellt:

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

Das Programm „testimpots“ verwendete dann die Standardsteuerklasse, die in der Datei „impots.dll“ enthalten ist. Der Code für das Programm „testimpots.vb“ lautete wie folgt:


Option Explicit On 
Option Strict On
 
' namespaces
Imports System
Imports Microsoft.VisualBasic
 
' test pg
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' interactive tax calculator
        ' the user enters three data points on the keyboard: married nbEnfants salary
        ' the program then displays the tax payable
        Const syntaxe1 As String = "pg DSNimpots tabImpots colLimites colCoeffR colCoeffN"
        Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"
 
        ' checking program parameters
        If arguments.Length <> 5 Then
            ' error msg
            Console.Error.WriteLine(syntaxe1)
            ' end
            Environment.Exit(1)
        End If        'if
        ' retrieve the arguments
        Dim DSNimpots As String = arguments(0)
        Dim tabImpots As String = arguments(1)
        Dim colLimites As String = arguments(2)
        Dim colCoeffR As String = arguments(3)
        Dim colCoeffN As String = arguments(4)
 
        ' tax object creation
        Dim objImpôt As impôt = Nothing
        Try
            objImpôt = New impôt(DSNimpots, tabImpots, colLimites, colCoeffR, colCoeffN)
        Catch ex As Exception
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
            Environment.Exit(2)
        End Try
 
        ' infinite loop
        While True
            ' initially no errors
            Dim erreur As Boolean = False
 
            ' tax calculation parameters are requested
            Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
            Dim paramètres As String = Console.In.ReadLine().Trim()
 
            ' anything to do?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
 
            ' check the number of arguments in the input line
            Dim args As String() = paramètres.Split(Nothing)
            Dim nbParamètres As Integer = args.Length
            If nbParamètres <> 3 Then
                Console.Error.WriteLine(syntaxe2)
                erreur = True
            End If
            Dim marié As String
            Dim nbEnfants As Integer
            Dim salaire As Integer
            If Not erreur Then
                ' checking the validity of parameters
                ' married
                marié = args(0).ToLower()
                If marié <> "o" And marié <> "n" Then
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                    erreur = True
                End If
                ' nbEnfants
                nbEnfants = 0
                Try
                    nbEnfants = Integer.Parse(args(1))
                    If nbEnfants < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntaxe2 + "\nArgument nbEnfants incorrect : tapez un entier positif ou nul")
                    erreur = True
                End Try
                ' salary
                salaire = 0
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine(syntaxe2 + "\nArgument salaire incorrect : tapez un entier positif ou nul")
                    erreur = True
                End Try
            End If
            If Not erreur Then
                ' parameters are correct - tax is calculated
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

Wir verwenden dasselbe Programm, um nun den impots-Webdienst über die zuvor erstellte impots-Proxy-Klasse zu nutzen. Dazu müssen wir den Code leicht anpassen:

  • Während die ursprüngliche Steuerklasse einen Konstruktor mit fünf Argumenten hatte, verfügt die Steuer-Proxy-Klasse über einen Konstruktor ohne Parameter. Wie wir gesehen haben, werden die fünf Parameter nun in der Konfigurationsdatei des Webdienstes festgelegt.
  • Daher ist es nicht mehr erforderlich, diese fünf Parameter als Argumente an das Testprogramm zu übergeben

Der neue Code lautet wie folgt:


Imports System
Imports Microsoft.VisualBasic
 
' test pg
Module testimpots
 
    Public Sub Main(ByVal arguments() As String)
        ' interactive tax calculator
        ' the user enters three data points on the keyboard: married nbEnfants salary
        ' the program then displays the tax payable
        Const syntaxe2 As String = "syntaxe : marié nbEnfants salaire" + ControlChars.Lf + "marié : o pour marié, n pour non marié" + ControlChars.Lf + "nbEnfants : nombre d'enfants" + ControlChars.Lf + "salaire : salaire annuel en F"
 
        ' tax object creation
        Dim objImpôt As impôt = Nothing
        Try
            objImpôt = New impôt
        Catch ex As Exception
            Console.Error.WriteLine(("L'erreur suivante s'est produite : " + ex.Message))
            Environment.Exit(2)
        End Try
 
        ' infinite loop
        Dim erreur As Boolean
        While True
            ' initially no error
            erreur = False
            ' tax calculation parameters are requested
            Console.Out.Write("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :")
            Dim paramètres As String = Console.In.ReadLine().Trim()
            ' anything to do?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' check the number of arguments in the input line
            Dim args As String() = paramètres.Split(Nothing)
            Dim nbParamètres As Integer = args.Length
            If nbParamètres <> 3 Then
                Console.Error.WriteLine(syntaxe2)
                erreur = True
            End If
            If Not erreur Then
                ' checking parameter validity
                ' married
                Dim marié As String = args(0).ToLower()
                If marié <> "o" And marié <> "n" Then
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument marié incorrect : tapez o ou n"))
                    erreur = True
                End If
                ' nbEnfants
                Dim nbEnfants As Integer = 0
                Try
                    nbEnfants = Integer.Parse(args(1))
                    If nbEnfants < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument nbEnfants incorrect : tapez un entier positif ou nul"))
                    erreur = True
                End Try
                ' salary
                Dim salaire As Integer = 0
                Try
                    salaire = Integer.Parse(args(2))
                    If salaire < 0 Then
                        Throw New Exception
                    End If
                Catch
                    Console.Error.WriteLine((syntaxe2 + ControlChars.Lf + "Argument salaire incorrect : tapez un entier positif ou nul"))
                    erreur = True
                End Try
                ' if the parameters are correct - the tax is calculated
                If Not erreur Then Console.Out.WriteLine(("impôt=" + objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

Wir haben die impots.dll-Proxy-Datei und den Quellcode von testimpots im selben Ordner.

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

Wir kompilieren die Quelldatei „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

und führen Sie es dann aus:

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

Wir erhalten das erwartete Ergebnis.