LAB: Integracja z HTTP/WWW
Celem laboratorium jest ukazanie podstawowych metod integracji języka Prolog z protokołem HTTP, używania gniazd TCP oraz zademonstrowanie wielowątkowości w SWI-Prolog.
1. Wątki
Tworzenie wątków
Tworzenie oddzielnego wątku odbywa się przy użyciu predykatu thread_create/3:
thread_create(:Goal, -Id, +Options)
Predykat thread_self/1 zwraca identyfikator wątku, w którym został wywołany.
Ćwiczenie:
Proszę wkleić poniższy kod do pliku i wczytać go w SWI-Prolog:
start :-
thread_create(write_ID(10),ID1,[]),
write('Thread with ID: '), write(ID1), writeln(' has been started!'),
thread_create(write_ID(10),ID2,[]),
write('Thread with ID: '), write(ID2), writeln(' has been started!').
write_ID(0).
write_ID(Times) :-
thread_self(ID),
write(ID),
NewTimes is Times - 1,
write_ID(NewTimes).
Proszę uruchomić program predykatem start/0 kilkakrotnie i zaobserwować działanie.
Synchronizacja wątków
SWI-Prolog implementuje semafor, który można wykorzystać do kontroli sekcji krytycznej - mutex (MUTual EXclusive device). Proszę zapoznać się z predykatami: mutex_lock/1 i mutex_unlock/1.
Ćwiczenie
Kod programu z poprzedniego ćwiczenia proszę zmodyfikować tak, aby zaobserwować niepożądany efekt korzystania z jednego zasobu przez więcej niż jeden wątek jednocześnie. Druga klauzula predykatu write_ID/1 mogłaby wyglądać np. tak:
write_ID(Times) :-
thread_self(ID),
write(ID), write(': I am running! '),
NewTimes is Times - 1,
write_ID(NewTimes).
Proszę przetestować, czy zamierzony efekt zachodzi, a następnie tak zmodyfikować program, aby komunikaty jednego wątku (np. 2: I am running!) nie przeplatały się z komunikatami drugiego.
Komunikacja między wątkami
Każdy wątek ma przypisaną kolejkę wiadomości, z której może odczytywać wiadomości wysyłane przez inne wątki w postaci termu.
thread_send_message(+ThreadId, +Term) - wysyła wiadomość w postaci termu do kolejki wiadomości wskazanego wątku.
thread_get_message(?Term) - czeka na wiadomość w kolejce, która będzie mogła być zunifikowana z termem Term.
threads/0 - podaje informacje o pracujących/zakończonych wątkach i status każdego z nich.
Ćwiczenie
Proszę wkleić poniższy kod do pliku i wczytać go w SWI-Prolog:
start :-
thread_create(do_nothing,ID1,[]),
write('Thread with ID: '), write(ID1), writeln(' has been started!'),
thread_create(do_nothing,ID2,[]),
write('Thread with ID: '), write(ID2), writeln(' has been started!').
do_nothing :-
thread_self(ID),
thread_get_message(say(A)),
write('Thread '), write(ID), write(' says: '), writeln(A).
Po użyciu predykatu start/0 dwa wątki pracują i czekają na wiadomość w postaci termu say/1.
Proszę użyć predykatu threads/0 i zauważyć, że główny wątek (ten, w którym wpisuje się polecenia w konsoli) także jest wylistowany i zachowuje się jak inne wątki (można mu wysłać wiadomość, można go synchronizować z innymi, itp.).
Proszę wpisać w konsoli: thread_send_message(ID,say('Something stupid!')), gdzie ID to identyfikator jednego z pracujących wątków. Po ponownym użyciu predykatu threads/0 proszę zauważyć, że jeden z wątków zmienił status. Został zakończony sukcesem (true). Proszę wysłać wiadomość także do drugiego oczekującego wątku.
Problem pięciu filozofów
Proszę zapoznać się z problemem pięciu filozofów w Wikipedii. To klasyczny problem, którego rozwiązanie korzysta z wielowątkowości i synchronizacji wątków. Proszę wczytać plik 5_philosophers.pl, zapoznać się z nim i uruchomić predykatem start/0. W początkowej części programu znajdują się ustawienia. Proszę je zmodyfikować wg własnego uznania i przetestować poprawność działania programu.
2. Gniazda TCP
Biblioteka socket udostępnia w SWI-Prolog obsługę gniazd TCP po stronie klienta i serwera.
Istotne predykaty:
tcp_socket(-SocketId) inicjalizuje gniazdo i unifikuje jego identyfikator ze zmienną logiczna SocketId,
tcp_connect(+Socket,+Host:+Port) dokonuje połączenia z hostem na wskazanym porcie przy użyciu uprzednio zainicjowanego gniazda,
tcp_open_socket(+SocketId,-InStream,-OutStream) otwiera dwa strumienie (wejścia i wyjścia) SWI-Prolog dla gniazda,
tcp_accept(+Socket,-Slave,-Peer) działa po stronie serwera, oczekuje na połączenie ze strony klienta, tworzy nowe gniazdo dla klienta o identyfikatorze Slave, zmienna logiczna Peer jest unifikowana z adresem IP klienta.
tcp_close_socket(+SocketId) zamyka gniazdo. Ten sam efekt można uzyskać zamykając strumienie gniazda predykatem close/1.
UWAGA! W poniższych ćwiczeniach z użyciem gniazd TCP proszę używać portów z zakresu 33700-33999.
Klient i serwer
W tym ćwiczeniu proszę zmienić numer portu na dowolny z zakresu 33700-33999, aby nie przeszkadzać kolegom.
Predykat create_client/0 tworzy obsługę gniazda TCP po stronie klienta, wysyła do serwera komunikat w postaci termu, a następnie zamyka połączenie.
Predykat create_server/0 dzieli się na dwie części. Pierwsza tworzy gniazdo, a druga obsługuję przychodzące połączenie. Predykat tcp_accept/3 oczekuje do momentu nawiązania połączenia przez klienta.
Ćwiczenie:
Proszę wczytać powyższy program, uruchomić serwer w osobnym wątku (thread_create/3) i nawiązać z nim połączenie.
Następnie proszę tak zmodyfikować predykat create_server/0, aby był w stanie obsłużyć więcej niż jedno połączenie (niekoniecznie symultanicznie, może być jedno po drugim) i przy każdym dopisywał przesłany term do bazy wiedzy (assert/1).
Komunikator
Program komunikator wykorzystuje gniazda TCP w celu stworzenia w Prologu prymitywnej wymiany wiadomości między użytkownikami. Proszę się z nim zapoznać Jest podzielony na część klienta i serwera. W rzeczywistości jest to rozbudowana wersja prostego programu z przykładu zamieszczonego wyżej. Dodana jest odpowiednia interpretacja komunikatów. Owe komunikaty są przesyłane w postaci termów, więc każdy musi być zakończony kropką.
Ćwiczenie:
Po zapoznaniu z programem proszę się porozumieć z kolegami wykonującymi to samo ćwiczenie. Niech jeden ze studentów uruchomi serwer komunikatora na ustalonym porcie przy użyciu predykatu start_server/0. Pozostali niech się z nim połączą przy użyciu predykatu start_client/1. Np. start_client(ania). Proszę przetestować działanie komunikatora.
Przykładowa sesja po stronie klienta może wyglądać następująco:
3. HTTP
W SWI-Prolog do dyspozycji są dwie biblioteki integrujące z protokołem HTTP od strony klienta:
http_open - get
Biblioteka http_open jest prostsza, daje mniejsze możliwości, ale wystarczająca, jeśli ograniczamy się do ściągnięcia kodu html strony.
Predykat http_open(+URL,-Stream,+Options) otwiera strumień dla wskazanego adresu URL. Po skorzystaniu ze strumienia należy go zamknąć tak jak każdy inny strumień w prologu: close(+Stream).
Ćwiczenie:
http_open - head
Opcja method(head) predykatu open/3 powoduje, że odczytywany jest tylko nagłówek strony. W połączeniu z opcją header(+Name,-AtomValue) można uzyskać konkretne pole nagłówka.
Ćwiczenie
Proszę odczytać także inne pola nagłowka, np. content_type (myślnik jest zastępowany przez podkreślenie).
http_client - GET
Predykat http_get(+URL, -Reply, +Options) unifikuje zmienna logiczną Reply z odpowiedzią serwera, uzyskaną przy użyciu metody GET.
Ćwiczenie:
:- use_module(library(http/http_client)).
view_site :-
http_get('http://www.google.pl',Reply,[]),
write(Reply).
Dla zainteresowanych
SWI-Prolog udostępnia także bibliotekę HTTP od strony serwera. Pełna dokumantacja znajduje się na stronie: SWI-Prolog HTTP support.
Uwagi, komentarze, propozycje