Skip to content

10. Usługi internetowe

10.1. Wprowadzenie

W poprzednim rozdziale przedstawiliśmy kilka aplikacji typu klient-serwer opartych na protokole TCP/IP. Ponieważ klienci i serwer wymieniają między sobą wiersze tekstu, mogą one być napisane w dowolnym języku programowania. Klient musi jedynie znać protokół komunikacji oczekiwany przez serwer. Usługi internetowe to aplikacje serwerowe oparte na protokole TCP/IP, charakteryzujące się następującymi cechami:

  • Są one hostowane przez serwery internetowe, a protokołem wymiany danych między klientem a serwerem jest zatem HTTP (HyperText Transport Protocol), protokół działający na poziomie wyższym niż TCP-IP.
  • Usługa internetowa posiada standardowy protokół komunikacyjny niezależnie od świadczonej usługi. Usługa internetowa oferuje różne usługi: S1, S2, …, Sn. Każda z nich oczekuje na parametry dostarczone przez klienta i zwraca mu wynik. W przypadku każdej usługi klient musi znać:
    • dokładną nazwę usługi oraz, jeśli
    • listę parametrów, które należy mu przekazać, oraz ich typ
    • typ wyniku zwracanego przez usługę

Gdy te elementy są już znane, komunikacja między klientem a serwerem przebiega według tego samego schematu, niezależnie od wywoływanej usługi internetowej. Dzięki temu sposób pisania kodu przez klientów jest ujednolicony.

  • Ze względów bezpieczeństwa przed atakami z Internetu wiele organizacji posiada sieci prywatne i udostępnia w Internecie jedynie niektóre porty swoich serwerów: głównie port 80 usługi internetowej. Wszystkie pozostałe porty są zablokowane. Dlatego też aplikacje typu klient-serwer, takie jak te przedstawione w poprzednim rozdziale, są tworzone w ramach sieci prywatnej (intranetu) i zazwyczaj nie są dostępne z zewnątrz. Umieszczenie usługi na serwerze internetowym sprawia, że staje się ona dostępna dla całej społeczności internetowej.
  • Usługę internetową można modelować jako obiekt zdalny. Oferowane usługi stają się wówczas metodami tego obiektu. Klient może uzyskać dostęp do tego obiektu zdalnego tak, jakby był on lokalny. Ukrywa to całą część związaną z komunikacją sieciową i pozwala na stworzenie klienta niezależnego od tej warstwy. Jeśli warstwa ta ulegnie zmianie, klient nie wymaga modyfikacji. Jest to ogromna zaleta i prawdopodobnie główny atut usług internetowych.
  • Podobnie jak w przypadku aplikacji klient-serwer TCP/IP omówionych w poprzednim rozdziale, zarówno klient, jak i serwer mogą być napisane w dowolnym języku programowania. Wymieniają one między sobą wiersze tekstu. Składają się one z dwóch części:
    • nagłówki wymagane przez protokół HTTP
    • treść wiadomości. W przypadku odpowiedzi serwera skierowanej do klienta ma ona format XML (eXtensible Markup Language). W przypadku żądania klienta skierowanego do serwera treść wiadomości może przybierać różne formy, w tym XML. Żądanie klienta o formacie XML może mieć specjalny format zwany SOAP (Simple Object Access Protocol). W takim przypadku odpowiedź serwera również ma format SOAP.

10.2. Przeglądarki i XML

Usługi internetowe wysyłają dane w formacie XML do swoich klientów. Przeglądarki mogą różnie reagować na odbiór tego strumienia danych XML. Przeglądarka Internet Explorer posiada predefiniowany arkusz stylów, który umożliwia jego wyświetlenie. Netscape Communicator nie posiada tego arkusza stylów i nie wyświetla otrzymanego kodu XML. Aby uzyskać dostęp do kodu XML, należy wyświetlić kod źródłowy otrzymanej strony. Oto przykład dla następującego kodu XML:

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

Przeglądarka Internet Explorer wyświetli następującą stronę:

Image

natomiast przeglądarka Netscape Navigator wyświetli:

Image

Jeśli wyświetlimy kod źródłowy strony otrzymanej przez przeglądarkę Netscape, otrzymamy:

Image

Netscape rzeczywiście otrzymał to samo co Internet Explorer, ale wyświetlił to inaczej. W dalszej części będziemy używać przeglądarki Internet Explorer do tworzenia zrzutów ekranu.

10.3. Pierwsza usługa internetowa

Zapoznamy się z usługami internetowymi na podstawie bardzo prostego przykładu, który występuje w trzech wersjach.

10.3.1. Wersja 1

W tej pierwszej wersji wykorzystamy VS.NET, który ma tę zaletę, że pozwala wygenerować szkielet usługi internetowej gotowy do natychmiastowego użycia. Gdy już zrozumiemy tę architekturę, będziemy mogli zacząć działać samodzielnie. Będzie to przedmiotem kolejnych wersji.

Korzystając z VS.NET, utwórzmy nowy projekt z opcją [Fichier/Nouveau/Projet]:

Image

Należy zwrócić uwagę na następujące kwestie:

  • typ projektu to Visual Basic (lewy panel)
  • szablon projektu to usługa internetowa ASP.NET (prawy panel)
  • lokalizacja jest dowolna. W tym przypadku usługa internetowa będzie hostowana przez lokalny serwer internetowy IIS. Jego adres będzie zatem http://localhost/[chemin], gdzie [chemin] należy zdefiniować. W tym miejscu wybieramy ścieżkę http://localhost/polyvbnet/demo. VS.NET utworzy wówczas folder dla tego projektu. Gdzie? Serwer IIS posiada katalog główny drzewa dokumentów internetowych, które udostępnia. Nazwijmy ten katalog główny <IISroot>. Odpowiada on adresowi http://localhost. Wynika z tego, że adres http://localhost/polyvbnet/demo będzie powiązany z folderem <IISroot>/polyvbnet/demo. <IISroot> to zazwyczaj folder \inetpub\wwwroot na dysku, na którym zainstalowano plik IIS. W naszym przykładzie jest to dysk E. Folder utworzony przez VS.NET to zatem folder e:\inetpub\wwwroot\polyvbnet\demo:

Image

Jak zawsze, powstało zbyt wiele folderów. Nie zawsze są one przydatne. Omówimy tylko te, które są nam w danym momencie potrzebne. VS.NET utworzył projekt:

Image

Znajdujemy tu niektóre pliki obecne w fizycznym folderze projektu. Najbardziej interesujący dla nas jest plik z rozszerzeniem asmx. Jest to rozszerzenie usług internetowych. Usługa internetowa jest zarządzana przez VS.NET jako aplikacja systemu Windows, c.a.d – aplikacja posiadająca interfejs graficzny oraz kod służący do jej obsługi. Dlatego też mamy okno projektowe:

Image

Usługa internetowa zazwyczaj nie posiada graficznego interfejsu użytkownika. Stanowi ona obiekt, który można wywołać zdalnie. Posiada ona metody, które są wywoływane przez aplikacje. Będziemy więc traktować go jako klasyczny obiekt, z tą szczególną cechą, że można go instancjonować zdalnie przez sieć. Nie będziemy więc korzystać z okna projektowego przedstawionego przez VS.NET. Skupmy się raczej na kodzie serwisu, korzystając z opcji Widok/Kod:

Image

Należy zwrócić uwagę na kilka kwestii:

  • plik nazywa się Service1.asmx.vb, a nie Service1.asmx. Do zawartości pliku Service1.asmx wrócimy nieco później.
  • widzimy okno kodu podobne do tego, które mieliśmy podczas tworzenia aplikacji dla systemu Windows za pomocą VS.NET

Kod wygenerowany przez VS.NET wygląda następująco:


Imports System.Web.Services

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

#Region „Kod wygenerowany przez narzędzie do projektowania usług internetowych”

    Public Sub New()
        MyBase.New()

        'To wywołanie jest wymagane przez Kreator usług internetowych.
        InitializeComponent()

        'Dodaj swój kod inicjalizacyjny po wywołaniu InitializeComponent()

    End Sub

    'Wymagane przez Kreator usług internetowych
    Private components As System.ComponentModel.IContainer

    'REMARQUE: następująca procedura jest wymagana przez Kreator usług internetowych
    'Można ją zmodyfikować za pomocą Projektanta usług internetowych.  
    'Nie należy jej modyfikować za pomocą edytora kodu.
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        components = New System.ComponentModel.Container()
    End Sub

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        'CODEGEN: ta procedura jest wymagana przez Kreator usług internetowych
        'Nie należy jej modyfikować za pomocą edytora kodu.
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

#Koniec regionu

    ' EXEMPLE DE SERVICE WEB
    ' Przykładowa usługa HelloWorld() zwraca ciąg znaków „Hello World”.
    ' Aby wygenerować projekt, nie należy komentować poniższych wierszy, a następnie należy zapisać i wygenerować projekt.
    ' Aby przetestować tę usługę internetową, upewnij się, że plik .asmx jest stroną startową
    ' i naciśnij F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = „Hello World”
    ' Koniec funkcji

End Class

Na początek zauważmy, że mamy tu klasę Service1, która wywodzi się z klasy WebService:

Public Class Service1
    Inherits System.Web.Services.WebService

W związku z tym musimy zaimportować przestrzeń nazw System.Web.Services:


Imports System.Web.Services

Deklaracja klasy jest poprzedzona atrybutem kompilacji:


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

Atrybut System.Web.Services.WebService() wskazuje, że następująca po nim klasa jest usługą internetową. Atrybut ten przyjmuje różne parametry, w tym jeden o nazwie NameSpace. Służy on do umieszczenia usługi internetowej w przestrzeni nazw. Można sobie bowiem wyobrazić, że na świecie istnieje kilka usług internetowych o nazwie meteo. Potrzebujemy sposobu, aby je rozróżnić. Umożliwia to przestrzeń nazw. Jedna z nich może nazywać się [espacenom1].meteo, a inna [espacenom2].meteo. Mamy tu do czynienia z koncepcją analogiczną do przestrzeni nazw klas. VS.NET automatycznie wygenerowało kod, który umieściło w określonym obszarze kodu źródłowego:

#Region „Kod wygenerowany przez Projektanta usług internetowych”

Jeśli przyjrzymy się temu kodowi, zauważymy, że jest to ten sam kod, który generował projektant podczas tworzenia aplikacji dla systemu Windows. Jest to kod, który można po prostu usunąć, jeśli nie mamy interfejsu graficznego, co ma miejsce w przypadku usług internetowych.

Klasa kończy się przykładem tego, jak mogłaby wyglądać usługa internetowa:


#Koniec regionu

    ' EXEMPLE DE SERVICE WEB
    ' Przykładowa usługa HelloWorld() zwraca ciąg znaków „Hello World”.
    ' Aby wygenerować projekt, nie należy komentować poniższych wierszy, a następnie należy zapisać i wygenerować projekt.
    ' Aby przetestować tę usługę internetową, upewnij się, że plik .asmx jest stroną startową
    ' i naciśnij F5.
    '
    '<WebMethod()> Public Function HelloWorld() As String
    '    HelloWorld = „Hello World”
    ' End Function

W oparciu o powyższe uwagi porządkujemy kod, tak aby wyglądał następująco:


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

Teraz sytuacja jest nieco bardziej przejrzysta.

  • usługa internetowa to klasa wywodząca się z klasy WebService
  • klasa ta jest oznaczona atrybutem <System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>. Umieszczamy więc naszą usługę w przestrzeni nazw st.istia.univ-angers.fr.
  • Metody klasy są kwalifikowane przez atrybut <WebMethod()>, wskazujący, że mamy do czynienia z metodą, którą można wywołać zdalnie przez sieć

Klasa obsługująca naszą usługę internetową nazywa się zatem Bonjour i posiada jedną metodę, również o nazwie Bonjour, która zwraca ciąg znaków. Jesteśmy gotowi do pierwszego testu.

  • Uruchommy serwer internetowy IIS, jeśli jeszcze tego nie zrobiliśmy
  • użyjmy opcji Debuguj/Uruchom bez debugowania. VS.NET

VS.NET skompiluje wówczas całą aplikację, uruchomi przeglądarkę (często Internet Explorer, jeśli jest zainstalowany) i wyświetli adres URL http://localhost/polyvbnet/demo/Service1.asmx:

Image

Dlaczego adres URL http://localhost/polyvbnet/demo/Service1.asmx? Ponieważ był to jedyny plik .asmx w projekcie:

Image

Gdyby istniało kilka plików .asmx, musielibyśmy określić, który z nich ma zostać uruchomiony jako pierwszy. Można to zrobić, klikając prawym przyciskiem myszy na dany plik .asmx i wybierając opcję [Définir comme page de démarrage].

Image

Można by się zastanawiać, co zawiera plik service1.asmx. W rzeczywistości przy pomocy pliku VS.NET pracowaliśmy nad plikiem service1.asmx.vb, a nie nad plikiem service1.asmx. Plik ten znajduje się w folderze projektu:

Image

Otwórzmy go w edytorze tekstu (Notatnik lub innym). Otrzymujemy następującą treść:

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

Plik zawiera prostą dyrektywę skierowaną do serwera IIS, wskazującą,

  • że mamy do czynienia z usługą internetową (słowo kluczowe WebService)
  • że językiem klasy tej usługi jest Visual Basic (Language="vb")
  • że kod źródłowy tej klasy znajduje się w pliku Service1.asmx.vb (Codebehind="Service1.asmx.vb")
  • że klasa implementująca usługę nosi nazwę demo.Bonjour (Class="demo.Bonjour"). Warto zauważyć, że plik VS.NET umieścił klasę Bonjour w przestrzeni nazw demo, która jest jednocześnie nazwą projektu.

Wróćmy do strony dostępnej pod adresem http://localhost/polyvbnet/demo/Service1.asmx:

Image

Kto napisał kod HTML na powyższej stronie? Nie my, to wiemy. Jest to IIS, który przedstawia usługi internetowe w standardowy sposób. Ta strona zawiera dwa linki. Kliknijmy pierwszy z nich, [Description du service]:

Image

Ups… to raczej niezrozumiały kod XML. Zwróćmy jednak uwagę na URL

http://localhost/polyvbnet/demo/Service1.asmx?WSDL. Otwórz przeglądarkę i wpisz bezpośrednio ten adres URL. Otrzymasz to samo, co poprzednio. Należy więc pamiętać, że adres URL http://serviceweb?WSDL zapewnia dostęp do opisu XML usługi internetowej. Wróćmy do strony startowej i kliknijmy link [Bonjour]. Przypomnijmy, że „Bonjour” jest metodą serwisu internetowego. Gdyby istniało więcej metod, wszystkie zostałyby tutaj wyświetlone. Otrzymujemy następującą stronę:

Image

Celowo skróciliśmy wyświetloną stronę, aby nie przeciążać naszej demonstracji. Zwróćmy ponownie uwagę na uzyskany adres URL:

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

Jeśli wpiszemy ten adres URL bezpośrednio w przeglądarce, otrzymamy to samo, co powyżej. Jesteśmy zachęcani do użycia przycisku [Appeler]. Zróbmy to. Otrzymujemy nową stronę:

Image

Znowu mamy do czynienia z XML. Znajdujemy tam dwie informacje, które były obecne w naszej usłudze internetowej:

  • przestrzeń nazw st.istia.univ-angers.fr naszej usługi

<System.Web.Services.WebService(Namespace:="st.istia.univ-angers.fr")>
  • wartość zwrócona przez metodę Bonjour:

        Return "bonjour !"

Czego się nauczyliśmy?

  • jak napisać usługę internetową S
  • jak z niej skorzystać

Teraz zajmiemy się pisaniem serwisu internetowego bez pomocy VS.NET.

10.3.2. Wersja 2

W poprzednim przykładzie narzędzie VS.NET wykonało wiele czynności samodzielnie. Czy można stworzyć usługę internetową bez tego narzędzia? Odpowiedź brzmi: tak, i właśnie to teraz pokażemy. Za pomocą edytora tekstu tworzymy następującą usługę internetową:


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

Klasa nosi nazwę Bonjour2 i posiada metodę o nazwie getBonjour. Została umieszczona w pliku demo2.vb, który z kolei znajduje się w drzewie katalogów serwera IIS, w folderze E:\Inetpub\wwwroot\polyvbnet\demo2. Jest to klasyczna klasa VB.NET, którą można zatem skompilować:

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

Umieszczamy zestaw demo2.dll w folderze o nazwie „bin” (ta nazwa jest obowiązkowa):

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

Teraz tworzymy plik demo2.asmx. To właśnie ten plik będzie wywoływany przez klientów internetowych. Jego zawartość jest następująca:

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

Spotkaliśmy się już z tą dyrektywą. Oznacza ona, że:

  • klasa serwisu internetowego nosi nazwę Bonjour2 i znajduje się w zestawie demo2.dll. IIS będzie szukać tego zestawu w różnych lokalizacjach, w szczególności w folderze bin serwisu internetowego. Dlatego właśnie umieściliśmy tam zestaw demo2.dll.

Teraz możemy przeprowadzić różne testy. Upewniamy się, że IIS jest aktywny, a następnie w przeglądarce wpisujemy adres http://localhost/polyvbnet/demo2/demo2.asmx:

Image

Następnie adres http://localhost/polyvbnet/demo2/demo2.asmx?WSDL

Image

Następnie URL http://localhost/polyvbnet/demo2/demo2.asmx?op=getBonjour, gdzie getBonjour to nazwa jedynej metody naszej usługi internetowej:

Image

Korzystamy z powyższego przycisku [Appeler]:

Image

Otrzymujemy prawidłowy wynik wywołania metody getBonjour serwisu internetowego. Wiemy już, jak zbudować serwis internetowy bez vs.net. Od tej pory pominiemy sposób budowy serwisu internetowego, skupiając się wyłącznie na podstawowych plikach.

10.3.3. Wersja 3

Dwie poprzednie wersje usługi internetowej [Bonjour] wykorzystywały dwa pliki:

  • plik .asmx, będący punktem wejścia do serwisu internetowego
  • plik .vb, zawierający kod źródłowy usługi internetowej

W tym miejscu pokazujemy, że wystarczy sam plik .asmx. Kod usługi demo3.asmx wygląda następująco:

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

Zauważamy, że kod źródłowy usługi znajduje się teraz bezpośrednio w pliku źródłowym demo3.asmx. Dyrektywa

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

nie odwołuje się już do klasy w zewnętrznym zestawie, lecz do klasy znajdującej się w tym samym pliku źródłowym. Umieśćmy go w folderze <IISroot>\polyvbnet\demo3:

Image

Uruchommy IIS i wpiszmy adres URL http://localhost/polyvbnet/demo3/demo3.asmx:

Image

Zauważamy istotną różnicę w stosunku do poprzedniej wersji: nie musieliśmy kompilować kodu VB usługi. IIS sam przeprowadził tę kompilację za pomocą kompilatora VB.NET zainstalowanego na tym samym komputerze. Następnie wygenerował stronę. Jeśli wystąpi błąd kompilacji, zostanie on zgłoszony przez IIS:

Image

10.3.4. Wersja 4

W tym miejscu skupiamy się na konfiguracji serwera IIS. Do tej pory zawsze umieszczaliśmy nasze serwisy internetowe w katalogu głównym <IISroot> serwera IIS, w tym przypadku [e:\inetpub\wwwroot]. W tym miejscu pokazujemy, że usługę internetową można umieścić w dowolnym miejscu. Odbywa się to za pomocą folderów wirtualnych serwera IIS. Umieśćmy naszą usługę w następującym folderze:

Image

Folder [D:\data\devel\vbnet\poly\chap9\demo3] nie znajduje się w drzewie katalogów serwera IIS. Należy to wskazać serwerowi, tworząc wirtualny folder IIS. Uruchommy IIS i wybierzmy poniższą opcję [Avancé]:

Image

Wyświetla się lista katalogów wirtualnych. Nie będziemy się nad nią rozwodzić. Tworzymy nowy katalog wirtualny za pomocą przycisku [Ajouter] powyżej:

Image

Za pomocą przycisku [Parcourir] wskazujemy fizyczny katalog zawierający serwis internetowy, w tym przypadku katalog [D:\data\devel\vbnet\poly\chap9\demo3]. Nadajemy temu katalogowi nazwę logiczną (wirtualną): [virdemo3]. Oznacza to, że dokumenty znajdujące się w fizycznym katalogu [D:\data\devel\vbnet\poly\chap9\demo3] będą dostępne w sieci pod adresem URL [http://<machine>/virdemo3]. Powyższe okno dialogowe zawiera inne parametry, które pozostawiamy bez zmian. Zatwierdzamy okno. Nowy folder wirtualny pojawia się na liście folderów wirtualnych IIS:

Image

Teraz otwieramy przeglądarkę i wpisujemy adres URL [http://localhost/virdemo3/demo3.asmx]. Otrzymujemy ten sam wynik co poprzednio:

Image

10.3.5. Wnioski

Pokazaliśmy kilka sposobów tworzenia serwisu internetowego. W dalszej części będziemy korzystać z metody opisanej w wersji 3 do utworzenia serwisu oraz z metody 4 do jego lokalizacji. Nie będziemy więc potrzebować adresu VS.NET. Warto jednak zwrócić uwagę na korzyści płynące z użycia adresu VS.NET, który ułatwia debugowanie. Istnieją bezpłatne narzędzia do tworzenia aplikacji internetowych, w szczególności produkt WebMatrix sponsorowany przez Microsoft, który można znaleźć na stronie URL [http://www.asp.net/webmatrix]. Jest to doskonałe narzędzie do rozpoczęcia programowania internetowego bez konieczności wcześniejszych inwestycji.

10.4. Usługa internetowa realizująca operacje

Rozważamy serwis internetowy, który oferuje pięć funkcji:

  1. dodaj(a,b), która zwraca a+b
  2. odejmowanie(a,b), które zwraca a-b
  3. mnożyć(a,b), która zwraca a*b
  4. dziel(a,b), która zwraca a/b
  5. wszystko(a,b), która zwraca tablicę [a+b, a-b, a*b, a/b]

Kod VB.NET tej usługi jest następujący:


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

Powtórzymy tutaj niektóre wyjaśnienia, które już podaliśmy, ale które warto przypomnieć lub uzupełnić. Klasa operations przypomina klasę VB.NET, jednak należy zwrócić uwagę na kilka kwestii:

  • metody są poprzedzone atrybutem <WebMethod()>, który wskazuje kompilatorowi, które metody mają zostać „opublikowane” (c.a.d) i udostępnione klientowi. Metoda, przed którą nie umieszczono tego atrybutu, byłaby niewidoczna dla klientów zdalnych. Mogłaby to być metoda wewnętrzna wykorzystywana przez inne metody, ale nieprzeznaczona do publikacji.
  • Klasa ta dziedziczy po klasie WebService zdefiniowanej w przestrzeni nazw System.Web.Services. Dziedziczenie to nie zawsze jest obowiązkowe. W tym konkretnym przykładzie można by się bez niego obejść.
  • Sama klasa jest poprzedzona atrybutem <WebService(Namespace="st.istia.univ-angers.fr")>, którego celem jest nadanie przestrzeni nazw usłudze internetowej. Dostawca klas nadaje swoim klasom przestrzeń nazw, aby zapewnić im unikalną nazwę i w ten sposób uniknąć konfliktów z klasami innych dostawców, które mogłyby nosić tę samą nazwę. W przypadku usług internetowych jest podobnie. Każda usługa internetowa musi być identyfikowana za pomocą unikalnej nazwy, w tym przypadku st.istia.univ-angers.fr.
  • Nie zdefiniowaliśmy konstruktora. Zostanie więc domyślnie użyty konstruktor klasy nadrzędnej.

Powyższy kod źródłowy nie jest przeznaczony bezpośrednio dla kompilatora VB.NET, lecz dla serwera WWW IIS. Musi on nosić rozszerzenie .asmx i zostać zapisany w drzewie katalogów serwera WWW. W tym przypadku zapisujemy go pod nazwą operations.asmx w folderze <IISroot>\polyvbnet\operations:

Image

Do tego fizycznego folderu przypisujemy folder wirtualny IIS [operations]:

Wejdźmy do serwisu za pomocą przeglądarki. Plik URl, o który należy poprosić, to [http://localhost/operations/operations.asmx]:

Image

Otrzymujemy dokument internetowy zawierający link do każdej z metod zdefiniowanych w usłudze internetowej operations. Przejdźmy do linku ajouter:

Image

Wyświetlona strona proponuje nam przetestowanie metody ajouter poprzez przekazanie jej dwóch argumentów a i b, których potrzebuje. Przypomnijmy definicję metody ajouter:

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

Warto zauważyć, że strona przejęła nazwy argumentów a i b użytych w definicji metody. Korzystamy z przycisku Appeler i otrzymujemy następującą odpowiedź w osobnym oknie przeglądarki:

Image

Jeśli w powyższym przykładzie wprowadzimy [Affichage/Source], otrzymamy następujący kod:

Image

Powtórzmy operację dla metody [toutfaire]:

Image

Otrzymujemy następującą stronę:

Image

Skorzystajmy z powyższego przycisku [Appeler]:

Image

W każdym przypadku odpowiedź serwera ma postać:

<?xml version="1.0" encoding="utf-8"?>
[réponse au format XML]
  • odpowiedź ma format XML
  • wiersz 1 jest standardowy i zawsze występuje w odpowiedzi
  • kolejne wiersze zależą od typu wyniku (double, ArrayOfDouble), liczby wyników oraz przestrzeni nazw serwisu internetowego (w tym przypadku st.istia.univ-angers.fr).

Istnieje kilka metod wysyłania zapytania do serwisu internetowego i uzyskiwania odpowiedzi. Wróćmy do kodu odpowiedzi URL serwisu:

Image

i kliknijmy link [ajouter]. Na wyświetlonej stronie przedstawiono dwie metody wywołania funkcji [ajouter] serwisu internetowego:

Image

Image

Image

Te dwie metody dostępu do funkcji serwisu internetowego noszą odpowiednio nazwy: HTTP-POST i SOAP. Przyjrzyjmy się im teraz kolejno.

Uwaga: w pierwszych wersjach VS.NET istniała trzecia metoda o nazwie HTTP-GET. W momencie pisania niniejszego dokumentu (marzec 2004 r.) metoda ta wydaje się już niedostępna. Oznacza to, że usługa internetowa wygenerowana przez VS.NET nie akceptuje żądań GET. Nie oznacza to jednak, że nie można tworzyć usług internetowych akceptujących żądania GET, zwłaszcza przy użyciu innych narzędzi niż VS.NET lub po prostu ręcznie.

10.5. Klient HTTP-POST

Postępujemy zgodnie z metodą proponowaną przez serwis internetowy:

Image

Omówmy to, co jest tam napisane. Przede wszystkim klient internetowy musi wysłać następujące nagłówki HTTP:

POST /operations/operations.asmx/ajouter HTTP/1.1
Klient internetowy wysyła żądanie POST do URL /operations/operations.asmx/dodaj zgodnie z protokołem HTTP w wersji 1.1
HOST: localhost
Określa się maszynę docelową żądania. W tym przypadku jest to localhost. Nagłówek ten stał się obowiązkowy w wersji 1.1 protokołu HTTP
Content-Type: application/x-www-form-urlencoded
W tym miejscu określa się, że po nagłówkach HTTP zostaną wysłane dodatkowe parametry w formacie urlencoded. Format ten zastępuje niektóre znaki ich kodami szesnastkowymi.
Content-length: 7
Jest to liczba znaków w ciągu parametrów, który zostanie wysłany po nagłówkach HTTP.

Po nagłówkach HTTP następuje pusty wiersz, a następnie ciąg parametrów o długości POST znaków, składający się z [Content-Length] znaków, w postaci a=XX&b=YY, gdzie XX i YY to „url-kodowane” ciągi znaków odpowiadające wartościom parametrów a i b. Wiemy już wystarczająco dużo, aby odtworzyć powyższy przykład za pomocą naszego ogólnego klienta TCP, z którego korzystaliśmy już w rozdziale poświęconym programowaniu TCP/IP:

  • uruchamiamy IIS
  • usługa jest dostępna pod adresem URL [http://localhost/operations/operations.asmx]
  • używamy generycznego klienta TCP w oknie o nazwie 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]

Zauważmy przede wszystkim, że dodaliśmy nagłówek [Connection: close], aby poprosić serwer o zamknięcie połączenia po wysłaniu odpowiedzi. Jest to tutaj konieczne. Jeśli tego nie zrobimy, serwer domyślnie pozostawi połączenie otwarte. Jednak jego odpowiedź to ciąg wierszy tekstu, z których ostatni nie jest zakończony znakiem końca wiersza. Okazuje się, że nasz ogólny klient TCP odczytuje wiersze tekstu zakończone znakiem końca wiersza za pomocą metody ReadLine. Jeśli serwer nie zamknie połączenia po wysłaniu ostatniego wiersza, klient zostanie zablokowany, ponieważ oczekuje znaku końca wiersza, który nie nadchodzi. Jeśli serwer zamknie połączenie, metoda klienta ReadLine zakończy działanie, a klient nie zostanie zablokowany.

Natychmiast po otrzymaniu pustego wiersza sygnalizującego koniec nagłówków HTTP serwer IIS wysyła pierwszą odpowiedź:

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

Ta odpowiedź, składająca się wyłącznie z nagłówków HTTP, informuje klienta, że może wysłać 7 znaków, które zamierzał wysłać. Co robimy:

a=2&b=3

Należy tutaj zauważyć, że nasz klient TCP wysyła więcej niż 7 znaków, ponieważ wysyła je wraz ze znakiem końca linii (WriteLine). Nie stanowi to problemu dla serwera, który z otrzymanych znaków pobierze tylko pierwsze 7, a następnie połączenie zostanie zamknięte (Connection: close). Te nadmiarowe znaki mogłyby stanowić problem, gdyby połączenie pozostało otwarte, ponieważ wówczas zostałyby one potraktowane jako pochodzące z kolejnego polecenia klienta. Po otrzymaniu parametrów serwer wysyła swoją odpowiedź:

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

Mamy teraz wszystkie elementy potrzebne do napisania klienta dla naszej usługi internetowej. Będzie to klient konsolowy o nazwie httpPost2, z którego korzysta się w następujący sposób:

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>

Klient jest wywoływany poprzez przekazanie mu kodu URL z serwisu internetowego:

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

Następnie klient odczytuje polecenia wpisane z klawiatury i je wykonuje. Polecenia te mają następujący format:

fonction a b

gdzie „funkcja” to nazwa wywoływanej funkcji serwisu internetowego (dodawanie, odejmowanie, mnożenie, dzielenie), a „a” i „b” to wartości, na których ta funkcja będzie operować. Na przykład:

ajouter 6 7

Na tej podstawie klient wysyła niezbędne żądanie HTTP do serwera internetowego i otrzymuje odpowiedź. Wymiana danych między klientem a serwerem jest wyświetlana na ekranie w celu lepszego zrozumienia procesu:

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]

Powyżej przedstawiono wymianę danych, z którą mieliśmy już do czynienia w przypadku generycznego klienta TCP, z jedną różnicą: nagłówek HTTP Connection: Keep-Alive nakazuje serwerowi, aby nie zamykał połączenia. Pozostaje ono zatem otwarte dla kolejnej operacji klienta, który dzięki temu nie musi ponownie łączyć się z serwerem. Wymusza to jednak na kliencie użycie innej metody niż ReadLine() do odczytania odpowiedzi serwera, ponieważ wiadomo, że jest to ciąg wierszy, z których ostatni nie kończy się znakiem końca wiersza. Po otrzymaniu całej odpowiedzi serwera klient analizuje ją w celu znalezienia wyniku żądanej operacji i wyświetlenia go:

[résultat=13]

Przyjrzyjmy się kodowi naszego klienta:


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

' klient usługi internetowej operations
Public Module clientPOST

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

        ' liczba argumentów
        If args.Length <> 1 Then
            erreur(syntaxe, 1)
        End If
        ' odnotowuje się żądanie URI
        Dim URIstring As String = args(0)

        ' nawiązywane jest połączenie z serwerem
        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
            ' połączenie z serwerem
            uri = New Uri(URIstring)
            client = New TcpClient(uri.Host, uri.Port)
            ' tworzy się strumienie wejścia-wyjścia klienta TCP
            [IN] = New StreamReader(client.GetStream())
            OUT = New StreamWriter(client.GetStream())
            OUT.AutoFlush = True
        Catch ex As Exception
            ' URI nieprawidłowy lub inny problem
            erreur("L'erreur suivante s'est produite : " + ex.Message, 2)
        End Try

        ' tworzenie słownika funkcji serwisu internetowego
        Dim dicoFonctions As New Hashtable
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i

        ' żądania użytkownika są wprowadzane za pomocą klawiatury
        ' w postaci funkcja a b
        ' kończą się poleceniem „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

        ' monit dla użytkownika
        Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b")

        ' obsługa błędów
        Dim erreurCommande As Boolean
        Try
            ' pętla wprowadzania poleceń wpisanych z klawiatury
            While True
                ' brak błędów na początku
                erreurCommande = False
                ' odczyt polecenia
                commande = Console.In.ReadLine().Trim().ToLower()
                ' koniec?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' podział polecenia na pola
                champs = Regex.Split(commande, "\s+")
                Try
                    ' potrzebne są trzy pola
                    If champs.Length <> 3 Then
                        Throw New Exception
                    End If
                    ' pole 0 musi być rozpoznaną funkcją
                    fonction = champs(0)
                    If Not dicoFonctions.ContainsKey(fonction) Then
                        Throw New Exception
                    End If
                    ' pole 1 musi być prawidłową liczbą
                    a = champs(1)
                    Double.Parse(a)
                    ' pole 2 musi być prawidłową liczbą
                    b = champs(2)
                    Double.Parse(b)
                Catch
                    ' nieprawidłowe polecenie
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    erreurCommande = True
                End Try
                ' wysyłane jest żądanie do serwisu internetowego
                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
        ' zakończenie połączenia klient-serwer
        Try
            [IN].Close()
            OUT.Close()
            client.Close()
        Catch
        End Try
    End Sub
...........
    ' wyświetlanie błędów
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlenie błędu
        System.Console.Error.WriteLine(msg)
        ' zakończenie z błędem
        Environment.Exit(exitCode)
    End Sub
End Module

Mamy tu elementy, z którymi spotkaliśmy się już kilkakrotnie i które nie wymagają szczególnych komentarzy. Przyjrzyjmy się teraz kodowi metody executeFonction, w której znajdują się nowości:


    ' 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)
        ' wykonuje funkcję(a,b) w usłudze internetowej o adresie URI URI
        ' komunikacja między klientem a serwerem odbywa się za pośrednictwem strumieni IN i OUT
        ' wynik funkcji znajduje się w wierszu
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' wysłana przez serwer

        ' tworzenie ciągu zapytania
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' tworzenie tablicy nagłówków HTTP do wysłania
        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) = ""

        ' wysyłamy nagłówki HTTP do serwera
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' wysyłanie do serwera
            OUT.WriteLine(entetes(i))
            ' wyświetlenie na ekranie
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

        ' odczyt pierwszej odpowiedzi z serwera WWW HTTP/1.1 100
        Dim ligne As String = Nothing
        ' wiersz strumienia odczytu
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' następna linia
            ligne = [IN].ReadLine()
        End While
        'echo ostatniego wiersza
        Console.Out.WriteLine(("<-- " + ligne))

        ' wysyłanie parametrów żądania
        OUT.Write(requête)
        ' echo
        Console.Out.WriteLine(("--> " + requête))

        ' tworzenie wyrażenia regularnego umożliwiającego ustalenie rozmiaru odpowiedzi XML
        ' w strumieniu odpowiedzi serwera WWW
        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

        ' odczyt drugiej odpowiedzi serwera WWW po wysłaniu żądania
        ' zapisywanie wartości wiersza Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' wyświetlenie na ekranie
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' następny wiersz
            ligne = [IN].ReadLine()
        End While
        ' wyświetlenie ostatniego wiersza
        Console.Out.WriteLine("<--")

        ' tworzenie wyrażenia regularnego umożliwiającego odzyskanie wyniku
        ' w strumieniu odpowiedzi serwera WWW
        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

        ' odczytuje się pozostałą część odpowiedzi serwera WWW
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' rozbijamy odpowiedź na wiersze tekstu
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' przeszukiwanie wierszy tekstu w poszukiwaniu wyniku
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' kontynuacja
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' porównanie bieżącego wiersza z wzorcem
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' czy znaleziono?
            If MatchRésultat.Success Then
                ' zapisujemy wynik
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' wyświetlanie wyniku
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

Po pierwsze, klient HTTP-POST wysyła swoje żądanie w formacie POST:


        ' tworzenie ciągu zapytania
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

        ' tworzenie tablicy nagłówków HTTP do wysłania
        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) = ""

        ' wysyłanie nagłówków HTTP na serwer
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' wysyłanie do serwera
            OUT.WriteLine(entetes(i))
            ' wyświetlenie na ekranie
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

W nagłówku

--> Content-Length: 7

należy podać rozmiar parametrów, które klient wyśle za nagłówkami HTTP:

--> a=6&b=7

W tym celu stosuje się następujący kod:


        ' tworzenie ciągu zapytania
        Dim requête As String = "a=" + HttpUtility.UrlEncode(a) + "&b=" + HttpUtility.UrlEncode(b)
        Dim nbChars As Integer = requête.Length

Metoda HttpUtility.UrlEncode(ciąg znaków) przekształca niektóre znaki z chaîne na %n1n2, gdzie n1n2 to kod ASCII przekształconego znaku. Zmianie podlegają wszystkie znaki mające szczególne znaczenie w zapytaniu POST (spacja, znak =, znak & itp.). W tym przypadku metoda HttpUtility.UrlEncode jest zazwyczaj zbędna, ponieważ a i b to liczby, które nie zawierają żadnego z tych szczególnych znaków. Została ona tutaj wykorzystana wyłącznie jako przykład. Wymaga ona przestrzeni nazw System.Web. Po wysłaniu przez klienta nagłówków HTTP:

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

serwer odpowiada nagłówkiem 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
<--

Kod ogranicza się do odczytania i wyświetlenia na ekranie tej pierwszej odpowiedzi:


        ' odczyt pierwszej odpowiedzi z serwera WWW HTTP/1.1 100
        Dim ligne As String = Nothing
        ' wiersz strumienia odczytu
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' kolejny wiersz
            ligne = [IN].ReadLine()
        End While
        'echo ostatniego wiersza
        Console.Out.WriteLine(("<-- " + ligne))

Po przeczytaniu tej pierwszej odpowiedzi klient musi wysłać swoje parametry:

--> a=6&b=7

Czyni to za pomocą następującego kodu:

        ' wysyłanie parametrów żądania
        OUT.Write(requête)
        ' echo
        Console.Out.WriteLine(("--> " + requête))

Serwer wyśle wówczas swoją odpowiedź. Składa się ona z dwóch części:

  1. nagłówki HTTP zakończone pustym wierszem
  2. odpowiedź w formacie 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>

W pierwszej kolejności klient odczytuje nagłówki HTTP, aby znaleźć w nich wiersz Content-Length i uzyskać rozmiar odpowiedzi XML (w tym przypadku 90). Rozmiar ten jest uzyskiwany za pomocą wyrażenia regularnego. Można było to zrobić inaczej i prawdopodobnie w bardziej efektywny sposób.


        ' tworzenie wyrażenia regularnego umożliwiającego ustalenie rozmiaru odpowiedzi XML
        ' w strumieniu odpowiedzi serwera WWW
        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

        ' odczyt drugiej odpowiedzi serwera WWW po wysłaniu żądania
        ' zapisywanie wartości z wiersza Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' wyświetlenie na ekranie
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' następny wiersz
            ligne = [IN].ReadLine()
        End While
        ' wyświetlenie ostatniego wiersza
        Console.Out.WriteLine("<--")

Gdy znamy już długość N odpowiedzi XML, wystarczy odczytać N znaków ze strumienia IN odpowiedzi serwera. Ten ciąg N znaków jest rozkładany na wiersze tekstu na potrzeby monitorowania ekranu. Wśród tych wierszy szukamy wiersza z wynikiem:

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

– również za pomocą wyrażenia regularnego. Po znalezieniu wyniku jest on wyświetlany.

[résultat=13]

Koniec kodu klienta wygląda następująco:


        ' tworzenie wyrażenia regularnego umożliwiającego odzyskanie wyniku
        ' w strumieniu odpowiedzi serwera WWW
        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

        ' odczytuje się pozostałą część odpowiedzi serwera WWW
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' podział odpowiedzi na wiersze tekstu
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' przeszukiwanie wierszy tekstu w poszukiwaniu wyniku
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' kontynuacja
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' porównanie bieżącego wiersza z wzorcem
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' czy znaleziono wynik?
            If MatchRésultat.Success Then
                ' zapisujemy wynik
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i
        ' wyświetlanie wyniku
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.6. Klient SOAP

W tym miejscu analizujemy drugiego klienta, który będzie korzystał z dialogu klient-serwer typu SOAP (Simple Object Access Protocol). Przedstawiono nam przykładowy dialog dla funkcji ajouter:

Image

Image

Żądanie klienta to żądanie POST. Zatem spotkamy się z niektórymi mechanizmami znanymi z poprzedniego klienta. Główna różnica polega na tym, że podczas gdy klient HTTP-POST wysyłał parametry a i b w postaci

    a=A&b=B

, to klient SOAP wysyła je w bardziej złożonym formacie 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>

W odpowiedzi otrzymuje komunikat XML, który jest również bardziej złożony niż poprzednie odpowiedzi:

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>

Mimo że żądanie i odpowiedź są bardziej złożone, chodzi tu o ten sam mechanizm HTTP, co w przypadku klienta HTTP-POST. Kod klienta SOAP można zatem wzorować na kodzie klienta HTTP-POST. Oto przykładowe wykonanie:

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]

Zmienia się jedynie metoda executeFonction. Klient SOAP wysyła nagłówki HTTP swojego żądania. Są one po prostu nieco bardziej złożone niż te z 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"
-->

Kod, który je generuje:


    ' 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)
        ' wywołuje funkcję(a,b) w usłudze internetowej o adresie URI URI
        ' komunikacja między klientem a serwerem odbywa się za pośrednictwem strumieni IN i OUT
        ' wynik funkcji znajduje się w wierszu
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' wysłana przez serwer
        ' tworzenie ciągu zapytania 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

        ' tworzenie tablicy nagłówków HTTP do wysłania
        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) = ""

        ' wysyłamy nagłówki HTTP do serwera
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' wysyłanie do serwera
            OUT.WriteLine(entetes(i))
            ' wyświetlenie na ekranie
            Console.Out.WriteLine(("--> " + entetes(i)))
        Next i

Po otrzymaniu tego żądania serwer wysyła swoją pierwszą odpowiedź, którą klient wyświetla:

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

Kod odczytu tej pierwszej odpowiedzi jest następujący:


        ' odczyt pierwszej odpowiedzi z serwera WWW HTTP/1.1 100
        Dim ligne As String = Nothing
        ' jedna linia strumienia odczytu
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            Console.Out.WriteLine(("<-- " + ligne))
            ' następny wiersz
            ligne = [IN].ReadLine()
        End While        'while
        'echo ostatniego wiersza
        Console.Out.WriteLine(("<-- " + ligne))

Klient wyśle teraz swoje parametry w formacie XML w tak zwanej kopercie 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>

Kod:

1
2
3
4
        ' wysyłanie parametrów żądania
        OUT.Write(requêteSOAP)
        ' echo
        Console.Out.WriteLine(("--> " + requêteSOAP))

Serwer wyśle wówczas ostateczną odpowiedź:

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

Klient wyświetla na ekranie otrzymane nagłówki HTTP, szukając w nich wiersza Content-Length:


        ' tworzenie wyrażenia regularnego umożliwiającego ustalenie rozmiaru odpowiedzi XML
        ' w strumieniu odpowiedzi serwera WWW
        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

        ' odczyt drugiej odpowiedzi serwera WWW po wysłaniu żądania
        ' zapisywanie wartości wiersza Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' wyświetlenie na ekranie
            Console.Out.WriteLine(("<-- " + ligne))
            ' Content-Length?
            MatchLength = RegexLength.Match(ligne)
            If MatchLength.Success Then
                longueur = Integer.Parse(MatchLength.Groups(1).Value)
            End If
            ' następny wiersz
            ligne = [IN].ReadLine()
        End While        'while
        ' wyświetlenie ostatniego wiersza
        Console.Out.WriteLine("<--")

Po ustaleniu rozmiaru N odpowiedzi XML klient odczytuje N znaków ze strumienia odpowiedzi serwera, dzieli pobrany ciąg znaków na wiersze tekstu w celu wyświetlenia ich na ekranie i wyszukuje w nich znacznik XML wyniku: <ajouterResult>7</ajouterResult> oraz wyświetla tę wartość:


        ' tworzenie wyrażenia regularnego umożliwiającego odzyskanie wyniku
        ' w strumieniu odpowiedzi serwera WWW
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' odczytuje się pozostałą część odpowiedzi serwera WWW
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' podział odpowiedzi na wiersze tekstu
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' przeszukiwanie wierszy tekstu w poszukiwaniu wyniku
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' kontynuacja
            Console.Out.WriteLine(("<-- " + lignes(i)))
            ' porównanie bieżącego wiersza z wzorcem
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' czy znaleziono wynik?
            If MatchRésultat.Success Then
                ' zapisujemy wynik
                strRésultat = MatchRésultat.Groups(1).Value
            End If
            'następny wiersz
        Next i
        ' wyświetlanie wyniku
        Console.Out.WriteLine(("[résultat=" + strRésultat + "]" + ControlChars.Lf))
    End Sub

10.7. Kapsułkowanie komunikacji klient-serwer

Załóżmy, że z naszej usługi internetowej operations korzystają różne aplikacje. Interesujące byłoby udostępnienie im klasy, która pełniłaby rolę interfejsu między aplikacją kliencką a usługą internetową oraz ukrywałaby większość komunikacji sieciowej, która dla większości programistów nie jest sprawą trywialną. Mielibyśmy wówczas następujący schemat:

Aplikacja kliencka zwracałaby się do interfejsu klient-serwer w celu wysłania żądań do serwisu internetowego. Interfejs ten przeprowadzałby całą niezbędną komunikację sieciową z serwerem i zwracałby uzyskany wynik do aplikacji klienckiej. Aplikacja kliencka nie musiałaby już zajmować się komunikacją z serwerem, co znacznie ułatwiłoby jej tworzenie.

10.7.1. Klasa enkapsulacyjna

Po omówieniu powyższych zagadnień dobrze znamy już komunikację sieciową między klientem a serwerem. Zapoznaliśmy się nawet z trzema metodami. Postanawiamy enkapsulować metodę SOAP. Klasa wygląda następująco:


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

' clientSOAP z serwisu Web operations
Public Class clientSOAP

    ' zmienne instancji
    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
    ' słownik funkcji
    Private dicoFonctions As New Hashtable
    ' lista funkcji
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
    ' tryb szczegółowy
    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)

        ' tryb szczegółowy
        Me.verbose = verbose

        ' połączenie z serwerem
        uri = New Uri(uriString)
        client = New TcpClient(uri.Host, uri.Port)

        ' tworzenie strumieni wejścia-wyjścia klienta TCP
        [IN] = New StreamReader(client.GetStream())
        OUT = New StreamWriter(client.GetStream())
        OUT.AutoFlush = True

        ' tworzenie słownika funkcji serwisu internetowego
        Dim i As Integer
        For i = 0 To fonctions.Length - 1
            dicoFonctions.Add(fonctions(i), True)
        Next i
    End Sub

    ' zamknięcie połączenia z serwerem
    Public Sub Close()
        ' zakończenie połączenia klient-serwer
        [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
        ' wykonuje funkcję(a,b) w usłudze internetowej o adresie URI URI
        ' komunikacja między klientem a serwerem odbywa się za pośrednictwem strumieni IN i OUT
        ' wynik funkcji znajduje się w wierszu
        ' <double xmlns="st.istia.univ-angers.fr">double</double>
        ' wysłana przez serwer

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

        ' czy argumenty a i b są prawidłowe?
        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

        ' dzielenie przez zero?
        If fonction = "diviser" And doubleB = 0 Then
            Return "[division par zéro]"
        End If

        ' tworzenie ciągu zapytania 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

        ' tworzenie tablicy nagłówków HTTP do wysłania
        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) = ""

        ' wysyłamy nagłówki HTTP na serwer
        Dim i As Integer
        For i = 0 To entetes.Length - 1
            ' wysyłanie do serwera
            OUT.WriteLine(entetes(i))
            ' wyświetlenie na ekranie
            If verbose Then
                Console.Out.WriteLine(("--> " + entetes(i)))
            End If
        Next i

        ' odczyt pierwszej odpowiedzi z serwera WWW HTTP/1.1 100
        Dim ligne As String = Nothing
        ' jedna linia strumienia odczytu
        ligne = [IN].ReadLine()
        While ligne <> ""
            'echo
            If verbose Then
                Console.Out.WriteLine(("<-- " + ligne))
            End If
            ' następny wiersz
            ligne = [IN].ReadLine()
        End While
        'echo ostatniego wiersza
        If verbose Then
            Console.Out.WriteLine(("<-- " + ligne))
        End If
        ' wysyłanie parametrów żądania
        OUT.Write(requêteSOAP)
        ' echo
        If verbose Then
            Console.Out.WriteLine(("--> " + requêteSOAP))
        End If

        ' tworzenie wyrażenia regularnego umożliwiającego ustalenie rozmiaru odpowiedzi XML
        ' w strumieniu odpowiedzi serwera WWW
        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

        ' odczyt drugiej odpowiedzi serwera WWW po wysłaniu żądania
        ' zapisywanie wartości z wiersza Content-Length
        ligne = [IN].ReadLine()
        While ligne <> ""
            ' wyświetlenie na ekranie
            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
            ' następny wiersz
            ligne = [IN].ReadLine()
        End While

        ' wyświetlenie ostatniego wiersza
        If verbose Then
            Console.Out.WriteLine("<--")
        End If

        ' tworzenie wyrażenia regularnego umożliwiającego odnalezienie wyniku
        ' w strumieniu odpowiedzi serwera WWW
        Dim modèle As String = "<" + fonction + "Result>(.+?)</" + fonction + "Result>"
        Dim ModèleRésultat As New Regex(modèle)
        Dim MatchRésultat As Match = Nothing

        ' odczytuje się pozostałą część odpowiedzi serwera WWW
        Dim chrRéponse(longueur) As Char
        [IN].Read(chrRéponse, 0, longueur)
        Dim strRéponse As String = New [String](chrRéponse)

        ' podział odpowiedzi na wiersze tekstu
        Dim lignes As String() = Regex.Split(strRéponse, ControlChars.Lf)

        ' przeszukuje się wiersze tekstu w poszukiwaniu wyniku
        Dim strRésultat As String = "?"        ' résultat de la fonction
        For i = 0 To lignes.Length - 1
            ' kontynuacja
            If verbose Then
                Console.Out.WriteLine(("<-- " + lignes(i)))
            End If            ' comparaison ligne courante au modèle
            MatchRésultat = ModèleRésultat.Match(lignes(i))
            ' czy znaleziono wynik?
            If MatchRésultat.Success Then
                ' zapisujemy wynik
                strRésultat = MatchRésultat.Groups(1).Value
            End If
        Next i

        ' zwracamy wynik
        Return strRésultat
    End Function
End Class

Nie ma tu nic nowego w porównaniu z tym, co już widzieliśmy. Po prostu wykorzystaliśmy kod klienta SOAP, który wcześniej analizowaliśmy, i nieco go przerobiliśmy, aby stworzyć klasę. Klasa ta posiada konstruktor oraz dwie metody:


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


    ' zamknięcie połączenia z serwerem
    Public Sub Close()

i posiada następujące atrybuty:


     ' zmienne instancji
    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
     ' słownik funkcji
    Private dicoFonctions As New Hashtable
     ' lista funkcji
    Private fonctions As String() = {"ajouter", "soustraire", "multiplier", "diviser"}
     ' szczegółowe
    Private verbose As Boolean = False    ' à vrai, affiche à l'écran les échanges client-serveur

Do konstruktora przekazujemy dwa parametry:

  1. identyfikator URI usługi internetowej, z którą ma się połączyć
  2. wartość logiczną verbose, która w rzeczywistości określa, czy operacje sieciowe mają być wyświetlane na ekranie; w przeciwnym razie nie będą one wyświetlane.

W trakcie tworzenia obiektu generowane są strumienie IN do odczytu sieciowego, OUT do zapisu sieciowego, a także słownik funkcji obsługiwanych przez usługę. Po utworzeniu obiektu otwierane jest połączenie klient-serwer, a jego strumienie IN i OUT stają się dostępne.

Metoda Close pozwala zamknąć połączenie z serwerem.

Metoda ExecuteFonction jest tą, którą napisaliśmy dla analizowanego klienta SOAP, z kilkoma drobnymi różnicami:

  1. Parametry uri, IN i OUT, które wcześniej były przekazywane jako parametry do metody, nie muszą już być przekazywane, ponieważ są one teraz atrybutami instancji dostępnymi dla wszystkich metod tej instancji
  2. Metoda ExecuteFonction, która wcześniej zwracała typ void i wyświetlała wynik funkcji na ekranie, teraz zwraca ten wynik, a zatem typ string.

Zazwyczaj klient będzie korzystał z klasy clientSOAP w następujący sposób:

  1. utworzenie obiektu clientSOAP, który nawiąże połączenie z serwisem internetowym
  2. wielokrotne wywołanie metody executeFonction
  3. zakończenie połączenia z serwisem internetowym za pomocą metody Close.

Przyjrzyjmy się pierwszemu klientowi.

10.7.2. Klient konsolowy

W tym miejscu powracamy do klienta SOAP, który analizowaliśmy, gdy klasa clientSOAP jeszcze nie istniała, i przerabiamy go tak, aby teraz korzystał z tej klasy:


' przestrzenie nazw
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports Microsoft.VisualBasic

Public Module testClientSoap

    ' żądanie URI od serwisu internetowego operations
    ' interaktywne wykonywanie poleceń wpisanych z klawiatury
    Public Sub Main(ByVal args() As String)
        ' składnia
        Const syntaxe As String = "pg URI [verbose]"

        ' liczba argumentów
        If args.Length <> 1 And args.Length <> 2 Then
            erreur(syntaxe, 1)
        End If
        ' szczegółowe wyświetlanie?
        Dim verbose As Boolean = False
        If args.Length = 2 Then
            verbose = args(1).ToLower() = "verbose"
        End If
        ' nawiązywanie połączenia z usługą internetową 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
            ' błąd połączenia
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

        ' żądania użytkownika są wprowadzane za pomocą klawiatury
        ' w postaci funkcji a b – kończą się poleceniem „end”
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

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

        ' obsługa błędów
        Dim erreurCommande As Boolean
        Try
            ' pętla wprowadzania poleceń wpisywanych za pomocą klawiatury
            While True
                ' na początku brak błędu
                erreurCommande = False
                ' odczyt polecenia
                commande = Console.In.ReadLine().Trim().ToLower()
                ' koniec?
                If commande Is Nothing Or commande = "fin" Then
                    Exit While
                End If
                ' podział polecenia na pola
                champs = Regex.Split(commande, "\s+")
                ' potrzebne są trzy pola
                If champs.Length <> 3 Then
                    Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                    ' odnotowano błąd
                    erreurCommande = True
                End If
                ' wysyłamy zapytanie do serwisu internetowego
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))
                ' kolejne żądanie
            End While
        Catch e As Exception
            Console.Out.WriteLine(("L'erreur suivante s'est produite : " + e.Message))
        End Try
        ' zakończenie połączenia klient-serwer
        Try
            client.Close()
        Catch
        End Try
    End Sub

    ' wyświetlenie błędów
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlenie błędu
        System.Console.Error.WriteLine(msg)
        ' zakończenie z błędem
        Environment.Exit(exitCode)
    End Sub
End Module

Klient jest teraz znacznie prostszy i nie zawiera żadnej komunikacji sieciowej. Klient przyjmuje dwa parametry:

  1. identyfikator URI usługi internetowej operations
  2. opcjonalny parametr verbose. Jeśli zostanie podany, komunikacja sieciowa będzie wyświetlana na ekranie.

Te dwa parametry służą do utworzenia obiektu clientSOAP, który zapewni komunikację z usługą internetową.


         ' nawiązywanie połączenia z usługą internetową 
        Dim client As clientSOAP = Nothing
        Try
            client = New clientSOAP(args(0), verbose)
        Catch ex As Exception
             ' błąd połączenia
            erreur("L'erreur suivante s'est produite lors de la connexion au service web : " + ex.Message, 2)
        End Try

Po nawiązaniu połączenia z serwisem internetowym klient może wysyłać swoje żądania. Są one wprowadzane za pomocą klawiatury, analizowane, a następnie wysyłane do serwera poprzez wywołanie metody executeFonction obiektu clientSOAP.


                 ' wysyłanie żądania do serwisu internetowego
                If Not erreurCommande Then Console.Out.WriteLine(("résultat=" + client.executeFonction(champs(0).Trim().ToLower(), champs(1).Trim(), champs(2).Trim())))

Klasa clientSOAP jest skompilowana w „zestawie”:

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

Następnie aplikacja kliencka testClientSoap jest kompilowana przez:

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

Oto przykładowe wykonanie bez komentarzy:

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

Można śledzić operacje sieciowe, żądając trybu „szczegółowego”:

dos>testClientSOAP http://localhost/operations/operations.asmx tryb szczegółowy
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

Zbudujmy teraz klienta graficznego.

10.7.3. Graficzny klient dla systemu Windows

Teraz będziemy wysyłać zapytania do naszej usługi internetowej za pomocą klienta graficznego, który również będzie korzystał z klasy clientSOAP. Interfejs graficzny będzie wyglądał następująco:

Elementy interfejsu to:

nr
typ
nazwa
rola
1
TextBox
txtURI
URI z serwisu web operations
2
Przycisk
btnOuvrir
otwiera połączenie z serwisem internetowym
3
Przycisk
btnFermer
zamyka połączenie z serwisem internetowym
4
ComboBox
cmbFonctions
lista funkcji (dodawanie, odejmowanie, mnożenie, dzielenie)
5
TextBox
txtA
argumenty funkcji
6
TextBox
txtB
argument b funkcji
7
TextBox
txtRésultat
wynik funkcji(a,b)
8
Przycisk
btnCalculer
uruchamia obliczenia funkcji(a,b)
9
TextBox
txtErreur
wyświetla komunikat o stanie połączenia

Istnieje kilka ograniczeń dotyczących działania:

  • przycisk btnOuvrir jest aktywny tylko wtedy, gdy pole txtURI nie jest puste i nie ma już otwartego połączenia
  • przycisk btnFermer jest aktywny tylko wtedy, gdy otwarto połączenie z serwisem internetowym
  • przycisk btnCalculer jest aktywny tylko wtedy, gdy połączenie jest otwarte, a pola txtA i txtB nie są puste
  • pola txtRésultat i txtErreur mają atrybut ReadOnly ustawiony na wartość „prawda”

Klient rozpoczyna od nawiązania połączenia z serwisem internetowym za pomocą przycisku [Ouvrir]:

Image

Następnie użytkownik może wybrać funkcję oraz wartości dla a i b:

Image

Image

Image

Image

Image

Poniżej znajduje się kod aplikacji. Pominęliśmy kod formularza, który nie ma tutaj znaczenia.


'przestrzenie nazw
Imports System
Imports System.Windows.Forms

' klasa formularza
Public Class FormClientSOAP
    Inherits System.Windows.Forms.Form

    ' atrybuty instancji
    Dim client As clientSOAP    ' client SOAP du service web operations

#Region „Kod wygenerowany przez Windows Form Designer”

    Public Sub New()
        MyBase.New()
        'To wywołanie jest wymagane przez projektant Windows Form.
        InitializeComponent()
        ' inne inicjalizacje
        myInit()
    End Sub

    'Zastąpiona metoda Dispose formularza służy do wyczyszczenia listy komponentów.
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
....
    End Sub

...

    Private Sub InitializeComponent()
....
    End Sub

#Koniec regionu


    Private Sub myInit()
        ' inicjalizacja formularza
        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
        ' Zmieniła się zawartość pola wprowadzania danych – ustalamy stan przycisku „Otwórz”
        btnOuvrir.Enabled = txtURI.Text.Trim <> ""
    End Sub

    Private Sub btnOuvrir_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnOuvrir.Click
        ' żądanie nawiązania połączenia z serwisem internetowym
        Try
            ' utworzenie obiektu typu [clientSOAP]
            client = New clientSOAP(txtURI.Text.Trim, False)
            ' stany przycisków
            btnOuvrir.Enabled = False
            btnFermer.Enabled = True
            ' URI nie może być już modyfikowany
            txtURI.ReadOnly = True
            ' status klienta
            txtErreur.Text = "Liaison au service web ouverte"
        Catch ex As Exception
            ' wystąpił błąd – wyświetlany jest komunikat
            txtErreur.Text = ex.Message
        End Try
    End Sub

    Private Sub btnFermer_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnFermer.Click
        ' zamknięcie połączenia z serwisem internetowym
        client.Close()
        ' stany przycisków
        btnOuvrir.Enabled = True
        btnFermer.Enabled = False
        ' URI
        txtURI.ReadOnly = False
        ' stan klienta
        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
        ' obliczanie funkcji f(a,b)
        ' kasowanie poprzedniego wyniku
        txtRésultat.Text = ""
        Try
            txtRésultat.Text = client.executeFonction(cmbFonctions.Text, txtA.Text.Trim, txtB.Text.Trim)
        Catch ex As Exception
            ' wystąpił błąd sieciowy
            txtErreur.Text = ex.Message
            ' zamykanie połączenia
            btnFermer_Click(Nothing, Nothing)
        End Try
    End Sub

    Private Sub txtA_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtA.TextChanged
        ' zmiana wartości 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
        ' zmiana wartości B
        txtA_TextChanged(Nothing, Nothing)
    End Sub

    ' metoda główna
    Public Shared Sub main()
        Application.Run(New FormClientSOAP)
    End Sub
End Class

Również w tym przypadku klasa clientSOAP ukrywa całą część sieciową aplikacji. Aplikacja została zbudowana w następujący sposób:

  • moduł clientSOAP.dll zawierający klasę clientSOAP został umieszczony w folderze projektu
  • interfejs graficzny clientsoapgui.vb został skompilowany przy użyciu pliku VS.NET, a następnie skompilowany w oknie DOS:
dos>vbc /r:system.dll /r:system.windows.forms.dll /r:system.drawing.dll /r:clientSOAP.dll clientsoapgui.vb

dos>dir
04/03/2004  09:13                7 168 clientSOAP.dll
04/03/2004  16:44                9 866 clientsoapgui.vb
04/03/2004  16:44               11 264 clientsoapgui.exe

Następnie uruchomiono interfejs graficzny za pomocą polecenia:

dos>clientsoapgui

10.8. Klient proxy

Przypomnijmy, co właśnie zrobiliśmy. Utworzyliśmy klasę pośredniczącą, która hermetyzuje komunikację sieciową między klientem a serwisem internetowym zgodnie z poniższym schematem:

Platforma .NET rozwija tę logikę jeszcze dalej. Gdy znamy już serwis internetowy, do którego chcemy się połączyć, możemy automatycznie wygenerować klasę, która posłuży nam jako pośrednik w dostępie do funkcji serwisu internetowego i która ukryje całą część sieciową. Klasę tę nazywamy proxy dla serwisu internetowego, dla którego została wygenerowana.

Jak wygenerować klasę proxy dla serwisu internetowego? Do każdego serwisu internetowego dołączony jest plik opisu w formacie XML. Jeśli plik URI naszego serwisu internetowego „operations” to http://localhost/operations/operations.asmx, to jej plik opisowy jest dostępny pod adresem http://localhost/operations/operations.asmx?wsdl, jak pokazuje poniższy zrzut ekranu:

Image

Mamy tu plik XML, który dokładnie opisuje wszystkie funkcje serwisu internetowego, podając dla każdej z nich typ i liczbę parametrów oraz typ wyniku. Plik ten nazywamy plikiem WSDL usługi, ponieważ wykorzystuje on język WSDL (Web Services Description Language). Na podstawie tego pliku można wygenerować klasę proxy za pomocą narzędzia wsdl:

dos>wsdl http://localhost/operations/operations.asmx?wsdl /language=vb
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Écriture du fichier 'D:\data\devel\vbnet\poly\chap9\clientproxy\operations.vb'.

dos>dir
04/03/2004  17:17                6 663 operations.vb

Narzędzie wsdl generuje plik źródłowy VB.NET (opcja /language=vb) o nazwie odpowiadającej nazwie klasy implementującej usługę internetową, w tym przypadku operations. Przyjrzyjmy się fragmentowi wygenerowanego kodu:

'------------------------------------------------------------------------------
' <autogenerowane>
'      Ten kod został wygenerowany przez narzędzie.
'      Wersja środowiska uruchomieniowego: 1.1.4322.573
'
'      Zmiany w tym pliku mogą spowodować nieprawidłowe działanie i zostaną utracone, jeśli 
'      kod zostanie wygenerowany ponownie.
' </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

'
'Ten kod źródłowy został automatycznie wygenerowany przez wsdl, Wersja=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

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

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

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

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

Na pierwszy rzut oka ten kod wydaje się nieco skomplikowany. Nie musimy jednak rozumieć wszystkich szczegółów, aby móc z niego korzystać. Przyjrzyjmy się najpierw deklaracji klasy:

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

Klasa nosi nazwę operations, pochodzącą od serwisu internetowego, dla którego została stworzona. Wywodzi się ona z klasy SoapHttpClientProtocol:

Image

Nasza klasa proxy posiada jeden konstruktor:

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

Konstruktor przypisuje do atrybutu url wartość URL z serwisu internetowego powiązanego z proxy. Powyższa klasa operations sama nie definiuje atrybutu url. Atrybut ten jest dziedziczony z klasy, od której pochodzi proxy: System.Web.Services.Protocols.SoapHttpClientProtocol. Przyjrzyjmy się teraz temu, co odnosi się do metody ajouter:

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

Można zauważyć, że ma ona ten sam podpis, co w usłudze internetowej operations, gdzie została zdefiniowana w następujący sposób:

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

Sposób, w jaki ta klasa komunikuje się z usługą internetową, nie jest tutaj przedstawiony. Komunikacja ta jest w całości obsługiwana przez klasę nadrzędną System.Web.Services.Protocols.SoapHttpClientProtocol. W proxy znajduje się jedynie to, co odróżnia ją od innych proxy:

  • URL powiązanej usługi internetowej
  • definicja metod powiązanej usługi.

Aby korzystać z metod serwisu internetowego operations, klient potrzebuje jedynie wygenerowanej wcześniej klasy proxy operations. Skompilujmy tę klasę do pliku 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

Teraz napiszmy program konsolowy. Jest on wywoływany bez argumentów i wykonuje polecenia wpisywane z klawiatury:

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

Kod klienta brzmi następująco:


' przestrzenie nazw
Imports System
Imports System.IO
Imports System.Text.RegularExpressions
Imports System.Collections
Imports Microsoft.VisualBasic

Public Module testClientProxy

    ' interaktywnie wykonuje polecenia wpisane z klawiatury
    ' i wysyła je do serwisu internetowego operations
    Public Sub Main()
        ' nie ma już argumentów – identyfikator URL usługi internetowej jest zakodowany na stałe w proxy

        ' tworzenie słownika funkcji serwisu internetowego
        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

        ' tworzy się obiekt proxy operacji 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' błąd połączenia
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

        ' żądania użytkownika są wpisywane za pomocą klawiatury
        ' w postaci funkcja a b – kończą się poleceniem „end”
        Dim commande As String = Nothing        ' commande tapée au clavier
        Dim champs As String() = Nothing        ' champs d'une ligne de commande

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

        ' niektóre dane lokalne
        Dim erreurCommande As Boolean
        Dim fonction As String
        Dim a, b As Double
        ' pętla wprowadzania poleceń wpisywanych za pomocą klawiatury
        While True
            ' na początku brak błędów
            erreurCommande = False
            ' odczyt polecenia
            commande = Console.In.ReadLine().Trim().ToLower()
            ' koniec?
            If commande Is Nothing Or commande = "fin" Then
                Exit While
            End If
            ' podział polecenia na pola
            champs = Regex.Split(commande, "\s+")
            Try
                ' potrzebne są trzy pola
                If champs.Length <> 3 Then
                    Throw New Exception
                End If
                ' pole 0 musi być rozpoznawalną funkcją
                fonction = champs(0)
                If Not dicoFonctions.ContainsKey(fonction) Then
                    Throw New Exception
                End If
                ' pole 1 musi być prawidłową liczbą
                a = Double.Parse(champs(1))
                ' pole 2 musi być prawidłową liczbą
                b = Double.Parse(champs(2))
            Catch
                ' nieprawidłowe polecenie
                Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b")
                erreurCommande = True
            End Try
            ' wysyłamy zapytanie do serwisu internetowego
            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

    ' wyświetlanie błędów
    Public Sub erreur(ByVal msg As String, ByVal exitCode As Integer)
        ' wyświetlanie błędu
        System.Console.Error.WriteLine(msg)
        ' zatrzymanie z błędem
        Environment.Exit(exitCode)
    End Sub
End Module

Analizujemy wyłącznie kod związany z wykorzystaniem klasy proxy. Najpierw tworzony jest obiekt proxy o nazwie operations:


        ' tworzenie obiektu proxy operacji 
        Dim myOperations As operations = Nothing
        Try
            myOperations = New operations
        Catch ex As Exception
            ' błąd połączenia
            erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : " + ex.Message, 2)
        End Try

Za pomocą klawiatury wpisano wiersze funkcji a i b. Na podstawie tych informacji wywoływane są odpowiednie metody obiektu proxy:


            ' wysyłanie żądania do serwisu internetowego
            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

Po raz pierwszy przetwarzamy tutaj operację toutfaire, która wykonuje cztery operacje. Do tej pory była ona pomijana, ponieważ wysyła tablicę liczb zamkniętą w obudowie XML, która jest bardziej skomplikowana w obsłudze niż proste odpowiedzi XML z innych funkcji, które zwracają tylko jeden wynik. Widać, że dzięki klasie proxy użycie metody toutfaire nie jest bardziej skomplikowane niż korzystanie z innych metod. Aplikacja została skompilowana w oknie DOS w następujący sposób:

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. Konfiguracja usługi internetowej

Usługa internetowa może wymagać informacji konfiguracyjnych, aby poprawnie się zainicjować. W przypadku serwera IIS informacje te można umieścić w pliku o nazwie web.config, znajdującym się w tym samym folderze co usługa internetowa. Załóżmy, że chcemy utworzyć usługę internetową, która do zainicjowania potrzebuje dwóch informacji: imienia i wieku. Te dwie informacje można umieścić w pliku web.config w następującej postaci:

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

Parametry inicjalizacji znajdują się w kopercie XML:

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

Parametr inicjalizacyjny o nazwie P, którego wartość wynosi V, zostanie zadeklarowany za pomocą następującego wiersza:


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

W jaki sposób usługa internetowa pobiera te informacje? Gdy IIS ładuje usługę internetową, sprawdza, czy w tym samym folderze znajduje się plik o nazwie web.config. Jeśli tak, odczytuje go. Wartość V parametru P uzyskuje się za pomocą instrukcji:

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

gdzie ConfigurationSettings jest klasą w przestrzeni nazw System.Configuration.

Sprawdźmy tę technikę na następującej usłudze internetowej:


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

Imports System.Web.Services
imports System.Configuration

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

   ' atrybuty
   Private nom As String
   Private age As Integer

   ' konstruktor
   Public Sub New()
      ' inicjowanie atrybutów
      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 

Usługa internetowa personne posiada dwa atrybuty: nom oraz age, które są inicjowane w jej konstruktorze bez argumentów na podstawie wartości odczytanych z pliku konfiguracyjnego web.config usługi personne. Plik ten ma następującą treść:


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

Usługa internetowa posiada ponadto identyfikator <WebMethod> bez parametrów, który ogranicza się do zwracania atrybutów nom i age. Usługa jest zarejestrowana w pliku źródłowym personne.asmx, który wraz z plikiem konfiguracyjnym znajduje się w folderze c:\inetpub\wwwroot\st\personne:

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

Przypiszmy wirtualny folder IIS/config do powyższego fizycznego folderu. Uruchommy plik IIS, a następnie w przeglądarce wpiszmy adres URL http://localhost/config/personne.asmx usługi personne:

Image

Przejdźmy do linku o unikalnym identyfikatorze metody:

Image

Metoda id nie ma żadnych parametrów. Skorzystajmy z przycisku Appeler:

Image

Udało nam się pobrać informacje zawarte w pliku web.config z serwisu.

10.10. Usługa internetowa IMPOTS

Wracamy do dobrze już nam znanej aplikacji IMPOTS. Kiedy ostatnio z nią pracowaliśmy, przekształciliśmy ją w serwer zdalny, do którego można było uzyskać dostęp przez Internet. Teraz przekształcamy ją w usługę internetową.

10.10.1. Usługa internetowa

Wychodzimy od klasy impôt utworzonej w rozdziale poświęconym bazom danych, która opiera się na informacjach zawartych w bazie danych ODBC:


' opcje
Option Strict On
Option Explicit On 

' przestrzenie nazw
Imports System
Imports System.Data
Imports Microsoft.Data.Odbc
Imports System.Collections

Public Class impôt
    ' dane niezbędne do obliczenia podatku
    ' pochodzą ze źródła zewnętrznego
    Private limites(), coeffR(), coeffN() As Decimal

    ' twórca
    Public Sub New(ByVal LIMITES() As Decimal, ByVal COEFFR() As Decimal, ByVal COEFFN() As Decimal)
        ' sprawdza się, czy wszystkie trzy tabele mają ten sam rozmiar
        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
        ' w porządku
        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)
        ' inicjuje trzy tablice graniczne: coeffR, coeffN na podstawie
        ' zawartości tabeli Timpots z bazy danych ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN to trzy kolumny tej tabeli
        ' może wywołać wyjątek
        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
        ' zapytanie SELECT
        Dim selectCommand As String = "select " + colLimites + "," + colCoeffR + "," + colCoeffN + " from " + Timpots
        ' tabele służące do pobierania danych
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' próbuje się uzyskać dostęp do bazy danych
        impotsConn = New OdbcConnection(connectString)
        impotsConn.Open()
        ' tworzy się obiekt polecenia
        sqlCommand = New OdbcCommand(selectCommand, impotsConn)
        ' wykonuje się zapytanie
        Dim myReader As OdbcDataReader = sqlCommand.ExecuteReader()
        ' Analiza pobranej tabeli
        While myReader.Read()
            ' dane z bieżącego wiersza są umieszczane w tablicach
            tLimites.Add(myReader(colLimites))
            tCoeffR.Add(myReader(colCoeffR))
            tCoeffN.Add(myReader(colCoeffN))
        End While
        ' zwolnienie zasobów
        myReader.Close()
        impotsConn.Close()

        ' tabele dynamiczne są umieszczane w tabelach statycznych
        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

    ' Obliczanie podatku
    Public Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' obliczenie liczby udziałów
        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
        ' obliczenie dochodu podlegającego opodatkowaniu i współczynnika rodzinnego
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' obliczenie podatku
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' zwrot wyniku
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function
End Class

W usłudze internetowej można używać wyłącznie konstruktora bez parametrów. Dlatego konstruktor klasy będzie wyglądał następująco:


    ' konstruktor
    Public Sub New()
        ' inicjuje trzy tabele limitów: coeffR, coeffN na podstawie
        ' zawartości tabeli Timpots z bazy danych ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN to trzy kolumny tej tabeli
        ' może wygenerować wyjątek

        ' pobiera parametry konfiguracyjne usługi
        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")

        ' wykorzystuje bazę danych
        Dim connectString As String = "DSN=" + DSNimpots + ";"     ' chaîne de connexion à la base

Pięć parametrów konstruktora poprzedniej klasy jest teraz odczytywanych z pliku web.config usługi. Kod pliku źródłowego impots.asmx wygląda następująco. Zawiera on większość poprzedniego kodu. Ograniczyliśmy się do zaznaczenia fragmentów kodu właściwych dla usługi internetowej:


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

' tworzenie serwisu internetowego dotyczącego podatków
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

    ' dane niezbędne do obliczenia podatku
    ' pochodzą ze źródła zewnętrznego
    Private limites(), coeffR(), coeffN() As Decimal
    Private OK As Boolean = False
    Private errMessage As String = ""


    ' konstruktor
    Public Sub New()
        ' inicjuje trzy tabele limitów: coeffR, coeffN na podstawie
        ' zawartości tabeli Timpots z bazy danych ODBC DSNimpots
        ' colLimites, colCoeffR, colCoeffN to trzy kolumny tej tabeli
        ' może wygenerować wyjątek

        ' pobiera parametry konfiguracyjne usługi
        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")

        ' wykorzystuje bazę danych
        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

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

        ' tabele służące do pobierania danych
        Dim tLimites As New ArrayList
        Dim tCoeffR As New ArrayList
        Dim tCoeffN As New ArrayList

        ' próba uzyskania dostępu do bazy danych
        Try
            impotsConn = New OdbcConnection(connectString)
            impotsConn.Open()
            ' tworzy się obiekt polecenia
            sqlCommand = New OdbcCommand(selectCommand, impotsConn)
            ' wykonuje się zapytanie
            myReader = sqlCommand.ExecuteReader()
            ' Analiza pobranej tabeli
            While myReader.Read()
                ' dane z bieżącego wiersza są umieszczane w tablicach
                tLimites.Add(myReader(colLimites))
                tCoeffR.Add(myReader(colCoeffR))
                tCoeffN.Add(myReader(colCoeffN))
            End While
            ' zwolnienie zasobów
            myReader.Close()
            impotsConn.Close()

            ' tabele dynamiczne są umieszczane w tabelach statycznych
            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
            ' wszystko w porządku
            OK = True
            errMessage = ""
        Catch ex As Exception
            ' błąd
            OK = False
            errMessage += "[" + ex.Message + "]"
        End Try
    End Sub

    ' obliczenie podatku
    <WebMethod()> _
    Function calculer(ByVal marié As Boolean, ByVal nbEnfants As Integer, ByVal salaire As Integer) As Long
        ' obliczenie liczby udziałów
        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
        ' obliczenie dochodu podlegającego opodatkowaniu i współczynnika rodzinnego
        Dim revenu As Decimal = 0.72D * salaire
        Dim QF As Decimal = revenu / nbParts
        ' obliczenie podatku
        limites((limites.Length - 1)) = QF + 1
        Dim i As Integer = 0
        While QF > limites(i)
            i += 1
        End While
        ' zwrot wyniku
        Return CLng(revenu * coeffR(i) - nbParts * coeffN(i))
    End Function

    ' identyfikator
    <WebMethod()> _
    Function id() As String
        ' sprawdzenie, czy wszystko jest w porządku OK
        Return "[" + OK + "," + errMessage + "]"
    End Function
End Class

Wyjaśnijmy kilka zmian wprowadzonych w klasie impots, poza tymi niezbędnymi do przekształcenia jej w usługę internetową:

  • odczytanie bazy danych w konstruktorze może się nie powieść. Dlatego dodaliśmy do naszej klasy dwa atrybuty oraz jedną metodę:
    • Wartość logiczna OK przyjmuje wartość vrai, jeśli udało się odczytać bazę danych, a w przeciwnym razie – faux
    • ciąg znaków errMessage zawiera komunikat o błędzie, jeśli nie udało się odczytać bazy danych.
    • Metoda id bez parametrów pozwala uzyskać wartości tych dwóch atrybutów.
  • Aby obsłużyć ewentualny błąd dostępu do bazy danych, część kodu konstruktora związana z tym dostępem została otoczona blokiem try-catch.

Plik konfiguracyjny usługi web.config ma następującą treść:


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

Podczas pierwszej próby załadowania usługi impots kompilator zgłosił, że nie może znaleźć przestrzeni nazw Microsoft.Data.Odbc użytej w dyrektywie:

Imports Microsoft.Data.Odbc

Po zapoznaniu się z dokumentacją

  • w pliku web.config dodano dyrektywę kompilacji, aby wskazać, że należy użyć modułu asemblerowego Microsoft.Data.odbc
  • kopia pliku microsoft.data.odbc.dll została umieszczona w folderze bin projektu. Folder ten jest systematycznie przeszukiwany przez kompilator serwisu internetowego podczas poszukiwania „assembly”.

Wydaje się, że możliwe są inne rozwiązania, ale nie zostały one tutaj szczegółowo omówione. Plik konfiguracyjny przybrał zatem następującą postać:


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

Zawartość folderu impots\bin:

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

Usługa wraz z plikiem konfiguracyjnym została umieszczona w folderze impots:

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

Fizyczny katalog serwisu internetowego został powiązany z wirtualnym katalogiem /impots w IIS. Strona serwisu wygląda zatem następująco:

Image

Jeśli klikniemy link id:

Image

Jeśli użyjemy przycisku Appeler:

Image

Poprzedni wynik wyświetla wartości atrybutów OK (true) i errMessage („”). W tym przykładzie baza została poprawnie załadowana. Nie zawsze tak było i dlatego dodaliśmy metodę id, aby uzyskać dostęp do komunikatu o błędzie. Błąd polegał na tym, że nazwa bazy danych DSN została zdefiniowana jako DSN (użytkownik), podczas gdy należało ją zdefiniować jako DSN (system). Rozróżnienie to wprowadza się w 32-bitowym menedżerze źródeł ODBC:

Wróćmy do strony serwisu:

Image

Kliknijmy link calculer:

Image

Określamy parametry wywołania i je wykonujemy:

Image

Wynik jest poprawny.

10.10.2. Wygeneruj proxy usługi impots

Teraz, gdy mamy już działającą usługę internetową impots, możemy wygenerować jej klasę proxy. Przypominamy, że będzie ona wykorzystywana przez aplikacje klienckie do przezroczystego dostępu do usługi internetowej impots. Najpierw używamy narzędzia wsdl do wygenerowania pliku źródłowego klasy proxy, a następnie kompilujemy go do biblioteki DLL.

dos>wsdl /language=vb http://localhost/impots/impots.asmx
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.1.4322.573]
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Écriture du fichier 'D:\data\serge\devel\vbnet\poly\chap9\impots\impots.vb'.

D:\data\serge\devel\vbnet\poly\chap9\impots>dir
09/03/2004  10:20    <REP>          bin
09/03/2004  10:58             4 651 impots.asmx
09/03/2004  11:05             3 364 impots.vb
09/03/2004  10:19               431 web.config

dos>vbc /t:library /r:system.dll /r:system.web.services.dll /r:system.xml.dll impots.vb
Compilateur Microsoft (R) Visual Basic .NET version 7.10.3052.4
pour Microsoft (R) .NET Framework version 1.1.4322.573
Copyright (C) Microsoft Corporation 1987-2002. Tous droits réservés.

dos>dir
09/03/2004  10:20    <REP>          bin
09/03/2004  10:58             4 651 impots.asmx
09/03/2004  11:09             5 120 impots.dll
09/03/2004  11:05             3 364 impots.vb
09/03/2004  10:19               431 web.config

10.10.3. Korzystanie z serwera proxy za pomocą klienta

W rozdziale poświęconym bazom danych stworzyliśmy aplikację konsolową służącą do obliczania podatku:

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

Program testimpots korzystał wówczas z klasycznej klasy impôt, zawartej w pliku impots.dll. Kod programu testimpots.vb wyglądał następująco:


Option Explicit On 
Option Strict On

' przestrzenie nazw
Imports System
Imports Microsoft.VisualBasic

' strona testowa
Module testimpots
    Sub Main(ByVal arguments() As String)
        ' interaktywny program do obliczania podatku
        ' użytkownik wprowadza trzy dane za pomocą klawiatury: żonaty nbEnfants wynagrodzenie
        ' następnie program wyświetla kwotę podatku do zapłaty
        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"

        ' sprawdzanie parametrów programu
        If arguments.Length <> 5 Then
            ' komunikat o błędzie
            Console.Error.WriteLine(syntaxe1)
            ' koniec
            Environment.Exit(1)
        End If        'if
        ' pobieranie argumentów
        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)

        ' tworzenie obiektu podatku
        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

        ' pętla nieskończona
        While True
            ' na początku brak błędów
            Dim erreur As Boolean = False

            ' żądane są parametry obliczenia podatku
            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()

            ' co należy zrobić?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If

            ' sprawdzanie liczby argumentów w wprowadzonej linii
            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
                ' sprawdzanie poprawności parametrów
                ' żonaty
                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
                ' wynagrodzenie
                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
                ' parametry są poprawne – obliczamy podatek
                Console.Out.WriteLine(("impôt=" & objImpôt.calculer(marié = "o", nbEnfants, salaire).ToString + " F"))
            End If
        End While
    End Sub
End Module

Wykorzystujemy ten sam program, aby teraz korzystał z usługi internetowej impots za pośrednictwem utworzonej wcześniej klasy proxy impots. Jesteśmy zmuszeni do niewielkiej modyfikacji kodu:

  • podczas gdy pierwotna klasa impôt miała konstruktor z pięcioma argumentami, klasa proxy impots posiada konstruktor bez parametrów. Jak widzieliśmy, te pięć parametrów jest teraz ustalanych w pliku konfiguracyjnym usługi internetowej.
  • nie ma więc już potrzeby przekazywania tych pięciu parametrów jako argumentów do programu testowego

Nowy kod wygląda następująco:


Imports System
Imports Microsoft.VisualBasic

' strona testowa
Module testimpots

    Public Sub Main(ByVal arguments() As String)
        ' interaktywny program do obliczania podatku
        ' użytkownik wprowadza trzy dane za pomocą klawiatury: żonaty nbEnfants wynagrodzenie
        ' następnie program wyświetla kwotę podatku do zapłaty
        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"

        ' utworzenie obiektu „podatek”
        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

        ' pętla nieskończona
        Dim erreur As Boolean
        While True
            ' na początku nie ma błędu
            erreur = False
            ' wymagane są parametry obliczenia podatku
            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()
            ' co należy zrobić?
            If paramètres Is Nothing Or paramètres = "" Then
                Exit While
            End If
            ' sprawdzanie liczby argumentów w wprowadzonej linii
            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
                ' sprawdzanie poprawności parametrów
                ' żonaty
                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
                ' wynagrodzenie
                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
                ' jeśli parametry są poprawne – obliczamy podatek
                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

W tym samym folderze mamy proxy impots.dll oraz kod źródłowy testimpots.

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

Kompilujemy kod źródłowy 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

a następnie uruchamiamy:

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

Otrzymujemy oczekiwany wynik.