Zadanie polega na na rozszerzeniu funkcjonalności nadajnika i odbiornika radia internetowego z zadania 1.
"Zmienne" użyte w treści
-
MCAST_ADDR
- adres rozgłaszania ukierunkowanego, ustawiany obowiązkowym parametrem -a nadajnika -
DISCOVER_ADDR
- adres używany przez odbiornik do wykrywania aktywnych nadajników, ustawiany parametrem -d odbiornika, domyślnie 255.255.255.255 -
DATA_PORT
- port UDP używany do przesyłania danych, ustawiany parametrem -P nadajnika, domyślnie 20000 + (numer_albumu % 10000) -
CTRL_PORT
- port UDP używany do transmisji pakietów kontrolnych, ustawiany parametrem -C nadajnika i odbiornika, domyślnie 30000 + (numer_albumu % 10000) -
UI_PORT
- port TCP, na którym udostępniany jest prosty interfejs tekstowy do przełączania się między stacjami, domyślnie 10000 + (numer_albumu % 10000); ustawiany parametrem -U odbiornika -
PSIZE
- rozmiar w bajtach pola audio_data paczki, ustawiany parametrem -p nadajnika, domyślnie 512B -
BSIZE
- rozmiar w bajtach bufora, ustawiany parametrem -b odbiornika, domyślnie 64kB (65536B) -
FSIZE
- rozmiar w bajtach kolejki FIFO nadajnika, ustawiany parametrem -f nadajnika, domyślnie 128kB -
RTIME
- czas (w milisekundach) pomiędzy wysłaniem kolejnych raportów o brakujących paczkach (dla odbiorników) oraz czas pomiędzy kolejnymi retransmisjami paczek, ustawiany parametrem -R, domyślnie 250 -
NAME
- nazwa to nazwa nadajnika, ustawiana parametrem -n, domyślnie "Nienazwany Nadajnik"
Nadajnik służy do wysyłania strumienia danych otrzymanego na standardowe wejście do odbiorników. Nadajnik powinien otrzymywać na standardowe wejście strumień danych z taką prędkością, z jaką odbiorcy są w stanie dane przetwarzać, a następnie wysyłać te dane zapakowane w datagramy UDP na port DATA_PORT
na wskazany w linii poleceń adres ukierunkowanego rozgłaszania MCAST_ADDR
. Dane powinny być przesyłane w paczkach po PSIZE
bajtów, zgodnie z protokołem opisanym poniżej. Prędkość wysyłania jest taka jak podawania danych na wejście odbiornika, nadajnik nie ingeruje w nią w żaden sposób.
Nadajnik powinien przechowywać w kolejce FIFO ostatnich FSIZE
bajtów przeczytanych z wejścia tak, żeby mógł ponownie wysłać te paczki, o których retransmisję poprosiły odbiorniki.
Nadajnik cały czas zbiera od odbiorników prośby o retransmisje paczek. Gromadzi je przez czas RTIME
, następnie wysyła serię retransmisji (podczas ich wysyłania nadal zbiera prośby, do wysłania w kolejnej serii), następnie znów gromadzi je przez czas RTIME
, itd.
Nadajnik nasłuchuje na UDP na porcie CTRL_PORT
, przyjmując także pakiety rozgłoszeniowe. Powinien rozpoznawać dwa rodzaje komunikatów:
-
LOOKUP (prośby o identyfikację): na takie natychmiast odpowiada komunikatem REPLY zgodnie ze specyfikacją protokołu poniżej.
-
REXMIT (prośby o retransmisję paczek): na takie nie odpowiada bezpośrednio; raz na jakiś czas ponownie wysyła paczki, według opisu powyżej.
Po wysłaniu całej zawartości standardowego wejścia nadajnik się kończy z kodem wyjścia 0. Jeśli rozmiar odczytanych danych nie jest podzielny przez PSIZE
, ostatnia (niekompletna) paczka jest porzucana; nie jest wysyłana.
Na przykład, żeby wysłać ulubioną MP-trójkę w jakości płyty CD, można użyć takiego polecenia:
sox -S "05 Muzyczka.mp3" -r 44100 -b 16 -e signed-integer -c 2 -t raw - | pv -q -L \$((44100\*4)) | ./sikradio-sender -a 239.10.11.12 -n "Radio Muzyczka"
Pierwsza część polecenia konwertuje plik MP3 na strumień surowych danych (44100 4-bajtowych sampli na każdą sekundę pliku wejściowego), druga część ogranicza prędkość przekazywania danych do nadajnika tak, żeby odbiorniki wyrabiały się z pobieraniem danych.
Żeby wysyłać dane z mikrofonu (w formacie jak powyżej), można użyć takiego polecenia:
arecord -t raw -f cd | ./sikradio-sender -a 239.10.11.12 -n "Radio Podcast"
Polecenie arecord
można znaleźć w pakiecie alsa-utils.
Odbiornik odbiera dane wysyłane przez nadajnik i wyprowadza je na standardowe wyjście.
Odbiornik co ok. 5s wysyła na adres DISCOVER_ADDR
na port CTRL_PORT
prośbę o identyfikację (komunikat LOOKUP). Na podstawie otrzymanych odpowiedzi (komunikatów REPLY) tworzy listę dostępnych stacji radiowych. Stacja, od której przez 20 sekund odbiornik nie otrzymał komunikatu REPLY, jest usuwana z listy. Jeśli to była stacja aktualnie odtwarzana, rozpoczyna się odtwarzanie innej stacji. Jeśli podano parametr -n, odbiornik rozpoczyna odtwarzanie stacji o zadanej nazwie, gdy tylko ją wykryje. Jeśli nie podano argumentu -n, odbiornik rozpoczyna odtwarzanie pierwszej wykrytej stacji.
Odbiornik posiada bufor o rozmiarze BSIZE
bajtów, przeznaczony do przechowywania danych z maksymalnie ⌊BSIZE
/PSIZE
⌋ kolejnych paczek.
Rozpoczynając odtwarzanie, odbiornik:
-
Czyści bufor, w szczególności porzucając dane w nim się znajdujące, a jeszcze nie wyprowadzone na standardowe wyjście.
-
Jeśli potrzeba, wypisuje się z poprzedniego adresu grupowego, a zapisuje się na nowy.
-
Po otrzymaniu pierwszej paczki audio, zapisuje z niej wartość pola session_id oraz numer pierwszego odebranego bajtu (nazwijmy go BYTE0; patrz specyfikacja protokołu poniżej), oraz rozpoczyna wysyłanie próśb o retransmisję zgodnie z opisem poniżej.
-
Aż do momentu odebrania bajtu o numerze BYTE0 + ⌊
BSIZE
*3/4⌋ lub większym, odbiornik nie przekazuje danych na standardowe wyjście. Gdy jednak to nastąpi, przekazuje dane na standardowe wyjście tak szybko, jak tylko standardowe wyjście na to pozwala.
Powyższą procedurę należy zastosować wszędzie tam, gdzie w treści zadania mowa jest o rozpoczynaniu odtwarzania.
Jeśli odbiornik miałby wyprowadzić na standardowe wyjście dane, których jednakże brakuje w buforze, choćby to była tylko jedna paczka, rozpoczyna odtwarzanie od nowa. UWAGA: to wymaganie nie występowało w zadaniu 1.
Jeśli odbiornik odbierze nową paczkę, o numerze większym niż dotychczas odebrane, umieszcza ją w buforze i w razie potrzeby rezerwuje miejsce na brakujące paczki, których miejsce jest przed nią. Jeśli do wykonania tego potrzeba usunąć stare dane, które nie zostały jeszcze wyprowadzone na standardowe wyjście, należy to zrobić.
Odbiornik wysyła prośby o retransmisję brakujących paczek. Prośbę o retransmisję paczki o numerze n
powinien wysłać w momentach t + k * RTIME
, gdzie t
oznacza moment odebrania pierwszej paczki o numerze większym niż n
, dla k = 1, 2, …
(w nieskończoność, póki dana stacja znajduje się na liście dostępnych stacji). Odbiornik nie wysyła próśb o retransmisję paczek zawierających bajty wcześniejsze niż BYTE0
, ani tak starych, że i tak nie będzie na nie miejsca w buforze.
Odbiornik oczekuje połączeń TCP na porcie UI_PORT
. Jeśli użytkownik podłączy się tam np. programem telnet, powinien zobaczyć prosty tekstowy interfejs, w którym za pomocą strzałek góra/dół można zmieniać stacje (bez konieczności wciskania Enter). Oczywiście, jeśli jest kilka połączeń, wszystkie powinny wyświetlać to samo i zmiany w jednym z nich powinny być widoczne w drugim. Podobnie, wyświetlana lista stacji powinna się aktualizować w przypadku wykrycia nowych stacji lub porzucenia już niedostępnych. Powinno to wyglądać dokładnie tak:
------------------------------------------------------------------------
SIK Radio
------------------------------------------------------------------------
PR1
Radio "357"
> Radio "Disco Pruszkow"
------------------------------------------------------------------------
Stacje powinny być posortowane alfabetycznie po nazwie. Przy każdorazowej zmianie stanu (aktywnej stacji, listy dostępnych stacji itp.) listę należy ponownie wyświetlić w całości; w ten sposób będzie można w sposób automatyczny przetestować działanie programów.
./sikradio-receiver | play -t raw -c 2 -r 44100 -b 16 -e signed-integer --buffer 32768 -
Polecenia play
należy szukać w pakiecie z programem sox.
-
Wymiana danych: wymiana danych odbywa się po UDP. Komunikacja jest jednostronna - nadajnik wysyła paczki audio, a odbiornik je odbiera.
-
Format datagramów: w datagramach przesyłane są dane binarne, zgodne z poniżej zdefiniowanym formatem komunikatów.
-
Porządek bajtów: w komunikatach wszystkie liczby przesyłane są w sieciowej kolejności bajtów (big-endian).
-
Paczka audio
struct __attribute__((packed)) audio_packet {
uint64_t session_id;
uint64_t first_byte_num;
char audio_data[];
}
-
Pole
session_id
jest stałe przez cały czas uruchomienia nadajnika. Na początku jego działania inicjowane jest datą wyrażoną w sekundach od początku epoki. -
Odbiornik zaś zapamiętuje wartość
session_id
z pierwszej paczki, jaką otrzymał po rozpoczęciu odtwarzania. W przypadku odebrania paczki z:- mniejszym
session_id
, ignoruje ją, - z większym
session_id
, rozpoczyna odtwarzanie od nowa.
- mniejszym
-
Bajty odczytywane przez nadajnik ze standardowego wejścia numerowane są od zera. Nadajnik w polu
first_byte_num
umieszcza numer pierwszego spośród bajtów zawartych w audio_data. -
Nadajnik wysyła paczki, w których pole
audio_data
ma dokładniePSIZE
bajtów (afirst_byte_num
jest podzielne przezPSIZE
).
-
Wymiana danych odbywa się po UDP.
-
W datagramach przesyłane są dane tekstowe, zgodne z formatem opisanym poniżej.
-
Każdy komunikat to pojedyncza linia tekstu zakończona uniksowym znakiem końca linii. Poza znakiem końca linii dopuszcza się jedynie znaki o numerach od 32 do 127 według kodowania ASCII.
-
Poszczególne pola komunikatów oddzielone są pojedynczymi spacjami. Ostatnie pole (np. nazwa stacji w REPLY) może zawierać spacje.
-
Komunikat LOOKUP wygląda następująco:
ZERO_SEVEN_COME_IN
- Komunikat REPLY wygląda następująco:
BOREWICZ_HERE [`MCAST_ADDR`] [`DATA_PORT`] [nazwa stacji]
Maksymalna długość nazwy stacji wynosi 64 znaki.
- Komunikat REXMIT wygląda następująco:
LOUDER_PLEASE [lista numerów paczek oddzielonych przecinkami]
gdzie numer paczki to wartość jej pola first_byte_num, np.
LOUDER_PLEASE 512,1024,1536,5632,3584
-
Programy powinny umożliwiać komunikację przy użyciu IPv4. Obsługa IPv6 nie jest konieczna.
-
W implementacji programów duże kolejki komunikatów, zdarzeń itp. powinny być alokowane dynamicznie.
-
Programy muszą być odporne na sytuacje błędne, które dają szansę na kontynuowanie działania. Intencja jest taka, że programy powinny móc być uruchomione na stałe bez konieczności ich restartowania, np. w przypadku kłopotów komunikacyjnych, czasowej niedostępności sieci, zwykłych zmian jej konfiguracji itp.
-
Programy powinny być napisane zrozumiale. Tu można znaleźć wartościowe wskazówki w tej kwestii: https://www.kernel.org/doc/html/v5.6/process/coding-style.html
-
W obydwu aplikacjach opóźnienia w komunikacji z podzbiorem klientów nie mogą wpływać na jakość komunikacji z pozostałymi klientami. Patrz: https://stackoverflow.com/questions/4165174/when-does-a-udp-sendto-block
-
Odtwarzany dźwięk musi być płynny, bez częstych lub niewyjaśnionych trzasków.
-
Polecam stosować zasadę niezawodności Postela: https://en.wikipedia.org/wiki/Robustness_principle
-
Przy przetwarzaniu sieciowych danych binarnych należy używać typów o ustalonej szerokości: http://en.cppreference.com/w/c/types/integer
-
W przypadku otrzymania niepoprawnych argumentów linii komend, programy powinny wypisywać stosowny komunikat na standardowe wyjście błędów i zwracać kod 1.
Można oddać rozwiązanie tylko części A lub tylko części B, albo obu części.
Rozwiązanie ma:
-
działać w środowisku Linux w LK
-
być napisane w języku C lub C++ z wykorzystaniem interfejsu gniazd (nie wolno korzystać z
boost::asio
) -
kompilować się za pomocą GCC (polecenie
gcc
lubg++
) – wśród parametrów należy użyć-Wall
i-O2
, można korzystać ze standardów-std=c2x
,-std=c++20
(w zakresie wspieranym przez kompilator na maszynie students)
Można korzystać z powszechnie znanych bibliotek pomocniczych (np. boost::program_options
), o ile są zainstalowane na maszynie students.
Jako rozwiązanie należy dostarczyć pliki źródłowe oraz plik makefile, które należy umieścić na moodle w archiwum ab123456.tgz
gdzie ab123456 to standardowy login osoby oddającej rozwiązanie, używany na maszynach wydziału, wg schematu: inicjały, nr indeksu. Nie wolno umieszczać tam plików binarnych ani pośrednich powstających podczas kompilacji.
W wyniku wykonania polecenia make dla części A zadania ma powstać plik wykonywalny sikradio-sender, a dla części B zadania – plik wykonywalny sikradio-receiver.
Ponadto makefile powinien obsługiwać cel 'clean', który po wywołaniu kasuje wszystkie pliki powstałe podczas kompilacji.