Wykłady są w piątki o 14:15. Forma zdalna, w MS Teams został założony dedykowany zespół. Jeśli macie Państwo problem z dostępem do tego zespołu, to proszę mi to zgłosić.
Ćwiczenia prowadzone są klasycznie. Grupa nr 1 w środy o 10:30 w sali G-1-08, grupa nr 2 o 14:30 w sali G-1-03.
Zapraszam również na konsultacje (pokój C-2-29 lub online, terminy w USOSwebie).
Slajdy i inne materiały będą sukcesywnie umieszczane na karcie „Udostępnione” w zespole MS Teams założonym na potrzeby wykładu.
Przykładowe programy są dodatkowo dostępne w /home/palacz/PS/ po zalogowaniu się na którejś z linuksowych maszyn.
W. Richard Stevens, „UNIX Network Programming” (2 tomy), wydane po polsku jako „UNIX: programowanie usług sieciowych” przez WNT w 2000 i 2001 roku.
Gniazdka sieciowe BSD omówione są w tomie 1. W tomie 2 jeden z rozdziałów omawia Sun RPC.
Douglas E. Comer, David L. Stevens, „Internetworking with TCP/IP, Vol. III”, wydane po polsku jako „Sieci komputerowe TCP/IP. Tom 3. Programowanie w trybie klient-serwer: wersja BSD”, WNT 1997.
Dokumenty RFC.
Definicje protokołów, w oparciu o które działa Internet. Dostępne online na witrynie https://www.rfc-editor.org/.
Standardy POSIX oraz SUS (Single UNIX Specification).
W chwili obecnej jest to jeden i ten sam standard pod dwiema różnymi nazwami. Jest czymś w rodzaju wspólnego mianownika dla uniksopodobnych systemów operacyjnych. Specyfikuje zbiór plików nagłówkowych oraz funkcji dostępnych w bibliotece standardowej języka C. Kod korzystający tylko z tych funkcji ma dużą szansę skompilować się i poprawnie działać m.in. na Linuksie, macOS-ie, iOS-ie i Androidzie.
Specyfikacja POSIX.1-2008 / SUSv4 dostępna jest online i obejmuje funkcje potrzebne do obsługi gniazdek (socket, bind, itd.).
Manual systemowy.
Zbiór stron opisujących różne aspekty danego systemu operacyjnego: polecenia wydawane z linii komend, funkcje jądra, funkcje biblioteki standardowej, itp. Dostęp poprzez polecenie man: man socket, man bind, itd. Odstępstwa w zachowaniu funkcji od tego, co mówi POSIX powinny być wyraźnie odnotowane. Podobnie wyraźnie oznaczone powinny być specyficzne dla danego systemu funkcje rozszerzające POSIX (dla Linuksa patrz np. man epoll_create).
Manual podzielony jest na sekcje, i może się zdarzyć że w dwóch różnych sekcjach są strony o tej samej nazwie. Poleceniu man można podać dodatkowy argument wskazujący o którą sekcję chodzi: man 2 socket, man 7 socket.
Najnowsza wersja linuksowego manuala jest dostępna online.
Dokumentacja glibc.
W systemach linuksowych standardową biblioteką jest przeważnie GNU C Library. Ma ona swoją własną dokumentację (dostępna online), tworzoną niezależnie od manuala systemowego. Jeden z rozdziałów podręcznika jest poświęcony gniazdkom.
Informacje z witryny Stack Overflow oraz te znalezione w innych zakątkach Internetu przy pomocy wyszukiwarki Google.
Znalezione w sieci odpowiedzi na rozmaite pytania często zawierają gotowe fragmenty kodu. Można się na nich wzorować, albo wręcz je skopiować w całości, ale wcześniej trzeba je zrozumieć. Bez tego nie będziecie Państwo wiedzieć w jakich sytuacjach kod może zadziałać w inny sposób niż tego oczekujecie, nie będziecie też wiedzieć czy kod jest kompletny czy też może jego autor pominął pewne fragmenty (np. zwalnianie przydzielonych zasobów).
W trakcie semestru można mieć co najwyżej trzy nieusprawiedliwione nieobecności. Przekroczenie tego limitu oznacza brak zaliczenia.
Zaliczenia wystawiane są podstawie zadań zaliczeniowych, kolokwiów oraz wykazanego przez Państwa podczas zajęć poziomu wiedzy i umiejętności (możecie o tym myśleć jako o plusach i minusach zapisywanych w moich notatkach po przeprowadzonych z Wami rozmowach).
Poniżej znajdziecie Państwo spis zagadnień przerabianych podczas poszczególnych spotkań. Są tam też listy zadań. Jeśli nie zdążycie ich zrobić podczas ćwiczeń, to automatycznie stają się one zadaniami domowymi.
Rozwiązania wszystkich zrobionych od początku semestru zadań należy mieć pod ręką (najlepiej zapisywać je na wydziałowym koncie linuksowym), bo podczas ćwiczeń wspólnie analizujemy kod Waszych rozwiązań. Jest to robione albo w formie indywidualnych rozmów, albo w formie dyskusji z udziałem całej grupy.
Zadania mają różny poziom ważności. Zdecydowana większość jest mało ważna: powinniście je zrobić i móc je na żądanie pokazać, ale nie musicie mi ich przesyłać. Jeśli zdarzy się Wam raz na miesiąc któreś z tych zadań pominąć, to nie będzie miało to negatywnych konsekwencji dla Waszej oceny końcowej.
Drugi poziom to tzw. zadania zaliczeniowe. W semestrze jest ich około pięciu, pierwsze pojawia się na trzecich ćwiczeniach. Dla nich wymagane jest wysłanie rozwiązania poprzez Pegaza do określonego dnia — termin będzie podany przy zadaniu, spóźnienia skutkują obniżeniem oceny za dane zadanie.
Trzeci poziom to zadania oznaczone jako nieobowiązkowe. Osoby szczególnie zainteresowane tematem mogą spróbować się z nimi zmierzyć, można je ze mną przedyskutować, ale nie wpływają na końcową ocenę z ćwiczeń.
Oficjalnym środowiskiem są narzędzia zainstalowane na linuksowych komputerach w pracowniach studenckich oraz na serwerze spk-ssh.if.uj.edu.pl (można się na niego zalogować z domu). Oddawany kod musi dawać się za ich pomocą uruchomić.
W szczególności kod w języku C musi się kompilować bez ostrzeżeń za pomocą gcc -std=c99 -pedantic -Wall. Obowiązuje standard języka C z 1999 roku, bo POSIX 2008 zakłada dostępność kompilatora z nim zgodnego.
Przy pisaniu kodu w C++ proszę używać standardu C++17, tak aby kod można było skompilować używając g++ -std=c++17 -pedantic -Wall.
Proszę w miarę możności korzystać tylko z funkcji bibliotecznych opisanych
w standardzie POSIX. Gniazdka początkowo pojawiły się w systemach z rodziny
BSD, wiele przykładów w podręcznikach i na witrynach korzysta więc z funkcji
udostępnianych przez bibliotekę standardową BSD, ale nieobecnych w POSIX-ie.
Pamiętajcie, że zazwyczaj można je łatwo zastąpić POSIX-owymi odpowiednikami
(zamiast bzero użyć memset, itp.).
Na następny tydzień musicie mieć Państwo działające konta linuksowe. Sprawy związane z tymi kontami załatwia się u pana Damiana Lisa.
Dzisiejsze zajęcia poświęcone są zagadnieniom przerabianym wcześniej na przedmiotach „Język C” i „Systemy operacyjne”. Gniazdka sieciowe pojawią się dopiero w następnym tygodniu.
Zadania:
Napisz program w C deklarujący w funkcji main tablicę
int liczby[50] i wczytujący do niej z klawiatury kolejne liczby.
Wczytywanie należy przerwać gdy użytkownik wpisze zero albo gdy skończy się
miejsce w tablicy (tzn. po wczytaniu 50 liczb).
Z main należy następnie wywoływać pomocniczą funkcję
drukuj, przekazując jej jako argumenty adres tablicy oraz liczbę
wczytanych do niej liczb. Funkcję tę zadeklaruj jako void drukuj(int
tablica[], int liczba_elementow). W jej ciele ma być pętla
for drukująca te elementy tablicy, które są większe od 10
i mniejsze od 100.
Przypomnij sobie wiadomości o wskaźnikach i arytmetyce wskaźnikowej w C.
Napisz alternatywną wersję funkcji drukującej liczby, o sygnaturze void
drukuj_alt(int * tablica, int liczba_elementow). Nie używaj w niej
indeksowania zmienną całkowitoliczbową (nie może się więc pojawić ani
tablica[i], ani *(tablica+i)), zamiast tego użyj
wskaźnika przesuwanego z elementu na element przy pomocy ++.
W dwóch następnych zadaniach też używaj przesuwanego wskaźnika zamiast indeksowania zmienną całkowitoliczbową.
Opracuj funkcję sprawdzającą, czy przekazany jej bufor zawiera tylko
i wyłącznie drukowalne znaki ASCII, tzn. bajty o wartościach z przedziału
domkniętego [32, 126]. Funkcja ma mieć następującą sygnaturę:
bool is_printable_buf(const void * buf, int len). Pamiętaj
o włączeniu nagłówka <stdbool.h>, bez niego kompilator nie
rozpozna ani nazwy typu bool, ani nazw stałych true
i false.
Konieczne będzie użycie rzutowania wskaźników, bo typ void *
oznacza „adres w pamięci, ale bez informacji o tym co w tym fragmencie pamięci
się znajduje”. Na początku ciała funkcji trzeba go więc zrzutować do typu
„adres fragmentu pamięci zawierającego ciąg bajtów”.
Napisz też jakiś prosty program, który pozwoli Ci przetestować działanie
funkcji is_printable_buf.
Opracuj alternatywną wersję funkcji, biorącą jako argument łańcuch w sensie
języka C, czyli ciąg niezerowych bajtów zakończony bajtem równym zero (ten
końcowy bajt nie jest uznawany za należący do łańcucha). Ta wersja funkcji
ma mieć sygnaturę bool is_printable_str(const char * str).
W dokumentacji POSIX API znajdź opisy czterech podstawowych funkcji
plikowego wejścia-wyjścia, tzn. open, read,
write i close. Czy zgadzają się one z tym, co
pamiętasz z przedmiotu „Systemy operacyjne”? Jakie znaczenie ma wartość 0
zwrócona jako wynik funkcji read?
Zaimplementuj program kopiujący dane z pliku do pliku przy pomocy
powyższych funkcji. Zakładamy, że nazwy plików są podawane przez użytkownika
jako argumenty programu (tzn. będą dostępne w tablicy argv).
Zwróć szczególną uwagę na obsługę błędów — każde wywołanie funkcji we-wy
musi być opatrzone testem sprawdzającym, czy zakończyło się ono sukcesem, czy
porażką.
Funkcje POSIX zwracają -1 aby zasygnalizować wystąpienie błędu, i dodatkowo
zapisują w globalnej zmiennej errno kod wskazujący przyczynę
wystąpienia błędu (na dysku nie ma pliku o takiej nazwie, brak wystarczających
praw dostępu, itd.). Polecam Państwa uwadze pomocniczą funkcję
perror, która potrafi przetłumaczyć ten kod na zrozumiały dla
człowieka komunikat i wypisać go na ekranie.
(nieobowiązkowe) Modyfikacja powyższego zadania. Zakładamy, że kopiowany
plik jest plikiem tekstowym. Linie są zakończone bajtami o wartości 10 (znaki
LF, w języku C zapisywane jako '\n'). Podczas kopiowania należy
pomijać parzyste linie (tzn. w pliku wynikowym mają się znaleźć pierwsza,
trzecia, piąta linia, a druga, czwarta, szósta nie).
(nieobowiązkowe) Kolejna modyfikacja: popraw program tak, aby i znaki
'\n', i dwubajtowe sekwencje złożone ze znaku '\r'
i następującego po nim znaku '\n' były traktowane jako
terminatory linii.
Na dzisiejszych zajęciach obowiązuje język C. Dzięki temu Wasze programy będą bezpośrednio korzystały z funkcji jądra systemu operacyjnego. Pamiętajcie o konieczności sprawdzania rezultatów przez te funkcje zwracanych!
Zadania:
Linuksowe dystrybucje zazwyczaj zawierają program netcat (może być też dostępny pod nazwą nc) lub jego ulepszoną wersję, ncat. Pozwala on m.in. nawiązać połączenie ze wskazanym serwerem, a następnie wysyłać do niego znaki wpisywane z klawiatury; odpowiedzi zwracane przez serwer są drukowane na ekranie. Pozwala też uruchomić się w trybie serwera czekającego na połączenie na wskazanym numerze portu.
Otwórz dwa okna terminalowe, w pierwszym z nich uruchom
ncat -v -l 20123
a w drugim
ncat -v 127.0.0.1 20123
(adres 127.0.0.1 to taki magiczny adres IPv4, który zawsze oznacza lokalny komputer). Jeśli wszystko poszło dobrze i netcaty nawiązały połączenie, to linie wpisywane w jednym z okien powinny pojawiać się w drugim. Aby przerwać działanie netcata użyj kombinacji klawiszy Ctrl-C.
Sprawdź co się dzieje, jeśli spróbujesz uruchomić klienta (netcat bez opcji -l) jako pierwszego. Sprawdź, w jaki sposób netcat-klient oraz netcat-serwer reagują, gdy proces na drugim końcu połączenia zostanie zabity przez Ctrl-C. Proszę nie uogólniać tych obserwacji na wszystkie programy korzystające z gniazdek, inne programy mogą się zachowywać trochę inaczej niż netcat.
Uwaga: jeśli pracujesz zdalnie na spk-ssh, to uruchomiony przez Ciebie netcat może wejść w kolizję z netcatami uruchomionymi w tym samym czasie przez innych studentów. Wybierz wtedy numer portu inny niż 20123, ale większy niż 1024.
Wszystkie wersje netcata domyślnie korzystają z TCP. Trzeba im podać w linii komend opcję -u, aby zamiast gniazdka TCP utworzone zostało gniazdko UDP. Powtórz eksperymenty używając poleceń
ncat -v -u -l 20123 ncat -v -u 127.0.0.1 20123
Sprawdź co się teraz będzie działo, gdy jeden z działających netcatów zabijesz przez Ctrl-C. Co się zmieniło w porównaniu do eksperymentów z TCP? I czy treść wyświetlanych komunikatów o błędach jest taka sama?
Przejrzyj dokumentację netcata, upewnij się co do znaczenia opcji -v, -l oraz -u. Sprawdź też co robi opcja -C, czyli --crlf. W jakich sytuacjach może ona być potrzebna?
(nieobowiązkowe) Jeśli oprócz polecenia ncat dostępna jest również któraś z odmian polecenia nc albo polecenie socat, to sprawdź czy za jego pomocą też da się wykonać powyższe eksperymenty. Może to wymagać zmiany lub dodania opcji w poleceniach uruchamiających serwer i klienta.
Napisz prosty serwer zwracający wizytówkę. Powinien tworzyć gniazdko TCP
nasłuchujące na porcie o numerze podanym jako argv[1] (użyj
socket, bind i listen), następnie
w pętli czekać na przychodzące połączenia (accept), wysyłać ciąg
bajtów Hello, world!\r\n jako swoją wizytówkę, zamykać odebrane
połączenie i wracać na początek pętli. Pętla ma działać w nieskończoność, aby
przerwać działanie programu trzeba będzie użyć Ctrl-C.
Zamiast pisać kod programu od zera możesz wykorzystać szkielet tcp_srv_skel.c, odpowiednio go rozbudowując (albo przycinając, jeśli są w nim rzeczy niepotrzebne w tym zadaniu).
Przetestuj netcatem powyższy serwer.
Napisz prostego klienta, który łączy się (użyj socket
i connect) z usługą wskazaną argumentami podanymi w linii komend
(adres IPv4 w argv[1], numer portu TCP w argv[2]),
drukuje na ekranie wizytówkę zwróconą przez serwer i kończy pracę. Pamiętaj
o zasadzie ograniczonego zaufania i przed przesłaniem odebranego bajtu na
stdout weryfikuj, czy jest to znak drukowalny lub znak
kontrolny używany do zakończenia linii bądź wstawienia odstępu
('\n', '\r' oraz '\t').
Możesz użyć tcp_clnt_skel.c jako punktu startowego.
Sprawdź, czy program-klient poprawnie współdziała z programem-serwerem.
Spróbuj napisać podobną parę klient-serwer komunikującą się za pomocą
protokołu UDP. Pamiętaj, że UDP nie jest protokołem połączeniowym: wywołanie
connect na gniazdku UDP nie powoduje wysłania w sieć żadnych
pakietów. Klient musi jako pierwszy wysłać jakiś datagram, a serwer dowiaduje
się o istnieniu klienta dopiero gdy ten datagram do niego dotrze.
Sprawdź, czy możliwe jest wysyłanie pustych datagramów (tzn. o długości zero
bajtów) — jeśli tak, to może niech klient właśnie taki wysyła?
(nieobowiązkowe) Przepisz powyższe rozwiązania w innym języku, np. w Javie lub Pythonie. Porównaj obie wersje i oceń, czy nowy kod jest krótszy i / lub czytelniejszy od starego.
Dalej obowiązuje język C. Ostatnie zadanie jest ważne, macie Państwo sześć dni na jego zrobienie. W przyszłym tygodniu przedyskutujemy programy, które przysłaliście, i w razie potrzeby będziecie mieli następne sześć dni na opracowanie poprawionych wersji.
Testy łączności w dwóch pierwszych zadaniach proszę robić w parach lub trzyosobowych zespołach.
Zadania:
Dokończ pisanie par klient-serwer dla TCP/IPv4 oraz UDP/IPv4 (co razem daje cztery programy). Przetestuj czy działają poprawnie gdy klient i serwer są uruchomione na dwóch różnych komputerach w SPK. Wymaga to znajomości adresu IP przydzielonego komputerowi, na którym uruchamiany jest serwer — można go znaleźć w wynikach polecenia ip address show.
Sprawdź co się dzieje, gdy podasz zły adres IP albo zły numer portu serwera. Czy jądro systemu operacyjnego daje nam w jakiś sposób o tym znać? Jeśli tak, to jak długo trzeba czekać, aż jądro poinformuje nasz proces o wystąpieniu błędu?
Pamiętaj, że protokoły sieciowe z korekcją błędów wykonują wielokrotne retransmisje pakietów w zwiększających się odstępach czasu. Może to zająć nawet kilkadziesiąt minut. Nie pomyl sytuacji „proces zawiesza się na pięć minut zanim jądro zwróci -1” z sytuacją „zawiesza się na stałe”.
Jeśli któryś z klientów może się zawiesić czekając w nieskończoność na odpowiedź z nieistniejącego serwera, to popraw jego kod aby tego nie robił. W slajdach z wykładu są pokazane funkcje, które pozwalają na wykonywanie operacji we-wy z timeoutem (można go ustawić np. na 10 sekund).
Przeanalizuj niniejszą specyfikację protokołu sprawdzania, czy wyrazy są palindromami. Czy jest ona jednoznaczna, czy też może zostawia pewne rzeczy niedopowiedziane?
Komunikacja pomiędzy klientem a serwerem odbywa się przy pomocy datagramów. Klient wysyła datagram zawierający wyrazy do sprawdzenia. Serwer odpowiada datagramem zawierającym albo ułamek z liczbą otrzymanych wyrazów w mianowniku i liczbą wyrazów-palindromów w liczniku, albo komunikat o błędzie.
Zawartość datagramów interpretujemy jako tekst w ASCII. Datagramy wysyłane
przez klienta mogą zawierać litery i spacje. Datagramy wysyłane przez serwer
mogą zawierać albo cyfry i znak /, albo pięć liter składających
się na słowo „ERROR”. Żadne inne znaki nie są dozwolone (ale patrz następny
akapit).
Aby ułatwić ręczne testowanie serwera przy pomocy ncat, serwer
może również akceptować datagramy mające na końcu dodatkowy znak
\n (czyli bajt o wartości 10) albo dwa znaki \r\n
(bajty 13, 10). Serwer może wtedy, ale nie musi, dodać \r\n do
zwracanej odpowiedzi.
Napisz serwer UDP/IPv4 nasłuchujący na porcie nr 2020 i implementujący powyższy protokół.
Serwer musi weryfikować odebrane dane i zwracać komunikat o błędzie jeśli są one nieprawidłowe w sensie zgodności ze specyfikacją protokołu.
To zadanie jest ważne — zaimplementowane rozwiązanie trzeba oddać najpóźniej we wtorek 24 marca, nawet jeśli nie jest jeszcze w pełni gotowe.
Proszę uwzględnić poniższe decyzje przy implementowaniu serwera.
Nie. Nie pozwalamy na używanie ciągu dwóch lub więcej spacji jako separatora.
Spacje przed pierwszym wyrazem bądź za ostatnim też są zabronione.
Tak. Jeśli klient jako jeden z wyrazów prześle „Ala”, to serwer powinien ten wyraz uznać za palindrom.
Tak.
„0/0”. Zapytania zawierające pusty zbiór wyrazów uznajemy za poprawne.
Na poziomie naszego protokołu aplikacyjnego klientowi nie stawiamy żadnych wymagań. Jedynym ograniczeniem rozmiaru wysyłanych przez niego zapytań jest więc limit narzucany przez używany protokół transportowy. W szczególności, UDP/IPv4 ma limit równy 65507 bajtom (ciut poniżej 64 KiB).
Serwer musi być w stanie przetwarzać zapytania mające 1024 bajty lub mniej, na większe może odpowiadać „ERROR”. Zalecane jest, aby podczas implementowania serwera przyjąć wyższe ograniczenie, jeśli jest to możliwe. Najlepiej, gdy serwer może przetwarzać zapytania maksymalnego rozmiaru dopuszczanego przez używany protokół transportowy.
Raporty z próbnych kompilacji oddanych programów i testu sprawdzającego, czy na „kajak” odpowiadają „1/1”, można znaleźć na spk-ssh.if.uj.edu.pl w katalogu /home/palacz/PS/.
W tym tygodniu zajmiemy się testowaniem. Przy implementowaniu protokołów sieciowych samo napisanie kodu to tylko początek pracy — potem trzeba sprawdzić, czy ten kod jest zgodny ze specyfikacją protokołu. Trzeba przetestować, czy akceptuje on wszystkie zapytania, które wg specyfikacji są poprawne, czy wykrywa i odrzuca wszystkie te, które są niepoprawne, i czy zwracane odpowiedzi mają zgodny ze specyfikacją format.
Jeśli dzięki testowaniu znajdziecie Państwo błędy w kodzie i je poprawicie, albo jeśli wczoraj przesłana wersja serwera ma jakieś braki, to możecie wysłać poprawioną wersję najpóźniej we wtorek 31 marca.
Zadania:
Przetestuj ręcznie znajdujący palindromy serwer UDP. Jeśli akceptuje
końcowe \r\n, to możesz to zrobić uruchamiając
ncat --udp --crlf 127.0.0.1 2020albo
socat stdio udp4:127.0.0.1:2020,crlf
i wpisując kolejne zapytania z klawiatury.
Zapytanie bez końcowego \r\n czy też \n można
wygenerować poleceniem printf podłączonym do wejścia socata:
printf "Ala i kot" | socat -t 5.0 stdio udp4:127.0.0.1:2020
Przełącznik -t 5.0 nakazuje socatowi odczekać pięć sekund po zakończeniu wysyłania danych do serwera i zakończyć działanie. Jakiś timeout jest niezbędny, bo socat nie wie, że serwer zwróci dokładnie jeden datagram. W ogólnym przypadku odpowiedź z serwera UDP może się przecież składać z wielu datagramów, a na poziomie bezpołączeniowego protokołu transportowego, jakim jest UDP, nie ma po czym poznać że już je wszystkie odebrano.
Często spotykanym w poprzednich latach błędem było zwracanie przez serwer
dodatkowych bajtów o wartości zero, bo np. ktoś w kodzie zadeklarował sobie
char wynik[20], użył sprintf aby „wydrukować” do
tej tablicy tekstową reprezentację wyniku (która zajęła tylko kilka
początkowych elementów tablicy), a potem przez pomyłkę wysłał klientowi całą
20-bajtową tablicę. Ten błąd łatwo przegapić, bo bajty o wartości zero są
niewidoczne gdy się je wyświetla na ekranie.
Aby sprawdzić jakie dokładnie bajty są w strumieniu danych trzeba ten strumień wysłać nie wprost na ekran, lecz np. na wejście programu od. Proszę porównać to, co wypisują dwa poniższe polecenia:
printf "abc ijk\0xyz\n" printf "abc ijk\0xyz\n" | od -A d -t u1 -t c
Użyte przełączniki nakazują wyświetlić kolejne bajty w postaci dziesiętnej oraz jako znaki ASCII (bajty odpowiadające niedrukowalnym znakom kontrolnym są wyświetlane jako sekwencje z backslashem na początku).
Proszę spróbować zapisać zwrócone przez serwer dane do pliku, a potem ten plik wyświetlić za pomocą od:
ncat --udp --crlf 127.0.0.1 2020 > wynik-z-serwera.txt printf "xyz" | socat -t 5.0 stdio udp4:127.0.0.1:2020 > wynik-z-serwera.txt od -A d -t u1 -t c < wynik-z-serwera.txt
Możliwość przekierowania równocześnie wejścia i wyjścia socata można
wykorzystać do stworzenia powtarzalnych testów. Załóżmy, że w pliku
socat -t 5.0 stdio udp4:127.0.0.1:2020 < test-dane.txt > wynik-z-serwera.txt
Jeśli przygotowaliśmy również plik
socat nie radzi sobie z datagramami mającymi długość zero bajtów, nie da
się więc przy jego pomocy wysłać pustego zapytania. Nie ma też jak odróżnić
sytuacji, gdy serwer zwrócił pustą odpowiedź, od sytuacji gdy w ogóle żadna
odpowiedź nie została zwrócona. Z tych powodów zamiast socata możesz chcieć
użyć programu
Przygotuj kilka par plików z przykładowymi zapytaniami i oczekiwanymi wynikami. Uwzględnij także błędne zapytania, na które odpowiedzią powinno być „ERROR”.
Wymień się tymi plikami z dwiema-trzema innymi osobami z grupy. Sprawdź,
czy Twój serwer poprawnie obsługuje zapytania przygotowane przez inne osoby.
Jeśli nie, to spróbujcie wspólnie ustalić przyczynę: różnice w rozumieniu
specyfikacji protokołu, korzystanie w teście z opcjonalnej funkcjonalności,
którą nie wszystkie serwery muszą implementować (u nas: \r\n na
końcu datagramu), błędy w kodzie serwera, coś innego?
(nieobowiązkowe, ale przydatne) Przygotuj sobie narzędzie automatycznie testujące sumator w oparciu o powyższe pliki z zapytaniami i odpowiedziami. Na przykład skrypt dla uniksowej powłoki, wywołujący polecenia używane w poprzednich punktach. Możesz też napisać program w C albo innym języku, wczytujący te pliki oraz komunikujący się przez gniazdko z serwerem. Zanim jednak się zabierzesz za jego pisanie, to lepiej sprawdź czy w sieci nie da się znaleźć gotowego narzędzia do testowania usług UDP — wielce możliwe, że ktoś już coś takiego zaimplementował.
Wyniki testów przesłanych mi serwerów UDP dostępne są jako pliki komentarzy na Pegazie.
Jeśli wszystkie testy są poprawnie zaliczone to gratuluję, pierwsze zadanie zaliczeniowe jest za Wami. W przeciwnym razie macie Państwo następne siedem dni na zrobienie poprawek i przesłanie ostatecznej wersji kodu. Należy to zrobić najpóźniej w środę 8 kwietnia (bo we wtorek 7.04 jest dzień wolny od zajęć).
To byłoby wszystko, jeśli chodzi o serwer datagramowy. Na dzisiejszych zajęciach przechodzimy do protokołów korzystających z transportu strumieniowego. Dalej będziemy rozważać problem znajdywania wyrazów-palindromów, ale teraz zapytania i odpowiedzi będą przesyłane za pomocą TCP.
Zadania (głównie praca koncepcyjna):
Napisz specyfikację strumieniowego protokołu zliczania palindromów. Dopuść
możliwość przesyłania przez jedno połączenie wielu zapytań i wielu odpowiedzi
(obliczonych wyników albo komunikatów o wystąpieniu błędu). Zastanów się,
czego użyć jako terminatora mówiącego „w tym miejscu kończy się zapytanie”
— dwuznaku \r\n, tak jak w wielu innych protokołach
sieciowych? A może czegoś innego (ale wtedy miej jakieś uzasadnienie odejścia
od powszechnie przyjętej konwencji)? Czy odpowiedzi serwera będą używać
takiego samego terminatora?
Rozważ, czy trzeba do specyfikacji dodawać warunek ograniczający długość przesyłanych przez klienta zapytań, np. 1024 bajty łącznie z terminatorem. To ułatwiłoby implementowanie serwera, bo dzięki temu programista piszący serwer mógłby zadeklarować roboczy bufor o rozmiarze 1024 bajtów i to na pewno wystarczyłoby, aby wczytać do niego całe zapytanie. Ale czy to jest niezbędne? Czy nasz problem wykrywania palindromów wymaga, aby serwer odebrał całe zapytanie, zanim zacznie je przetwarzać?
Zastanów się nad algorytmem serwera. Będzie on musiał być bardziej złożony niż w przypadku serwera UDP. Tam pojedyncza operacja odczytu zawsze zwracała jeden datagram, czyli jedno kompletne zapytanie. W przypadku połączeń TCP niestety tak łatwo nie jest.
Po pierwsze, jeśli klient od razu po nawiązaniu połączenia wysłał kilka
zapytań jedno za drugim, to serwer może je odebrać sklejone ze sobą.
Pojedyncza operacja odczytu ze strumienia może np. zwrócić 15 bajtów
odpowiadających znakom xyz\r\nucho oko\r\n — jak widać, są
to dwa zapytania. Serwer w odpowiedzi powinien zwrócić
0/1\r\n1/2\r\n.
Po drugie, operacja odczytu może zwrócić tylko początkową część zapytania.
Kod serwera musi wtedy ponownie wywołać read(). Takie ponawianie
odczytów i odbieranie kolejnych fragmentów wyrażenia musi trwać aż do chwili
odebrania \r\n — dopiero wtedy wiemy, że dotarliśmy do
końca zapytania.
Po trzecie, mogą się zdarzyć oba powyższe przypadki równocześnie. Serwer
może np. odczytać ze strumienia 7 bajtów odpowiadających znakom
xyz\r\nuc.
Spróbuj rozpisać w formie pseudokodu algorytm serwera obsługujący powyższe komplikacje i starannie przeanalizuj, czy na pewno poradzi on sobie nawet przy założeniu maksymalnie złej woli ze strony klienta.
Radzę wykorzystać jako inspirację przedstawiony na wykładzie automat, który otrzymywał kolejne bajty z wejścia i w swych wewnętrznych polach zapisywał wyniki ich przetworzenia. Zastanów się, jak tę koncepcję tu zastosować, skoro sprawdzenie czy słowo jest palindromem możliwe jest dopiero po wczytaniu całego słowa z gniazdka sieciowego.
(nieobowiązkowe) Jeśli chcesz, możesz zacząć implementować w C/C++ taki algorytm. Zdobyte doświadczenie i napisany kod przydadzą się na następnych zajęciach.