Zadaniem jest modyfikacja napisanego poprzednio programu do licytacji cen
wielu przedmiotów przez wielu agentów na wersję klient-serwer. Programy
klientów powinny komunikować się z serwerem przez gniazdka domeny Unix
PF_UNIX
(opcjonalnie: gniazdka internetowe PF_INET
). Komunikacja
powinna być bezpołączeniowa SOCK_DGRAM
.
Program z poprzedniego zadania należy przerobić na dwa oddzielne programy.
Jeden z nich nazwiemy serwerem i jego zadaniem będzie odbieranie podbić
licytacji i rejestrowanie ich w tablicach Bids
i NBids
. Drugi program
nazwiemy klientem, i jego zadaniem będzie stworzenie odpowiedniej liczby
podprocesów agentów, którzy będą generowali podbicia licytacji i wysyłali
je serwerowi. Każde pojedyncze pobicie wysyłane jest pojedynczym komunikatem.
Program serwera należy zrealizować jako serwer iteracyjny, który po odebraniu każdej oferty podbicia zwiększy cenę odpowiedniego przedmiotu, i wróci do oczekiwania na dalsze oferty.
Uwaga: w tym zadaniu liczenie ofert jest jednowątkowe i nie ma żadnej współdzielonej struktury danych. Zatem cały kod synchronizacji muteksami z poprzedniego zadania nie będzie tu potrzebny i należy go usunąć. Natomiast proszę pozostawić pomiar czasu działania programów (rzeczywistego i wirtualnego), zarówno serwera jak i klienta, dla porównania z poprzednią wersją programu.
Zadanie należy zrealizować przy użyciu gniazdek domeny UNIX
z adresami
wykorzystującymi ścieżkę (lub nazwę) dowolnego istniejącego i dostępnego
dla użytkownika pliku.
Ćwiczenie należy wykonać w dwóch etapach. W pierwszym etapie celem będzie
poprawne wysyłanie komunikatów od podbiciach przez klienta(ów), i
odbieranie ich przez serwer. Pojedynczy komunikat powinien zawierać numer
przedmiotu i wartość podbicia. Obie dane w programie są wartościami typu
int
, typowo zajmującymi cztery bajty. Komunikat może te wartości
przechowywać w formacie binarnym (jako osiem bajtów), lub w formacie
tekstowym, w postaci sformatowanego stringa.
W drugim etapie celem jest śledzenie przez serwer całego przebiegu licytacji do końca, z wyświetlaniem wyników końcowych i sum kontrolnych na wyjściu.
Jednak chcemy przyjąć założenie, że serwer licytacji nie ma informacji o klientach biorących udział w licytacji (liczbie agentów oraz liczbie podbić licytacji, które oni wykonają), i pracuje oraz przyjmuje zgłoszenia w pętli nieskończonej (ale może być zakończony poleceniem użytkownika lub innego programu).
W tej sytuacji pojawia się problem zakończenia licytacji i wyświetlenia wyników. Istnieje szereg możliwych sposobów rozwiązania tego problemu.
Najprostsze rozwiązanie polega na odrzuceniu założenia o nieznajomości
liczby agentów i podbić, i prostym liczeniu otrzymanych komunikatów
przez serwer. W tej sytuacji powinien on przejść do wyświetlenia
wyników końcowych i podsumowania po otrzymaniu
N_AGENTS
*BIDDING_ROUNDS
komunikatów.
Jednak to rozwiązanie nie zadziała poprawnie gdyby część komunikatów z jakiegoś powodu została zgubiona. Łamie ono również przyjęte założenie, że serwer nie wie ilu jest klientów i ile podbić wykonają. Dlatego możemy traktować to rozwiązanie jako tymczasowe, i najlepiej byłoby w dalszym ciągu zastąpić je jednym z następujących.
Dość prostym rozwiązaniem zachowującym założenie o nieznajomości liczby agentów i podbić przez serwer byłoby wysłanie specjalnego komunikatu (znacznika) na zakończenie licytacji. Proces główny klienta, po zakończeniu pracy ostatniego podprocesu, mógłby wysłać taki komunikat. Na przykład, mógłby on zawierać numer przedmiotu spoza dozwolonego zakresu (-1 albo 40). Serwer zinterpretowałby otrzymanie takiego komunikatu jako sygnał zakończenia licytacji, wyświetlił wyniki końcowe i podsumowaniu, i sam zakończyłby pracę.
Inną możliwością jest wykorzystanie opóźnień czasowych. Ponieważ zakładamy, że licytacja przebiega w czasie rzeczywistym, i agenci przysyłają swoje podbicia bez żadnych opóźnień, serwer mógłby oczekiwać na każde zgłoszenie tylko przez jakiś czas (np. 0.1 sekundy). Jeśli w takim czasie nie przyjdzie żaden nowy komunikat podbicia, to serwer może uznać, że wszyscy agenci skończyli pracę i sam też przejść do zakończenia.
Realizacja tego rozwiązania wymaga połączenia oczekiwania na komunikat
(funkcja recv
lub recvfrom
) z funkcją select
, która ma możliwość
określenia czasu oczekiwania na deskryptorze pliku.
Jeszcze inną możliwością jest napisanie serwera jako interakcyjnego, pracującego w pętli nieskończonej, ale pod kontrolą użytkownika. Poza oczekiwaniem na komunikaty podbić serwer mógłby nasłuchiwać na standardowym wejściu (deskryptorze 0) na możliwe polecenia użytkownika. Na żądanie użytkownika program każdorazowo wyświetlałby bieżący stan licytacji (liczbę otrzymanych podbić i aktualne oferty wszystkich przedmiotów). Użytkownik sam oceniłby kiedy licytacja się zakończyła (obserwując sumę podbić, lub brak nowych podbić), i mógłby wtedy wydać polecenie zakończenia pracy serwera.
Na przykład, wpisanie 's' z klawiatury powinno spowodować wyświetlenie aktualnych ocen wszystkich produktów, natomiast wpisanie 'q' --- zakończenie pracy serwera.
Aby to zrealizować, serwer musiałby oczekiwać na komunikaty klientów na
gniazdku równocześnie z oczekiwaniem na polecenie użytkownika z
klawiatury, i reagować na to, które pojawi się pierwsze. Można do tego
wykorzystać funkcję select
w sposób pokazany na wykładzie.
Prawdopodobnie najlepszym rozwiązaniem jest połączenie powyższych opcji 3 i 4. Proces serwera oczekiwałby na polecenia użytkownika, ale jednocześnie sam śledziłby moment, kiedy komunikaty przestały napływać, i zawiadomiłby o tym użytkownika.
Minimalny wynik: uruchomiony program serwera poprawnie rejestruje swoje gniazdko odbierania komunikatów, i poprawnie odbiera komunikaty podbić, a program klienta poprawnie wysyła na to gniazdko wszystkie komunikaty podbić od wszystkich procesów agentów (2 punkty - na zajęciach).
Początkowo w celach testowych serwer może nawet wyświetlać na wyjściu fakt odebrania i treść każdego komunikatu. W tym celu można ustawić w programie jakąś minimalną liczbę podbić do wygenerowania (np. 10 na każdego agenta). Jednak po uruchomieniu komunikacji należy to zmienić i wysyłać większą liczbę komunikatów.
Pożądany wynik: program serwera odbiera wszystkie komunikaty wysłane przez klientów i wyświetla końcowe wyniki co najmniej za pomocą opisanego wyżej rozwiązania numer 1 (+1 punkt - na zajęciach).
Nie ma minimalnego wyniku w tym terminie. Ten termin jest premiowy.
Pożądany wynik: program realizuje któreś z rozwiązań problemu zakończenia licytacji i wyświetla wyniki końcowe (+1 punkt - za rozwiązanie numer 2 na zajęciach, albo +2 punkty - za rozwiązanie numer 3 lub numer 4 na zajęciach, albo +3 punkty - za rozwiązanie numer 5 na zajęciach, albo +1 punkt - za jedno z rozwiązań: 2, 3, 4 albo 5 w domu).
Raport z wykonania zadania, wysłany jednorazowo po jego zakończeniu całego zadania powinien zawierać krótkie podsumowanie uzyskanych wyników i/lub uwagi na temat ewentualnych problemów.
Proszę wyznaczyć maksymalną wartość parametru BIDDING_ROUNDS
przy którym
program wykonuje się w rozsądnym czasie (40-60 sekund na diablo, przy
pozostałych parametrach ustawionych na domyślne wartości jak w zadaniu nr
6), i takie wyniki podać w raporcie.
Razem z raportem proszę przysłać w eportalu wersje źródłowe programów serwera i klienta możliwe do skompilowania i uruchomienia na diablo. Proszę zadbać, by użyć adresu gniazdka serwera, które będzie mógł uruchomić dowolny użytkownik. Sugerowane rozwiązanie adresu gniazdka serwera:
#define WZORZEC_ADRESU_SERWERA "/tmp/adres_serwera_uid_%d"
sprintf(serv_addrstr.sun_path, WZORZEC_ADRESU_SERWERA, getuid());
unlink(serv_addrstr.sun_path);
Powyższy sposób zapewnia (w rozsądnych granicach), że proces serwera danego użytkownika utworzy sobie gniazdko ze swoim indywidualnym adresem. Dobrą praktyką jest również usunięcie pliku adresowego (utworzonego gniazdka) przed zakończeniem programu serwera.
Ocena za oddany raport i program: 3 punkty.
Uwaga: w tym zadaniu w ocenie będę zwracał uwagę na uporządkowania programów. Jeśli w programie pozostaną ,,śmieci'' z wcześniejszych wersji zadania, niepotrzebne elementy, nieużywane zmienne, itp. to będę za taki bałagan odejmował punkty. Proszę również zadbać aby programy kompilowały się bez ostrzeżeń kompilatora.
Prosty schemat pary programów klient-serwer komunikujących się przez gniazdka bezpołączeniowe domeny Unix można znaleźć na stronach 11 i 12 PDF-a z wykładu o komunikacji międzyprocesowej.
Inny przykład pary programów komunikujących się przez gniazdka
SOCK_DGRAM
:
http://man7.org/tlpi/code/online/dist/sockets/ud_ucase_cl.c.html
http://man7.org/tlpi/code/online/dist/sockets/ud_ucase_sv.c.html
komunikacja międzyprocesowa przez gniazdka:
https://beej.us/guide/bgipc/html/single/bgipc.html#unixsock
komunikacja międzyprocesowa przez gniazdka:
https://opensource.com/article/19/4/interprocess-communication-linux-networking
komunikacja międzyprocesowa przez gniazdka:
https://www.softprayog.in/programming/interprocess-communication-using-unix-domain-sockets
komunikacja międzyprocesowa przez gniazdka:
https://users.cs.cf.ac.uk/dave/C/node28.html
komunikacja międzyprocesowa przez gniazdka:
http://www.qnx.com/developers/docs/qnx_4.25_docs/tcpip50/prog_guide/sock_ipc_tut.html
Zamiast zastosowania komunikacji międzyprocesowej z wykorzystaniem gniazdek
domeny UNIX
z adresami wykorzystującymi ścieżkę (lub nazwę) wybranego
pliku jest możliwe zaimplementowanie komunikacji sieciowej z wykorzystaniem
gniazdek internetowych (domena INET
). Wymaga to użycia internetowych
struktur adresowych sockaddr_in
. Serwer nie potrzebuje znać adresu IP
komputera, natomiast musi znać numer portu. Może skorzystać z ustalonego
numeru portu (ale zachodzi ryzyko, że może on być w użyciu przez inny
program) albo może uzyskać dostępny numer portu od systemu. Klient powinien
uzyskiwać adres IP i numer portu serwera z argumentów wiersza poleceń. W
przypadku użycia numerycznego adresu IP wymagana jest tylko konwersja tego
adresu na postać wewnętrzną, natomiast w przypadku użycia adresu
symbolicznego (domenowego) konieczna jest jeszcze translacja adresu przez
program klienta.
W przypadku zaimplementowania komunikacji internetowej pojawia się jeszcze
jedna kwestia. Gdy komunikaty zawierają liczby zakodowane w postaci
tekstowej (dziesiętnej), są one wysyłane jako strumień bajtów i ich
transmisja przez sieć zawsze przebiega tak samo. Jednak w przypadku użycia
kodowania binarnego należy wziąć pod uwagę możliwość, że klient i serwer
mogą być uruchamiane na komputerach o innej architekturze procesora
(BIG-ENDIAN/LITTLE-ENDIAN). Gdy ta architektura po obu stronach będzie
inna, to wartości wielobajtowe (np. liczby int
) byłyby przez te strony
inaczej interpretowane. Przy komunikacji sieciowej stosuje się w tym celu
funkcje konwersji wartości liczbowych na tzw. porządek sieciowy takie jak
htonl/ntohl
itp. Należy zatem zastosować taką konwersję.
Zauważmy, że komputery Sun Microsystems (diablo) mają architekturę BIG-ENDIAN, natomiast wszystkie PC-ty (panamint) mają architekturę LITTLE-ENDIAN.
Uwaga: sieciowa komunikacja bezpołączeniowa jest z definicji zawodna. Komunikaty mogą być zgubione i ani klient ani serwer nie będą mieli tego świadomości, ani nie nastąpi retransmisja. Dlatego problem zakończenia licytacji musi być rozwiązany metodą inną niż metoda numer 1. Również metoda numer 2 nie jest teoretycznie w 100% niezawodna, ponieważ specjalny komunikat o zakończeniu licytacji może zostać zgubiony. Metody 3,4,5 nadal będą poprawne, aczkolwiek należy przyjąć nieco dłuższe wartości opóźnień.