Programowanie sieciowe

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).

Materiały do wykładu

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.

Literatura

  1. 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.

  2. 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.

  3. Dokumenty RFC.

    Definicje protokołów, w oparciu o które działa Internet. Dostępne online na witrynie https://www.rfc-editor.org/.

  4. 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.).

  5. 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.

  6. 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.

  7. 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).

Ćwiczenia

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ń.

Środowisko pracy

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.).

Zajęcia nr 1, 2026-03-04

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:

  1. 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.

  2. 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ą.

  3. 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.

  4. 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).

  5. 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?

  6. 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.

  7. (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).

  8. (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.

Zajęcia nr 2, 2026-03-11

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:

  1. 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.

  2. 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?

  3. 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?

  4. (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.

  5. 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).

  6. Przetestuj netcatem powyższy serwer.

  7. 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.

  8. Sprawdź, czy program-klient poprawnie współdziała z programem-serwerem.

  9. 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?

  10. (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.

Zajęcia nr 3, 2026-03-18

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:

  1. 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.

  2. 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).

  3. 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.

  4. 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.

Podsumowanie analizy specyfikacji z zajęć nr 3

Proszę uwzględnić poniższe decyzje przy implementowaniu serwera.

Czy między wyrazami może być więcej niż jedna spacja?

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.

Czy utożsamiamy wielkie i małe litery?

Tak. Jeśli klient jako jeden z wyrazów prześle „Ala”, to serwer powinien ten wyraz uznać za palindrom.

Czy jednoliterowe wyrazy są palindromami?

Tak.

Jak serwer odpowiada na puste zapytania? Zwraca „0/0” czy „ERROR”?

„0/0”. Zapytania zawierające pusty zbiór wyrazów uznajemy za poprawne.

Czy jest jakiś limit na długość zapytań-datagramów wysyłanych przez klienta?

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.

Zajęcia nr 4, 2026-03-25

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:

  1. 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 2020
    
    albo
    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.

  2. 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
    
  3. 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 test-dane.txt jest ciąg bajtów składający się na zapytanie testowe. Uruchamiamy

    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 test-wynik.txt, to przy pomocy poleceń cmp albo diff można łatwo porównać zawartość pliku wynik-z-serwera.txt ze wzorcowym wynikiem.

    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 mini-udpcat.py, który został napisany specjalnie na potrzeby tych zajęć.

  4. 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?

  5. (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ł.

Zajęcia nr 5, 2026-04-01

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):

  1. 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ć?

  2. 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.

  3. 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.

  4. (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.