Jerzy Czepiel 2 KOMUNIKACJA W SIECI 1 Sposób zaliczenia przedmiotu Wymagania: Maksymalnie 2 nieobecności nieusprawiedliwione. Projekty na zajęciach (40 punktów). Krótkie programiki, będące rozwinięciem programów przerabianych podczas ćwiczeń- mniej więcej 8 razy po 5 punktów. Termin nadsyłania projektów będzie powiązany z terminem ćwiczeń i uzależniony od złożoności programu. Maksymalnie do następnych ćwiczeń. Projekt końcowy (30 punktów). Większy program sieciowy (grupowy). Będzie to gra. Kolokwium z wiedzy teoretycznej (20 punktów). Aktywność (10 pkt) - jest to ilość zarezerwowana - punktów z aktywności może być wiecej (do 20), będą także punkty ujemne. Skala ocen: od do ocena 0 49 2.0 50 59 3.0 60 69 3.5 70 79 4.0 80 89 4.5 90 100 5.0 2 Komunikacja w sieci Sieć komputerowa to medium umożliwiające połączenie dwóch lub więcej komputerów w celu wzajemnego komunikowania się (Rys 1). Programy użytkowe, przeznaczone do wykonywania w sieci, można podzielić na dwie główne grupy. Są to programy serwerowe, udostępniające usługi sieciowe oraz programy klienckie, korzystające z tychże usług (Rys 2). Prostym przykładem takiego połączenia jest para przeglądarka internetowaserwer www. Klient na ogół komunikuje się z jednym serwerem, natomiast serwer najczęściej obsługuje wielu klientów równocześnie. Aby zrozumieć komunikację w sieci komputerowej zaczniemy od zapoznania się z modelem warstw ISO/OSI Wielość rozwiązań stosowanych w budowie pierwszych sieci komputerowych w sposób istotny utrudniała wzajemną komunikację, pomiędzy sieciami działającymi na podstawie różnych specyfikacji. Fakt ten był bezpośrednią przyczyną podjęcia działań w kierunku standaryzacji rozwiązań. Sieci Komputerowe 1
Jerzy Czepiel 2 KOMUNIKACJA W SIECI Rysunek 1: Schemat sieci komputerowej Rysunek 2: Schemat komunikacji klient-serwer Sieci Komputerowe 2
Jerzy Czepiel 2 KOMUNIKACJA W SIECI Rysunek 3: Model warstw ISO/OSI Model ISO/OSI dzieli proces komunikacji, między dwoma programami na siedem warstw (Rys 3). Każda warstwa zajmuje się odrębną częścią komunikacji. Każda warstwa, może komunikować się z dwiema warstwami sąsiednimi. Umożliwiło to podział komunikacji na serię różnych protokołów odpowiedzialnych za komunikację między różnymi warstwami, co w konsekwencji umożliwiło połączenie wielu sieci o różnej architekturze fizycznej w jedną ogólnoświatową sieć internet. Zalety Podział komunikacji na mniejsze, oddzielnie projektowane elementy. Standaryzacja pozwalająca na rozwój przez oddzielnych producentów. Umożliwienie wzajemnej komunikacji różnych typów urządzeń sieciowych. Zmiany w jednej warstwie nie pociągają za sobą wprowadzenia zmian w warstwach pozostałych. Mniejsze składowe komunikacji ułatwiają zrozumienie i tworzenie nowego oprogramowania sieciowego. Poszczególne warstwy: 1. Zadaniem warstwy fizycznej jest transmitowanie sygnałów cyfrowych pomiędzy urządzeniami sieciowymi. Jednostką informacji na poziomie tej warstwy jest pojedynczy bit. Parametry charakteryzujące tę warstwę to właściwości fizyczne łącza, takie jak częstotliwości, napięcia, opóźnienie, długość, zniekształcenia, poziom zakłóceń, itp. 2. Warstwa łącza danych odpowiada za komunikację pomiędzy hostami, podłączonymi do tego samego medium. Jej głównym zadaniem jest sterowanie dostępem do medium. Jednostką informacji w tej warstwie jest ramka składająca się z bitów o ściśle określonej strukturze zawierająca adresy nadawcy i adresata. Adresy urządzeń mogą mieć Sieci Komputerowe 3
Jerzy Czepiel 2 KOMUNIKACJA W SIECI dowolną postać, określoną w specyfikacji zastosowanego standardu komunikacji. Warstwa wyposażona jest w mechanizm kontroli poprawności transmisji, w celu zapewnienia niezawodnego przesyłania danych przez medium. (urządzenia tej warstwy to na przykład modemy, karty sieciowe). 3. Głównym zadaniem warstwy sieci jest umożliwienie komunikacji pomiędzy hostami znajdującymi się w różnych sieciach lokalnych. Realizacja tego zadania możliwa jest dzięki dwóm mechanizmom: jednolitej adresacji urządzeń w całej sieci oraz routingu. Podstawową jednostką informacji w tej warstwie jest pakiet o ściśle określonej strukturze zawierający oprócz danych, adresy: nadawcy i odbiorcy pakietu. Warstwa ta nie gwarantuje niezawodności transmisji, natomiast wyposażona jest w mechanizmy monitorowania transmisji, co pozwala m.in. na identyfikację przyczyn uniemożliwiających komunikację. 4. Warstwa transportowa odpowiedzialna jest za niezawodne przesyłanie danych między urządzeniami. Warstwa ta posiada mechanizmy umożliwiające inicjację, utrzymanie i zamykanie połączenia między urządzeniami, sterowanie przepływem danych oraz wykrywanie błędów transmisji. 5. Zadaniem warstwy sesji jest zarządzanie komunikacją między aplikacjami działającymi na danym hoście, a aplikacjami działającymi na innych hostach w sieci. Ze względu na funkcjonalność systemów operacyjnych zawsze występuje sytuacja, gdy liczba aplikacji korzystających z sieci jest większa od liczby fizycznych interfejsów sieciowych. Rola tej warstwy sieci polega na stworzeniu mechanizmu umożliwiającego dostarczanie danych jakie przyszły z sieci oraz wysyłanie danych do sieci do aplikacji, dla której te dane są przeznaczone. 6. Zadaniem warstwy prezentacji jest konwersja danych pod względem formatu oraz struktury, aby interpretacja tych danych była jednakowa na obu urządzeniach: wysyłającym i odbierającym. Najczęściej konieczność dostosowania danych wynika z różnic między platformami sprzętowymi, na których działają komunikujące się aplikacje. 7. Zadaniem warstwy aplikacji jest zapewnienie dostępu do usług sieciowych procesom aplikacyjnym, działającym na danym urządzeniu. Czas pokazał, że nie wszystkie warstwy są konieczne. Model warstw internetu oparty na rodzinie protokołów tcp/ip ograniczył ich liczbę do czterech (Rys 4). Programy użytkowe czy to klienckie, czy serwerowe, działają na warstwie aplikacji. Aby przesłać informacje, program wysyłający musi przesłać je warstwom niższym, aż do warstwy łącza danych. Podczas tego przesyłania Sieci Komputerowe 4
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE Rysunek 4: Model warstw ISO/OSI wyparty przez model warstw internetu Rysunek 5: Komunikacja klient serwer w sieci ethernet następuje tak zwana enkapsulacja. Pakowanie danych z warstwy wyższej do pakietów warstwy niższej. Podczas odbierania danych, następuje proces odwrotny. Schemat komunikacji między serwerem www, a przeglądarką uruchomioną w tej samej sieci lokalnej ethernet można zobaczyć na rysunku (Rys 5). Widzimy tam także przykładowe protokoły stosowane w internecie. Największa korzyść dla programistów z modelu warstwowego jest taka, że pisząc programy sieciowe (w znaczącej większości przypadków), wystarczy, że będziemy korzystać z mechanizmów dostępu do warstwy transportowej. Pozwala to tworzyć oprogramowanie uniwersalne, działające z każdym rodzajem sprzętu sieciowego, ponieważ za obsługę niższych warstw odpowiada stos protokołów zarządzany przez system operacyjny. 3 Programowanie sieciowe w Unixie 3.1 Narzędzia Narzędzia: Edytor vim kompilator g++ Sieci Komputerowe 5
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE Na dobry początek zajmiemy się edytorem. Do pracy na virgo będziemy używać edytora vim. Zaczniemy od zapoznania się z podstawowymi komendami vima, oraz napiszemy pierwszy prosty program, który wprowadzi nas w obsługę plików na maszynach Unixowych. Vim pracuje w dwóch trybach. Tryb NORMAL służy do wprowadzania komend i poleceń- to tryb domyślny, w nim jesteśmy po uruchomieniu programu. Drugim trybem jest tryb VISUAL. Jest to tryb, w którym vim działa jak każdy inny edytor tekstowy. Aby wejść do trybu VISUAL wciśnij i, aby powrócić wciśnij esc. vim nazwa_pliku // edycja pliku :wq // zapisz zmiany i wyjdź - w trybie NORMAL :w! // wyjdź bez zapisywania zmian - w trybie NORMAL Włączymy kolorowanie składni (nie zadziała na naszych serwerach) Zaloguj się na virgo Przejdź do katalogu domowego vim.vimrc //tworzymy i edytujemy plik.vimrc komendy wewnatrz programu: :read $VIMRUNTIME/vimrc_example.vim // zapełniamy plik konfiguracyjny // przykładową zawartością :wq // zapisujemy zmiany i wyłączamy vima Teraz wydając polecenie: vim a.cpp // powinniśmy pisząc kod c++ zauważyć, że vim koloruje nam składnię. g++ a.cpp // kompilacja pliku./a.out // uruchomienie gotowego programu Zadanie pierwsze: vimtutor pl // działa z pełną instalacją vima // vim. Poświęćcie na to 15 minut. Zobaczycie, jak przydatnym narzędziem jest // vim. 3.2 Obsługa plików w stylu c w linuxie-przypomnienie Jądro Uniksa zapamiętuje uchwyty do otwartych plików, jako nieduże liczby naturalne- są to tzw. deskryptory. Wykorzystywane są one do indeksowania tablicy otwartych plików, w którą jest wyposażony każdy działający proces (nie można nią bezpośrednio manipulować, zmiany są wynikiem otwierania/zamykania plików). Tradycyjnie deskryptor 0 to standardowe wejście, 1 wyjście, a 2 wyjście błędów (stdin, stdout, stderr). Oprócz zwykłych plików, deskryptory mogą być związane z terminalami (jak widać na przykładzie stdin), urządzeniami (np. /dev/null), potokami (prog1 prog2), a także połączeniami sieciowymi (sockety). Różnice sprowadzają się przeważnie tylko do sposobu, w jaki można utworzyć deskryptor danego typu - czytanie/pisanie/zamknięcie odbywa się w jednolity sposób. Sieci Komputerowe 6
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE // Program wypisujący pierwsze 256 znaków pliku /etc/passwd #include <unistd.h> /* oficjalny POSIX-owy plik nagłówkowy */ #include <sys/types.h> /* a te trzy, dołączamy dla tradycji */ #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> // funkcja exit #include <stdio.h> // putsf int main() int max=256; int fd, len; char buf[max+1]; fd = open("/etc/passwd", O_RDONLY); /* otwarcie tylko do odczytu */ if (fd == -1) exit(1); /* nie udało się otworzyć */ len = read(fd, buf, 256); if (len == -1) exit(1); /* wystąpił błąd odczytu */ else if (len == 0) /* dotarliśmy do końca pliku, end-of-file - przydatne w innych programach*/ else if (len < 256) /* przeczytaliśmy mniej niż chcieliśmy, czyżbyśmy byli tuż przed EOF? przydatne w innych programach*/ /* na koniec zamykamy plik za pomocą deskryptora */ close(fd); fputs(buf,stdout); // wyświetlamy na ekranie zawartość bufora return 0; Zapisywanie do pliku odbywa się przy pomocy funkcji write(), której też trzeba podać trzy argumenty - deskryptor, wskaźnik do początku danych i ilość bajtów do zapisania. Zwracana jest liczba rzeczywiście zapisanych bajtów, która może być mniejsza niż prosiliśmy. Trzeba wtedy wywołać write() jeszcze raz, z odpowiednio zmodyfikowanymi parametrami, i zapisać Sieci Komputerowe 7
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE poprzednio nie przesłane dane. Pofragmentowane odczyty/zapisy nie zdarzają się w przypadku pracy z plikami na dysku, ale są dosyć częste przy posługiwaniu się połączeniami sieciowymi - pamiętajmy, że dane przesyłane są przez sieć w pakietach o ograniczonej długości. Z tego względu sugerowałbym przygotowanie sobie funkcji narzędziowych read fully() i write fully(), które tak długo wywołują w pętli odpowiednio read() bądź write(), aż zostanie przetransmitowana żądana liczba bajtów. Zadanie 1: Przygotuj funkcję read_fully(deskryptor, bufor, ilość), która będzie odczytywać tak długo, aż odczyta tyle znaków ile chcemy, lub dojdzie do końca danych, lub nastąpi błąd odczytu. Zadanie 2: Przygotuj analogiczną funkcję write_fully(); Sieci Komputerowe 8
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE 3.3 Gniazda w c++ - pierwszy program sieciowy #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <arpa/inet.h> int main(int argc, char** argv) int s, n; int max = 1024; char buf[max+1]; struct sockaddr_in serwer_adres; if(argc!= 2) printf("podaj adres serwera \n"); exit(1); if( (s = socket(af_inet,sock_stream,0)) <0 ) //trzeci argument to zwykle 0 printf("nie udalo sie utworzyc gniazdka"); exit(1); bzero(&serwer_adres,sizeof(serwer_adres)); serwer_adres.sin_family = AF_INET; serwer_adres.sin_port = htons(25); // serwer nam sie przedstawi if( inet_pton(af_inet,argv[1],&serwer_adres.sin_addr)<=0) printf("podaj prawidlowy adres internetowy"); exit(1); if(connect(s,(sockaddr *)&serwer_adres,sizeof(serwer_adres))<0) Sieci Komputerowe 9
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE printf("nie udalo sie polaczyc"); exit(1); n = read(s,buf,max); buf[n]=0; // zabezpieczenie, zeby zakonczyl wypisywanie fputs(buf,stdout); if(n<0) close(s); return 0; printf("błąd odczytu z gniazdka"); exit(1); Jeśli program zapisaliśmy jako a.cpp, to możemy uruchomić go: g++ a.cpp./a.out 149.156.65.17 Powinien nam się przedstawić ELF. Aby skontaktować się z jakimś serwerem, potrzebujemy jego adres internetowy. W skład adresu wchodzą dwa istotne elementy: adres ip (32 bitowy ciąg identyfikujący konkretny komputer w sieci) oraz numer portu (jest to liczba całkowita identyfikująca proces uruchomiony na serwerze). Dla wielu standardowych usług mamy porty zarezerwowane, jak na przykład port 21 dla serwera ftp. Para: numer ip oraz numer portu, jest nazywana gniazdem (socket). Można to też nazywać punktem końcowym dla połączenia tcp. Serwer sam otwiera port, na którym oczekuje na połączenia. Z klientem jest inaczej, on dostaje automatycznie tak zwany numer krótkotrwały (efemeryczny). Jest to oczywiste- serwer musi jakoś zaadresować odpowiedź- a ona musi trafić do naszego programu klienckiego. Unix jest przystosowany do równoczesnej obsługi wielu protokołów sieciowych. To znaczy, że funkcje obsługi socketów muszą być w stanie akceptować jako argumenty, adresy różnych typów, i tworzyć gniazdka wysyłające dane w odpowiednich protokołach. Aby to umożliwić przyjęto, że adres końca gniazdka (sockaddr) jest zawsze przekazywany jako wskaźnik na obszar Sieci Komputerowe 10
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE pamięci, a w pierwszych dwóch bajtach tego obszaru jest zapisany identyfikator rodziny adresów sieciowych. Odpowiednie identyfikatory są zdefiniowane w sys/socket.h. Można tam znaleźć AF INET (IP wersji 4, używany w Internecie), AF IPX (Novell), AF APPLETALK (stare Macintoshe), AF INET6 (IP wersji 6, przyszłość Internetu) i inne. Aha, AF * == address family. Są również dostępne równoważne im PF * ( protocol family ). Zdefiniowany jest następujący typ: struct sockaddr uint16_t sa_family; ; Wszystkie funkcje systemowe deklarują typy swych argumentów jako (struct sockaddr *) i, jako dodatkowy argument biorą długość rzeczywistej struktury pokazywanej przez ten wskaźnik. Dla każdego protokołu jest zdefiniowana specyficzna wersja struktury. Np. dla AF INET w nagłówku netinetin.h jest dostarczone struct sockaddr_in uint16_t sin_family; uint16_t sin_port; /* port - w sieciowej kolejności bajtów */ struct in_addr sin_addr; /* być może jakieś dodatkowe składowe w roli wypełniacza */ ; struct in_addr uint32_t s_addr; /* adres IP - w sieciowej kolejności bajtów */ ; Wymagana jest sieciowa kolejność (starszy bajt na początku), po to by jądro mogło łatwo przepisać te liczby w odpowiednie miejsca generowanych pakietów. Problem ten polega na tym, że liczbe 4 bajtową, typu int, można przedstawić na dwa sposoby, od najmniej istotnego bajta do najważniejszego(littleendian) oraz na odwrót(big-endian). Różne procesory lubią różne kolejności, a standardowej nie ma, dlatego musimy zamienić na sieciową kolejność. Do zamiany z kolejności procesora na sieciową i z powrotem służą funkcje: htons(), htonl(), ntohs() i ntohl() - ich nazwy łatwo zdekodować, gdy wiemy że h = host, n = network, s = short int (2 bajty), l = long int (4 bajty). Więc w naszym przykładzie, gdybyśmy podawali adres jako liczbę 32 bitową(int), to konieczna byłaby zmiana kolejności bajtów: adres ip to 32 bitowa liczba typu int: serwer_adres.sin_addr.s_addr = htonl(((149 * 256 + 156) * 256 + 65) * 256 + 17); Sieci Komputerowe 11
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE sockaddr in - adres naszego serwera, in od słówka internetowy bzero - ustawia określoną liczbę bajtów na 0, pod podanym adresem inet pton - adres internetowy ipv4 lub v6, podany w formie ciągu znaków przekształca na liczbę 32 bitową (pton-presentation to numeric). Zwraca 1, jeśli ok, 0 jeśli forma prezentacji jest zła, -1 jeśli wystąpił błąd. AF INET rodzina protokołów internetowych wersji 4 SOCK STREAM gniazdko strumieniowe- protokół TCP. connect - łączymy się z serwerem. Nie ustalamy własnego portu. Jądro systemu samo stworzy dla nas port efemeryczny oraz ustawi adres źródłowy dla komunikacji. Ta funkcja także rozpoczyna połączenie internetowe, jeśli używamy protokołu TCP. close - Zamyka gniazdko i uniemożliwia pisanie i czytanie z niego. Jednak jeśli jakieś dane są jeszcze do odczytania lub zapisania, to czeka aż zostaną wysłane lub odebrane, a potem dla połączenia tcp wysyła pakiety standardowo kończące połączenie. 3.3.1 Jak sprawdzić jakie usługi są udostępnione na serwerze Jeśli rozważamy nasz serwer, na który mamy dostęp i możemy się zalogować to najprostszą metodą jest wyświetlenie listy otwartych portów przy pomocy polecenia netstat. UNIX: netstat -plntu p - pid procesu, który otworzył ten port l - porty słuchające n - wersja numeryczna wyświetlania t - tcp u - udp Można połączyć z grepem, aby sprawdzić dla konkretnego portu: netstat -plntu grep 3306 WINDOWS: netstat -aon Można dorzucić: netstat -aon find "3306" wtedy dowiemy się, czy port 3306 jest otwarty Sieci Komputerowe 12
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE Jeśli interesują nas porty na innym komputerze, to musimy skorzystać z narzędzia służącego do skanowania portów (Poszukajcie w internecie, co to jest i jakie pozytywne i negatywne skutki przynosi). Przykładowym skanerem portów jest nmap. nmap -p 1-1024 virgo.ii.uj.edu.pl W wyniku otrzymaliśmy listę otwartych portów na virgo. Możliwości programu nmap są zdecydowanie bardziej rozbudowane, a podstawowe skanowanie, użyte powyżej, nie zawsze zwróci dobry wynik, jednak dla naszych potrzeb wystarczy podstawowa komenda. Chcąc skorzystać z pomocy na temat używanych funkcji, wpisujcie: man 2 nazwa_funkcji Na drugiej stronie manuala znajdują się objaśnienia funkcji w c. Zadanie 1: Waszym zadaniem jest napisanie prostego skanera portów. Programu, którego wynik działania będzie bardzo podobny do tego z nmapa. Wymagania: Sposób uruchomienia programu:./program host port1 port2... portn // przeskanuje wszystkie porty po kolei lub./program host port1-port2 // przeskanuje wszystkie porty od port1 do port2 Wynik: Przeskanowano host: nazwa_hosta, numer_ip numer_portu1 open //dla otwartych portów... numer_portun open Sieci Komputerowe 13
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE Rysunek 6: Komunikacja pomiędzy klientem i serwerem 3.4 Pierwszy serwer Serwer przedstawiający się. Zabawę z serwerami zaczniemy od implementacji prostego serwera TCP. Jego jedynym zadaniem po odebraniu połączenia od klienta będzie wysłanie tekstu zawierającego nasze imię i nazwisko. Po wysłaniu naszych danych serwer natychmiast zakończy połączenie i zacznie czekać na kolejnego klienta. Schemat takiej komunikacji widzicie na rysunku 6. Działanie klienta już znacie. Najpierw tworzy sobie gniazdko, następnie wywołując funkcję connect() łączy się z serwerem (Jest to otwarcie aktywne połączenia). Wtedy system operacyjny przypisuje mu numer portu i wypełnia automatycznie dane adresowe w gniazdku, tak, że serwer wie, jak dane do danego klienta adresować. Następnie wymienia dane z serwerem przy użyciu funkcji read() i write(), a następnie kończy połączenie. Schemat działania serwera jest inny. Po pierwsze nie może on czekać, Sieci Komputerowe 14
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE aż system operacyjny sam nada mu numer portu- jest to oczywiste- adres serwera musi być znany w sieci- abyśmy mogli korzystać z jego usług. Dlatego zaraz po stworzeniu gniazdka funkcją socket(), musi przypisać mu adres i ustalić na której karcie sieciowej będzie nasłuchiwał. Używa do tego celu funkcji bind(). W tym momencie gniazdko jest już gotowe. Następny krok to ustalenie długości kolejki połączeń przychodzących-funkcja listen(). Chodzi o to, że kiedy obsługujemy jednego klienta, wielu innych może się do nas zgłosić. Muszą oni czekać, aż zakończymy komunikację z pierwszym klientem, dopiero wtedy obsłużymy następnego. Od tego momentu serwer czeka na połączenie. Możemy zobaczyć otwarty port w systemie przy użyciu komendy (netstat)(jest to otwarcie bierne połączenia-powstało gniazdko nasłuchujące jednorazowe dla całej pracy serwera). Ale połączenia należy obsłużyć. I tu dochodzi kolejna funkcja accept(). Zawiesza ona proces serwera, do momentu przyjścia połączenia lub jeśli są już jakieś oczekujące w kolejce, to wybiera jedno z nich. I tu, co istotne tworzy dla tego połączenia nowe gniazdko, służące do odbierania i wysyłania komunikatów. Funkcja ta odbiera także adres połączonego klienta i tworzy odpowiednią strukturę adresową do jego przetrzymywania. Po otrzymania gniazda połączonego serwer komunikuje się z klientem za pomocą funkcji read() write() a następnie kończy połączenie i pobiera kolejne połączenie od innego klienta z kolejki połączeń. Na rysunku widzimy prosty schemat komunikacji, oczywiste jest, że to co jest w prostokącie oznaczonym gwiazdką może powtarzać się wielokrotnie, a dane mogą płynąć w obie strony. Kod programu // serwer tcp wysyła Imię i nazwisko #include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <arpa/inet.h> int main(int argc,char* argv[]) int gniazdosluchajace, gniazdopolaczone; struct sockaddr_in serv_addr,clnt_addr; int clntlen; // deskryptory gniazdek Sieci Komputerowe 15
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE #define port 33333 // niech kazdy wybierze sobie inny numer, bo na jednej maszynie // tylko jeden program moze miec otwarty dany port gniazdosluchajace = socket(pf_inet,sock_stream,0); memset(&serv_addr,0,sizeof(serv_addr)); //memset to inny sposob wyzerowania pamieci-znamy juz bzero serv_addr.sin_family = AF_INET; // protokoly internetowe v4 serv_addr.sin_addr.s_addr = htonl(inaddr_any); /*istotne miejsce, INADDR_ANY oznacza, ze serwer bedzie sluchal * na wszystkich interfejsach sieciowych, htonl oczywiscie * ustawia kolejnosc sieciowa bajtow*/ serv_addr.sin_port = htons(port); bind(gniazdosluchajace,(struct sockaddr*)&serv_addr,sizeof(serv_addr)); // standardowa dlugosc kolejki, wynika z historii, dawno temu, gdy // powstawaly systemy unixowe, 5 to byla duza liczba :) listen(gniazdosluchajace, 5); std::cout<<"jestem sobie serwerem Pana Elfa. Moj port to: "<<port<<"\n"; while(1)//nieskonczona petla clntlen = sizeof(clnt_addr); gniazdopolaczone = accept(gniazdosluchajace,(struct sockaddr*)&clnt_addr, (socklen_t*)&clntlen); /*podczas laczenia adres klienta trafia nam pod clnt_addr.sin_addr*/ std::cout<< " Odebrano polaczenie z " ; std::cout<< inet_ntoa(clnt_addr.sin_addr); std::cout<< ":" << ntohs(clnt_addr.sin_port)<<"\n"; /*inet_ntoa jest nalogiczna funkcja do juz poznanej, zmienia adres na ciag znakow, natomiast ntohs zamienia kolejnosc w numerze portu z sieciowej na maszynowa*/ char buf[256]; // tworzymy bufor ::strcpy(buf,"pan Elf"); // uzupełniamy go danymi write(gniazdopolaczone,buf,sizeof(buf)); // wysylamy nasze dane do //gniazdka Sieci Komputerowe 16
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE close(gniazdopolaczone);//zamykamy gniazdo polaczone close(gniazdosluchajace);//zamykamy gniazdo sluchajace // to nieosiagalny fragment kodu, ale nauczymy sie korzystac z sygnalow, //to wejdziemy tez tu ; return 0; Uruchomienie: g++ -o serwer serwer.cpp // opcja -o pozwala nadac nazwe programowi./serwer // najprostrze uruchomienie // zabijamy go ctrl+c./serwer& // uruchamia serwer w tle mamy teraz dostep do konsoli, ale nie mozemy go tak latwo zabic. Potrzebujemy kombinacji komend: jobs // pokazuje liste zadan w tle fg numer_zadania // sprawia, ze zadanie stanie sie aktywne ctrl+c // pozwoli nam juz zabic takie zadanie Zadanie 1 Proszę stworzyć serwer niby prognozę pogody. Wymagania: Klient po uruchomieniu: 1 wyświetla pytanie: Prognoza dla jakiego miasta? 2 po wpisaniu miasta, klient pyta się: Temperatura(a), czy prognoza orientacyjna(b)? 3 po wybraniu odpowiedniej opcji leci zapytanie do serwera 4 po otrzymaniu odpowiedzi wyświetla ją na ekran 5 wyświetla pytanie: Nowe zapytanie (a), czy koniec(b)? 6 jeśli a to wracaj do 1, jeśli b to zakończ połączenie i program. Serwer po odebraniu polaczenia: 1 odbiera pytanie (miasto i typ pogody), jesli temperatura, to losuje temperature i odsyla odpowiedz 2 jesli prognoza, to wylosowuje odpowiedz w stylu: Zachmurzenie umiarkowane, noca burze, lub Bedzie slonecznie ale wietrznie... Komunikacja ma działać wielokrotnie bez zarzutu. Zadanie 2 Proszę stworzyć serwer i klienta. Serwer ma pobierać z dysku duży plik i wysyłać go porcjami do klienta, tak, Sieci Komputerowe 17
Jerzy Czepiel 3 PROGRAMOWANIE SIECIOWE W UNIXIE żeby to trwało długo. Klient ma odbierać ten plik i wypisywać na ekranie ile danych pobrał. Nie macie dużo miejsca, więc może zastępować dane w buforze kolejnymi, musi tylko pamiętać ile pobrał. Najlepiej niech wyświetla procenty. Spróbujcie uruchomić wielu klientów równocześnie. Ten przykład będziemy rozwijać na następnych zajęciach. Sieci Komputerowe 18
Jerzy Czepiel 4 ĆWICZENIA 4 4 Ćwiczenia 4 DNS - nazwy przyjemne dla ludzi Numerki ip wydają się przyjemne, jednak nikt z nas nie wchodzi na 213.180.146.27, żeby poczytać o sporcie, ale na onet.pl. Dlatego teraz zajmiemy się zamianą nazw DNS na adresy ip. Z punktu programistycznego jest to tylko jedna funkcja: #include <netdb.h> struct hostent *h; // struktura, która chwyci adres z dns h = gethostbyname("onet.pl"); // oto nasza sprytna funkcja if (h == NULL) /* coś się nie udało; możesz użyć herror() do wypisania błędu */ memcpy(&adres.sin_addr, h->h_addr, 4); // szybkie przekopiowanie do naszej // struktury adresowej Kopiujemy 4 bajty, bo wiemy, że taka jest długość adresu IP. Długość znalezionego adresu można również znaleźć w polu h h length. Obszar, w którym gethostbyname() zapisuje wyniki, zostanie zamazany przy kolejnym jego wywołaniu - jak najszybciej skopiuj interesujące cię dane w inne miejsce. Co ta funkcja robi i co tak naprawdę się w tedy dzieje? Żeby odpowiedzieć sobie na to pytanie musicie zrozumieć, w jaki sposób działa system DNS oraz komunikacja za pomocą protokołu IP. Jak wiecie, żeby wysłać cokolwiek do innego komputera w sieci internet, potrzebujemy jego adres IP. To jest najważniejszy fragment pakietu, który chcemy wysłać, on decyduje o tym, którędy pakiet popłynie przez sieć i do kogo dotrze. Każdy komputer ma indywidualny jednoznaczny numer IP (jeśli ma IP zewnętrzne) lub IP z sieci lokalnej, wtedy komunikacji z nim, z zewnątrz sieci lokalnej rozpocząć nie możemy (oczywiście można to obejść- przekierowując port na bramie z sieci wewnętrznej do internetu- jeśli chcecie, to mogę wam to dokładniej wyjaśnić na zajęciach). Co się dzieje, jeśli komputer nie zna adresu IP? Włączacie przeglądarkę, wpisujecie onet.pl. Skąd ładuje się treść strony? polecenie: ipconfig /all // zobaczycie tam coś takiego jak primary DNS To, co się kryje pod nazwą primary DNS to adres IP serwera DNS (np: 149.156.67.233). Wasz komputer nie wie, jaki jest IP onetu, jeśli wchodzicie tam pierwszy raz, ale wie, że serwer DNS powinien wiedzieć. Wysyła więc do niego pakiet DNS (enkapsulowany w protokole transportowym UDP) z zapytaniem o IP onetu. Jeśli serwer wie, to wysyła odpowiedź do nas z adresem IP. Jeśli nie wie, wysyła zapytanie dalej do jednego z głównych serwerów DNS... I po chwili wraca do niego odpowiedź, którą przesyła do Sieci Komputerowe 19
Jerzy Czepiel 4 ĆWICZENIA 4 nas. Nasz komputer zapamiętuje ten adres i następne zapytanie do onetu już wie gdzie wysłać. Zadanie 1 Zmodyfikuj nasz skaner portów, aby jako parametr z klawiatury dostawał nazwę dns serwera, którego porty chcemy zbadać. Sprawdź od razu czy serwer www.onet.pl ma otwarty port 80. Jest to domyślny port http, czyli serwera www. Errno Jeżeli podczas wykonywania funkcji systemu Unix (takiej jak prawie każda z funkcji gniazdowych, które używamy w przykładach) wystąpi błąd, to zmienna globalna errno otrzymuje wartość całkowitą dodatnią, wskazującą na rodzaj błędu, a funkcja zazwyczaj zwraca wartość -1. Na przykład jeżeli wartością zmiennej errno jest stała ETIMEDOUT, to komunikat brzmi: Connection timed out (dla połączenia został przekroczony czas oczekiwania). Funkcja nadaje wartość zmiennej errno tylko wtedy, kiedy wystąpił błąd. Jeżeli funkcja nie wykryje błędu, to wartość tej zmiennej nie jest zdefiniowana. Wszystkim dodatnim wartościom zmiennej errno odpowiadają stałe, których nazwy składają się z wielkich liter, zaczynających się od litery E i są zdefiniowane w pliku nagłówkowym sys/errno.h. Żaden z błędów nie ma wartości 0. Przechowywanie wartości errno w jednej zmiennej globalnej nie ma sensu wówczas, gdy wszystkie zmienne globalne są wspólne dla wielu wątków, ponieważ inny wątek może nam nadpisać błąd na inny, zanim go odczytamy. Znajdźcie w googlach errno.h i przyjrzyjcie się przykładowym kodom błędów. Można skorzystać z funkcji perror ( błąd czegośtam ), która oprócz podanego przez nas tekstu, dorzuci na końcu kod błędu na podstawie zmiennej errno. Zadanie 2 Zmodyfikuj program z zadania 1. Każ mu wypisywać kod errno po każdej funkcji gniazdowej i spróbuj połączyć się z portem, który jest zamknięty na virgo. Z nieistniejącym serwerem: nie.ma.mnie.pl. Jakie kody błędów zostały zwrócone? Skorzystaj z perror. Obsługa wielu klientów - podejście pierwsze fork() Sytuacja na rysunku 7. ma pewną wadę. Chodzi o nasz serwer. Przypomnijcie sobie serwer wysyłający plik. Obsługuje on tylko jednego klienta w danym momencie. Jest to zdecydowanie niepraktyczne. Nasz drugi klient musi czekać kilka minut, na obsługę. Taka sytuacja w większości zastosowań jest nieporządana. Współczesne serwery obsługują setki tysięcy połączeń dziennie. Jak to robią? Sieci Komputerowe 20
Jerzy Czepiel 4 ĆWICZENIA 4 Rysunek 7: Komunikacja pomiędzy klientem i serwerem Sieci Komputerowe 21
Jerzy Czepiel 4 ĆWICZENIA 4 Dziś poznamy rozwiązanie pierwsze. Stworzenie procesu potomnego dla każdego połączenia przychodzącego. Schemat jest taki: 1. Proces serwera A odbiera połączenie 2. Wywołuje fork(). Tworzona jest dokładna kopia procesu A, która ma dostęp do tych samych otwartych plików (deskryptorów). 3. Rodzic natychmiast wywołuje funkcje close()na deskryptorze połączonym z klientem. Zaraz sobie to dokładnie omówimy, bo zagadnienie wchodzi w obsługę plików w linuxie oraz w subtelne działanie funkcji close(). 4. dziecko prowadzi dialog z klientem, po zamknięciu połączenia uśmierca się funkcją exit(). W praktyce wygląda to tak: #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include <errno.h> #include <signal.h> #include <sys/wait.h> int main(int argc,char* argv[]) #define port 33333 signal(sigchld,sig_ign); // ustawiamy nieodbieranie sygnałów od dzieci int gniazdosluchajace; int gniazdopolaczone; int blad; struct sockaddr_in serv_addr,s_addr; gniazdosluchajace = socket(pf_inet,sock_stream,0); if(gniazdosluchajace <=0) Sieci Komputerowe 22
Jerzy Czepiel 4 ĆWICZENIA 4 perror("nie udalo sie otworzyc gniazda "); exit(1); memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(inaddr_any); serv_addr.sin_port = htons(port); if( bind(gniazdosluchajace,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) ==-1 ) perror("blad"); exit(1); if(listen(gniazdosluchajace,5) == -1) perror("blad nasluchu "); exit(1); std::cout<<"jestem sobie serwerem. Czekam na porcie"<<port<<"\n"; while(1) socklen_t s_len = sizeof(s_addr); gniazdopolaczone = accept(gniazdosluchajace,(struct sockaddr*)&s_addr,&s_len); if(gniazdopolaczone<=0) perror("blad laczenia z klientem"); exit(1); int pid = fork(); //kopiujemy proces if(pid == 0) close(gniazdosluchajace); // *** // jesteśmy w dziecku char *buf = "Witaj, jestem serwerem pana Jerzego"; std::cout<<"jestem dzieckiem "; write(gniazdopolaczone,buf,sizeof(buf)); std::cout<<"nacisnij dowolny klawisz..."; int asa; Sieci Komputerowe 23
Jerzy Czepiel 4 ĆWICZENIA 4 std::cin>>asa; close(gniazdopolaczone); //*** exit(0); // koniec dziecka else if (pid<0) perror("blad funkcji fork"); close(gniazdopolaczone);//*** ; close(gniazdosluchajace); //*** return 0; Zwróć uwagę na linie oznaczone gwiazdkami. Zauważ, że zarówno w dziecku, jak i w przodku zamykamy gniazda. W przodku połączone, w dziecku słuchające. Dlaczego połączenie się nie zrywa? Wynika to z systemu plików u Unixie. Tworząc kopie procesu kopiuje także deskryptory, tak więc tak naprawdę tworzy nam drugie dowiązanie do naszych gniazdek. I tu korzystamy ze specyfiki funkcji close(gniazdo). Ona kończy połączenie wysyłając odpowiednie pakiety dopiero wtedy, kiedy jest wywoływana na ostatnim dowiązaniu. Jeśli dowiązań jest więcej, to ona tylko zmniejsza ich liczbę o 1. Dlatego musimy wywołać ją raz w dziecku raz w rodzicu na każdym gnieździe. Tak mamy gwarancję, że zamkniemy prawidłowo wszystkie połączenia. fork() jest funkcją dosyć kosztowną, dlatego procesy potomne warto tworzyć tylko wtedy, gdy wiemy że obsługa danego klienta zajmie dużo czasu. Jeśli jedyną rzeczą którą mamy zrobić jest obliczenie sumy dwóch liczb, fork() byłby zdecydowanie nieopłacalny. Uwaga: poprzednie zdanie jest prawdziwe pod warunkiem że te dwie liczby są już dostępne; jeśli dopiero mają zostać odczytane z sieci, sforkowanie dziecka jest jak najbardziej uzasadnione (nie wiemy jak długo trzeba będzie czekać na przyjście danych). Drobne problemy W środowisku uniksowym proces-rodzic może uzyskać dostęp do informacji o sposobie zakończenia się procesu-dziecka, w szczególności o zwróconym kodzie powrotu. Z tego powodu jądro nie od razu usuwa z tablicy procesów wpisy odpowiadające dopiero co zakończonym procesom. Pozostają one w tablicy jako tzw. zombi do momentu odczytania ich statusu. Aby nie dopuścić do zapchania tablicy procesów przez hordę zombich, rodzic musi regularnie odczytywać informację o stanie swych dzieci. Zasadniczo służy do tego funkcja wait(), waitpid()(bo można jej podać flagę Sieci Komputerowe 24
Jerzy Czepiel 4 ĆWICZENIA 4 WNOHANG). ( sys/wait.h - Funkcja waitpid zawiesza wykonywanie bieżącego procesu dopóki potomek określony przez pid nie zakończy działania lub dopóki nie zostanie dostarczony sygnał, którego akcją jest zakończenie procesu lub wywołanie funkcji obsługującej sygnały. Jeśli potomek, podany jako pid, zakończył swoje działanie przed wywołaniem tej funkcji (tak zwany proces zombie ), funkcja kończy się natychmiast. WNOHANG oznacza natychmiastowy powrót z funkcji, jeśli potomek nie zakończył pracy.) Innym sposobem na radzenie sobie z zombi jest signal(sigchld, SIG IGN) wstawione gdzieś na początku main(). W większości odmian Uniksa jądro, widząc że nie jesteśmy zainteresowani sygnałami informującymi o zakończeniu pracy dziecka, nie będzie tworzyło zombich. Skorzystajmy z tego rozwiązania. (Mechanizm przekazywania sygnałów między procesami omówimy sobie na późniejszych ćwiczeniach). Synchronizacja Jeśli mamy wiele procesów, a korzystają z tych samych danych, np: obsługujemy konta bankowe i przelewy, to musimy zadbać o synchronizację dostępu do tych danych. Najprostszy mechanizm synchronizacyjny można stworzyć używając specjalnego pliku na dysku jako znacznika blokady. Plik istnieje - ktoś inny właśnie korzysta ze wspólnych danych, pliku brak - możemy go stworzyć i zacząć wprowadzać własne modyfikacje. Aby ten mechanizm działał poprawnie, testowanie i zakładanie blokady muszą być pojedynczą, atomową operacją. Daje się to uzyskać dzięki funkcji open() i jej flagom O CREAT O EXCL. Tradycja nakazuje, aby do pliku blokady zapisać PID procesu, który tę blokadę założył i jest jej właścicielem. Zadanie 3 (a)zmodyfikuj serwer wysyłający plik, tak, żeby potrafił obsługiwać wielu klientów równocześnie. Przetestuj uruchamiając 3 klientów równocześnie. (b)dodaj do programu plik dziennika(serwer.log). W którym każde dziecko przed rozpoczęciem wysyłania pliku wstawi linie: "Rozpoczynam wysylanie pliku do klienta o adresie:xxx.xxx.xxx.xxx:xxxxx" - którego odczyta z połączenia, a po zakończeniu wstawi linie: "Zakonczylem wysylanie danych do klienta :xxx.xxx.xxx.xxx:xxxxx". Jak widzicie, konieczne będzie wykorzystanie synchronizacji przy pomocy dodatkowego pliku. Sieci Komputerowe 25
Jerzy Czepiel 5 ĆWICZENIA 5 Rysunek 8: Działanie serwera współbieżnego opartego na funkcji fork 5 Ćwiczenia 5 Serwer współbieżny oparty na funkcji fork()- Dlaczego to działa? Skąd to pytanie w tytule? No jeśli zastanawialiście się chociaż przez chwilę jak działa program z poprzednich zajęć to musieliście je sobie zadać. Wiemy, że numer portu w połączeniu IP jest tak naprawdę identyfikatorem procesu sieciowego działającego na komputerze. Pewne porty są zarezerwowane dla ogólnie znanych usług jak na przykład www. Jeśli przychodzi do nas pakiet, zaadresowany na jakiś port np: 33333, to oprogramowanie protokołów tcp/ip przekazuje go do procesu, który odpowiada za ten port, otworzył go. Jeśli więc do naszego serwera podłączy się 10 klientów i 10 razy wywołamy funkcję fork, to na naszym komputerze będziemy mieli 11 proce- Sieci Komputerowe 26
Jerzy Czepiel 5 ĆWICZENIA 5 sów (1 gniazdo słuchające, 10 gniazd połączonych), które będą używać nasz port 33333. Jak to się stanie, że właściwe dane od jakiegoś klienta trafią do procesu, który tak naprawdę go obsługuje? Odpowiedź jest prosta. Każde gniazdo jest tak naprawdę parą gniazdową. Czwórką, która zawiera nasz adres ip oraz nasz port, a także adres ip oraz port drugiego połączonego komputera. Tak zwane dwa końce połączenia TCP. Widzicie to dokładnie na obrazku 8. Rysunek (a) pokazuje przyjście połączenia od klienta1. Wywołanie funkcji connect powoduje utworzenie na komputerze klienckim numeru portu tymczasowego 2776. Ten numer portu oraz adres ip klienta 1 jest wstawiany do pola adres nadawcy w pakiecie tcp inicjującym połączenie wysyłanym do serwera, umieszczony zostaje tam także port nadawcy. Serwer pozwala na połączenie (rysunek (b)) z klientem, wysyłając pakiet SYN/ACK, a następnie uzupełnia dane gniazda połączonego (tego zwracanego w funkcji accept()). Ustawia tam własny adres ip oraz port, a także dane klienta 1. To gniazdo po wywołaniu funkcji fork() trafia nam do procesu potomnego Serwer dziecko(a). W momencie zgłoszenia się drugiego klienta tworzone jest drugie gniazdo połączone, które zawiera adres serwera oraz dane klienta 2. Sytuację widać na rysunku (c). Gniazdo TCP to gniazdo stumieniowe Zauważcie, że dla programu, który piszecie gniazdo tcp jest strumieniem. Wysyłając dane do gniazda tak naprawdę nie wiecie ile pakietów oprogramowanie wyśle przez sieć. Nie trzeba się tym w ogóle przejmować. Oprogramowanie niższych warstw robi to za nas. My możemy wysyłać dane bez końca :). Pamiętajcie również, że strumień wychodzący od komputera a do b przez sieć i strumień w drugą stronę są całkowicie oddzielne. Mimo, że funkcje read i write wywołujecie na tym samym deskryptorze gniazda, to jednak nigdy nie zdarzy się, że przeczytacie wysyłane przez siebie dane, lub nadpiszecie coś w strumieniu, co miało przyjść do was. Obsługa wielu klientów - podejście drugie: Wątki Jak zapewne wiecie wątki to fragmenty kodu, wykonujące się równolegle i niezależnie w obrębie danego procesu. Mają wspólne dane wraz z całym procesem. Idea jest taka, żeby po nadejściu połączenia odpalić nowy wątek, który zajmie się jego obsługą. Aby zaimplementować serwer oparty na wątkach, musimy zacząć od zapoznania się z programowaniem na wątkach w systemie UNIX. Skorzystamy z pliku pthread.h, który zawiera definicje funkcji pozwalających na uruchomienie wątków zgodnych ze standardem POSIX. W praktyce wygląda to tak: #include <stdlib.h> Sieci Komputerowe 27
Jerzy Czepiel 5 ĆWICZENIA 5 #include <stdio.h> #include <unistd.h> #include <pthread.h> #include <malloc.h> char* sekcja = "sekcja krytyczna, watek :"; pthread_mutex_t sem1 = PTHREAD_MUTEX_INITIALIZER; // definicja semafora // struktura trzymająca parametry dla wątku struct parametry ; char literka; int licznik; // każdy wątek wykonuje ten kod void *funkcja(void* para) struct parametry* p=(struct parametry*)para; for( int i=0; i<p->licznik ; i++) pthread_mutex_lock(&sem1);/***/ printf("%s %c \n",sekcja,p->literka); //Tutaj jest miejsce, w którym wątki mogą operować //na współdzielonych danych, obiektach, deskryptorach //oddzielone semaforem sleep(p->licznik-3); pthread_mutex_unlock(&sem1);/***/ ; return NULL; int main (int argc,char* argv[]) pthread_t w1; pthread_t w2; parametry p1; //parametry dla wątków Sieci Komputerowe 28
Jerzy Czepiel 5 ĆWICZENIA 5 parametry p2; p1.literka = a ; p2.literka = b ; p1.licznik =4; p2.licznik = 5; // tworzenie wątków pthread_create(&w1,null,&funkcja,&p1); pthread_create(&w2,null,&funkcja,&p2); pthread_join(w1,null); // czekamy tutaj na zakończenie obu wątków pthread_join(w2,null); ; return 0; Kompilacja: g++ -lpthread watki.cpp Utworzenie wątku jak widzicie jest bardzo proste, wystarczy tylko wywołać funkcję pthread create. Przyjmuje ona jako parametr wskaźnik do funkcji. Funkcja, której adres tam wstawimy, będzie funkcją wykonywaną przez wątek. Wątek wykona tą funkcję i zginie. Dla funkcji trzeba jeszcze przekazać parametry, służy do tego czwarty argument funkcji pthread create(). funkcja pthread join() zawiesza wątek w którym jest wywołana, aż do momentu zakończenia wątku podanego jako argument. Jakbyśmy jej nie wstawili do naszego programu, to zakończyłby się po wypisaniu dwóch pierwszych linii, ponieważ zakończenie funkcji main jest zkończeniem działania całego procesu. Synchronizacja Problem rozwiązujemy korzystając z semaforów(linie oznaczone gwiazdkami /***/). Oczywiście rozwiązanie z plikiem także będzie skuteczne. Komunikacja Komunikacja między działającymi wątkami jest prosta, można korzystać z dowolnej zmiennej globalnej, ponieważ wątki mają dostęp do zmiennych procesu. Gorzej było przy użyciu funkcji fork(). Aby komunikować się między procesami musimy skorzystać z bardziej skomplikowanych mechanizmów, Sieci Komputerowe 29
Jerzy Czepiel 5 ĆWICZENIA 5 jak pamięć dzielona, potoki, pliki na dysku, czy sygnały. Odwołuję was do literatury dotyczącej programowania w UNIXIE oraz internetu. Serwer współbieżny przedstawiający się, oparty na wątkach #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <pthread.h> #include <malloc.h> #include <fcntl.h> #include <netinet/in.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <arpa/inet.h> #include <iostream> #include <errno.h> #include <sys/wait.h> struct parametry ; int numer; int gniazdko; void *funkcja_wysylajace_imie_przez_siec(void* para) struct parametry* p = (struct parametry*)para; // jestesmy w dziecku char *buf = "Witaj, jestem serwerem pana Jerzego"; std::cout<<"jestem watkiem :"<<p->numer<<"\n"; write(p->gniazdko,buf,sizeof(buf)); std::cout<<"podaj liczbe i nacisnij enter..."; int asa; std::cin>>asa; close(p->gniazdko); ; int main (int argc,char* argv[]) #define port 33333 Sieci Komputerowe 30
Jerzy Czepiel 5 ĆWICZENIA 5 int gniazdosluchajace; int gniazdopolaczone; int blad; struct sockaddr_in serv_addr,s_addr; gniazdosluchajace = socket(pf_inet,sock_stream,0); if(gniazdosluchajace <=0) perror("nie udalo sie otworzyc gniazda "); exit(1); memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(inaddr_any); serv_addr.sin_port = htons(port); if( bind(gniazdosluchajace,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) ==-1 ) perror("blad"); exit(1); if(listen(gniazdosluchajace,5) == -1) perror("blad nasluchu "); exit(1); std::cout<<"jestem sobie serwerem. Czekam na porcie"<<port<<"\n"; int numer_watku=1; while(1) socklen_t s_len = sizeof(s_addr); gniazdopolaczone = accept(gniazdosluchajace,(struct sockaddr*)&s_addr,&s_len); if(gniazdopolaczone<=0) perror("blad laczenia z klientem"); exit(1); Sieci Komputerowe 31
Jerzy Czepiel 5 ĆWICZENIA 5 pthread_t w; // tworzymy watek parametry p; p.numer= numer_watku++; p.gniazdko = gniazdopolaczone; pthread_create(&w,null,&funkcja_wysylajace_imie_przez_siec,&p); close(gniazdosluchajace); return 0; ; Jak widzicie, zmiana w stosunku do wersji serwera opartej o fork() polega na tym, że w pętli while() po odebraniu połączenia odpalamy nowy wątek, któremu jako parametr przekazujemy deskryptor do gniazdka i zlecamy mu całą obsługę połączenia. A proces główny rozpoczyna kolejną itrację funkcji while i wywołując accept blokuje się do przyjścia kolejnego połączenia. Zadanie 1 Zmodyfikuj serwer współbieżny wysyłający duży plik tak, aby działał na wątkach. Przetestuj. Dziś zadanko proste, za to następnym razem :) Sieci Komputerowe 32
Jerzy Czepiel 6 ĆWICZENIA 6 6 Ćwiczenia 6 TCP a UDP o co w tym chodzi? Czekałem z tym do tego momentu, ponieważ zrozumienie sposobu w jaki krążą pakiety w internecie wymaga podstawowej wiedzy o działaniu sieci. Czemu w sieci wysyłamy pakiety? (Aby nikogo nie zagłodzić, aby optymalnie korzystać z łącza, aby sprawdzać poprawność danych) Sieć Ethernet jako przykład sieci fizycznej. Wysyłanie bitów. Po co preambuła? Wielkość ramki? CSMA/CD? MAC adres? Komu nasz komputer może wysłać pakiet? Co to jest ARP? Topologia sieci? Switch, Hub, Most, Wzmacniak(na jakiej warstwie działają i co robią?)? Działanie innych typów sieci jest analogiczne? Ale jak przesyłać pakiety między sieciami? W jaki sposób podłączyć wiele sieci w całość? Jak zapewnić jednolite usługi - wprowadzając kombinację sprzętu i oprogramowania - oprogramowanie TCP/IP. Enkapsulacja pakietu IP w ramce Ethernetowej? Fragmentacja pakietów? Powstała sieć wirtualna(adresy ip nie są sprzętowe, lecz wirtualne). Routery - komputery łączące podsieci w jedną wielką sieć. Tablice routingu? Wyznaczanie tras? DNS? ICMP? Znając wymienione wyżej pojęcia i mechanizmy komunikacji w sieci, możemy zająć się opisem protokołów transportowych internetu. Istnieją dwa. Podstawowy protokół UDP oraz rozbudowany protokół połączeniowy TCP/IP. UDP Protokół UDP jest prostym protokołem warstwy transportowej. Nasz program odsyła dane do gniazda UDP. Po drodze tworzony jest datagram UDP, który następnie zostaje enkapsulowany w pakiecie IP. Następnie jest on wysyłany do odbiorcy. Jednak nie mamy żadnej pewności, że datagram UDP dotrze do końcowego odbiorcy. (Sprawdźcie sobie wygląd nagłówka UDP). UDP wykorzystuje się wtedy, gdy dopuszczalne jest gubienie co któregoś pakietu lub gdy nie możemy sobie pozwolić na narzuty związane z TCP. UDP jest po prostu szybszy oraz umożliwia wysyłanie pakietów rozgłoszeniowych (na adres rozgłoszeniowy). Pisząc oprogramowanie korzystające z UDP musimy uporać się z jego zawodnością. Jeżeli chcemy mieć pewność, że datagram zostanie odebrany przez adresata, musimy sami uzupełnić nasz program we właściwe mechanizmy, które o to zadbają(ponawianie transmisji, wysyłanie potwierdzeń). Każdy datagram UDP ma pewną długość i możemy go traktować tak jak rekord. Jeżeli datagram osiąga poprawnie swój ostateczny punkt docelowy (tzn. pakiet przybywa bez błędu sumy kontrolnej), to długość datagramu jest Sieci Komputerowe 33
Jerzy Czepiel 6 ĆWICZENIA 6 Rysunek 9: Wysyłanie pakietów przez połączenie TCP przekazywana do odbierającego go programu użytkowego. Protokół TCP jest strumieniowy-nie dzieli danych na rekordy. Protokół UDP jest bezpołączeniowy. Nie wymaga istnienia żadnego połączenia. Klient UDP może utworzyć gniazdo i wysłać datagram do jakiegoś serwera, po czym może natychmiast przez to samo gniazdo wysłać kolejne datagramy do różnych innych serwerów. Podobnie serwer przez jedno gniazdo może przyjmować datagramy od różnych klientów. TCP Oprogramowanie TCP świadczy programowi użytkowemu różne usługi w stosunku do oprogramowania UDP. Najistotniejszą różnicą jest to, że TCP tworzy połączenia między klientami a serwerami. Klient otwiera połączenie, przesyła i odbiera dane, po czym kończy połączenie. Drugą różnicą jest to, że oprogramowanie TCP zapewnia niezawodne przesyłanie danych. Wysyłając dane, oprogramowanie czeka na przyjście potwierdzenia. Jeśli takie nie przyjdzie po pewnym oszacowanym czasie powrotu, to wtedy transmisja pakietu jest ponawiana. Trzecia różnica to kolejność danych. Oprogramowanie TCP numeruje kolejne porcje danych, dzięki temu odbiorca jest w stanie odtworzyć kolejność pakietów i na tej podstawie odebrać dane w prawidłowej kolejności(istotne- Sieci Komputerowe 34
Jerzy Czepiel 6 ĆWICZENIA 6 jak przesyłamy plik, to odbiorca musi odebrać jego bajty w tej samej kolejności). Mechanizm ten wygląda tak: wysyłamy do gniazda TCP dużą porcję danych. Oprogramowanie dzieli je na segmenty, które następnie opakowuje w pakiecie TCP, zaznaczając numer segmentu, a następnie enkapsuluje wszystko w pakietach IP i wysyla przez sieć. Odbiorca, odbiera pakiety IP, wyciągając kolejne segmenty i ustalając je we właściwej kolejności. Jeśli odebrał pakiet, to wysyła potwierdzenie, jeśli nie, to czeka na retransmisję. Po odebraniu segmentów we właściwej kolejności przekazuje dane do warstwy aplikacji- oprogramowania serwera. (Może także dojść do zdublowania danych-serwer wysyła dane do klienta, sieć jest przeciążona i dane płyną długo. Serwer nie dostał potwierdzenia przez szacowany czas i ponawia wysłanie danych- tymczasem dane dochodza do klienta-wysyła potwierdzenie, po czym odbiera drugi pakiet z tymi samymi danymi. Oprogramowanie TCP również z tym sobie radzi i kasuje zdublowane dane) UDP nie zapewnia odpowiedniej kolejności. Najpierw może przyjść datagram wysłany później. Programy przez nas pisane muszą same sobie z tym radzić. Czwarta różnica to sterownie przepływem (flow control). Oprogramowanie TCP informuje partnera o tym, ile danych chce przyjąć. Nazywa się to oknem oferowanym. Rozmiar okna zawsze jest równy ilości wolnego miejsca w buforze odbiorcy. Chodzi o to, aby nadawca nie przepełnił bufora. Im szybciej dane napływają, tym bardziej bufor się zapełnia. Może dojść do tego, że okno równe jest 0. Oznacza to, że trzeba poczekać, aż program użytkowy u partnera pobierze dane z bufora, zanim będziemy mogli znów do niego wysłać pakiet. UDP nie zajmuje się sterowaniem przepływem. Jeśli program klienta bardzo szybko wysyła dane, to może się zdarzyć, że serwer nie będzie nadążał z ich odbieraniem i dane będą tracone. Piąta różnica. Połączenie TCP jest w pełni dwukierunkowe. Przez jedno połączenie program może wysyłać i odbierać dane. UDP też może być dwukierunkowe, ale tam nie ma połączeń :). Teraz wreszcie możemy zrozumieć w jaki sposób działają nasze funkcje: connect(), accept() oraz close() czyli funkcje typowe dla połączenia TCP. Potrójny hand shake Ustanawianie połączenia TCP: 1. Serwer otwiera połączenie biernie (wywołje socket(),bind() oraz listen()) - żadnego pakietu nigdzie nie wysyła. Zatrzymuje swoje działanie na wywołaniu funkcji accept() i czeka. 2. Klient rozpoczyna otwarcie aktywne -wywołujemy funkcję connect()- ona powoduje, że oprogramowanie TCP/IP wysyła do serwera pakiet TCP, zawierający ustawioną flagę SYN(synchronize) oraz numer kolejny danych, które klient będzie przesyłać. Sieci Komputerowe 35