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 z wykładu będą sukcesywnie umieszczane na karcie „Udostępnione” w zespole MS Teams.
Przykładowe programy są dodatkowo dostępne w /home/palacz/PS/ po zalogowaniu się na którejś z linuksowych maszyn.
Egzamin jest ustny. Aby do niego móc podejść trzeba wcześniej zaliczyć ćwiczenia. Przy wpisywaniu ocen do indeksów będę brał pod uwagę zaliczenia ćwiczeń: ocena końcowa może być co najwyżej o jeden stopień wyższa od oceny z ćwiczeń.
Listę kluczowych zagadnień wymaganych na egzaminie znajdziecie Państwo tutaj.
Egzamin w pierwszym terminie rozłożony będzie na dwa dni, wtorek 16.06 i środę 17.06. Egzaminuję po dwie osoby na raz. Proszę samodzielnie wpisać się na listę egzaminacyjną.
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. Może się zdarzyć, że w dwóch różnych sekcjach są strony o tej samej nazwie. Poleceniu man można więc 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).
Kod i odpowiedzi generowane przez narzędzia sztucznej inteligencji.
Jak wyżej, tylko z jeszcze większym naciskiem położonym na konieczność rozumienia tego, co SI zwróciła. SI potrafi halucynować, musicie weryfikować jej odpowiedzi.
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.
Osoby, które z wyprzedzeniem przesłały poprawione wersje serwerów UDP mają wyniki testów na Pegazie już dziś. Kod wysłany dzisiaj (macie czas do północy) zostanie przetestowany jutro.
Zaczynamy prace nad serwerem potrafiącym obsługiwać wielu klientów TCP
równocześnie. Na wykładzie zostały omówione ogólne zasady pisania takich
serwerów, patrz przykłady rot13_server.c oraz
rot13_server.py. Pierwszy z nich demonstruje
kilka uniksowych mechanizmów, które można wykorzystać do równoległej obsługi
klientów, drugi pokazuje jak zaimplementować w obiektowym stylu serwer
sterowany zdarzeniami (użytym językiem jest Python, ale da się coś bardzo
podobnego napisać w C++ używając funkcji epoll).
Zadania:
Przeczytaj poniższą specyfikację strumieniowego protokołu zliczania palindromów. Porównaj ją ze specyfikacją, którą samodzielnie napisałeś(-aś) w poprzednim tygodniu. Przeanalizuj, które części obu specyfikacji są ze sobą zgodne, a które są sprzeczne. Całkiem możliwe, że części sprzecznych nie będzie — przedstawione tydzień temu wytyczne prawdopodobnie doprowadziły większość z Was do specyfikacji takiej jak ta tutaj.
Komunikacja pomiędzy klientem a serwerem odbywa się przy pomocy połączenia strumieniowego. Klient wysyła jedną lub więcej linii zawierających wyrazy. Dla każdej odebranej linii serwer zwraca linię zawierającą albo obliczony wynik, albo komunikat o błędzie.
Ogólna definicja linii jest zapożyczona z innych protokołów tekstowych:
ciąg drukowalnych znaków ASCII (być może pusty) zakończony dwuznakiem
\r\n pełniącym rolę terminatora linii.
Linia z zapytaniem klienta może zawierać tylko litery oraz spacje pełniące rolę separatorów słów. Obowiązują te same wymagania i interpretacje co w uprzednio rozważanym protokole datagramowym (puste zapytanie z zero słów jest uznawane za poprawne, utożsamiamy wielkie i małe litery, itd.).
Linia z odpowiedzią serwera może zawierać albo dwa niepuste ciągi cyfr rozdzielone znakiem /, albo pięć liter składających się na słowo „ERROR”.
(Uwaga na marginesie: wszystkie linie, i te wysyłane przez klientów,
i przez serwer, mają oczywiście do opisanej powyżej zawartości dołączony
terminator linii, czyli \r\n.)
Serwer może, ale nie musi, zamykać połączenie w reakcji na nienaturalne zachowanie klienta. Obejmuje to wysyłanie danych binarnych zamiast znaków ASCII, wysyłanie linii o długości przekraczającej przyjęty w kodzie źródłowym serwera limit, długi okres nieaktywności klienta itd. Jeśli serwer narzuca maksymalną długość linii, to limit ten powinien wynosić co najmniej 1024 bajty (1022 drukowalne znaki i dwubajtowy terminator linii).
Serwer nie powinien zamykać połączenia gdy udało mu się odebrać poprawną linię w sensie ogólnej definicji, ale dane w niej zawarte są niepoprawne (np. oprócz liter i spacji są przecinki). Powinien wtedy zwracać komunikat błędu i przechodzić do przetwarzania następnej linii przesłanej przez klienta.
Napisz serwer TCP/IPv4 nasłuchujący na porcie nr 2020 i implementujący powyższy protokół. Serwer musi być w stanie równocześnie obsługiwać co najmniej 100 połączeń. Użytym językiem może być C albo C++. Proszę pisać kod tak, aby się kompilował bez ostrzeżeń odpowiednio pod gcc -std=c99 -pedantic -Wall bądź g++ -std=c++17 -pedantic -Wall.
To zadanie jest ważne. Wstępną wersję rozwiązania trzeba oddać najpóźniej we wtorek 14 kwietnia. Zostanie ona przeze mnie przetestowana i będziecie Państwo mieli kolejny tydzień na poprawienie wykrytych błędów.
Wyniki testów przesłanych serwerów strumieniowych są na Pegazie. Proszę poprawić wykryte błędy i wysłać wersję drugą najpóźniej we wtorek 21 kwietnia.
Zadania przygotowujące do pisania programów używających HTTP:
Zapoznaj się z co najmniej dwoma-trzema sposobami ściągania zasobów po
HTTP. Mogą to być narzędzia linii poleceń wget i curl,
biblioteka C/C++ libcurl, standardowe pythonowe moduły
urllib i http, niestandardowy requests,
klasa java.net.URL i jej metoda openConnection(),
itd.
Przez „zapoznaj się” rozumiem „znajdź dokumentację i przeczytaj w niej, co dane narzędzie potrafi zrobić”. Powinno to również obejmować przykłady użycia, tak abyście Państwo mieli orientację, czy najprostsze ściągnięcie dokumentu spod danego URL-a wymaga napisania trzech linii kodu, czy trzydziestu.
Dla każdego wybranych z narzędzi sprawdź, czy potrafi ono obsługiwać inne metody niż GET i POST, w jaki sposób specyfikuje się argumenty przesyłane w zapytaniach POST, czy potrafi obsługiwać ciasteczka i czy potrafi je zapisywać w tzw. cookie jar.
(nieobowiązkowe) Zapoznaj się z pythonową biblioteką Beautiful Soup lub jej javową wersją znaną jako jsoup. Bardzo ułatwiają pisanie programów przetwarzających treść dokumentów HTML.
Uwaga: eksperymentów ze ściąganiem danych z witryn nie da się wykonać na spk-ssh. Firewall jest tam tak skonfigurowany, że blokuje próby łączenia się z spk-ssh do innych hostów.
Wyniki testów przesłanych programów są na Pegazie. Zachęcam do opracowania i wysłania trzeciej wersji serwera, jeśli nie wszystkie testy są poprawnie zaliczone. Należy to zrobić najpóźniej we wtorek 28 kwietnia.
Na zajęciach 6 maja będzie kolokwium. Oto kilka pytań podobnych do tych, które się na kolokwium pojawią:
Poniższy pseudokod jest niekompletny. Co trzeba do niego dodać, aby zaczął odbierać przychodzące połączenia i wysyłać wizytówki?
s = socket(INET, STREAM)
bind(s, "0.0.0.0:2020") # 0.0.0.0 to INADDR_ANY
while (true) {
c = accept(s)
write(c, "Cześć! Jestem serwerem TCP/IPv4.\r\n")
close(c)
}
Klient wysłał serwerowi dwa datagramy UDP, liczące odpowiednio 5 i 20
bajtów. Serwer był przez kilka sekund zajęty czymś innym, więc w chwili gdy
wykonał instrukcję oba datagramy
już dotarły i czekały w buforach jądra. Co w tej sytuacji zrobi jądro?
Czy funkcja socket może zgłosić błąd (tzn. zwrócić -1)?
Czym się różni utworzenie nowego procesu potomnego od utworzenia nowego wątku?
Proszę spróbować samodzielnie na te pytania odpowiedzieć i gdzieś na boku odpowiedzi zapisać, pod koniec ćwiczeń wspólnie je przedyskutujemy.
W drugiej kolejności proszę zabrać się za dzisiejsze zadania. Można używać Pythona, Javy lub C/C++ wraz z biblioteką obsługującą wysyłanie zapytań HTTP i HTTPS. Programy mają się dawać skompilować i uruchomić w studenckiej pracowni komputerowej (na spk-ssh nie da się ich uruchomić, bo tam firewall blokuje wychodzące połączenia HTTP).
Zadania:
Serwery HTTP zazwyczaj podają swoją nazwę i czasem również wersję, np.
Napisz program, który w argv dostaje nazwy witryn, takie jak
google.com, www.uj.edu.pl itp. Do każdej z nich wysyła
zapytanie
Przykładowo, dla www.uj.edu.pl należy wypisać:
port 80: nginx port 443: nginx
Napisz program sprawdzający, czy pewna określona witryna działa poprawnie. Sprawdzenie ma polegać na pobraniu strony spod ustalonego adresu (np. spod http://th.if.uj.edu.pl/). Proszę nie zapomnieć o zweryfikowaniu, czy na pewno udało się ją poprawnie pobrać (status 200) i czy to jest strona HTML (typ text/html). Następnie należy sprawdzić, czy rzeczywiście jest to spodziewana strona, a nie np. komunikat o wewnętrznym błędzie serwera WWW — to można zweryfikować sprawdzając czy w pobranej treści znajduje się pewien zadany z góry ciąg znaków (np. „Institute of Theoretical Physics”).
Program, w zależności od wyniku sprawdzenia, musi zwracać jako wynik
funkcji main kod sukcesu (zero) bądź porażki (wartość większa od
zera). Osoby piszące w Pythonie powinny użyć sys.exit(0) albo
sys.exit(1), a w Javie należy wywołać metodę
System.exit z odpowiednim argumentem.
Programy tego typu używane są w systemach monitorowania usług sieciowych. Jeśli na filmie z centrum zarządzania siecią widać ekran z listą serwerów i usług, a przy nich zielone komunikaty „OK” i gdzieniegdzie czerwone komunikaty błędów, to za tymi kolorami kryją się uruchamiane w regularnych odstępach czasu programy sprawdzające status danej usługi.
To zadanie jest ważne — zaimplementowany program trzeba oddać najpóźniej we wtorek 5 maja. Nie będzie możliwości oddawania poprawionych wersji w późniejszym terminie, ten program jest na to zbyt prosty. Z tego samego powodu przy wystawianiu zaliczeń będę to zadanie brał z wagą 25% w porównaniu do zadań z palindromami.
(nieobowiązkowe) Sprawdź, czy w Twoim ulubionym języku programowania jest standardowa biblioteka pozwalająca serializować i deserializować dane w formacie JSON. Zapoznaj się z nią, spróbuj znaleźć przykład pokazujący zamianę ciągu bajtów / znaków na struktury danych tego języka programowania (czyli deserializację ciągu).
Wyniki testów trzeciej wersji palindromowego serwera TCP, o ile ją Państwo wysłaliście, są jak zwykle na Pegazie.
W tym tygodniu zajmujemy się techniką web scraping, czyli zautomatyzowanym pobieraniem danych z witryn internetowych.
Zadanie:
Proszę znaleźć stronę WWW zawierającą jakąś potencjalnie potrzebną
informację (aktualna temperatura w Krakowie, kurs dolara itp.), a następnie
napisać program ściągający tę stronę i wyłuskujący z niej te dane. Dane te
można zapisywać do jakiegoś pliku lub drukować na stdout, nie
jest to ważne — ważne za to jest to, aby format zapisywanych danych
pozwalał na ich wygodne dalsze przetwarzanie. Jeśli programowi z jakiejkolwiek
przyczyny nie uda się ściągnąć poszukiwanej informacji, to musi zakończyć swe
działanie zwracając z main kod porażki.
Radzę wykorzystać kod programu z poprzednich zajęć. Ściąganie strony już w nim jest, wystarczy dodać ekstrahowanie interesujących nas danych.
Wyłuskiwanie danych ze strony HTML jest dość kruchą techniką, bo witryna może nieoczekiwanie zmienić swój wygląd lub treść. Biorąc to pod uwagę łatwo zauważyć, że podejście typu „zwróć bajty od 5078 do 5081” jest skazane na rychłą porażkę; „zwróć zawartość czwartego elementu <p> znajdującego się wewnątrz elementu <div> o identyfikatorze »temp«” jest lepsze. Warto postarać się o to, aby program zauważał nieoczekiwane bądź podejrzane sytuacje i je zgłaszał (np. jeśli w tym czwartym <p> jest ciąg znaków nie będący liczbą, to raczej nie jest to temperatura; jeśli znaleziona liczba wykracza poza przedział [-30, 40] to raczej nie jest to temperatura w stopniach Celsjusza).
Nawigację po treści strony ułatwia zbudowanie drzewa obiektów reprezentujących elementy strony HTML, tu ponownie polecam Państwa uwadze bibliotekę Beautiful Soup (Python) i jej odpowiednik jsoup (Java). Do sprawdzania, czy łańcuch znaków pasuje do zadanego wzorca dobrze nadają się wyrażenia regularne.
Może się zdarzyć, że traficie na stronę, która w oknie przeglądarki wyświetla potrzebne nam informacje, ale gdy się ją ściągnie to nigdzie w jej treści nie można ich znaleźć. Prawie na pewno przyczyną jest jej dynamiczna, AJAX-owa natura. Takie strony mają puste miejsca, wypełniane zawartością ściąganą przez javascriptowy kod z innych URL-i.
Współczesne przeglądarki mają wbudowane narzędzia deweloperskie, jednym z nich jest analizator połączeń sieciowych. Można przy jego pomocy spróbować odnaleźć rzeczywiste źródło potrzebnych nam danych. Poszukiwania proponuję zacząć od połączeń zainicjowanych przez XHR (czyli javascriptową klasę XMLHttpRequest bądź funkcję fetch) i / lub tych, które ściągały dokumenty typu JSON.
Udane znalezienie JSON-owego źródła surowych danych jest dobrą wiadomością, bo ryzyko zmiany formatu tych danych jest znacznie mniejsze niż ryzyko zmiany struktury strony HTML.
Może się też zdarzyć, że witryna korzysta z CAPTCHA bądź mechanizmu sprawdzającego, czy łączy się z nią typowa przeglądarka internetowa (takie mechanizmy są częścią zabezpieczeń przed atakami DDoS). Stron z takiej witryny nie da się wykorzystać jako źródła automatycznie pobieranych danych.
Krótkie kolokwium na początku zajęć, proszę się nie spóźnić!
Zadania:
Zapoznaj się z witryną Discogs oraz ze specyfikacją udostępnianego przez nią REST API.
Spróbuj napisać program wyszukujący w bazie Discogs wszystkie albumy danego wykonawcy (np. Budki Suflera, czyli zespołu nr 359282). Sprawdź czy to, co drukuje program zgadza się z tym, co można ręcznie wyszukać na witrynie.
Może się okazać, że Discogs odrzuca zapytania, jeśli są one całkowicie anonimowe. Sprawdź wtedy w dokumentacji kim / czym trzeba być, aby uzyskać odpowiedź — zalogowanym użytkownikiem witryny, aplikacją napisaną przez zarejestrowanego dewelopera?
Napisz program, który w oparciu o informacje z Discogs sprawdza, czy muzycy
z zadanego zbioru grali razem w jakichś zespołach. Wyświetl nazwy tych zespołów
oraz imiona i nazwiska tych muzyków ze zbioru, którzy do nich należeli. Załóż,
że numeryczne identyfikatory muzyków podawane są w argv.
Dla przykładu: 516820 to Tomasz Zeliszewski, 532854 Marek Stefankiewicz, a 702387 Mieczysław Jurecki. Wszyscy trzej grali w Budce Suflera (id 359282), do tego Zeliszewski i Jurecki występowali w zespole Wieko (id 4751291), a Stefankiewicz i Jurecki — w Perfekcie (id 669348).
Zadbaj, aby drukowane wyniki były posortowane po nazwie zespołu. Nie zapomnij o weryfikowaniu poprawności odpowiedzi zwracanych przez serwer. Dobrze by było, gdyby w razie przekroczenia limitu zapytań na minutę program chwilę czekał i ponawiał zapytanie, ale nie jest to obowiązkowe (program może taką sytuację traktować jak każdy inny błąd i kończyć działanie).
To zadanie jest ważne — zaimplementowany program oddaj najpóźniej we wtorek 19 maja. Możesz użyć Pythona, Javy lub C/C++. Nie będzie możliwości oddawania poprawionych wersji w późniejszym terminie. Przy wystawianiu zaliczeń będę to zadanie brał z wagą 50% w porównaniu do zadań z palindromami.
Uwaga: nie wolno Państwu używać bibliotek dedykowanych bazie Discogs, które są wymienione na początku strony https://www.discogs.com/developers (pod nagłówkiem „Quickstart”). To podkopałoby cel dydaktyczny, którym jest sprawdzenie, czy za pomocą zwykłej biblioteki klienckiej HTTP potraficie pobrać dane z serwera REST. Innymi słowy, zadanie sprawdza, czy potraficie przeczytać ze zrozumieniem dokumentację serwera REST i napisać kod konstruujący odpowiednie zapytania HTTP.
(nieobowiązkowe) Rozszerz powyższy program tak, aby jako argument można było podawać nie tylko identyfikator, lecz również nazwisko (albo imię i nazwisko) muzyka. Najprawdopodobniej będzie wymagać to zarejestrowania się na witrynie Discogs w celu uzyskania tokena dla aplikacji, bo operacji wyszukiwania po nazwie zdaje się nie można wywoływać anonimowo.