serwer - Instytut Informatyki Teoretycznej i Stosowanej
Transkrypt
serwer - Instytut Informatyki Teoretycznej i Stosowanej
Architektura typu klient – serwer: przesyłanie pliku tekstowo oraz logowania do serwera za pomocą szyfrowanego hasła Wydział Inżynierii Mechanicznej i Informatyki Instytut Informatyki Teoretycznej i Stosowanej dr inż. Łukasz Szustak Komunikacja bezpołączeniowa (SOCK_DGRAM) ● Główna koncepcja komunikacji bezpołączeniowej Komunikacja bezpołączeniowa (SOCK_DGRAM) ● ● ● Gniazda typu SOCK_DGRAM – komunikacja w modelu bezpołączeniowym korzystającą z tzw. datagramów Datagram – blok danych pakietowych przesyłany przez sieć między komputerami, zawierający wszelkie niezbędne informacje do przesłania danych z hosta źródłowego do hosta docelowego, bez konieczności wcześniejszej wymiany informacji przez te hosty Datagram jest podstawową jednostką przesyłania danych w sieciach pakietowych, w których czas i kolejność dostarczenia kolejnych jednostek danych nie są gwarantowane Komunikacja bezpołączeniowa (SOCK_DGRAM) ● ● ● Gniazdo może zostać opisane przy pomocy domeny adresowej, w której wyróżniamy m.in. domenę internetową PF_INET W przypadku komunikacji bezpołączeniowej oraz domeny internetowej komunikacji odbywać się będzie w oparciu o protokół UDP (User Datagram Protocol) W dalszej części prezentacji zostanie przedstawiony przykład pary programów: klient oraz serwer, których zadaniem będzie przesłanie plików tekstowych z wykorzystaniem gniazd w modelu komunikacji bezpołączeniowej w domenie internetowej PF_INET Serwer #define _XOPEN_SOURCE #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <netdb.h> #include <cstdlib> #include <cstring> #include <string> #include <iostream> #include <fcntl.h> #define SRVPORT 1111 /* numer portu serwera */ #define CLIPORT 31337 /* numer portu gniazda klienta */ #define SRVADDR "localhost" /* adres serwera */ #define PASSWD "alaKota" /* haslo wymagane do polaczenia z serwerem */ #define CSALT "Zz" /* dwuznakowy tzw. salt dla funkcji crypt() */ #define PLIK "/proc/cpuinfo" /* plik, który wyślemy klientowi */ Serwer ● Otwieramy lokalne gniazdo (PF_INET, SOCK_DGRAM) int main (void) { int sd, fd, ret; /* deskryptor gniazda, deskryptor pliku, pomocnicza */ struct sockaddr_in saddr, caddr; /* adres lokalnego gniazda, adres zdalnego gniazda */ char buf[1024], /* bufor */ hash[20]; /* bufor na zahashowane hasło */ socklen_t len; sd = socket (PF_INET, SOCK_DGRAM, 0); if (sd < 0) { perror ("socket()"); exit (1); } ● Następnie przypisujemy mu adres aby zdalni klienci byli w stanie wysyłać do serwera datagramy (gniazdo musi mieć przypisany adres aby możliwa była komunikacja z nim) Serwer saddr.sin_family = PF_INET; saddr.sin_port = htons (SRVPORT); saddr.sin_addr.s_addr = INADDR_ANY; if (bind (sd, (struct sockaddr *) &saddr, sizeof (saddr)) < 0) { perror ("bind()"); exit (1); } ● Następnie jest obliczany tzw. one way hash na podstawie hasła (PASSWD) i tzw. salt. Salt jest prawie dowolnym dwuliterowym ciągiem znaków ([a-z, A-Z, 0-9]) i jest stosowane przez algorytm liczenia funkcji hashującej sprintf (hash, "%s", crypt (PASSWD, CSALT)); printf ("Czekam na datagramy ...\n"); Serwer ● W dalszej kolejności jest pętla, w której są obsługiwane nadchodzące żądania od klientów while (true) { bzero (buf, sizeof (buf)); len = sizeof (caddr); recvfrom (sd, buf, sizeof (buf), 0, (struct sockaddr *) &caddr, &len); ● ● Wywołanie recvfrom() blokuje do czasu, aż nadejdą jakieś dane do odczytania Kiedy tak się stanie struktura wskazywana przez *from zostanie wypełniona adresem gniazda, z którego te dane nadeszły Serwer ● ● ● ● Poniższy fragment kodu sprawdza, czy klient jest upoważniony do komunikacji z serwerem Procedura autoryzacji klienta oparta jest na dwóch elementach: haśle i porcie źródłowym (porcie, z którego nadaje klient) Serwer oczekuje od klienta datagramu zawierającego ciąg (hash) utworzony na maszynie klienta w ten sam sposób, co na serwerze Jeśli wyniki działania funkcji hashujących na serwerze i na kliencie pokrywają się oraz jeśli port źródłowy zgadza się z wcześniej ustalonym po obu stronach to serwer wyśle w stronę klienta datagram zawierający odpowiedź 'OK' printf ("Datagram od %s:%i ... ", inet_ntoa (caddr.sin_addr), ntohs (caddr.sin_port)); if (!strncmp (buf, hash, strlen (hash)) && ntohs (caddr.sin_port) == CLIPORT) { printf ("Przyjęty.\n"); sendto (sd, "OK\n", 3, 0, (struct sockaddr *) &caddr, sizeof (caddr)); Serwer ● ● W dalszej części programy otwierany jest wcześniej zdefiniowany plik i po kawałku przesyłamy go do klienta Kiedy cały plik zostanie przesłany, wysyłany jest klientowi ciąg znaków 'END' aby poinformować, że transmisja się powiodła fd = open(PLIK, O_RDONLY); if (fd < 1) { perror ("open()"); sendto (sd, "Błąd: open()\n", 13, 0, (struct sockaddr *) &caddr, sizeof (caddr)); } bzero (buf, sizeof (buf)); while (read (fd, buf, sizeof (buf)) > 0) { sendto (sd, buf, strlen (buf), 0, (struct sockaddr *) &caddr, sizeof (caddr)); bzero (buf, sizeof (buf)); } close (fd); sendto (sd, "END\n", 4, 0, (struct sockaddr *) &caddr, sizeof (caddr)); } Serwer ● ● Blok „else” wykonuje się jeśli któryś z elementów autoryzacji (hasło, port źródłowy) jest niepoprawny W tym miejscu również się kończy główna pętla while oraz cały kod serwera else { printf ("Odrzucony.\n"); sendto (sd, "Błąd: złe hasło albo port źródłowy\n", 4, 0, (struct sockaddr *) &caddr, sizeof (caddr)); } } return 0; } Klient ● Kod klienta niewiele różni się od kodu serwera #define _XOPEN_SOURCE #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <netdb.h> #include <cstdlib> #include <cstring> #include <string> #include <iostream> #include <fcntl.h> #define SRVPORT 1111 #define CLIPORT 31337 #define SRVADDR "localhost" #define PASSWD "alaKota" #define CSALT "Zz" /* numer portu serwera */ /* numer portu gniazda klienta */ /* adres serwera */ /* haslo wymagane do polaczenia z serwerem */ /* dwuznakowy tzw. salt dla funkcji crypt() */ Klient ● ● ● W pierwszej kolejności tworzone jest lokalne gniazda Drugim krokiem w tym przypadku jest ręczne przypisanie adresu do lokalnego gniazda Tą czynność można pozostawić systemowi, jednakże w przedstawianym przypadku ze względu na autoryzację należy numer portu musi zostać dobrany we właściwy sposób, ponieważ serwer nie zgodzi się na sesję jeśli nie będziemy wysyłali datagramów ze ściśle określonego gniazda Klient int main () { int sd; struct sockaddr_in saddr, caddr; struct hostent *sent; /* struktura opisująca host-serwer */ char buf[1024]; /* bufor */ socklen_t len; sd = socket (PF_INET, SOCK_DGRAM, 0); if (sd < 0) { perror ("socket()"); exit(1); } caddr.sin_family = PF_INET; caddr.sin_port = htons (CLIPORT); caddr.sin_addr.s_addr = INADDR_ANY; if (bind (sd, (struct sockaddr *) &caddr, sizeof (caddr)) < 0) { perror ("bind()"); exit(1); } Klient ● Kolejnym krokiem jest wypełnienie struktury sockaddr gniazda zdalnego printf ("Szukam adresu IP serwera %s ...\n", SRVADDR); sent = gethostbyname (SRVADDR); if (!sent) { herror ("gethostbyname()"); exit (1); } saddr.sin_family = PF_INET; saddr.sin_port = htons (SRVPORT); bcopy (sent->h_addr, (char *) &saddr.sin_addr, sent->h_length); ● Do bufora jest kopiowany wynik działania funkcji hashującej bzero (buf, sizeof (buf)); sprintf (buf, "%s", crypt (PASSWD, CSALT)); Klient ● Do serwera jest wysyłane zakodowane hasło, a następnie klient oczekuje na przyzwolenie do dalszej komunikacji printf ("Wysyłam hasło do %s:%i ...\n", inet_ntoa(saddr.sin_addr), SRVPORT); sendto (sd, buf, strlen (buf), 0, (struct sockaddr *) &saddr, sizeof (saddr)); printf ("Czekam na odpowiedź ...\n"); do { bzero (buf, sizeof(buf)); len = sizeof(caddr); recvfrom (sd, buf, sizeof(buf), 0, (struct sockaddr *) &caddr, &len); }while (caddr.sin_addr.s_addr != saddr.sin_addr.s_addr); ● ● ● Należy tutaj zwrócić szczególną uwagę na fakt, że istnieje możliwość przyjścia na nasz adres jakiegoś przypadkowego datagramu nie związanego zupełnie z sesją między nami a serwerem W pętli do-while są odbierane kolejne datagramy, które przychodzą na adres klienta W sytuacji gdy otrzymany datagram pochodzi od serwera, kod programy pozwoli na kontynuację programu Klient ● W dalszej części programu weryfikowane jest czy odebrana informacja jest to odpowiedź na wysłany wcześniej przez program klienta datagram z hasłem (ciąg 'OK') if( strncmp(buf, "OK", 2) ) { fprintf (stderr, "Nieprawidlowe haslo albo zly port zrodlowy. Koncze ...\n"); exit(1); } else printf ("Polaczenie przyjete.\n\n"); ● Ostatnim fragmentem programu jest pętla, która ponownie przegląda wszystkie nadchodzące datagramy, sprawdza, czy pochodzą one od serwera, a następnie podejmuje jedno z trzech działań: – jeśli datagram zawiera ciąg 'END' to znaczy, że serwer zakończył już transmisję pliku – jeśli zawiera ciąg 'Błąd:' to wyświetlany jest komunikat błędu oraz kończymy program Klient – ostatnia możliwość jest nadejście datagramu zawierającego część przesyłanego pliku: w tym przypadku jest wyświetlana cała zawartość datagramu na standardowym wyjściu bzero (buf, sizeof (buf)); len = sizeof (caddr); while (recvfrom (sd, buf, sizeof (buf), 0, (struct sockaddr *) &caddr, &len) > 0) { if (caddr.sin_addr.s_addr == saddr.sin_addr.s_addr) { if (!strncmp (buf, "END", 3)) { break; } else if (!strncmp (buf, "Błąd:", 5)) { printf ("%s", buf); break; } else printf ("%s", buf); } bzero (buf, sizeof (buf)); len = sizeof (buf); } return 0; } Podsumowanie ● ● ● ● W omawianym przypadku wystarczy jedno gniazdo aby obsłużyć wielu klientów W bardziej skomplikowanych przypadkach uzasadnione może stać się przydzielenie każdemu klientowi osobnego procesu Główną wadą omawianego przykładu jest brak gwarancji, że dane dotrą w niezmienionej postaci do klienta oraz, że w ogóle dotrą Wadę tej można uniknąć stosując komunikację w modelu połączeniowym z użyciem pakietu TCP Podsumowanie ● ● ● Komunikacja połączeniowa wykorzystująca protokół TCP, wysyła klientowi pakiety, a następnie oczekuje od niego potwierdzenia, że pakiet pomyślnie dotarł do celu Jeśli w pewnym przedziale czasu nie ma potwierdzenia to następuje ponowna próba przesłania pakietu – w przypadku definitywnego braku połączenia warstwa TCP zasygnalizuję błąd Powyższym mechanizmem nie dysponuje protokół UDP, jednakże można samemu zaimplementować podobną procedurę weryfikacji Podsumowanie ● ● Należy również zwrócić uwagę na niewielkie różnice pomiędzy serwerem i klientem opartymi na tym protokole Oba programy przeglądają wszystkie nadchodzące datagramy, a różnica pojawia się właściwie dopiero w warstwie aplikacji modelu sieciowego