====== Wstęp – przypomnienia ====== ===== Trochę o C/C++ ===== ==== Komendy do kompilacji ==== Większość przykładów w materiałach i programów pisanych na laboratoriach jest w %%C++%%.\\ Stąd przypominam jak obsługiwać kompilator GCC / clang w Linuksie. Przykładowe polecenie do kompilacji przykładów ze strony: \\ g++ --std=c++20 -Wall -O0 -g -pthread -o example example.cpp |''c+////+''|domyślny kompilator C+////+. Zwykle link do ''g+////+'' lub ''clang+////+''| |''c+////+ zrodlo.cpp -o prog''|kompiluje plik ''zrodlo.cpp'' do programu ''prog''| |''c+////+ -Wall z.cpp -o p''|włącza wszystkie(("Wszystkie" oznacza wybrany zbiór ostrzeżeń o nazwie wszystkie, poza ''-Wall'' warto też dodać ''-Wextra'' i rozważyć dodanie ''-pedantic''. Szczegóły w dokumentacji kompilatora [[https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html|gcc]]/[[https://clang.llvm.org/docs/UsersManual.html#enabling-all-diagnostics|clang]])) ostrzeżenia kompilatora (''-W'' = warn, ''all'' = wszystkie)| |''c+////+ -O0 -g z.cpp -o p''|wyłącza optymalizacje i dodaje do programu dane umożliwiające debugowanie| |''c+////+ --std=c+////+20 z.cpp -o p''|włącza używanie standardu ISO C+////+ z 2020 roku| |c++ -pthread z.cpp -o p|włącza obsługę wątków standardu POSIX \\ wymagane do wersji glibc ≤ 2.33 [[https://developers.redhat.com/articles/2021/12/17/why-glibc-234-removed-libpthread|[1]]] [[https://lists.gnu.org/archive/html/info-gnu/2021-08/msg00001.html|[2]]]| ==== Przekazywanie struktur do funkcji ==== W C%%++%% do funkcji można przekazać argumenty przez wartość lub referencję.\\ W C do funkcji można przekazać argumenty tylko przez wartość.\\ Często do funkcji przekazuje się (jako wartość) adres w pamięci pod którym znajduje się zmienna którą ma używać funkcja. Takie przekazanie zmiennej do funkcji bywa nazywane przekazaniem przez wskaźnik/adres. Argumenty przekazywane do funkcji przez wartość – w tym struktury/klasy – są kopiowane i funkcja pracuje na własnej kopii. \\ Aby uniknąć kopiowania struktur/klas (które kosztuje czas procesora i zajmuje pamięć na stosie) tam gdzie to możliwe wykorzystuje się przekazywanie adresu do zmiennej (w C) lub przekazywanie referencji (w C%%++%%). \\ Jednocześnie taki sposób przekazania argumentu powoduje że funkcja może zmienić wartość przekazanej zmiennej. Dlatego wskaźnik bądź referencja będąca argumentem funkcji jest często kwalifikowana jako ''const'', by kompilator gwarantował że zmienna nie będzie modyfikowana przez funkcję. ~~Zadanie.#~~ Porównaj programy: |#include struct bigStrct { int version; char data1[1024*1024]; }; void prVer(struct bigStrct var){ printf("%d\n", var.version); } int main(){ struct bigStrct myData = {5, "hello"}; prVer(myData); return 0; }|#include struct bigStrct { int version; char data1[1024*1024]; }; void prVer(const struct bigStrct * var){ printf("%d\n", var->version); } int main(){ struct bigStrct myData = {5, "hello"}; prVer(&myData); return 0; }| |#include struct bigStrct { int version; char data1[4*1024*1024]; }; void prVer(struct bigStrct var){ printf("%d\n", var.version); } int main(){ struct bigStrct myData = {5, "hello"}; prVer(myData); return 0; }|#include struct bigStrct { int version; char data1[4*1024*1024]; }; void prVer(const struct bigStrct * var){ printf("%d\n", var->version); } int main(){ struct bigStrct myData = {5, "hello"}; prVer(&myData); return 0; }|
Czym się od siebie różnią? Dlaczego jeden z nich w trakcie uruchomienia ulega awarii? ==== Przekazywanie danych nieznanego z góry rozmiaru ==== W C/C%%++%% kompilator nie potrafi określić tego ile bajtów ma coś do czego wskaźnik został przekazany jako argument funkcji. (Czasami mógłby, ale są sytuacje gdzie się nie da.) Dlatego przekazując jako argument adres trzeba określić ile bajtów można spod niego odczytać. Możliwe opcje to m. inn: * funkcja zawsze czyta tyle samo bajtów, wywołujący musi zagwarantować że przynajmniej tyle jest, * funkcja czyta do trafienia na określoną wartość, np. [[https://en.wikipedia.org/wiki/Null-terminated_string|ciągi znaków kończą się zerem]], * w innym argumencie wywołujący podaje ile bajtów funkcja może przeczytać. ~~Zadanie.#~~ Dlaczego funkcja ''[[https://en.cppreference.com/w/c/string/byte/strchr|strchr]]'' nie przyjmuje jako argument długości ciągu znaków, a ''[[https://en.cppreference.com/w/c/string/byte/memchr.html|memchr]]'' przyjmuje? ~~Zadanie.#~~ Patrząc na programy: |#include unsigned max(unsigned tab[16]){ unsigned res = 0; for(size_t i = 0 ; i < 16; ++i) if(res < tab[i]) res = tab[i]; return res; } int main(){ unsigned data[4] = {5,6,8,7}; printf("%u\n", max(data)); return 0; }| #include unsigned max(unsigned tab[], size_t len){ unsigned res = 0; for(size_t i = 0 ; i < len; ++i) if(res < tab[i]) res = tab[i]; return res; } int main(){ unsigned data[4] = {5,6,8,7}; printf("%u\n", max(data, 4)); return 0; }|
Czym się od siebie różnią? Dlaczego w lewym kompilator pozwala na umieszczenie na liście argumentów ''unsigned tab[16]''? Dlaczego mimo to pozwala skompilować kod w którym do tego argumentu przekazywana jest tablica czteroelementowa? ===== Kolejność bajtów wielobajtowych liczb całkowitych ===== Twórcy procesorów mieli różne pomysły na to jak zapisywać liczby (szczególnie niecałkowite). Jednym z pytań było: skoro w pamięci adresowane są pojedyncze bajty i chcę zapisać liczbę całkowitą 1000 (0x03e8) na dwóch bajtach, to czy wartość 232 (0xe8) ma być zapisana w bajcie o niższym czy wyższym adresie? \\ Taki spór o [[https://pl.wikipedia.org/wiki/Kolejność_bajtów|kolejność bajtów]] początkowo nie został rozstrzygnięty, część procesorów używała kolejności big endian, część little endian. \\ Dzisiaj przeważa użycie little endian (najmniej znaczący bajt ma najmniejszy adres, co jednocześnie oznacza że odczytywane w porządku rosnących adresów wartości kolejnych bajtów są w odwrotnym porządku niż naturalny zapis liczby). Wszystkie systemowe funkcje programistyczne do obsługi sieci oczekują że programista przekaże wielobajtowe liczy w //[[https://en.wikipedia.org/wiki/Endianness#Networking|sieciowej kolejności bajtów]]// (networking byte order), którą ustalono na to big endian – odwrotnie niż na większości współczesnych procesorów. Do zmiany kolejności bajtów z lokalnej na sieciową / z sieciowej na lokalną można użyć funkcji htons(uint16_t num), htonl(uint32_t num), ntohs(uint16_t num), ntohl(uint32_t num) (więcej w ''[[https://man7.org/linux/man-pages/man3/htons.3.html|man byteorder]]''). ====== Wybrane elementy z API systemu operacyjnego ====== ===== Deskryptory plików ===== W API bibliotek standardowych języka C pliki reprezentowane są przez typ ''[[https://en.cppreference.com/w/c/io/FILE.html|FILE*]]'', a każdy program przy starcie ma otwarte pliki ''stdin'', ''stdout'' i ''stderr''. \\ Zauważ że np. funkcja ''printf(…)'' pisze właśnie do pliku ''stdin'', a nie "na ekran"). W API bibliotek standardowych języka C++ pliki reprezentowane są przez typy dziedziczące po ''[[https://en.cppreference.com/w/cpp/io/basic_iostream.html|std::basic_istream/std::basic_ostream]]'', a każdy program przy starcie ma otwarte pliki ''std::cin'', ''std::cout'' i ''std::cerr''. \\ W API uniksopodobnych systemów operacyjnych pliki są reprezentowane przez liczby (zmienną typu ''int''), nazywane zwyczajowo **[[https://en.wikipedia.org/wiki/File_descriptor|deskryptorami plików]]**, a każdy program przy starcie ma otwarte pliki ''0'', ''1'' i ''2'' (odpowiadające standardowemu wejściu, standardowemu wyjściu i standardowemu [strumieniu] błędów). Każdy nowo stworzony/otwarty plik dostaje najniższy wolny numer. \\ Programiści wywołując systemowe operacje na plikach (tzn. prosząc system operacyjny o wykonanie jakiejś akcji, takiej jako odczyt kolejnej porcji danych) musi podać numer pliku na którym ta operacja ma być wykonana. **Dla uniksopodobnego systemu operacyjnego** wiele rzeczy jest plikiem; poza zwykłymi plikami (które są tratowane jako rodzaj pliku) **plikami są też** katalogi, urządzenia i **połączenia sieciowe**. ===== Odczytywanie i zapisywanie danych, zamykanie plików ===== Podstawową systemową funkcją do odczytywania danych jest funkcja ''read'': \\   ''ssize_t read(int fd, void * buf, size_t len);'' \\ (typ ''ssize_t'' to liczba całkowita ze znakiem reprezentująca rozmiar, np. ''int'', a ''size_t'' to liczba całkowita bez znaku reprezentująca rozmiar, np. ''unsigned int''.) Funkcja read umieszcza odczytane dane, maksymalnie ''len'' znaków, w miejscu wskazanym przez ''buf''. Read zwraca: * jeżeli przeczyta dane i umieści je w buforze, to zwraca wartość dodatnią - ilość przeczytanych znaków * wartość 0 jeżeli nie ma więcej danych w pliku((uwaga – w UDP wartość 0 ma inne znaczenie)) * wartość -1 jeżeli wystąpił błąd
Uwaga, ''read'' może przeczytać mniej znaków niż ''len'' – np. jeśli plik się skończył. Podstawową systemową funkcją do zapisu danych jest funkcja ''write'': \\   ''ssize_t write(int fd, const void * buf, size_t len);'' Funkcja write zapisuje dp pliku ''len'' znaków zaczynających się w miejscu wskazanym przez ''buf'' i zwraca: * jeżeli zapisze dane do pliku, to zwraca wartość dodatnią - ilość zapisanych znaków * wartość -1 jeżeli wystąpił błąd Każdy plik trzeba zamknąć żeby zwolnic zasoby które system operacyjny w swojej pamięci trzyma dla tego pliku. Do tego służy funkcja ''close()''. Opisane tutaj funkcje są w pliku nagłówkowym ''unistd.h'' (''#include ''). ===== Określanie przyczyn niepowodzenia – perror i errno ===== Jeżeli funkcja systemowa nie powiedzie się, to nadpisze wartość ostatniego kodu błędu trzymanego w zmiennej ''[[https://man7.org/linux/man-pages/man3/errno.3.html|errno]]''.\\ Zmienna ''errno'' jest dostępna dla programisty (''#include ''). Funkcje które mogą ustawić taki kod błędu mają w dokumentacji podane jakie kody mogą ustawić i co te kody błędu oznaczają. Programista może wyświetlić funkcją ''[[https://en.cppreference.com/w/c/io/perror|perror(const char *msg)]]'' (''#include '') komunikat powiązany z kodem błędu ze zmiennej ''errno''. Jeżeli w ''msg'' będzie //"tekst"//, to funkcja ''perror'' na standardowe wyjście wypisze ''tekst: //komunikat//'', a jeżeli ''msg'' będzie pustym wskaźnikiem, to wypisze ''//komunikat//'' (gdzie ''//komunikat//'' objaśnia co poszło nie tak). ====== Sockets ====== ===== BSD socket API ===== Gniazdo (socket) – interfejs między systemem operacyjnym a programem użytkownika używany do dwukierunkowej komunikacji poza program. API stworzone dla systemu BSD zostało przyjęte przez praktycznie wszystkie systemy operacyjne (POSIX socket API, WinSock). Po więcej informacji o BSD Socket API zajrzyj na [[https://en.wikipedia.org/wiki/Berkeley_sockets|wikipedię]] oraz ''man 7 [[https://man7.org/linux/man-pages/man7/socket.7.html|socket]] [[https://man7.org/linux/man-pages/man7/tcp.7.html|tcp]] [[https://man7.org/linux/man-pages/man7/udp.7.html|udp]]'' Co ważne, BSD socket API zostało zaprojektowane tak żeby korzystać z różnych protokołów sieciowych (i niesieciowych), np. bluetooth, [[https://pl.wikipedia.org/wiki/Controller_Area_Network|CAN]], [[https://en.wikipedia.org/wiki/Unix_domain_socket, komunikacja międzyprocesowa]] . Stąd korzystając z IPv4/IPv6 trzeba przekazać tą informację do wielu funkcji. ===== Zapis adresu gniazda ===== **Protokół** trzeba podać na etapie tworzenia gniazda. \\ **Adres IP** oraz **numer portu** potrzebny np. przy łączeniu do podanego celu trzeba podać w odpowiedniej strukturze. Do przekazywania adresu gniazda funkcje używają wskaźnika na strukturę **''sockaddr''** (w C – ''struct sockaddr''). API celowo przekazuje wskaźnik na adres nieokreślonego typu, tak żeby móc działać dla wielu rodzajów adresów. C nie pozwala na dziedziczenie, więc zamiast tego ([[https://en.wikipedia.org/wiki/Type_punning#Sockets_example]]) struktura ''sockaddr'' ma kilka "specjalizacji" dla konkretnej rodziny adresów:
*''sockaddr_in'' (INET, czyli IPv4) *''sockaddr_in6'' (INET6, czyli IPv6) *''sockaddr_un'' (dla unix socket).
Do przekazania adresu gniazda IPv4 trzeba stworzyć zmienną typu **''sockaddr_in''** (w C – ''struct sockaddr_in''), wpisać do niej adres IP i numer portu (pola ''sin_port'' i ''sin_addr'') oraz wpisać informację że to jest adres gniazda typu IPv4 **uzupełniając wartość pola ''sin_family'' na AF_INET** (INET to skrót od Internet Protocol version 4). \\ Funkcje które przyjmują jako argument **''sockaddr*''** zawsze sprawdzają co wpisano w pole ''sin_family'' żeby sprawdzić czy dostały właściwego typu adres. Kompilator nie pozwala na przekazanie adresu zmiennej typu ''sockaddr_in'' jako adresu zmiennej typu ''sockaddr'', wskazując że typ ''sockaddr_in*'' nie może być automatycznie zmieniony w ''sockaddr*''. \\ **Przekazując wskaźnik na adres konkretnego typu** (np. ''sockaddr_in*'') **jako wskaźnik na adres nieokreślonego typu ''sockaddr*'' konieczne jest rzutowanie. Dodatkowo funkcje przyjmujące adres jako kolejny argument oczekują rozmiaru przekazanej struktury**, np:
sockaddr_in ipv4addr = {AF_INET, ………}; connect(sockFd, (sockaddr*)&ipv4addr, sizeof(ipv4addr)); Struktura ''sockaddr_in'' jest w pliku nagłówkowym ''#include '' \\ Struktura ''sockaddr'' jest w pliku nagłówkowym ''#include '' Pełny opis struktury i jej pól – patrz ''[[https://man7.org/linux/man-pages/man3/sockaddr.3type.html|man sockaddr]]'', ''man 7 [[https://man7.org/linux/man-pages/man7/ip.7.html|ip]]'' i ''man [[https://man7.org/linux/man-pages/man0/netinet_in.h.0p.html|netinet_in.h]]''. ===== "Hello world" ===== ~~Zadanie.#~~ Połącz się pod port 13 swojego komputera używając protokołu TCP. ~~Zadanie.#~~ Napisz program, który kolejno: * stworzy bufor na dane (np. tablicę znaków). * stworzy zmienną typu ''sockaddr_in'' (zdefiniowaną w pliku nagłówkowym ''netinet/in.h'') i wypełni: * rodzinę adresów na IPv4 – stała ''AF_INET'' * port – wartość 13, ale zapisane w sieciowym porządku bajtów * adres IP – wpisz tam adres 127.0.0.1, używając do konwersji na liczbę funkcji ''inet_addr'' lub ''inet_aton'' \\ Uwaga: ''.sin_addr'' to struktura typu ''in_addr'' z jedną składową ''.s_addr'' typu ''uint32_t'' \\ ''inet_aton'' przyjmuje wskaźnik na strukturę, natomiast ''inet_addr'' zwraca liczbę którą trzeba przypisać składowej ''.s_addr'': \\
sockaddr_in addr;
wersja 1: addr.sin_addr.s_addr = inet_addr("8.8.8.8");
wersja 2: inet_aton("8.8.8.8", &addr.sin_addr);
można też ustawić adres na stałą ''htonl(INADDR_LOOPBACK)''.
* tworzył funkcją ''socket(…)'' (zdefiniowaną w pliku nagłówkowym ''sys/socket.h'') nowe gniazdo, podając jako argumenty: * jako domenę komunikacyjną poda protokół IPv4 – stała ''PF_INET'' (zamiennie ''AF_INET'') * określi typ jako strumieniowy – stała ''SOCK_STREAM'' (czyli typ komunikacji używany przez TCP) * ustawi protokół na TCP – stała ''IPPROTO_TCP'' (można też podać ''0'', oznaczające domyślny protokół; TCP jest domyślnym protokołem typu strumieniowego) * funkcja ''socket(…)'' zwraca deskryptor nowo utworzonego gniazda – ten numer trzeba potem podawać do wszystkich funkcji które mają na tym gnieździe operować * funkcją ''connect(…)'' nakaże systemowi operacyjnemu wykonać na wcześniej utworzonym gnieździe połączenie do wcześniej przygotowanego adresu * pierwszym argumentem jest informacja które gniazdo ma się połączyć * drugim jest informacja na jaki adres – pamiętaj że funkcja ''connect'' oczekuje ''sockaddr*'' zamiast ''sockaddr_in*'' (wymagane rzutownaie) * trzeci argument funkcji connect to rozmiar struktury opisującej adres – ''sizeof()'' zmiennej lub typu * funkcją ''read(…)'' (zdefiniowaną w pliku nagłówkowym ''unistd.h'') odczyta dane z gniazda, przy czym: * pierwszy argument wskazuje skąd czytać dane * drugi wskazuje gdzie system operacyjny ma umieścić przeczytane dane – tutaj podaj wcześniej przygotowany bufor * trzeci argument podaje ile co najwyżej bajtów system operacyjny może odczytać (tzn. ile jest miejsca w buforze) * funkcja zwróci informację ile danych faktycznie zostało przeczytane * wypisze na standardowe wyjście odebrane dane (funkcją ''write(…)'' lub inną) * zamknie gniazdo funkcją ''shutdown(…)'', która: * jako pierwszy argument oczekuje informacji na którym gnieździe ma zamknąć połączenie * jako drugi chce wiedzieć czy zakończyć zarówno nadawanie jak i odbiór (''SHUT_RDWR''), czy tylko jedno z nich (co jest rzadko używane) * zwolni zasoby systemowe związane z gniazdem funkcją ''close()''
Szczegółowy opis każdej z funkcji znajdziesz w podręczniku systemowym (''man 3 …'' / ''man 3p …'')\\ Potrzebne pliki nagłówkowe to:
#include #include #include #include ~~Zadanie.#~~ Zmień poprzedni program tak, by czytał dane aż druga strona nie zamknie połączenia. ~~Zadanie.#~~ Dodaj do programu obsługę błędów zwracanych przez funkcje ''connect'' i ''read''. ~~Zadanie.#~~ Przekształć poprzedni program tak, by czytał dane z adresu IP i portu podanego w argumentach programu. ~~Zadanie.#~~ Zmień IP na losowe (tak, by nie odpowiadało na próbę połączenia). Programem ''netstat -tnp'' / ''ss -tnp'' wyświetl utworzone połączenie. ===== Funkcje send/recv/… ===== Poza funkcją ''read(…)'' do odbierania danych można używać funkcji ''recv'', ''recvfrom'' i ''recvmsg'', przy czym ''read(fd, buf, len)'' jest równoważne ''recv(fd, buf, len, 0)'' i ''recvfrom(sockfd, buf, len, 0, NULL, NULL)''. \\ Podobnie poza funkcją ''write'' do wysyłania można używać też funkcji ''send'', ''sendto'' i ''sendmsg'', analogicznych do powyższych. \\ Dodatkowy argument ''recv''/''send'' (pole flag) pozwala na zmianę zachowania tych funkcji i będzie omawiany później.\\ ''recvfrom'' i ''sendto'' mają dodatkowe pole na adres nadawcy/odbiorcy i są przeznaczone dla protokołów warstwy transportowej pozwalających na komunikację po jednym gnieździe z wieloma partnerami. Będą omawiane przy obsłudze UDP. ~~Zadanie.#~~ Zmień program tak, by zamiast ''read(…)'' używał funkcji ''recv(…)''