Od pewnego czasu aplikacje internetowe nie są już samowystarczalnymi i skoncentrowanymi w jednym miejscu fragmentami kodu przeznaczonymi do wykonywania tylko jednego ściśle zdefiniowanego zadania. Dzięki coraz powszechniejszej modularyzacji oraz obiektowości stają się one niezależnymi komponentami, które mogą działać na wielu różnych platformach sprzętowych.
Systemy takie nazywane są rozproszonymi
i pozwalają na wyodrębnienie pewnych
funkcjonalności do innych autonomicznych
modułów. Zaletą tego typu rozwiązań jest przede
wszystkim możliwość ponownego wykorzystania
kodu komponentów, które zostały już wcześniej
stworzone. Ponadto ułatwia to zarządzanie całymi
projektami oraz umożliwia ich dalszy rozwój i konserwację
kodu.
Istnieje wiele metod pozwalających na
implementowanie systemów rozproszonych.
Jedną z popularniejszych technologii służących do
rozdzielenia funkcjonalności modułów i tworzenia
systemów zorientowanych na komponenty są
z pewnością Web Services. Swoją popularność
zawdzięczają przede wszystkim dostępności oraz
prostocie używania.
Czym jest WebService?
Web Service (usługa webowa/sieciowa) najłatwiej
jest zdefiniować jako niezależny od platformy
i implementacji komponent, którego zadaniem jest
dostarczenie konkretnej funkcjonalności. Rozwijając
słowo \”niezależny\”, warto zauważyć, że liczba
wspieranych platform jest naprawdę imponująca,
a można do nich zaliczyć nie tylko Javę i .NET, ale
także języki PHP, Python, Ruby i wiele innych.
Ponadto dzięki otwartej specyfikacji powstało
wiele frameworków, które w znaczny sposób
uproszczają modularyzację aplikacji i szybkie tworzenie
nawet złożonych usług webowych.
Omawiając koncept usług sieciowych, nie
można zapomnieć o wprowadzeniu pojęcia SOAP
(ang. Simple Object Access Protocol), który jest
niczym innym jak protokołem umożliwiającym
zdalny dostęp oraz wywoływanie obiektów.
W praktyce SOAP oparty jest na wielu standardach
służących do transportu danych, jak
chociażby XML, SMTP lub HTTP. Implementacja
tej warstwy nie jest jednak w żaden sposób
narzucona i możliwe jest wykorzystanie innych
protokołów w celu przesyłania danych.
Definicja usługi webowej
Największym problemem podczas implementowania
wielu systemów rozproszonych jest zapewnienie
im dobrego i spójnego interfejsu wymiany
informacji. Do często spotykanych problemów
zaliczyć można przede wszystkim różnice w architekturze
systemów operacyjnych, wynikające
z tego kłopoty z konwersją typów danych oraz
ich odwzorowaniem.
Dzięki wykorzystaniu języka
WSDL (ang. Web Service Definition Language)
możliwe jest uniknięcie wszystkich wymienionych
problemów. Ponadto zapewniony jest interfejs
wymiany informacji pomiędzy komponentami oraz
opis punktów dostępowych Web Services. Ponieważ
WSDL zgodny jest w całości ze specyfikacją
języka XML, każdy, nawet początkujący programista,
może w łatwy sposób podglądać zawartość
wymienianych komunikatów oraz w razie potrzeby
ręcznie je modyfikować.
W chwili obecnej istnieją dwie wersje standardu
WSDL. Zalecam używanie nowszej (z numerem 2.0), gdyż w przeciwieństwie do pierwszej, rekomendowana jest ona przez konsorcjum W3C.
Struktura pliku WSDL
Dokument WSDL to nic innego jak odpowiednio
uporządkowana kolekcja tagów zgodnych ze
specyfikacją XML Schema. Rzadko kiedy tworzy się
ją ręcznie. Zazwyczaj używane są generatory pozwalające
na automatyzację procesu wystawiania
usługi webowej. Warto jednak chociaż w niewielkim
stopniu rozumieć strukturę pliku WSDL, tak
aby w razie błędu potrafić ją naprawić.
Do najistotniejszych tagów należą:
- {html}
{/html} Pozwalający na definicję komunikatów
przesyłanych pomiędzy serwerem Web
Service a jego klientem. - {html}
{/html} Definiujący typy złożone,
w które opakowywane są dane przesyłane
pomiędzy komponentami - {html}
{/html} Definiujący metody
dostępne dla klienta i implementowane przez
Web Service - {html}
{/html} Definiujący
element wejściowy dla wskazanej metody -
{html}
- {html}
{/html} Wiążący metodę/
funkcjonalność wystawianą przez Web Service
z jej implementacją działającą na serwerze
Cykl życia usługi webowej
Omawiając cykl życia komponentów typu
Web Service, warto wiedzieć, że dostęp do usługi
odbywa się w trybie klient-serwer. Klientem jest
aplikacja, która chce uzyskać dostęp do funkcjonalności
implementowanej po stronie serwera
i wystawionej za pomocą Web Service.
Pierwszym etapem każdej komunikacji jest
nawiązanie połączenia. W przypadku architektury
opartej o Web Services sprowadza się to do udostępnienia
klientowi pliku WSDL zawierającego opis
komponentu serwera oraz listę implementowanych
funkcji. Dzięki mechanizmowi SOAP klient wywołuje
zdalnie żądaną procedurę. Rezultat wykonanej
operacji opakowany jest w odpowiednią kopertę
XML-ową i na koniec przesłany zostaje do klienta.
Implementacja architektury Web
Service w języku PHP
Koncepcja Component Based Development, czyli
programowania zorientowanego na komponenty,
nie jest często wykorzystywana w języku PHP. Do tej
pory PHP uważane było za język służący do pisania
prostych serwisów internetowych, często w sposób
prowizoryczny i nieprzemyślany. W takim przypadku
trudno było mówić o modularyzacji, a tym bardziej
tworzeniu aplikacji rozproszonej.
Wraz z pojawieniem się wszystkich rewolucji
związanych z wersją piątą coraz częściej zaczęto
wykorzystywać PHP do bardziej złożonych projektów.
Dzięki temu pojawiło się wiele rozszerzeń
i bibliotek implementujących mechanizmy SOAP.
Jedną z ciekawszych inicjatyw jest z pewnością
open source\’owy projekt o nazwie WSO2 Web
Services Framework for PHP, który przeniesiony
został bezpośrednio ze świata Javy. Dzięki temu
posunięciu uzyskujemy dostęp do potężnego
narzędzia, pozwalającemu na wykorzystanie wielu
technik i wzorców zapożyczonych z Javy.
Instalacja frameworka
Aby móc w pełni korzystać z dobrodziejstw
WSF for PHP, trzeba przejść przez prostą procedurę
konfiguracyjną. W zależności od platformy sprzętowej,
na jakiej pracujemy, poszczególne etapy
instalacji mogą się niewiele różnić. Niezależnie od
tego w systemie musi być zainstalowany moduł
do obsługi PHP5. W artykule przedstawiona jest
najbardziej ogólna metoda przeznaczona dla systemu
Linux. Korzystając ze strony domowej projektu,
można jednak w łatwy sposób zmodyfikować ją
tak, aby działa również dla Windows.
Najprostszą opcją instalacji rozszerzenia WSF jest
posłużenie się jednym z gotowych pakietów binarnych
dla wskazanej dystrybucji systemu Linux. Jeżeli
nie będzie jej na liście, skazani będziemy na ręczną
kompilację. Najlepiej jest pobrać wtedy kod źródłowy
rozszerzenia z oficjalnej strony projektu. Po jego
rozpakowaniu należy wejść do nowo utworzonego
katalogu i wykonać poniższą sekwencję komend:
./configure
make
make install
Jeżeli proces kompilacji zakończy się błędem,
oznaczać to będzie, że najprawdopodobniej
brakuje nam jednej z bibliotek, które wykorzystywane
są w projekcie. W takiej sytuacji najlepiej jest
sprawdzić, czy w naszym systemie zainstalowano
pakiety openssl, libxml2 oraz php_mod.
Gdy proces kompilacji zakończy się pomyślnie,
wszystkie pliki binarne zostaną skopiowane do
katalogu z rozszerzeniami (nie mylić z rozszerzeniem
pliku) PHP. W zależności od dystrybucji może
znajdować się on w różnych miejscach. Najczęściej
będzie miał jednak postać podobną do tej:
/usr/local/lib/php/extensions/debug-zts-***
Na koniec pozostaje zmodyfikowanie pliku php.
ini oraz odblokowanie skompilowanego wcześniej
rozszerzenia. W tym celu należy dodać lub odkomentować
następujące linie w pliku:
extension=wsf.so
extension=xsl.so
extension_dir=\"usr/local/lib/php/extensions/debug-zts-***\"
include_path = \".:/sciezka/do/katalogu/scripts\"
Ostatnia linia wskazuje na katalog, w którym
znajdują się skrypty PHP frameworku WSF,
odpowiadające między innymi za automatyczną
generację plików WSDL. Katalog scripts należy
skopiować ręcznie z archiwum rozszerzenia i umieścić
go w dowolnej lokalizacji wskazanej później
w include_path.
Procedura instalacji dla systemu Windows jest
bardzo podobna. Oczywiście plik wsf.so będzie
miał odpowiednie rozszerzenie DLL, a ścieżki
dostępu ulegną niewielkiej zmianie. W razie problemów
odsyłam na stronę domową projektu.
Implementacja prostej usługi webowej
Ponieważ najlepiej jest uczyć się na przykładach,
stworzymy prosty Web Service umożliwiający
wyświetlenie informacji na temat wybranej
książki znajdującej się w fikcyjnej bazie danych. Tak
naprawdę zadaniem naszej usługi webowej będzie
udostępnienie dwóch metod pozwalających na wyświetlenie
ceny książki oraz jej tytułu na podstawie
numeru ISBN, podanego jako argument. Oczywiście
wszystkie operacje wykonywane będą po
stronie serwera, rola klienta sprowadza się tylko
do wysyłania i odbierania żądań oraz wyświetlenia
końcowego wyniku.
Przystępując do tworzenia nawet prostego
Web Service, zmuszeni jesteśmy do wyboru kolejności
implementacji poszczególnych jego części.
Zazwyczaj pracę rozpoczyna się od stworzenia
serwera, później aplikacji klienckiej. Bywa jednak,
że najpierw definiowany jest plik WSDL zawierający
specyfikację usługi webowej. Później na jego
podstawie powstaje serwer oraz klient. Rozwiązanie
to jest rzadziej stosowane, gdyż najczęściej wymaga
ręcznego tworzenia pliku WSDL, co w wielu
przypadkach może być dosyć uciążliwe.
Łatwiejszym rozwiązaniem jest utworzenie
serwera oraz wyodrębnienie odpowiednich
funkcjonalności i automatyczne wygenerowanie
dokumentu WSDL. Rozwiązanie to jest jednak
zależne od bibliotek, z jakich korzystamy. Na
szczęście w naszym przypadku framework WSF
oferuje opcję dynamicznego tworzenia pliku WSDL.
Bazuje ona na mechanizmie refleksji oraz adnotacji.
Jest to rozwiązanie zaczerpnięte z języka Java
(w wersji 5.0).
Stosowanie adnotacji sprowadza się do umieszczenia
odpowiednich przypisów nad metodami
w kodzie. Umożliwiają one udzielenie pewnych
podpowiedzi dla interpretera tak, aby mógł on
poprawnie analizować kod i podjąć odpowiednie
działania już na etapie uruchomienia (ang. run time).
W przypadku Javy adnotacje są elementem języka,
w PHP muszą być niestety nadal umieszczane
w komentarzach. Ich struktura wykorzystuje jednak
mechanizm znany chociażby z phpDocumentatora.
Przykładową adnotację pokazano na następnej
stronie:
/** Pobierz Tytuł książki po numerze ISBN
* @param string $isbn Numer ISBN ksiazki
* (mapowanie do typu xs:string dla XML schema )
* @return string $title Book title
*(mapowanie do xs:string dla XML schema )
*/
Mechanizm refleksji jest natomiast częścią
języka PHP. Pozwala on na dynamiczne określanie
nazw metod oraz pól wskazanych obiektów lub
klas w trakcie wykonywania skryptu. W pewnych
sytuacjach umożliwia to znaczne uproszczenie aplikacji
lub walidację poprawności kodu tworzonego
przez programistę.
Tworzenie serwera dla usługi
Web Service
Stworzymy teraz prosty serwer odpowiedzialny
za wystawienie dwóch funkcji, umożliwiających
wyświetlenie informacji na temat książki o wskazanym
numerze ISBN. Całość kodu serwera zostanie
umieszczona w pliku bookShop_server.php.
Ponieważ zadaniem usługi webowej jest zwracanie
informacji na temat książek, należy w jakiś
sposób zaimplementować ich bazę danych. Oczywiście
słowo baza zostało zastosowane jako przenośnia.
Ponieważ artykuł ten nie koncentruje się na
wykorzystaniu baz danych w PHP, dla uproszczenia
wszystkie informacje trzymane są w tablicy asocjacyjnej.
Dla ułatwienia, książki reprezentowane
są jako obiekty klasy Book, posiadającej tylko
cztery pola oraz konstruktor, służący jedynie do ich
inicjalizacji. Pokazano to na listingu 1.
isbn = $isbn;
$this->title = $title;
$this->author = $author;
$this->price = $price;
}
}
class SimpleStorage {
private $hashMap = array();
public function addBookToDB($book) {
$this->hashMap[$book->isbn] = $book;
}
public function getBookFromDB($isbn)
{
if (array_key_exists($isbn,$this->hashMap)) {
return $this->hashMap[$isbn];
}
return null;
}
}
$simpleDB = new SimpleStorage();
$simpleDB->addBookToDB( new Book(\"0000130378143\", \"ANSI C\", \"Mark Williams\", 35.00));
$simpleDB->addBookToDB( new Book(\"0000130142646\", \"Gtk+ Programming in C\", \"Syd Logan\", 32.00));
$simpleDB->addBookToDB( new Book(\"9788324605668\", \"ASP.NET 2.0.Gotowe rozwiazania\", \"Imar Spaanjaars\",25.00));
?>
Rolę warstwy dostępu do informacji zapisanych
w tablicy asocjacyjnej spełnia klasa SimpleStorage.
W praktyce oferuje ona dwie metody, addBookToDB
oraz getBookFromDB. Pierwsza z nich służy do dodania
książki do tablicy, druga zwraca obiekt klasy
Book dla danego numeru ISBN. Gdy książka nie istnieje,
zwracany jest null. Kolejne linie w kodzie służą
do inicjalizacji obiektu SimpleStorage oraz dodania
do niego trzech przykładowych książek.
Przyszedł czas na zaimplementowanie metod,
które zostaną wystawione przez Web Service, co
pokazano na listingu 2. Tak naprawdę ich nazwa
może być dowolna, użytkownikowi bowiem udostępniony
zostanie odpowiedni interfejs za pomocą
usługi sieciowej.
/** Pobierz cenę wskazanej liczby książek o numerze danym numerze ISBN
* @param string $isbn Numer ISBN książki
* (mapowanie do typu xs:string z XMLschema )
* @param int $amount Ilość książek do kupienia
* (mapowanie do xs:nonNegativeIntegerz XML schema)
* @return float $price Całkowita cena książek
*(mapowanie do xs:double dla XML schema)
*/
function getPriceByISBN($isbn, $amount)
{
global $simpleDB;
$book = $simpleDB->getBookFromDB($isbn);
if ($book != null) {
return array(\"price\"=> ($book->price * $amount));
}
return null;
}
/** Pobierz Tytuł książki po numerze ISBN
* @param string $isbn Numer ISBN ksiazki
* (mapowanie do typu xs:string dla XML schema )
* @return string $title Book title
*(mapowanie do xs:string dla XML schema)
*/
function getTitleByISBN($isbn) {
global $simpleDB;
$book = $simpleDB->getBookFromDB($isbn);
if ($book != null) {
return array(\"title\"=> $book->title);
}
return null;
}
Przyjrzyjmy się bliżej pierwszej metodzie,
getPriceByISBN. Przyjmuje ona dwa argumenty,
numer ISBN książki oraz liczba egzemplarzy, dla
których zostanie policzona cena. Implementacja
metody jest mało istotna, sprowadza się jedynie do
odszukania odpowiedniej pozycji po numerze ISBN
oraz przemnożenia jej ceny przez liczbę egzemplarzy
($amount). Warto zwrócić uwagę na to, w jaki sposób
funkcja zwraca swój wynik. W przypadku porażki
jest to zwykły null, gdy książka zostanie jednak
znaleziona, odpowiednio policzona cena zwracana
jest poprzez opakowanie jej w tablicę asocjacyjną.
Dzięki takiemu rozwiązaniu możliwe jest przypisanie
nazwy klucza do zwracanej wartości.
Szczególną uwagę należy zwrócić na komentarze
umieszczone nad funkcją. Zawierają one
adnotacje, które podpowiadają frameworkowi
WSF, w jaki sposób należy wygenerować dokument
WSDL. Adnotacje @param służą do definiowania
argumentów, jakie przyjmuje funkcja. Po spacji
należy podać typ argumentu oraz jego nazwę.
Adnotacja @return określa typ zwracanej wartości
oraz nazwę, pod jaką można ją znaleźć w tablicy
asocjacyjnej. Pod każdą adnotacją możliwe jest
umieszczenie komentarza (pomiędzy nawiasami).
Na koniec pozostało nam utworzenie dwóch
tablic odwzorowań, co ilustruje listing 3. Pierwsza
z nich, czyli $operations, tworzy mapowanie pomiędzy
nazwą funkcji dostępnej poprzez interfejs
usługi webowej i jej implementacją po stronie serwera. Druga tablica określa typy argumentów
przyjmowanych przez funkcje zaimplementowane
przez serwer (MIXED oznacza typ mieszany, do
którego można przypisać dowolny obiekt).
$operations = array(\"getPrice\"=>\"get-PriceByISBN\", \"getTitle\"=> \"getTitle-ByISBN\");
$opParams = array(\"getPriceByISBN\"=>\"MIXED\", \"getTitleByISBN\" => \"MIXED\");
$svr = new WSService(array(\"operations\"=>$operations,\"opParams\"=>$opParams,\"serviceName\"=>\"bookShopService\"));
$svr->reply();
Stworzone w ten sposób tablice przekazywane
są do konstruktora obiektu WSService, którego
zadanie polega na utworzeniu instancji usługi
sieciowej. Ostatnim argumentem konstruktora
jest nazwa usługi, pod jaką będzie występował
Web Service.
Właściwe uruchomienie Web Service\’a następuje
w momencie wywołania metody {stala}reply(){/stala}
obiektu $srv.
Uruchomienie skryptu bookShop_server.php
poprzez zwykłą przeglądarkę internetową powinno
wyświetlić informację o poprawnym uruchomieniu
usługi webowej: http://localhost/WebService/bookshop_server.php.
Wygenerowanie dokumentu WSDL odbywa się
poprzez odwołanie do adresu Web Service\’a z dodatkowym
parametrem ?wsdl2: http://localhost/
WebService/bookshop_server.php?wsdl2.
Implementacje klienta usługi webowej
Po lekturze dotychczasowej części artykułu,
odwołanie do stworzonej usługi webowej nie
powinno stanowić większego problemu. Podobnie
jak wcześniej, kod skryptu klienckiego podzielony
został na dwie części i najlepiej jest umieścić go
w jednym pliku o nazwie: bookShop_client.php.
Żądania, podobnie jak odpowiedzi odbierane
od Web Service\’a, muszą być opakowane w obiekty
odpowiednich klas, co pokazano na listingu 4.
class PriceRequestWrapper
{
public $isbn;
public $amount;
}
class PriceResponseWrapper
{
public $price;
}
class TitleRequestWrapper
{
public $isbn;
}
class TitlteResponseWrapper
{
public $title;
}
$class_map = array(\"getPrice\" => \"PriceRequestWrapper\",\"getPriceResponse\"=> \"PriceResponseWrapper\",\"getTitle\" => \"TitleRquestWrapper\",\"getTitleResponse\"=> \"TitleResponseWrapper\");
Klasy te nie wyróżniają sie niczym nadzwyczajnym.
Ich pola odpowiadają jedynie wartościom, jakie
wysyłamy lub otrzymujemy od Web Service\’a.
Komunikaty wysyłane opakowywane są w klasy
z przyrostkiem RequestWrapper, otrzymywane
ResponseWrapper.
Na tym etapie warto jest również stworzyć
mapowanie o nazwie $class_map, którego zadaniem
jest odwzorowanie metod usług sieciowych
i klas opakowujących. Przykładowo dla metody
{stala}getPrice(){/stala} zdefiniowano wrapper PriceRequestWrapper
służący do wysłania argumentów żądania.
Odebranie odpowiedzi pochodzącej z wywołania
wymienionej metody odwzorowane jest jako
{stala}getPriceResponse(){/stala} i zamapowane na obiekt typu
PriceResponseWrapper.
Uzyskanie dostępu do Web Service odbywa się
poprzez utworzenie obiektu klasy WSClient (zobacz
listing 5). Jako argument należy podać tablicę
asocjacyjną zawierającą przynajmniej dwa klucze.
Pierwszy z nich \”wsdl\” wskazuje na lokalizację
dokumentu WSDL. W naszym przypadku jest on
generowany dynamicznie przez serwer. Drugi klucz
\”classmap\” wskazuje na wcześniej stworzoną
tablicę odwzorowań.
try {
$client = new WSClient(array(\"wsdl\" => \"http://localhost/WebService/bookshop_server.php?wsdl2\",\"classmap\" => $class_map));
$proxy = $client->getProxy();
$input = new PriceRequestWrapper();
$input->isbn = \"9788324605668\";
$input->amount = 2;
$priceResponse = $proxy->getPrice($input);
$input = new TitleRequestWrapper();
$input->isbn = \"0000130142646\";
$titleResponse = $proxy->getTitle($input);
echo \"Suma zamowionych ksiazek $\" . $priceResponse->price . \"
\";
echo \"Tytul ksiazki z zapytania drugiego:\" . $titleResponse->title .\"
\";
} catch (Exception $e) {
if($e instanceof WSFault) {
printf(\"Wystapil blad w komunikacji z WebServicem. Powod: %s\n\", $e->Reason);
printf(\"Numer bledu: %s \n\", $e->Code);
} else {
printf(\"Inny blad: %s\n\",$e->getMessage());
}
}
Klasa WSClient implementuje wzorzec projektowy
Pośrednika (ang. Proxy). Innymi słowy, jest
to lokalny reprezentant przekazujący wszystkie
wywołania do obiektu zdalnego. Instancję pośrednika
można uzyskać za pomocą metody {stala}getProxy(){/stala}.
Dzięki temu możliwe jest wywołanie takich
metod jak {stala}getTitle(){/stala} i {stala}getPrice(){/stala}. Niedoświadczony
programista może mieć wrażenie, że metody te
wykonywane są lokalnie. Tak naprawdę ich implementacja
oddelegowana jest jednak do stworzonej
wcześniej usługi webowej. Wywołując metody get-
{stala}Title(){/stala} oraz {stala}getPrice(){/stala}, należy pamiętać o opakowaniu
argumentów, które są do nich przekazywane.
W tym celu najlepiej posłużyć się odpowiednimi
obiektami opakowującymi (tymi z przyrostkiem
RequestWrapper). Podobnie wartości zwracane
przez funkcję opakowywane są w odpowiedni
obiekt o sufiksie ResponseWrapper.
Całość kodu znajduje się w bloku {html}try {} catch()
{}{/html} tak, aby przechwytywać wszelkiego rodzaju
sytuacje wyjątkowe.
Zakończenie
Przedstawiony tutaj Web Service jest tylko
prostym przykładem ukazującym możliwości
architektury rozproszonej. Zaciekawionych tym
zagadnieniem zachęcam do dalszego eksperymentowania.