Skip to content

10. Webdienste

10.1. Einführung

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

  • Sie werden von Webservern gehostet, und das Protokoll für den Client-Server-Austausch ist daher HTTP (HyperText Transport Protocol), ein Protokoll, das auf TCP-IP aufbaut.
  • Der Webdienst verfügt unabhängig vom angebotenen Dienst über ein Standard-Kommunikationsprotokoll. Ein Webdienst bietet verschiedene Dienste an: S1, S2, …, Sn. Jeder dieser Dienste erwartet vom Client übergebene Parameter und liefert diesem ein Ergebnis zurück. Für jeden Dienst muss der Client Folgendes wissen:
    • den genauen Namen des Dienstes, falls
    • die Liste der Parameter, die bereitgestellt werden müssen, sowie deren Typ
    • den Typ des vom Dienst zurückgegebenen Ergebnisses

Sobald diese Elemente bekannt sind, folgt der Client-Server-Dialog unabhängig vom abgefragten Webdienst dem gleichen Format. Die Programmierung der Clients wird dadurch standardisiert.

  • Aus Sicherheitsgründen vor Angriffen aus dem Internet verfügen viele Organisationen über private Netzwerke und öffnen nur bestimmte Ports ihrer Server für das Internet: im Wesentlichen Port 80 des Webdienstes. Alle anderen Ports sind gesperrt. Daher werden Client-Server-Anwendungen, wie sie im vorigen Kapitel vorgestellt wurden, innerhalb des privaten Netzwerks (Intranet) aufgebaut und sind in der Regel von außen nicht zugänglich. Die Bereitstellung eines Dienstes auf einem Webserver macht ihn für die gesamte Internet-Community zugänglich.
  • Der Webdienst lässt sich als entferntes Objekt modellieren. Die angebotenen Dienste werden dann zu Methoden dieses Objekts. Ein Client kann auf dieses entfernte Objekt zugreifen, als wäre es lokal. Dadurch wird der gesamte Teil der Netzwerkkommunikation verborgen, und es kann ein von dieser Schicht unabhängiger Client entwickelt werden. Sollte sich diese Schicht ändern, muss der Client nicht angepasst werden. Dies ist ein enormer Vorteil und wahrscheinlich der wichtigste Trumpf der Webdienste.
  • Wie bei den im vorigen Kapitel vorgestellten TCP/IP-Client-Server-Anwendungen können Client und Server in einer beliebigen Sprache geschrieben werden. Sie tauschen Textzeilen aus. Diese bestehen aus zwei Teilen:
    • die für das Protokoll HTTP erforderlichen Kopfzeilen
    • den Hauptteil der Nachricht. Bei einer Antwort des Servers an den Client hat dieser das Format XML (eXtensible Markup Language). Bei einer Anfrage des Clients an den Server kann der Hauptteil der Nachricht verschiedene Formen annehmen, darunter XML. Die Anfrage XML des Clients kann ein spezielles Format haben, das als SOAP (Simple Object Access Protocol) bezeichnet wird. In diesem Fall folgt auch die Antwort des Servers dem Format SOAP.

10.2. Browser und XML

Webdienste senden XML an ihre Clients. Browser können beim Empfang dieses XML-Datenstroms unterschiedlich reagieren. Der Internet Explorer verfügt über ein vordefiniertes Stylesheet, das die Anzeige ermöglicht. Netscape Communicator verfügt hingegen nicht über dieses Stylesheet und zeigt den empfangenen XML-Code nicht an. Man muss den Quellcode der empfangenen Seite anzeigen, um Zugriff auf den XML-Code zu erhalten. Hier 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>

Der Internet Explorer zeigt die folgende Seite an:

Image

während Netscape Navigator Folgendes anzeigt:

Image

Betrachtet man den Quellcode der von Netscape empfangenen Seite, erhält man:

Image

Netscape hat zwar dasselbe wie der Internet Explorer empfangen, es jedoch anders dargestellt. Im weiteren Verlauf werden wir für die Screenshots den Internet Explorer verwenden.

10.3. Ein erster Webdienst

Wir werden Webdienste anhand eines sehr einfachen Beispiels kennenlernen, das in drei Varianten vorliegt.

10.3.1. Version 1

Für diese erste Version verwenden wir VS.NET, das den Vorteil bietet, ein sofort einsatzbereites Webdienst-Grundgerüst zu generieren. Sobald wir diese Architektur verstanden haben, können wir anfangen, auf eigenen Beinen zu stehen. Das wird Gegenstand der folgenden Versionen sein.

Erstellen wir mit VS.NET ein neues Projekt mit der Option [Fichier/Nouveau/Projet]:

Image

Beachten Sie dabei folgende Punkte:

  • Der Projekttyp ist Visual Basic (linker Rahmen)
  • Die Projektvorlage ist „Webdienst ASP.NET“ (rechter Rahmen)
  • Der Speicherort ist frei wählbar. Hier wird der Webdienst auf einem lokalen Webserver mit der Bezeichnung IIS gehostet. Die Adresse lautet daher http://localhost/[chemin], wobei URL noch festgelegt werden muss. Hier wählen wir den Pfad http://localhost/polyvbnet/demo. VS.NET erstellt daraufhin einen Ordner für dieses Projekt. Wo? Der Server IIS verfügt über ein Stammverzeichnis für die Baumstruktur der von ihm bereitgestellten Webdokumente. Nennen wir dieses Stammverzeichnis <IISroot>. Es entspricht dem 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 somit der Ordner e:\inetpub\wwwroot\polyvbnet\demo:

Image

Wie immer wurden übermäßig viele Ordner erstellt. Diese sind nicht immer von Bedeutung. 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 wieder, die im physischen Projektordner vorhanden sind. Am interessantesten für uns ist die Datei mit der Endung „asmx“. Dies ist die Endung für Webdienste. Ein Webdienst wird von VS.NET wie eine Windows-Anwendung verwaltet, nämlich c.a.d – eine Anwendung mit einer grafischen Benutzeroberfläche und Code zu deren Verwaltung. Aus diesem Grund haben wir ein Entwurfsfenster:

Image

Ein Webdienst verfügt normalerweise nicht über eine grafische Benutzeroberfläche. Er stellt ein Objekt dar, das aus der Ferne aufgerufen werden kann. Er verfügt über Methoden, die von Anwendungen aufgerufen werden. Wir betrachten ihn daher als klassisches Objekt mit der Besonderheit, dass er aus der Ferne über das Netzwerk instanziiert werden kann. Daher werden wir das von VS.NET angezeigte Entwurfsfenster nicht verwenden. Wenden wir uns stattdessen dem Code des Dienstes zu, indem wir die Option „Ansicht/Code“ nutzen:

Image

Es sind mehrere Punkte zu beachten:

  • Die Datei heißt Service1.asmx.vb und nicht Service1.asmx. Wir werden etwas später auf den Inhalt der Datei Service1.asmx zurückkommen.
  • Es erscheint ein Codefenster, das dem ähnelt, das wir hatten, als wir Windows-Anwendungen mit VS.NET erstellt haben

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 „Vom Web-Service-Designer generierter Code“

    Public Sub New()
        MyBase.New()

        'Dieser Aufruf wird vom Web-Service-Designer benötigt.
        InitializeComponent()

        'Fügen Sie Ihren Initialisierungscode nach dem Aufruf von InitializeComponent() ein

    End Sub

    'Vom Web-Service-Designer erforderlich
    Private components As System.ComponentModel.IContainer

    'REMARQUE: Die folgende Prozedur wird vom Web-Service-Designer benötigt
    'Sie kann mithilfe des Webdienst-Designers geändert werden.  
    'Ändern Sie sie nicht mit dem 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: Diese Vorgehensweise wird vom Web-Service-Designer benötigt
        'Ändern Sie dies nicht mit dem Code-Editor.
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

#Ende des Bereichs

    ' EXEMPLE DE SERVICE WEB
    ' Das Beispiel für den Dienst HelloWorld() gibt die Zeichenfolge „Hello World“ zurück.
    ' Um das Projekt zu generieren, kommentieren Sie die folgenden Zeilen nicht aus, speichern Sie das Projekt und generieren Sie es.
    ' Um diesen Webdienst zu testen, stellen Sie sicher, dass die .asmx-Datei als Startseite festgelegt ist
    ' und klicken Sie auf F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = „Hello World“
    ' End Function

End Class

Zunächst ist festzuhalten, dass es sich hier um eine Klasse handelt, nämlich 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 Namensraum „System.Web.Services“ importieren müssen:


Imports System.Web.Services

Der Deklaration der Klasse ist ein Kompilierungsattribut vorangestellt:


<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 es sich bei der nachfolgenden Klasse um einen Webdienst handelt. Dieses Attribut akzeptiert verschiedene Parameter, darunter einen namens NameSpace. Er dient dazu, den Webdienst in einen Namensraum einzuordnen. Man kann sich nämlich vorstellen, dass es weltweit mehrere Webdienste mit dem Namen „meteo“ gibt. Wir benötigen eine Möglichkeit, sie voneinander zu unterscheiden. Dies wird durch den Namensraum ermöglicht. Der eine könnte „[espacenom1].meteo“ heißen, der andere „[espacenom2].meteo“. Hier handelt es sich um ein Konzept, das den Namensräumen von Klassen ähnelt. VS.NET hat automatisch Code generiert und diesen in einen Bereich des Quellcodes eingefügt:

#Region „Vom Webdienst-Designer generierter Code“

Wenn man sich diesen Code ansieht, erkennt man denselben, den der Designer generierte, als wir Windows-Anwendungen entwickelten. Diesen Code kann man schlicht und einfach entfernen, wenn keine grafische Benutzeroberfläche vorhanden ist, was bei Webdiensten der Fall sein wird.

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


#Ende Region

    ' EXEMPLE DE SERVICE WEB
    ' Das Dienstbeispiel HelloWorld() gibt die Zeichenfolge „Hello World“ zurück.
    ' Um das Projekt zu generieren, kommentieren Sie die folgenden Zeilen nicht aus, speichern Sie das Projekt und generieren Sie es.
    ' Um diesen Webdienst zu testen, stellen Sie sicher, dass die .asmx-Datei als Startseite festgelegt ist
    ' und klicken Sie auf 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 ist die Sache etwas übersichtlicher.

  • Ein Webdienst ist eine Klasse, die von der Klasse WebService abgeleitet ist
  • Die Klasse wird durch das Attribut <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")> qualifiziert. Wir ordnen unseren Dienst also dem Namensraum st.istia.univ-angers.fr zu.
  • Die Methoden der Klasse werden durch das Attribut <WebMethod()> qualifiziert, 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 also „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 wir den Webserver IIS, falls dies noch nicht geschehen ist
  • verwenden wir die Option „Debuggen/Ohne Debugging ausführen“. VS.NET

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

Image

Warum die URL http://localhost/polyvbnet/demo/Service1.asmx? Weil dies 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 sollte. Dazu klickt man mit der rechten Maustaste auf die betreffende .asmx-Datei und wählt die Option [Définir comme page de démarrage].

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 an den Server IIS, die angibt:

  • dass es sich um einen Webdienst handelt (Schlüsselwort WebService)
  • dass die Sprache der Klasse dieses Webdienstes Visual Basic ist (Language="vb")
  • dass der Quellcode dieser Klasse in der Datei Service1.asmx.vb zu finden ist (Codebehind="Service1.asmx.vb")
  • dass die Klasse, die den Dienst implementiert, demo.Bonjour heißt (Class="demo.Bonjour"). Man beachte, dass VS.NET die Klasse „Bonjour“ im Namensraum „demo“ platziert hat, der auch der Name des Projekts ist.

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

Image

Wer hat den Code HTML der obigen Seite geschrieben? Nicht wir, das wissen wir. Es ist IIS, das Webdienste auf standardisierte Weise präsentiert. Diese Seite bietet uns zwei Links an. Folgen wir dem ersten, [Description du service]:

Image

Ups… das ist ein ziemlich kryptisches XML. Beachten wir dennoch das 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. Wir erinnern uns also daran, dass die URL http://serviceweb?WSDL Zugriff auf die Beschreibung XML des Webdienstes gewährt. Kehren wir zur Startseite zurück und wählen wir den Link [Bonjour] aus. Denken wir daran, dass „Bonjour“ eine Methode des Webdienstes ist. Hätten wir mehrere Methoden gehabt, wären sie alle hier aufgeführt worden. Wir gelangen auf die folgende neue Seite:

Image

Wir haben die angezeigte Seite bewusst gekürzt, um unsere Demonstration nicht zu überfrachten. Beachten wir noch einmal die angezeigte URL:

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

Wenn wir diese URL direkt in einen Browser eingeben, erhalten wir dasselbe Ergebnis wie oben. Wir werden dazu aufgefordert, die Schaltfläche „[Appeler]“ zu verwenden. Machen wir das. Wir gelangen auf eine neue Seite:

Image

Das ist wieder XML. Dort finden wir zwei Informationen, die bereits in unserem Webdienst vorhanden waren:

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

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

        Return "bonjour !"

Was haben wir gelernt?

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

Wir beschäftigen uns nun damit, einen Webdienst ohne die Hilfe von VS.NET zu schreiben.

10.3.2. Version 2

Im vorherigen Beispiel hat VS.NET vieles von selbst erledigt. Ist es möglich, einen Webdienst ohne dieses Tool zu erstellen? Die Antwort lautet „Ja“, und das zeigen wir nun. 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 wurde in die Datei „demo2.vb“ eingefügt, die sich wiederum im Verzeichnisbaum des Servers „IIS“ im Ordner „E:\Inetpub\wwwroot\polyvbnet\demo2“ befindet. Es handelt sich um eine klassische VB.NET-Klasse, die somit 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 Baugruppe demo2.dll in einem Ordner namens „bin“ ab (dieser Name ist obligatorisch):

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

Nun erstellen wir die Datei demo2.asmx. Diese wird von den Web-Clients aufgerufen. Ihr Inhalt lautet wie folgt:

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

Diese Anweisung ist uns bereits bekannt. Sie besagt, dass:

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

Nun können wir verschiedene Tests durchführen. Wir vergewissern uns, dass IIS aktiv ist, und rufen mit einem Browser die URL http://localhost/polyvbnet/demo2/demo2.asmx auf:

Image

Anschließend die URL http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Dann URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, wobei getBonjour der Name der einzigen Methode unseres Webdienstes ist:

Image

Wir verwenden die oben genannte Schaltfläche [Appeler]:

Image

Wir erhalten tatsächlich das Ergebnis des Aufrufs der Methode getBonjour des Webdienstes. Wir wissen nun, wie man einen Webdienst ohne vs.net aufbaut. Wir werden nun die Art und Weise, wie der Webdienst aufgebaut ist, außer Acht lassen und uns ausschließlich auf die grundlegenden Dateien konzentrieren.

10.3.3. Version 3

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

  • eine .asmx-Datei, den Einstiegspunkt des Webdienstes
  • eine .vb-Datei, die den Quellcode des Webdienstes enthält

Wir zeigen hier, dass die .asmx-Datei allein ausreicht. Der Code des Webdienstes 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 stellen fest, 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 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 wesentlichen Unterschied zur vorherigen Version fest: Wir mussten den Code VB des Dienstes nicht kompilieren. IIS hat diese Kompilierung selbst über den auf demselben Rechner installierten Compiler VB.NET durchgeführt. Anschließend hat er die Seite ausgegeben. Sollte ein Kompilierungsfehler auftreten, wird dieser von IIS gemeldet:

Image

10.3.4. Version 4

Wir befassen uns hier mit der Konfiguration des Servers IIS. Bisher haben wir unsere Webdienste immer im Stammverzeichnis <IISroot> des Servers IIS, hier [e:\inetpub\wwwroot], abgelegt. Wir zeigen hier, dass wir den Webdienst an beliebiger Stelle platzieren können. Dies geschieht mithilfe der virtuellen Ordner von IIS. Platzieren wir unseren Dienst im folgenden Ordner:

Image

Der Ordner [D:\data\devel\vbnet\poly\chap9\demo3] befindet sich nicht in der Verzeichnisstruktur des Servers IIS. Dies muss dem Server mitgeteilt werden, indem ein virtueller Ordner „IIS“ angelegt wird. Starten wir IIS und wählen wir die untenstehende Option [Avancé] aus:

Image

Es wird eine Liste virtueller Verzeichnisse angezeigt. Wir werden uns damit nicht näher befassen. Erstellen Sie ein neues virtuelles Verzeichnis mit der Schaltfläche [Ajouter] oben:

Image

Mit der Schaltfläche [Parcourir] 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] im Netzwerk über die URL [http://<machine>/virdemo3] zugänglich sind. Das oben gezeigte Dialogfeld enthält weitere Einstellungen, die wir unverändert lassen. Wir bestätigen die Eingaben. Der neue virtuelle Ordner erscheint in der Liste der virtuellen Ordner von IIS:

Image

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

Image

10.3.5. Fazit

Wir haben verschiedene Vorgehensweisen zur Erstellung eines Webdienstes aufgezeigt. Im weiteren Verlauf werden wir die Methode aus Version 3 für die Erstellung des Dienstes und die Methode 4 für dessen Lokalisierung verwenden. Somit benötigen wir VS.NET nicht. Dennoch sei angemerkt, dass die Verwendung von VS.NET aufgrund der Unterstützung beim Debuggen von Vorteil ist. Es gibt kostenlose Tools zur Entwicklung von Webanwendungen, insbesondere das von Microsoft gesponserte Produkt WebMatrix, das unter 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

Wir betrachten einen Webdienst, der fünf Funktionen bietet:

  1. add(a,b), das a+b zurückgibt
  2. subtrahieren(a,b), das a-b zurückgibt
  3. multiplier(a,b), das a*b zurückgibt
  4. divide(a,b), der a/b zurückgibt
  5. „alles“(a,b), der das Array [a+b, a-b, a*b, a/b] zurückgibt

Der Code VB.NET 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 greifen hier einige bereits gegebene Erläuterungen wieder auf, die jedoch einer Wiederholung oder Ergänzung bedürfen. Die Klasse operations ähnelt einer Klasse VB.NET, wobei jedoch einige Punkte zu beachten sind:

  • Den Methoden ist ein Attribut <WebMethod()> vorangestellt, das dem Compiler angibt, welche Methoden „veröffentlicht“ werden sollen (c.a.d) und dem Client zur Verfügung gestellt werden sollen. Eine Methode, der dieses Attribut nicht vorangestellt ist, wäre für Remote-Clients unsichtbar. Dabei könnte es sich um eine interne Methode handeln, die von anderen Methoden verwendet wird, aber nicht zur Veröffentlichung bestimmt ist.
  • Die Klasse leitet sich von der Klasse WebService ab, die im Namensraum System.Web.Services definiert ist. Diese Vererbung ist nicht immer zwingend erforderlich. Insbesondere in diesem Beispiel könnte man darauf verzichten.
  • Der Klasse selbst ist ein Attribut <WebService(Namespace="st.istia.univ-angers.fr")> vorangestellt, das dem Webdienst einen Namensraum zuweisen soll. Ein Klassenanbieter weist seinen Klassen einen Namensraum zu, um ihnen einen eindeutigen Namen zu geben und so Konflikte mit Klassen anderer Anbieter zu vermeiden, die denselben Namen tragen könnten. Bei Webdiensten verhält es sich genauso. Jeder Webdienst muss durch einen eindeutigen Namen identifiziert werden können, in diesem Fall durch st.istia.univ-angers.fr.
  • Wir haben keinen Konstruktor definiert. Daher wird implizit der Konstruktor der übergeordneten Klasse verwendet.

Der vorstehende Quellcode ist nicht direkt für den Compiler VB.NET bestimmt, sondern für den Webserver IIS. Er muss das Suffix .asmx tragen und im Verzeichnisbaum des Webservers gespeichert werden. Hier speichern wir ihn unter dem Namen operations.asmx im Ordner <IISroot>\polyvbnet\operations:

Image

Diesem physischen Ordner ordnen wir den virtuellen Ordner IIS [operations] zu:

Rufen wir den Dienst mit einem Browser auf. Die abzufragende Datei URl ist [http://localhost/operations/operations.asmx]:

Image

Wir erhalten ein Webdokument mit einem Link für jede der im Webdienst operations definierten Methoden. Folgen wir dem Link ajouter:

Image

Auf der angezeigten Seite wird uns angeboten, die Methode ajouter zu testen, indem wir ihr die beiden erforderlichen Argumente a und b übergeben. Erinnern wir uns an die Definition der Methode ajouter:

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

Es ist zu beachten, dass die Seite die Namen der Argumente a und b übernommen hat, die in der Methodendefinition verwendet wurden. Wir klicken auf die Schaltfläche Appeler und erhalten die folgende Antwort in einem separaten Browserfenster:

Image

Wenn man oben stattdessen [Affichage/Source] eingibt, erhält man den folgenden Code:

Image

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

Image

Wir erhalten die folgende Seite:

Image

Verwenden wir die Schaltfläche [Appeler] oben:

Image

In jedem Fall hat die Antwort des Servers folgende Form:

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • Die Antwort hat das Format XML
  • Zeile 1 ist standardmäßig und immer in der Antwort enthalten
  • Die folgenden Zeilen hängen von der Art des Ergebnisses (double, ArrayOfDouble), der Anzahl der Ergebnisse und dem Namensraum des Webdienstes (hier st.istia.univ-angers.fr) ab.

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

Image

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

Image

Image

Image

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

Hinweis: In den ersten 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 Anfragen vom Typ GET akzeptiert. Das bedeutet jedoch nicht, dass man keine Webdienste schreiben kann, die Anfragen nach dem Muster GET akzeptieren, insbesondere mit anderen Tools als VS.NET oder einfach von Hand.

10.5. Ein HTTP-POST-Client

Wir folgen der vom Webdienst vorgeschlagenen Vorgehensweise:

Image

Lassen Sie uns den Text erläutern. Zunächst muss der Web-Client die folgenden HTTP-Header senden:

POST /operations/operations.asmx/ajouter HTTP/1.1
Der Web-Client sendet eine Anfrage an POST an URL /operations/operations.asmx/ajouter gemäß dem Protokoll HTTP Version 1.1
HOST: localhost
Hier wird der Zielrechner der Anfrage angegeben. In diesem Fall „localhost“. Dieser Header wurde durch Version 1.1 des Protokolls HTTP verbindlich vorgeschrieben
Content-Type: application/x-www-form-urlencoded
Hier wird angegeben, dass nach den Headern HTTP zusätzliche Parameter im URL-kodierten Format gesendet werden. Bei diesem Format werden bestimmte Zeichen durch ihren Hexadezimalcode ersetzt.
Content-length: 7
Dies ist die Zeichenanzahl der Parameterzeichenfolge, die nach den Headern HTTP gesendet wird.

Auf die Header HTTP folgt eine Leerzeile, dann die Parameterzeichenfolge von POST mit [Content-Length] Zeichen in der Form a=XX&b=YY, wobei XX und YY die „url-kodierten“ Zeichenfolgen der Parameterwerte a und b sind. Wir wissen nun genug, um das oben Beschriebene mit unserem generischen TCP-Client nachzubilden, der bereits im Kapitel über die 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 Fenster unter DOS
dos>clttcpgenerique localhost 80
Commandes :
POST /operations/operations.asmx/ajouter HTTP/1.1
HOST: localhost
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-length: 7

<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:17 GMT
<-- X-Powered-By: ASP.NET
<--
a=2&b=3
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 13:55:26 GMT
<-- X-Powered-By: ASP.NET
<-- Connection: close
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>
[fin du thread de lecture des réponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]

Zunächst ist anzumerken, 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 erforderlich. Wenn dies nicht angegeben wird, hält der Server die Verbindung standardmäßig offen. Die Antwort des Servers besteht jedoch aus einer Folge von Textzeilen, von denen die letzte nicht mit einem Zeilenendezeichen endet. Unser generischer Client TCP liest Textzeilen, die mit einem Zeilenendezeichen enden, mit der Methode ReadLine. Wenn der Server die Verbindung nach dem Senden der letzten Zeile nicht schließt, bleibt der Client hängen, da er auf ein Zeilenendezeichen wartet, das nicht kommt. Wenn der Server die Verbindung schließt, wird die Methode ReadLine des Clients beendet und der Client bleibt nicht hängen.

Unmittelbar nach dem Empfang der leeren Zeile, die das Ende der Header signalisiert (HTTP), sendet der Server (IIS) 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 den Kopfzeilen HTTP besteht, teilt dem Kunden mit, dass er die 7 Zeichen senden kann, die er senden wollte. Was wir tun:

a=2&b=3

Hier ist zu beachten, dass unser TCP-Client mehr als 7 Zeichen sendet, da er diese mit einem Zeilenendezeichen (WriteLine) versendet. Das stellt für den Server kein Problem dar, da er von den empfangenen Zeichen nur die ersten 7 übernimmt und die Verbindung anschließend geschlossen wird (Connection: close). Diese überzähligen 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 notwendigen Elemente, um einen Client für unseren Webdienst zu programmieren. Es handelt sich um einen Konsolen-Client mit dem Namen 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 der Parameter 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 „Funktion“ die aufgerufene Webdienstfunktion ist (addieren, subtrahieren, multiplizieren, dividieren) und „a“ und „b“ die Werte, auf die diese Funktion angewendet wird. Zum Beispiel:

ajouter 6 7

Daraufhin sendet der Client die erforderliche Anfrage HTTP an den Webserver und erhält eine Antwort. Der Austausch zwischen Client und Server wird auf dem Bildschirm dargestellt, um den Ablauf besser verständlich zu machen:

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]

Oben ist der bereits bekannte Datenaustausch mit dem generischen TCP-Client zu sehen, mit einem Unterschied: Der Header „HTTP Connection: Keep-Alive“ fordert den Server auf, die Verbindung nicht zu schließen. Diese bleibt somit für den nächsten Vorgang des Clients offen, der sich daher nicht erneut mit dem Server verbinden muss. Dies zwingt ihn jedoch dazu, eine andere Methode als ReadLine() zu verwenden, um die Antwort des Servers zu lesen, da bekannt ist, dass es sich dabei um eine Folge von Zeilen handelt, von denen die letzte nicht mit einem Zeilenendezeichen endet. Sobald die gesamte Antwort des Servers empfangen wurde, analysiert der Client diese, um das Ergebnis der angeforderten Operation zu finden und anzuzeigen:

[résultat=13]

Sehen wir uns den Code unseres Clients an:


' Namensräume
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic
Imports System.Web

' Webdienst-Client „operations“
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"}

        ' Anzahl der Argumente
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' man beachte die angeforderte URI
        Dim URIstring As String = args(0)

        ' Verbindung zum Server herstellen
        Dim uri As Uri = Nothing        ' l'URI du service web
        Dim client As TcpClient = Nothing        ' la liaison tcp du client avec le serveur
        Dim [IN] As StreamReader = Nothing        ' le flux de lecture du client
        Dim OUT As StreamWriter = Nothing        ' le flux d'écriture du client
        Try
            ' Verbindung zum Server
            uri = New Uri(URIstring)
            client = New TcpClient(uri.Host, uri.Port)
            ' Die Ein- und Ausgabeströme des Clients TCP werden erstellt
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
        Catch ex As Exception
            ' URI falsch oder anderes Problem
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

        ' Erstellung eines Funktionsverzeichnisses für den Webdienst
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i

        ' Die Benutzeranfragen werden über die Tastatur eingegeben
        ' in der Form „Funktion a b“
        ' sie enden mit dem Befehl „end“
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande
        Dim fonction As String = Nothing        ' nom d'une fonction du service web
        Dim a, b As String        ' les arguments des fonctions du service web

        ' Eingabeaufforderung an den Benutzer
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")

        ' Fehlerbehandlung
        Dim erreurCommande As Boolean
        Try
            ' Schleife zur Erfassung der über die Tastatur eingegebenen Befehle
            While True
                ' zunächst kein Fehler
                erreurCommande = False
                ' Befehl auslesen
                commande = Console.In.ReadLine().Trim().ToLower()
                ' Fertig?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' Aufteilung des Befehls in Felder
                champs = Regex.Split(commande, "\s+")
                Try
                    ' Es sind drei Felder erforderlich
                    If champs.Length <> 3 Then
                        Throw New Exception
                    End If
                    ' Feld 0 muss eine erkannte Funktion sein
                    fonction = champs(0)
                    If Not dicoFonctions.ContainsKey(fonction) Then
                        Throw New Exception
                    End If
                    ' Feld 1 muss eine gültige Zahl sein
                    a = champs(1)
                    Double.Parse(a)
                    ' Feld 2 muss eine gültige Zahl sein
                    b = champs(2)
                    Double.Parse(b)
                Catch
                    ' Ungültiger Befehl
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    erreurCommande = True
                End Try
                ' Die Anfrage wird an den Webdienst gesendet
                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
        ' Ende der Client-Server-Verbindung
        Try
            [IN].Close()
            OUT.Close()
            client.Close()
        Catch
        End Try
    End Sub
...........
    ' Anzeige der Fehler
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' Fehleranzeige
        System.Console.Error.WriteLine(msg)
        ' Beendigung mit Fehler
        Environment.Exit(exitCode)
    End Sub
End Module

Hier finden sich Elemente, die wir bereits mehrfach gesehen haben und die keiner besonderen Erläuterung bedürfen. Sehen wir uns nun den Code der Methode executeFonction an, in der die Neuerungen enthalten sind:


    ' 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)
        ' Führung der Funktion (a, b) auf dem Webdienst mit der URI URI
        ' Der Client-Server-Austausch erfolgt über die Datenströme IN und OUT
        ' Das Ergebnis der Funktion steht in der Zeile
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' vom Server gesendet

        ' Erstellung der Abfragezeichenfolge
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' Erstellung des Header-Arrays HTTP zum Senden
        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) = ""

        ' Die Header HTTP werden an den Server gesendet
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' Senden an den Server
            OUT.WriteLine(entetes(i))
            ' Bildschirmanzeige
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

        ' Die erste Antwort des Webservers wird gelesen: HTTP/1.1 100
        Dim ligne As String = Nothing
        ' eine Zeile aus dem Lesestrom
        ligne = [IN].ReadLine()
        While ligne <> ""
            'Echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' nächste Zeile
            ligne = [IN].ReadLine()
        End While
        'Echo der letzten Zeile
        Console.Out.WriteLine(("<-- " + ligne))

        ' Senden der Abfrageparameter
        OUT.Write(requête)
        ' Echo
        Console.Out.WriteLine(("--> " + requête))

        ' Erstellung des regulären Ausdrucks zur Ermittlung der Antwortgröße XML
        ' im Datenstrom der Antwort des Webservers
        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

        ' Auslesen der zweiten Antwort des Webservers nach dem Senden der Anfrage
        ' Der Wert der Zeile „Content-Length“ wird gespeichert
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' Bildschirmoutput
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' nächste Zeile
            ligne = [IN].ReadLine()
        End While
        ' Ausgabe der letzten Zeile
        Console.Out.WriteLine("<--")

        ' Erstellung des regulären Ausdrucks zum Abrufen des Ergebnisses
        ' im Antwortstrom des Webservers
        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

        ' Der Rest der Antwort des Webservers wird gelesen
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' Die Antwort wird in Textzeilen zerlegt
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' die Textzeilen werden nach dem Ergebnis durchsucht
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' Weiter
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' Vergleich der aktuellen Zeile mit dem Muster
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' Wurde etwas gefunden?
            If MatchRésultat.Success Then
                ' Das Ergebnis wird notiert
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' Das Ergebnis wird angezeigt
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

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


        ' Erstellung der Abfragezeichenfolge
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' Erstellung des Header-Arrays HTTP zum Senden
        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) = ""

        ' Die Header HTTP werden an den Server gesendet
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' Senden an den Server
            OUT.WriteLine(entetes(i))
            ' Bildschirmanzeige
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

Im Header

--> Content-Length: 7

muss die Größe der Parameter angegeben werden, die vom Client hinter den Headern HTTP gesendet werden:

--> a=6&b=7

Dazu wird der folgende Code verwendet:


        ' Erstellung der Abfragezeichenfolge
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

Die Methode HttpUtility.UrlEncode(Zeichenkette) wandelt bestimmte Zeichen von chaîne in %n1n2 um, wobei n1n2 der Code ASCII des umgewandelten Zeichens ist. Die von dieser Umwandlung betroffenen Zeichen sind alle Zeichen, die in einer Abfrage eine besondere Bedeutung haben (Leerzeichen, das Gleichheitszeichen =, das Und-Zeichen &, ...). Hier ist die Methode HttpUtility.UrlEncode normalerweise nicht erforderlich, da a und b Zahlen sind, die keines dieser speziellen Zeichen enthalten. Sie wird hier lediglich als Beispiel verwendet. Sie benötigt den Namensraum System.Web. Sobald der Client seine Header HTTP 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 Header HTTP 100 Continue:

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

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


        ' Die erste Antwort des Webservers wird gelesen: HTTP/1.1 100
        Dim ligne As String = Nothing
        ' Eine Zeile aus dem Lesestrom
        ligne = [IN].ReadLine()
        While ligne <> ""
            'Echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' nächste Zeile
            ligne = [IN].ReadLine()
        End While
        'Echo der letzten Zeile
        Console.Out.WriteLine(("<-- " + ligne))

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

--> a=6&b=7

Dies geschieht mit dem folgenden Code:

        ' Anfrageparameter senden
        OUT.Write(requête)
        ' Ausgabe
        Console.Out.WriteLine(("--> " + requête))

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

  1. den Kopfzeilen HTTP, die mit einer Leerzeile enden
  2. die Antwort im Format XML
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 03 Mar 2004 14:56:38 GMT
<-- X-Powered-By: ASP.NET
<-- X-AspNet-Version: 1.1.4322
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<--
<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">13</double>

Zunächst liest der Client die Header HTTP, um darin die Zeile Content-Length zu finden und die Größe der Antwort XML (hier 90) zu ermitteln. Diese wird mithilfe eines regulären Ausdrucks ermittelt. Man hätte es auch anders und zweifellos effizienter machen können.


        ' Erstellung des regulären Ausdrucks zur Ermittlung der Antwortgröße XML
        ' im Datenstrom der Antwort des Webservers
        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

        ' Auslesen der zweiten Antwort des Webservers nach dem Senden der Anfrage
        ' Der Wert der Zeile „Content-Length“ wird gespeichert
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' Bildschirmoutput
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' nächste Zeile
            ligne = [IN].ReadLine()
        End While
        ' Ausgabe der letzten Zeile
        Console.Out.WriteLine("<--")

Sobald man die Länge N der Antwort XML kennt, muss man nur noch N Zeichen aus dem Datenstrom IN der Serverantwort auslesen. Diese Zeichenkette aus N Zeichen wird für die Bildschirmüberwachung wieder in Textzeilen zerlegt. Unter diesen Zeilen sucht man nach der Zeile mit dem Ergebnis:

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

wobei auch hier wieder ein regulärer Ausdruck verwendet wird. Sobald das Ergebnis gefunden wurde, wird es angezeigt.

[résultat=13]

Der Client-Code endet wie folgt:


        ' Erstellung des regulären Ausdrucks zum Abrufen des Ergebnisses
        ' im Antwortstrom des Webservers
        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

        ' Der Rest der Antwort des Webservers wird gelesen
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' Die Antwort wird in Textzeilen zerlegt
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' die Textzeilen werden nach dem Ergebnis durchsucht
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' Weiter
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' Vergleich der aktuellen Zeile mit dem Muster
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' Wurde etwas gefunden?
            If MatchRésultat.Success Then
                ' Das Ergebnis wird notiert
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' Das Ergebnis wird angezeigt
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.6. Ein Client SOAP

Wir betrachten hier einen zweiten Client, der einen Client-Server-Dialog vom Typ SOAP (Simple Object Access Protocol) verwendet. Ein Beispiel für einen Dialog wird uns für die Funktion ajouter vorgestellt:

Image

Image

Die Anfrage des Clients ist eine Anfrage vom Typ POST. Wir werden also einige der Mechanismen des vorherigen Clients wiederfinden. Der Hauptunterschied besteht darin, dass der Client HTTP-POST die Parameter a und b in der Form

    a=A&b=B

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

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 er eine Antwort mit dem Code XML, 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 doch um denselben Mechanismus HTTP wie beim Mandanten HTTP-POST. Der Code für den Client SOAP kann somit an den des Clients HTTP-POST angelehnt 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 Client SOAP sendet die Header HTTP seiner 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)
        ' führt die Funktion (a, b) auf dem Webdienst mit der URI URI aus
        ' Der Datenaustausch zwischen Client und Server erfolgt über die Datenströme IN und OUT
        ' Das Ergebnis der Funktion steht in der Zeile
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' vom Server gesendet
        ' Erstellung der Abfragezeichenfolge 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

        ' Erstellung des Header-Arrays HTTP zum Senden
        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) = ""

        ' Die Header HTTP werden an den Server gesendet
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' Senden an den Server
            OUT.WriteLine(entetes(i))
            ' Bildschirmanzeige
            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 Lesecode dieser ersten Antwort lautet wie folgt:


        ' Die erste Antwort des Webservers wird gelesen: HTTP/1.1 100
        Dim ligne As String = Nothing
        ' eine Zeile aus dem Lesestrom
        ligne = [IN].ReadLine()
        While ligne <> ""
            'Echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' nächste Zeile
            ligne = [IN].ReadLine()
        End While        'while
        'Echo der letzten Zeile
        Console.Out.WriteLine(("<-- " + ligne))

Der Client sendet nun seine Parameter im Format XML in einem sogenannten „Umschlag“ SOAP:

--> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</soap:Body>
</soap:Envelope>

Der Code:

1
2
3
4
        ' Senden der Abfrageparameter
        OUT.Write(requêteSOAP)
        ' Echo
        Console.Out.WriteLine(("--> " + requêteSOAP))

Der Server sendet daraufhin 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 Kunde zeigt die empfangenen Kopfzeilen HTTP auf dem Bildschirm an und sucht dabei nach der Zeile Content-Length:


        ' Erstellung des regulären Ausdrucks zur Ermittlung der Größe der Antwort XML
        ' im Antwortstrom des Webservers
        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

        ' Auslesen der zweiten Antwort des Webservers nach dem Absenden der Anfrage
        ' Der Wert der Zeile „Content-Length“ wird gespeichert
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' Bildschirmalarm
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' nächste Zeile
            ligne = [IN].ReadLine()
        End While        'while
        ' Ausgabe der letzten Zeile
        Console.Out.WriteLine("<--")

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


        ' Erstellung des regulären Ausdrucks zum Abrufen des Ergebnisses
        ' im Antwortstrom des Webservers
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' Der Rest der Antwort des Webservers wird gelesen
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' Die Antwort wird in Textzeilen zerlegt
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' die Textzeilen werden nach dem Ergebnis durchsucht
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' Weiter
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' Vergleich der aktuellen Zeile mit dem Muster
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' Wurde etwas gefunden?
            If MatchRésultat.Success Then
                ' Das Ergebnis wird notiert
                strRésultat = MatchRésultat.Groups(1).Value
            End If
            'nächste Zeile
        Next i
        ' das Ergebnis wird angezeigt
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.7. Kapselung des Client-Server-Datenaustauschs

Nehmen wir an, unser Webdienst operations wird von verschiedenen Anwendungen genutzt. Es wäre sinnvoll, diesen eine Klasse zur Verfügung zu stellen, die als Schnittstelle zwischen der Client-Anwendung und dem Webdienst fungiert und den Großteil des Netzwerkverkehrs verbirgt, der für die meisten Entwickler nicht trivial ist. Damit ergäbe sich folgendes Schema:

Die Client-Anwendung würde sich an die Client-Server-Schnittstelle wenden, um ihre Anfragen an den Webdienst zu stellen. Diese würde den gesamten erforderlichen Netzwerkverkehr mit dem Server abwickeln und das erhaltene Ergebnis an die Client-Anwendung zurückgeben. Letztere müsste sich nicht mehr um den Austausch mit dem Server kümmern, was ihre Programmierung erheblich vereinfachen würde.

10.7.1. Die Kapselungsklasse

Nach den Ausführungen in den vorangegangenen Abschnitten sind wir nun mit dem Datenaustausch zwischen Client und Server bestens vertraut. Wir haben sogar drei Methoden kennengelernt. Wir entscheiden uns dafür, die Methode SOAP zu kapseln. Die Klasse sieht wie folgt aus:


' Namensräume
Imports System
Imports System.Net.Sockets
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports System.Web
Imports Microsoft.VisualBasic

' clientSOAP des Webdienstes „operations“
Public Class clientSOAP

    ' Instanzvariablen
    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
    ' Funktionswörterbuch
    Private dicoFonctions As New Hashtable
    ' Funktionsliste
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' Ausführlichkeit
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

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

        ' Ausführliche Ausgabe
        Me.verbose = verbose

        ' Verbindung zum Server
        uri = New Uri(uriString)
        client = New TcpClient(uri.Host, uri.Port)

        ' Eingabe- und Ausgabeströme des Clients erstellen TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' Erstellung des Funktionsverzeichnisses des Webdienstes
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
    End Sub

    ' Schließen der Verbindung zum Server
    Public Sub Close()
        ' Beendigung der Client-Server-Verbindung
        [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
        ' Führung der Funktion (a, b) auf dem Webdienst mit der URI URI
        ' Der Client-Server-Austausch erfolgt über die Datenströme IN und OUT
        ' Das Ergebnis der Funktion steht in der Zeile
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' vom Server gesendet

        ' Ist die Funktion gültig?
        fonction = fonction.Trim().ToLower()
        If Not dicoFonctions.ContainsKey(fonction) Then
            Return "[fonction [" + fonction + "] indisponible : (ajouter, soustraire,multiplier,diviser)]"
        End If

        ' Sind die Argumente a und b gültig?
        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 durch Null?
        If fonction = "diviser" And doubleB = 0 Then
            Return "[division par zéro]"
        End If

        ' Erstellung der Abfragezeichenfolge 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

        ' Erstellung des zu sendenden Header-Arrays HTTP
        Dim entetes(6) As String
        entetes(0) = "POST " + uri.AbsolutePath + " HTTP/1.1"
        entetes(1) = "Host: " + uri.Host + ":" + uri.Port.ToString
        entetes(2) = "Content-Type: text/xml; charset=utf-8"
        entetes(3) = "Content-Length: " + nbCharsSOAP.ToString
        entetes(4) = "Connection: Keep-Alive"
        entetes(5) = "SOAPAction: ""st.istia.univ-angers.fr/" + fonction + """"
        entetes(6) = ""

        ' Die Header HTTP werden an den Server gesendet
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' Senden an den Server
            OUT.WriteLine(entetes(i))
            ' Bildschirmausgabe
            If verbose Then
                Console.Out.WriteLine(("--> " + entetes(i)))
            End If
        Next i

        ' Die erste Antwort des Webservers wird gelesen: HTTP/1.1 100
        Dim ligne As String = Nothing
        ' eine Zeile aus dem Lesestrom
        ligne = [IN].ReadLine()
        While ligne <> ""
            'Echo
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' nächste Zeile
            ligne = [IN].ReadLine()
        End While
        'Echo der letzten Zeile
        If verbose Then
            Console.Out.WriteLine(("<-- " + ligne))
        End If
        ' Senden der Abfrageparameter
        OUT.Write(requêteSOAP)
        ' Echo
        If verbose Then
            Console.Out.WriteLine(("--> " + requêteSOAP))
        End If

        ' Erstellung des regulären Ausdrucks zur Ermittlung der Antwortgröße XML
        ' im Datenstrom der Antwort des Webservers
        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

        ' Auslesen der zweiten Antwort des Webservers nach dem Senden der Anfrage
        ' Der Wert der Zeile „Content-Length“ wird gespeichert
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' Bildschirmalarm
            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
            ' nächste Zeile
            ligne = [IN].ReadLine()
        End While

        ' Ausgabe der letzten Zeile
        If verbose Then
            Console.Out.WriteLine("<--")
        End If

        ' Erstellung des regulären Ausdrucks zum Abrufen des Ergebnisses
        ' im Antwortstrom des Webservers
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' Der Rest der Antwort des Webservers wird gelesen
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' Die Antwort wird in Textzeilen zerlegt
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' die Textzeilen werden nach dem Ergebnis durchsucht
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' Weiter
            If verbose Then
                Console.Out.WriteLine(("<-- " + lignes(i)))
            End If            ' comparaison ligne courante au modèle
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' Wurde es gefunden?
            If MatchRésultat.Success Then
                ' Das Ergebnis wird notiert
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i

        ' Das Ergebnis wird zurückgegeben
        Return strRésultat
    End Function
End Class

Wir finden hier nichts Neues im Vergleich zu dem, was wir bereits gesehen haben. Wir haben lediglich den Code des untersuchten Clients SOAP übernommen und ihn ein wenig umgestaltet, um daraus eine Klasse zu erstellen. Diese verfügt über einen Konstruktor und zwei Methoden:


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


    ' Schließen der Verbindung zum Server
    Public Sub Close()

und hat folgende Attribute:


    ' Instanzvariablen
    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
    ' Funktionswörterbuch
    Private dicoFonctions As New Hashtable
    ' Funktionsliste
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' Ausführliche Ausgabe
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

Dem Konstruktor werden zwei Parameter übergeben:

  1. die URI des Webdienstes, mit dem er sich verbinden soll
  2. einen booleschen Wert verbose, der eigentlich festlegt, dass der Netzwerkverkehr auf dem Bildschirm angezeigt werden soll; andernfalls erfolgt keine Anzeige.

Während der Erstellung werden die Datenströme IN für den Netzwerk-Lesezugriff, OUT für den Netzwerk-Schreibzugriff sowie das Verzeichnis der vom Dienst verwalteten Funktionen erstellt. Sobald das Objekt erstellt ist, wird die Client-Server-Verbindung geöffnet und die Datenströme IN und OUT stehen zur Verfügung.

Mit der Methode Close lässt sich die Verbindung zum Server schließen.

Die Methode ExecuteFonction ist diejenige, die wir für den untersuchten Client SOAP geschrieben haben, bis auf einige Details:

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

In der Regel wird ein Client die Klasse clientSOAP wie folgt verwenden:

  1. Erstellung eines Objekts vom Typ clientSOAP, das die Verbindung zum Webservice herstellt
  2. wiederholte Verwendung der Methode executeFonction
  3. Schließen der Verbindung zum Webdienst mithilfe der Methode Close.

Betrachten wir einen ersten Client.

10.7.2. Ein Konsolen-Client

Wir greifen hier auf den Client SOAP zurück, den wir bereits untersucht haben, als die Klasse clientSOAP noch nicht existierte, und passen ihn so an, dass er nun diese Klasse verwendet:


' Namensräume
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

Public Module testClientSoap

    ' fordert den URI vom Webdienst „operations“ an
    ' führt die über die Tastatur eingegebenen Befehle interaktiv aus
    Public Sub Main(ByVal args() As String)
        ' Syntax
        Const syntaxe As String = "pg URI [verbose]"

        ' Anzahl der Argumente
        If args.Length <> 1 And args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' Ausführliche Ausgabe?
        Dim verbose As Boolean = False
        If args.Length = 2 Then
            verbose = args(1).ToLower() = "verbose"
        End If
        ' Verbindung zum Webdienst herstellen 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' Verbindungsfehler
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

        ' Die Benutzeranfragen werden über die Tastatur eingegeben
        ' in der Form „Funktion a b“ – sie enden mit dem Befehl „end“
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

        ' Eingabeaufforderung an den Benutzer
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b" + ControlChars.Lf)

        ' Fehlerbehandlung
        Dim erreurCommande As Boolean
        Try
            ' Schleife zur Erfassung der über die Tastatur eingegebenen Befehle
            While True
                ' zunächst kein Fehler
                erreurCommande = False
                ' Befehl auslesen
                commande = Console.In.ReadLine().Trim().ToLower()
                ' Fertig?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' Aufteilung des Befehls in Felder
                champs = Regex.Split(commande, "\s+")
                ' Es sind drei Felder erforderlich
                If champs.Length <> 3 Then
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    ' Fehler wird vermerkt
                    erreurCommande = True
                End If
                ' Anfrage an den Webdienst stellen
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
                ' Nächste Anfrage
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' Ende der Client-Server-Verbindung
        Try
            client.Close()
        Catch
        End Try
    End Sub

    ' Anzeige der Fehler
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' Fehleranzeige
        System.Console.Error.WriteLine(msg)
        ' Beendigung mit Fehler
        Environment.Exit(exitCode)
    End Sub
End Module

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

  1. die URI des Webdienstes operations
  2. das optionale Schlüsselwort verbose. Ist dieses vorhanden, wird der Netzwerkverkehr auf dem Bildschirm angezeigt.

Diese beiden Parameter werden verwendet, um ein Objekt clientSOAP zu erstellen, das den Datenaustausch mit dem Webdienst gewährleistet.


        ' Verbindung zum Webdienst wird hergestellt 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' Verbindungsfehler
            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 anschließend durch Aufruf der Methode executeFonction des Objekts clientSOAP an den Server gesendet.


                ' Anfrage an den Webdienst
                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 in eine „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 anschließend kompiliert durch:

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 nicht-verbose Ausführung:

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

Man kann den Netzwerkverkehr verfolgen, indem man eine „ausführliche“ Ausführung anfordert:

dos>testClientSOAP http://localhost/operations/operations.asmx ausführlich
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

Lassen Sie uns nun einen grafischen Client erstellen.

10.7.3. Ein grafischer Windows-Client

Wir werden nun unseren Webdienst mit einem grafischen Client abfragen, der ebenfalls die Klasse clientSOAP verwendet. Die grafische Benutzeroberfläche sieht wie folgt aus:

Die Steuerelemente sind folgende:

Nr.
Typ
Name
Rolle
1
TextBox
txtURI
URI des Webdienstes „Operations“
2
Schaltfläche
btnOuvrir
stellt die Verbindung zum Webdienst her
3
Schaltfläche
btnFermer
schließt die Verbindung zum Webdienst
4
ComboBox
cmbFonctions
die Liste der Funktionen (addieren, subtrahieren, multiplizieren, dividieren)
5
TextBox
txtA
Die Argumente der Funktionen
6
TextBox
txtB
das Argument b der Funktionen
7
TextBox
txtRésultat
das Ergebnis der Funktion(a,b)
8
Schaltfläche
btnCalculer
startet die Berechnung von Funktion(a,b)
9
TextBox
txtErreur
Zeigt eine Statusmeldung zur Verbindung an

Es gibt einige Einschränkungen bei der Funktionsweise:

  • Die Schaltfläche btnOuvrir ist nur aktiv, wenn das Feld txtURI nicht leer ist und noch keine Verbindung besteht
  • Die Schaltfläche btnFermer ist nur aktiv, wenn eine Verbindung zum Webdienst hergestellt wurde
  • Die Schaltfläche btnCalculer ist nur aktiv, wenn eine Verbindung geöffnet ist und die Felder txtA und txtB nicht leer sind
  • die Felder txtRésultat und txtErreur haben das Attribut ReadOnly auf „wahr“ gesetzt

Der Client stellt zunächst mithilfe der Schaltfläche [Ouvrir] eine Verbindung zum Webdienst her:

Image

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

Image

Image

Image

Image

Image

Der Anwendungscode folgt. Wir haben den Code des Formulars weggelassen, da er hier nicht von Interesse ist.


'Namensräume
Imports System
Imports System.Windows.Forms

' Formularklasse
Public Class FormClientSOAP
    Inherits System.Windows.Forms.Form

    ' Instanzattribute
    Dim client As clientSOAP    ' client SOAP du service web operations

#Region „Vom Windows-Form-Designer generierter Code“

    Public Sub New()
        MyBase.New()
        'Dieser Aufruf wird vom Windows-Form-Designer benötigt.
        InitializeComponent()
        ' Weitere Initialisierungen
        myInit()
    End Sub

    'Die überschriebene Dispose-Methode des Formulars dient zum Bereinigen der Komponentenliste.
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
    End Sub

...

    Private Sub InitializeComponent()
....
    End Sub

#Ende Region


    Private Sub myInit()
        ' Formular initialisieren
        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
        ' Der Inhalt des Eingabefelds hat sich geändert – der Status der Schaltfläche „Öffnen“ wird festgelegt
        btnOuvrir.Enabled = txtURI.Text.Trim <> ""
    End Sub

    Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
        ' Anfrage zum Aufbau einer Verbindung mit dem Webdienst
        Try
            ' Erstellung eines Objekts vom Typ [clientSOAP]
            client = New clientSOAP(txtURI.Text.Trim, False)
            ' Zustände der Schaltflächen
            btnOuvrir.Enabled = False
            btnFermer.Enabled = True
            ' Das Objekt URI kann nicht mehr geändert werden
            txtURI.ReadOnly = True
            ' Kundenzustand
            txtErreur.Text = "Liaison au service web ouverte"
        Catch ex As Exception
            ' Es ist ein Fehler aufgetreten – dieser wird angezeigt
            txtErreur.Text = ex.Message
        End Try
    End Sub

    Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
        ' Die Verbindung zum Webdienst wird geschlossen
        client.Close()
        ' Schaltflächenstatus
        btnOuvrir.Enabled = True
        btnFermer.Enabled = False
        ' URI
        txtURI.ReadOnly = False
        ' Kundenzustand
        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
        ' Berechnung einer Funktion f(a,b)
        ' Das vorherige Ergebnis wird gelöscht
        txtRésultat.Text = ""
        Try
            txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
        Catch ex As Exception
            ' Es ist ein Netzwerkfehler aufgetreten
            txtErreur.Text = ex.Message
            ' Verbindung wird geschlossen
            btnFermer_Click(Nothing, Nothing)
        End Try
    End Sub

    Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
        ' Änderung des Wertes von 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
        ' Änderung des Wertes von B
        txtA_TextChanged(Nothing, Nothing)
    End Sub

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

Auch hier verbirgt die Klasse clientSOAP den gesamten Netzwerkaspekt der Anwendung. Die Anwendung wurde wie folgt aufgebaut:

  • Die Assembly clientSOAP.dll, die die Klasse clientSOAP enthält, wurde im Projektordner abgelegt
  • Die grafische Benutzeroberfläche 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 anschließend mit folgendem Befehl gestartet:

dos>clientsoapgui

10.8. Ein Proxy-Client

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

Die Plattform .NET treibt diese Logik noch weiter voran. Sobald der anzusteuernde Webdienst bekannt ist, können wir automatisch die Klasse generieren, die uns als Vermittler dient, um auf die Funktionen des Webdienstes zuzugreifen, und die den gesamten Netzwerkteil verbirgt. Diese Klasse wird als Proxy für den Webdienst bezeichnet, für den sie generiert wurde.

Wie generiert man die Proxy-Klasse eines Webdienstes? Ein Webdienst wird immer von einer Beschreibungsdatei im Format XML begleitet. Wenn die URI-Datei unseres Webdienstes „operations“ http://localhost/operations/operations.asmx lautet, ist seine Beschreibungsdatei unter der Adresse http://localhost/operations/operations.asmx?wsdl verfügbar, wie der folgende Screenshot zeigt:

Image

Hier handelt es sich um eine Datei XML, die alle Funktionen des Webdienstes genau beschreibt, einschließlich des Typs und der Anzahl der Parameter sowie des Ergebnistyps für jede einzelne Funktion. Diese Datei wird als WSDL-Datei des Dienstes bezeichnet, da sie die Sprache WSDL (Web Services Description Language) verwendet. Aus dieser Datei lässt sich mithilfe des Tools wsdl eine Proxy-Klasse generieren:

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 Tool wsdl generiert eine Quelldatei VB.NET (Option /language=vb) mit dem Namen der Klasse, die den Webdienst implementiert, in diesem Fall operations. Sehen wir uns einen Ausschnitt aus dem generierten Code an:

'------------------------------------------------------------------------------
' <automatisch generiert>
'     Dieser Code wurde von einem Tool generiert.
'     Laufzeitversion: 1.1.4322.573
'
'     Änderungen an dieser Datei können zu Fehlfunktionen führen und gehen verloren, wenn 
'     der Code neu generiert wird.
' </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

'
'Dieser Quellcode wurde automatisch von wsdl generiert, 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

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

    '<Anmerkungen/>
    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 wirkt auf den ersten Blick etwas komplex. Wir müssen die Details jedoch nicht verstehen, um ihn nutzen zu können. Schauen wir uns zunächst die Klassendeklaration an:

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

Die Klasse trägt den Namen operations 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 den Wert URL des mit dem Proxy verbundenen Webdienstes zu. Die oben genannte Klasse operations definiert das Attribut url nicht selbst. Dieses wird von der Klasse geerbt, von der der Proxy abgeleitet ist: System.Web.Services.Protocols.SoapHttpClientProtocol. Sehen wir uns nun an, was sich auf die Methode ajouter bezieht:

    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

Man sieht, dass sie dieselbe Signatur aufweist wie im Webdienst operations, wo sie wie folgt definiert war:

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

Die Art und Weise, wie diese Klasse mit dem Webdienst kommuniziert, wird hier nicht dargestellt. Diese Kommunikation wird vollständig von der übergeordneten Klasse System.Web.Services.Protocols.SoapHttpClientProtocol übernommen. Im Proxy findet sich 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 Web-Service operations zu nutzen, benötigt ein Client lediglich die zuvor generierte Proxy-Klasse operations. Kompilieren wir diese Klasse in eine Datei namens assembly:

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

Schreiben wir nun einen Konsolen-Client. Er wird ohne Parameter aufgerufen und führt die über die Tastatur eingegebenen Anfragen 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 Kundencode lautet wie folgt:


' Namensräume
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic

Public Module testClientProxy

    ' führt die über die Tastatur eingegebenen Befehle interaktiv aus
    ' und sendet sie an den Webdienst „operations“
    Public Sub Main()
        ' Es gibt keine Argumente mehr – die URL des Webdienstes ist fest im Proxy hinterlegt

        ' Erstellung eines Verzeichnisses der Funktionen des Webdienstes
        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

        ' Es wird ein Operations-Proxy-Objekt erstellt 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' Verbindungsfehler
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

        ' Die Anfragen des Benutzers werden über die Tastatur eingegeben
        ' in der Form „Funktion a b“ – sie enden mit dem Befehl „end“
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

        ' Eingabeaufforderung an den Benutzer
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b" + ControlChars.Lf)

        ' einige lokale Daten
        Dim erreurCommande As Boolean
        Dim fonction As String
        Dim a, b As Double
        ' Schleife zur Erfassung der über die Tastatur eingegebenen Befehle
        While True
            ' zunächst kein Fehler
            erreurCommande = False
            ' Befehl auslesen
            commande = Console.In.ReadLine().Trim().ToLower()
            ' Fertig?
            If commande Is Nothing Or commande = "fin" Then
                Exit While
            End If
            ' Aufteilung des Befehls in Felder
            champs = Regex.Split(commande, "\s+")
            Try
                ' Es sind drei Felder erforderlich
                If champs.Length <> 3 Then
                    Throw New Exception
                End If
                ' Feld 0 muss eine erkannte Funktion sein
                fonction = champs(0)
                If Not dicoFonctions.ContainsKey(fonction) Then
                    Throw New Exception
                End If
                ' Feld 1 muss eine gültige Zahl sein
                a = Double.Parse(champs(1))
                ' Feld 2 muss eine gültige Zahl sein
                b = Double.Parse(champs(2))
            Catch
                ' ungültiger Befehl
                Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                erreurCommande = True
            End Try
            ' Die Anfrage wird an den Webdienst gesendet
            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

    ' Anzeige der Fehler
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' Fehleranzeige
        System.Console.Error.WriteLine(msg)
        ' Abbruch mit Fehler
        Environment.Exit(exitCode)
    End Sub
End Module

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


        ' Erstellung eines Proxy-Objekts für Operationen 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' Verbindungsfehler
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

Die Zeilen der Funktionen a und b werden über die Tastatur eingegeben. Anhand dieser Informationen werden die entsprechenden Methoden des Proxys aufgerufen:


            ' Anfrage an den Webdienst
            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 wird zum ersten Mal die Operation toutfaire verarbeitet, die die vier Operationen ausführt. Sie wurde bisher ignoriert, da sie ein Zahlenarray sendet, das in einem XML-Container gekapselt ist, der komplizierter zu handhaben ist als die einfachen XML-Antworten der anderen Funktionen, die nur ein einziges Ergebnis liefern. Man sieht, dass hier mit der Proxy-Klasse die Verwendung der Methode toutfaire 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. Einen Webdienst konfigurieren

Ein Webdienst benötigt möglicherweise Konfigurationsinformationen, um ordnungsgemäß initialisiert zu werden. Beim Server IIS können diese Informationen in einer Datei namens web.config gespeichert werden, die sich im selben Ordner wie der Webdienst befindet. Angenommen, man möchte einen Webdienst erstellen, der zur Initialisierung zwei Angaben benötigt: einen Namen und ein Alter. Diese beiden Angaben können in der Datei „web.config“ in folgender Form hinterlegt werden:

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

Die Initialisierungsparameter werden in einem Umschlag mit der Kennung XML abgelegt:

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

Ein Initialisierungsparameter mit dem Namen P und dem Wert V wird mit folgender Zeile deklariert:


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

Wie ruft der Webdienst diese Informationen ab? Wenn IIS einen Webdienst lädt, prüft er, ob sich im selben Ordner eine Datei mit dem Namen web.config befindet. Ist dies der Fall, liest er sie ein. Der Wert V eines Parameters P wird durch die Anweisung ermittelt:

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

wobei ConfigurationSettings eine Klasse im Namensraum 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

   ' Attribute
   Private nom As String
   Private age As Integer

   ' Konstruktor
   Public Sub New()
      ' Attribute initialisieren
      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 personne verfügt über zwei Attribute, nom und age, die in seinem Konstruktor ohne Parameter anhand der Werte initialisiert werden, die aus der Konfigurationsdatei web.config des Dienstes personne. Diese Datei lautet wie folgt:


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

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

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

Ordnen wir dem oben genannten physischen Ordner einen virtuellen Ordner „IIS/config“ zu. Starten wir „IIS“ und rufen wir anschließend mit einem Browser die URL http://localhost/config/personne.asmx des Dienstes „personne“ auf:

Image

Folgen wir dem Link der einzigen Methode-ID:

Image

Die Methode id hat keine Parameter. Verwenden wir die Schaltfläche Appeler:

Image

Wir haben die in der Datei web.config des Dienstes enthaltenen Informationen erfolgreich abgerufen.

10.10. Der Webdienst IMPOTS

Wir greifen nun auf die mittlerweile bekannte Anwendung IMPOTS zurück. Als wir das letzte Mal damit gearbeitet haben, hatten wir daraus einen Remote-Server gemacht, den man über das Internet aufrufen konnte. Jetzt machen wir daraus einen Webdienst.

10.10.1. Der Webdienst

Wir gehen von der Klasse impôt aus, die im Kapitel über Datenbanken erstellt wurde und auf den Informationen einer Datenbank namens ODBC basiert:


' Optionen
Option Strict On
Option Explicit On 

' Namensräume
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections

Public Class impôt
    ' die für die Steuerberechnung erforderlichen Daten
    ' stammen aus einer externen Quelle
    Private limites(), coeffR(), coeffN() As Decimal

    ' Hersteller
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' Es wird überprüft, ob alle drei Tabellen dieselbe Größe haben
        Dim OK As Boolean = LIMITES.Length = COEFFR.Length And LIMITES.Length = COEFFN.Length
        If Not OK Then
            Throw New Exception("Les 3 tableaux fournis n'ont pas la même taille(" & LIMITES.Length & "," & COEFFR.Length & "," & COEFFN.Length & ")")
        End If
        ' alles in Ordnung
        Me.limites = LIMITES
        Me.coeffR = COEFFR
        Me.coeffN = COEFFN
    End Sub

    ' Konstruktor 2
    Public Sub New(ByVal DSNimpots As String, ByVal Timpots As String, ByVal colLimites As String, ByVal colCoeffR As String, ByVal colCoeffN As String)
        ' initialisiert die drei Begrenzungstabellen coeffR, coeffN anhand
        ' des Inhalts der Tabelle „Timpots“ aus der Datenbank ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN sind die drei Spalten dieser Tabelle
        ' kann eine Ausnahme auslösen
        Dim connectString As String = "DSN=" + DSNimpots + ";"        ' chaîne de connexion à la base
        Dim impotsConn As OdbcConnection = Nothing        ' la connexion
        Dim sqlCommand As OdbcCommand = Nothing        ' la commande SQL
        ' die Abfrage SELECT
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' Tabellen zum Abrufen der Daten
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' Es wird versucht, auf die Datenbank zuzugreifen
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' ein Befehlsobjekt wird erstellt
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' Die Abfrage wird ausgeführt
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' Auswertung der abgerufenen Tabelle
        While myReader.Read()
            ' Die Daten der aktuellen Zeile werden in die Tabellen übertragen
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' Freigabe der Ressourcen
        myReader.Close()
        impotsConn.Close()

        ' Die dynamischen Arrays werden in statische Arrays umgewandelt
        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

    ' Berechnung der Steuer
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' Berechnung der Anzahl der Anteile
        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
        ' Berechnung des steuerpflichtigen Einkommens und des Familienquotienten
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' Steuerberechnung
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' Ergebnisrückgabe
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

Im Webdienst darf nur ein Konstruktor ohne Parameter verwendet werden. Daher wird der Konstruktor der Klasse wie folgt aussehen:


    ' Konstruktor
    Public Sub New()
        ' initialisiert die drei Grenzwerttabellen coeffR und coeffN anhand
        ' des Inhalts der Tabelle „Timpots“ aus der Datenbank ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN sind die drei Spalten dieser Tabelle
        ' kann eine Ausnahme auslösen

        ' hiermit werden die Konfigurationsparameter des Dienstes abgerufen
        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")

        ' die Datenbank wird abgefragt
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' chaîne de connexion à la base

Die fünf Parameter des Konstruktors der vorherigen Klasse werden nun aus der Datei web.config des Webdienstes gelesen. Der Code der Quelldatei impots.asmx lautet wie folgt. Er übernimmt den Großteil des vorherigen Codes. Wir haben uns darauf beschränkt, die für den Webdienst spezifischen Codeabschnitte einzurahmen:


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

' Erstellung eines Webdienstes für Steuern
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

    ' Die für die Steuerberechnung erforderlichen Daten
    ' stammen aus einer externen Quelle
    Private limites(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""


    ' Konstruktor
    Public Sub New()
        ' initialisiert die drei Begrenzungstabellen coeffR, coeffN anhand
        ' des Inhalts der Tabelle „Timpots“ aus der Datenbank ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN sind die drei Spalten dieser Tabelle
        ' kann eine Ausnahme auslösen

        ' hiermit werden die Konfigurationsparameter des Dienstes abgerufen
        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")

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

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

        ' Tabellen zum Abrufen der Daten
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' Es wird versucht, auf die Datenbank zuzugreifen
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' ein Befehlsobjekt wird angelegt
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' Die Abfrage wird ausgeführt
            myReader = sqlCommand.ExecuteReader()
            ' Auswertung der abgerufenen Tabelle
            While myReader.Read()
                ' Die Daten der aktuellen Zeile werden in die Arrays übertragen
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' Freigabe der Ressourcen
            myReader.Close()
            impotsConn.Close()

            ' Die dynamischen Arrays werden in statische Arrays übertragen
            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
            ' Alles in Ordnung
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' Fehler
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub

    ' Steuerberechnung
    <WebMethod()> _
    Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' Berechnung der Anzahl der Anteile
        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
        ' Berechnung des steuerpflichtigen Einkommens und des Familienquotienten
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' Steuerberechnung
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' Ergebnisrückgabe
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function

    ' ID
    <WebMethod()> _
    Function id() As String
        ' um zu prüfen, ob alles in Ordnung ist OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

Lassen Sie uns die wenigen Änderungen erläutern, die an der Klasse impots vorgenommen wurden, abgesehen von denen, die für die Umwandlung in einen Webdienst erforderlich waren:

  • Das Einlesen der Datenbank im Konstruktor kann fehlschlagen. Daher haben wir unserer Klasse zwei Attribute und eine Methode hinzugefügt:
    • Der boolesche Wert OK ist gleich vrai, wenn die Datenbank gelesen werden konnte, andernfalls gleich faux
    • Die Zeichenkette errMessage enthält eine Fehlermeldung, wenn die Datenbank nicht gelesen werden konnte.
    • Mit der Methode id ohne Parameter lassen sich die Werte dieser beiden Attribute abrufen.
  • Um einen möglichen Fehler beim Zugriff auf die Datenbank zu behandeln, wurde der für diesen Zugriff relevante Teil des Konstruktorkodes mit einem try-catch umschlossen.

Die Konfigurationsdatei web.config für den Dienst lautet wie folgt:


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

Bei einem ersten Versuch, den Service impots zu laden, meldete der Compiler, dass er den in der Anweisung verwendeten Namensraum Microsoft.Data.Odbc nicht finden konnte:

Imports Microsoft.Data.Odbc

Nach Einsicht in die Dokumentation

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

Andere Lösungen scheinen möglich zu sein, wurden hier jedoch nicht näher untersucht. Die Konfigurationsdatei lautet nun also:


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

Inhalt des Ordners impots\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 des Webdienstes wurde dem virtuellen Ordner /impots von IIS zugeordnet. Die Seite des Dienstes sieht nun wie folgt aus:

Image

Wenn man dem Link id folgt:

Image

Wenn man die Schaltfläche „Appeler“ verwendet:

Image

Das vorstehende Ergebnis zeigt die Werte der Attribute OK (true) und errMessage („“) an. In diesem Beispiel wurde die Datenbank korrekt geladen. Das war nicht immer der Fall, weshalb wir die Methode id hinzugefügt haben, um Zugriff auf die Fehlermeldung zu erhalten. Der Fehler bestand darin, dass der Name DSN 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-Quellmanager ODBC vorgenommen:

Kehren wir zur Seite des Dienstes zurück:

Image

Folgen wir dem Link calculer:

Image

Wir legen die Parameter für den Aufruf fest und führen diesen aus:

Image

Das Ergebnis ist korrekt.

10.10.2. Proxy für den Dienst „impots“ generieren

Da wir nun über einen funktionsfähigen Webdienst impots verfügen, können wir dessen Proxy-Klasse generieren. Zur Erinnerung: Diese wird von Client-Anwendungen verwendet, um den Webdienst impots transparent anzurufen. Zunächst verwenden wir das Dienstprogramm wsdl, um die Quelldatei der Proxy-Klasse zu generieren; anschließend wird diese zu einer DLL kompiliert.

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. Den Proxy mit einem Client verwenden

Im Kapitel über Datenbanken hatten wir eine Konsolenanwendung erstellt, mit der sich Steuern berechnen lassen:

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 damals die klassische Klasse impôt, die in der Datei impots.dll enthalten war. Der Programmcode von testimpots.vb lautete wie folgt:


Option Explicit On 
Option Strict On

' Namensräume
Imports System
Imports Microsoft.VisualBasic

' Test-PG
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' Interaktives Programm zur Steuerberechnung
        ' Der Benutzer gibt drei Daten über die Tastatur ein: verheiratet nbEnfants Gehalt
        ' Das Programm zeigt daraufhin die zu zahlende Steuer an
        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"

        ' Überprüfung der Programmparameter
        If arguments.Length <> 5 Then
            ' Fehlermeldung
            Console.Error.WriteLine(syntaxe1)
            ' Ende
            Environment.Exit(1)
        End If        'if
        ' Die Argumente werden abgerufen
        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)

        ' Erstellung eines Steuerobjekts
        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

        ' Endlosschleife
        While True
            ' zunächst keine Fehler
            Dim erreur As Boolean = False

            ' die Parameter für die Steuerberechnung werden abgefragt
            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()

            ' Was ist zu tun?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If

            ' Überprüfung der Anzahl der Argumente in der eingegebenen Zeile
            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
                ' Überprüfung der Gültigkeit der Parameter
                ' verheiratet
                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
                ' Gehalt
                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
                ' Die Parameter sind korrekt – die Steuer wird berechnet
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

Wir greifen auf dasselbe Programm zurück, um es nun den Webdienst impots über die zuvor erstellte Proxy-Klasse impots nutzen zu lassen. Dazu müssen wir den Code geringfügig anpassen:

  • Während die ursprüngliche Klasse impôt einen Konstruktor mit fünf Argumenten hatte, verfügt die Proxy-Klasse impots über einen Konstruktor ohne Parameter. Die fünf Parameter sind, wie wir gesehen haben, nun in der Konfigurationsdatei des Webdienstes festgelegt.
  • Es ist daher 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

' Testseite
Module testimpots

    Public Sub Main(ByVal arguments() As String)
        ' Interaktives Programm zur Steuerberechnung
        ' Der Benutzer gibt drei Daten über die Tastatur ein: verheiratet nbEnfants Gehalt
        ' Das Programm zeigt dann die zu zahlende Steuer an
        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"

        ' Erstellung eines Steuerobjekts
        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

        ' Endlosschleife
        Dim erreur As Boolean
        While True
            ' zunächst kein Fehler
            erreur = False
            ' Die Parameter für die Steuerberechnung werden abgefragt
            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()
            ' Was ist zu tun?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' Überprüfung der Anzahl der Argumente in der eingegebenen Zeile
            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
                ' Überprüfung der Gültigkeit der Parameter
                ' verheiratet
                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
                ' Gehalt
                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
                ' Wenn die Parameter korrekt sind, wird die Steuer berechnet
                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 den Proxy impots.dll und die Quelle 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 den Quellcode 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 wir ihn 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 tatsächlich das erwartete Ergebnis.