SOP322/ćw - temat D

Gniazda BSD.

--> literatura:
    * Grey "Komunikacja miedzy procesami w Unix-ie", rozdz 10
    * Gabassi, Dupouy "Przetwarzanie rozproszone w UNIX-ie", rozdz 4
    * Bach "Budowa systemu operacyjnego UNIX", rozdz 11.4, str 414

    * The Linux Programmer's Guide:
              Gniazda sieciowe - podstawy
              Zaawansowane programowanie gniazd
       UWAGA: to jest bardzo dobra dokumentacja fun sys Unixa/Linuxa w języku polskim !

--> "gniazdko" to końcowy punkt kanału komunikacyjnego,
      przez który proces może wysyłać lub odbierać dane

--> gniazdka mogą pracować w następujących dziedzinach :
      * internetowej (AF_INET)
             - umozliwiaja tworzenie aplikacji klient/serwer
               ktorych procesy pracuja na różnych maszynach
      * uniksowej (AF_UNIX)
             - umozliwiaja tworzenie aplikacji klient/serwer
               ktorych procesy pracuja na jednej maszynie
               (można to zrobić także przy pomocy łączy nazwanych, lecz jest to uciążliwe !)
             - obsluguje sie je dokładnie tak samo jak w dziedzinie AF_INET
             - gniazdko jest (moze byc) widoczne jako plik w strukturze katalogow

--> są dwa typy gniazdek :
      * datagramowe (SOCK_DGRAM)
             - zaimplementowane w dziedzinie AF_INET przez protokol UDP
             - przesyła się komunikaty (=datagramy)
             - nie nawiązuje się połączenia
             - nie ma gwarancji że dane dotrą do miejsca przeznaczenia
      * strumieniowe (SOCK_STREAM)
             - zaimplementowane w dziedzinie AF_INET przez protokol TCP
             - nawiązuje się połączenie
             - przesyła się dowolne ciągi bajtow (w obie strony)
             - gniazda strumieniowe zachowują się zupełnie jak łącza !
             - jest automatyczne potwierdzanie dostarczenia danych
             - istnieja bufory na obu końcach polaczenia, gromadzące dane
             - nie mylić pojęć "gniazdka strumieniowe" i "gniazdka zrobione ze strumieni"

UWAGA: gniazdka obojga typów występują w obu dziedzinach !
 

--> powiązanie, adresowanie oraz inne pojęcia ...

      * "powiązanie" w dziedzinie AF_INET jest zdefiniowane przez:
             1) protokol transportu: TCP lub UDP
             2) adres_internetowy_1, nr_portu_1
                     // to jest 1 koniec powiazania (można go utożsamiać z 1 gniazdkiem)
             3) adres_internetowy_2, nr_portu_2
                     // to jest 2 koniec powiazania (można go utożsamiać z 2 gniazdkiem)
 
      * adresowanie w dziedzinie AF_INET
         (czyli jak się definiuje "jeden koniec powiązania")

        struct sockaddr_in adres;
           // w tej strukturze przechowuje sie adres i port
           // jest ona zdefiniowana w pliku "netinet/in.h"
        memset(&adres,0,sizeof(adres));
           // wyzerowanie
        adres.sin_family=AF_INET;
        adres.sin_addr.s_addr=inet_addr("1.2.3.4");
           // funkcja "inet_addr (char *)"
           // przeksztalca string z adresem "1.2.3.4"
           // (musi to byc adres kropkowy/cyfrowy !)
           // na odpowiedni "long int"
        adres.sin_port=htons(7000);
           // "htons()" uniezaleznia nas od sposobu reprezentacji
           // "short" na naszej maszynie (ktory to sposob moze byc
           // inny na odleglej maszynie !)
      * do czego służy nr_portu w dziedzinie AF_INET ???
             - adres internetowy identyfikuje maszynę, jednak na pojedyńczej maszynie
               może działać wiele serwerów oczekujących na klientów;
               "nr portu" określa o który serwer nam chodzi
               (serwer oczekuje na klientów "na określonym porcie")

      * adresowanie w dziedzinie AF_UNIX

        struct sockaddr_un adres;
        memset(&adres,0,sizeof(adres));
        adres.sun_family=AF_UNIX;
        strcpy(adres.sun_path,"gniazdko");
           // ten plik będzie widoczny dla innych procesów
 

--> co powinien robić klient, a co serwer
      (omawiamy tutaj dziedzinę INTERNETOWĄ, gniazdka STRUMIENIOWE)

*** klient
***

*** serwer
***
Uwaga: gniazdka strumieniowe doskonale pasują do modelu klient/serwer ...


--> funkcje pomocnicze

         // konwersja zapisu liczb calkowitych z formatu "maszynowego" na "sieciowy"
         // i odwrotnie
    htons(), ntohs() // konwersja "unsigned short"
    htonl(), ntohl() // konwersja "unsigned long"
                 // z maszynowego do sieciowego "hton" = "host to net"
                 // z sieciowego do maszynowego "ntoh" = "net to host"

    unsigned int inet_addr (char *string) ;
         // konwersja adresu typu "1.2.3.4" na "unsigned long"
         // (a wlasciwie "unsigned int"; zdaje sie ze tutaj "unsigned int" ma 32 bity !?)
    char *inet_ntoa (struct in_addr net_addr);
         // wykonuje odwrotna czynnosc

    struct hostent *gethostbyname(char * hostname);
        // hostname to nazwa typu "main.amu.edu.pl"
        // funkcja ta tworzy strukture "hostent"
        // zawierajaca adresy maszyny w postaci 32-bitowej
            // przyklad zastosowania w pliku "sock01.cc"
            // w funkcji "AdresCyfrowyKropkowy()"
    struct hostent {
            char    * h_name;       /* official name of host */
            char    ** h_aliases;   /* alias list */
            short   h_addrtype;     /* host address type */
            short   h_length;       /* length of address */
            char    ** h_addr_list; /* list of addresses */
    };

    getsockname();
      // pobiera adres z gniazdka

    getpeername();
      // pobiera adres z gniazdka po przeciwnej stronie

    getsockopt();  // konfigurowane gniazdek
    setsockopt();
            // odczytywanie dlugosci bufora "nadawczego"  i "odbiorczego"
            // (AF_INET, gniazda strumieniowe)
         int  optval, optlen=sizeof(optval);
         getsockopt(sock, SOL_SOCKET, SO_SNDBUF, &optval, &optlen);
         getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &optval, &optlen);
                /* SOL_SOCKET oznacza, że chodzi o opcje tzw "warstwy gniazd";
                    mozna także czytac/pisac opcje innych "warstw"
                */
 
      fcntl(sock, F_SETFL, O_NONBLOCK|fcntl(sock,F_GETFL,0));
             /*gniazdko mozna przelaczyc w tryb NIEblokujacy
                tak jak w przypadku innych deskryptorow;
                mozna takze w tradycyjny sposob uzywac funkcji "select()"
             */
 

--> struktury: "in_addr", "sockaddr_in", "sockaddr_un", ...
    /*
     * Internet address
     */
    struct in_addr {
            union { // chodzi o dostep do poszczegolnych
                    // czesci adresu internetowego !!!
                    struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
                    struct { u_short s_w1,s_w2; } S_un_w;
                    u_long S_addr;
            } S_un;
    };
    #define s_addr  S_un.S_addr
    #define s_host  S_un.S_un_b.s_b2
    #define s_net   S_un.S_un_b.s_b1
    #define s_imp   S_un.S_un_w.s_w2
    #define s_impno S_un.S_un_b.s_b4
    #define s_lh    S_un.S_un_b.s_b3

    /*
     * Socket address, internet style.
     */
    struct sockaddr_in {
            short   sin_family; /* AF_INET */
            u_short sin_port;
            struct  in_addr sin_addr;
            char    sin_zero[8];
    };

    /*
     * Socket address, unix style.
     */
    struct sockaddr_un {
       short sun_family; /* AF_UNIX */
       char sun_path[104]; /* sciezka do pliku */
    }
 
Uwaga: programy używające gniazd w dziedzinie INTERNETOWEJ i w dziedzinie UNIKSOWEJ różnią się jedynie sposobem określania adresu ...

--> najprostszy przykład użycia gniazdek: sock01.cc(+unix.h)

Uwaga: przy wielokrotnym włączaniu programu sock01 trzeba chwilę odczekać; protokoły sieciowe są tak zaprojektowane aby złośliwy użytkownik nie mógł się "podszyć" pod serwer w razie jego nagłego wyłączenia ...

Zadanie 60


Zmodyfikuj program potok.cc, tak aby uzywal gniazdek strumieniowych w dziedzinie uniksowej zamiast laczy.  Uzyj funkcji "socketpair()" ktora tworzy pare gniazdek (jest to funkcja podobna do "pipe()"); pierwsze 3 parametry socketpair() jak przy funkcji socket(). Czy gniazdka zachowuja sie tak samo jak lacza ?  Sprawdz efekt "konczenia sie potoku" !

Zadanie 61


Zmodyfikuj przyklad "sock01.cc", tak aby uzywal gniazdek STRUMIENIOWYCH w dziedzinie UNIKSOWEJ. 
Wskazówki:
struktura "sockaddr_un" jest zdefiniowana w pliku naglowkowym "sys/un.h".  Dlugosc adresu typu "sockaddr_un" to: sizeof(sun_family)+strlen(sun_path).  Gdy serwer wykona "bind()", to plik okreslony w "sun_path" pojawi się i klient bedzie się mógł podlaczyc do serwera podajac ta sama nazwe pliku w wywolaniu "connect()". UWAGA: ten plik należy usunąć funkcją unlink() gdy serwer kończy działanie, w przeciwnym wypadku nie będzie można uruchomić tego prgramu po raz drugi !.

Zadanie 62


Napisz oprogramowanie serwera i klienta uslugi KROPKA, ktorej dzialanie polega na zwracaniu "wykropkowanego" tekstu: jesli klient wysle "ABCD" to powinien po chwili odczytac "A.B.C.D.".
  • Dzialanie klienta polega na czytaniu linii tekstu z terminala i wysylaniu jej do serwera, pobieraniu odpowiedzi z serwera i wyswietlaniu jej na terminalu. Uruchom kilku klientow na roznych maszynach. Pamietaj ze gniazdka BSD sa dostepne takze na WinNT, być może klienta będzie można skompilować bez zmian !.
  • Musisz użyć gniazd STRUMIENIOWYCH w dziedzinie INTERNETOWEJ. Serwer powinien tworzyc osobny proces dla kazdego klienta (w danej chwili moze byc wielu klientow).
  • Serwer powinien składać się z 2 programów: jeden z nich czyta stdin, dopisuje kropki między literami, a następnie zapisuje wynik do stdout.  Główny program serwera powinien uruchamiać ten program w procesie potomnym po każdym zgłoszeniu się klienta (+ odpowiednie przeadresowania, oczywiście !).
  • Serwer powinien oczekiwać na klientów na porcie 10000.
  • Uzyj polecenia "netstat" aby zobaczyc istniejace powiazania !.

  • Wskazówki:  aby nie powstawaly duze ilosci "mumii" wystarczy ignorowac SIGCLD. Serwer powinien dzialac takze po wylogowaniu się osoby która go uruchomiła (czyli powinien ignorowac SIGHUP, SIGTTIN, SIGTTOU).

     

     

    --> tutaj omawiamy dziedzine INTERNETOWA, gniazdka DATAGRAMOWE
          (2 procesy "proces nr 1" i "proces nr 2" zamierzaja sie
          komunikowac; co powinny zrobic ???)

          UWAGA: unikam okreslen klient/serwer poniewaz gniazdka datagramowe
             nie narzucaja tego modelu (tak jak to robily gniazdka strumieniowe);
             komunikujace sie procesy sa tutaj równorzędne !
                   "proces nr 1" zachowuje się jak klient
                         (tj wysyla dane do "proc nr 2" i oczekuje odpowiedzi)
                   "proces nr 2" zachowuje się jak serwer
                        (pobiera dane i zaraz je odsyla do nadawcy)
     

    *** proces nr 1
    ***
              1) socket() // tworzy gniazdko

    	int sock; 
    sock=socket (AF_INET,SOCK_DGRAM,0);
    *** proces nr 2
    ***
              1) socket() // tworzy gniazdko
    	int sock; 
    sock=socket (AF_INET,SOCK_DGRAM,0);
     

     

    Zadanie 63


    Zmodyfikuj przyklad "sock01.cc", tak aby uzywal gniazdek DATAGRAMOWYCH w dziedzinie internetowej.

    Zadanie 64


    Zaprogramuj wersję serwera i klienta usługi KROPKA, używających gniazdek DATAGRAMOWYCH w dziedzinie internetowej.  Serwer ma się składać tylko z jednego procesu (czyli pojedynczy proces ma obsługiwać wszystkich klientów).


    .......................................................................

     

    Konfigurowanie terminala; fun. sys. ioctl(), struct termio.

    Można zmieniać pewne własności terminala czyli go "konfigurować" przy pomocy funcji ioctl() oraz struktury termio lub termios :

    Struktura "termio" ma następującą budowę:

    Informacje o tym co można "zrobić" z terminalem, w jakich trybach może się on znajdować itp, można uzyskać przy pomocy polecenia:

    W pliku tty7.txt znajduje się zawartość wyciągnięta z manuala.

    Krótki opis terminala tekstowego:

    Tryb kanoniczny terminala polega głównie na tym, że dane z terminala są wczytywane "liniami". Dopiero po wprowadzeniu linii i po wykonaniu ewentualnych modyfikacji (można kasować znaki z prawej strony) oraz po naciśnięciu klawisza ENTER funkcja read(0,...) przestanie blokować i może odczytywać znaki linii w jej ostatecznej postaci !. W trybie niekanonicznym znaki wpisywane na terminalu są dostępne natychmiast. [Dokładniejszy opis znajduje się w książce Bacha, str 354, rozdział 10.3]

    Wyłączenie echa powoduje, że znaki które wpisujemy z klawiatury nie pojawiają się automatycznie na terminalu. Zmienna c_cc[VMIN] w trybie niekanonicznym oznacza minimalną liczbę znaków, które należy wpisać, aby stały się dostępne dla funkcji read() (jeśli c_cc[VMIN]==1 to znaki są dostępne od razu).

    Przykład tty02.cc to eksperymenty z wyłączaniem trybu kanonicznego oraz echa. Przykład ten powinien działać z każdym typem terminala (także "xterm"), w każdej wersji Unixa, kompatybilnej z SVR4 (System V Release 4).

    Zauważmy, że wyłączenie echa i trybu kanonicznego pozwoliło by nam na zaprogramowanie "historii poleceń"  oraz wygodnej "edycji wprowadzanych komend" w naszej mini-powłoce !.
     

    Zbiory deskryptorów, tryb nieblokujący; select(), fcntl().

    Przypuśćmy, że mamy dwa (otwarte) deskryptory x1 i y1, oraz zamierzamy napisać program przepisujący dane z x1 do y1.  Oczywiście program taki powinen zawierać następujący kod:

    Co zrobić jeśli mamy deskryptory x1, x2 i y1, y2 oraz zamierzamy (w jednym procesie) przepisywac dane z x1 do y1 oraz z x2 do y2 ?  Jednym z możliwych rozwiązań jest przełączenie tych deskryptorów w tryb "nieblokujący", przy pomocy funkcji fcntl():

    Stała F_GETFL oznacza odczytywanie "file status flag" (tych samych, które podaje sie przy otwieraniu pliku funkcją open()), natomiast F_SETFL służy do zmiany tych flag. Flaga O_NONBLOCK wprowadza deskryptor w tzw tryb nieblokujący: jeśli wywołanie jakiejść funkcji systemowej z tym deskryptorem mogłoby blokować proces, to w trybie nieblokującym funkcja po prostu zwróci błąd (errno==EAGAIN).

    Po wprowadzeniu wszystkich deskryptorów w tryb nieblokujący moglibyśmy zaprogramować pętle, która by sprawdzała czy nie da się jakiś danych przepisać.  Oczywiście jest to raczej złe rozwiązanie, gdyż bazuje na aktywnym czekaniu !.

    Lepsze rozwiązanie stanowi funkcja select() oraz towarzyszące jej makra:

    Funkcja ta pozwala czekać na możliwość zapisu lub odczytu na zbiorze deskryptorów.  Zbiory deskryptorów to zmienne typu fd_set. Makra służą do manipulowania tymi zbiorami.  Makro FD_SET() wstawia deskryptor do zbioru, FD_ISSET() testuje czy deskryptor jest w zbiorze, FD_ZERO() zeruje cały zbiór.  Po zakończeniu działania funkcji select() w zbiorach pozostają tylko te deskryptory, na których można wykonać operację.

    Dokładniejsze informacje są w pliku select2.txt

     
    A oto dwa przykłady: są to programy do "rozmowy z modemem". Można wydawać komendy i otrzymywać odpowiedzi od modemu, np:

    Pierwszy z przykładów tty03.cc, używa trybu nieblokującego. Drugi przykład tty03a.cc używa funkcji select(), dzięki czemu w znacznie mniejszym stopniu obciąża procesor. (Oczywiście oba przykłady można tylko oglądać, jeśli nie mamy modemu i przywilejów root-a - plik /dev/ttyS3 jest dostępny tylko dla root-a).
     

    Zadanie 66

    Program potok4.cc zawiera specjalną wersję programu "potok2.cc", w której śledzi się przepływ informacji przez łącza.  Jest to zrealizowane w ten sposób, że każda komenda jest połączona dwoma łączami z procesem macierzystym.  Proces macierzysty jest odpowiedzialny za przepisywanie danych między odpowiednimi końcówkami łączy.  Przy okazji,  może wyświetlać informacje o tym, co komendy sobie przesyłają.  W tym rozwiązaniu wykorzystuje się funkcję select().  Sposób wywołania:

    1)  W programie "potok4" jest coś nie w porządku, zużywa on zbyt wiele czasu procesora, jak na program w którym nie ma aktywnego czekania, o czym można się przekonać wydając polecenie:

    Twoim zadaniem jest wykryć na czym polega problem i naprawić go.
    2)  Program "potok4" przepisuje dane po jednym bajcie, zastanów się czy jest możliwe przepisywanie większymi porcjami (np po 10 bajtów).  Będzie potrzebna dokładna dokumentacja funkcji read() i write() w odniesieniu do łączy, dostępna oczywiście w manualu, ale na wszelki wypadek umieszczam pliki: read2.txt i write2.txt.

     

    .................................................................

    c.d. zadań z gniazdkami STRUMIENIOWYMI w dziedzinie INTERNETOWEJ

    Zadanie 67
    Napisz własną wersję serwera i klienta usługi TELNET (obowiązują uwagi podane w zadaniu 62).
  • serwer powinien dla każdego zgłaszającego się klienta tworzyć osobny proces obsługujący tego klienta, i uruchamiać w nim program "bash"
  • klient powinien czytać z terminala i przesyłać odczytywane teksty do procesu obsługującego (przez połączenie TCP), oraz powinien wyświetlać na terminalu teksty przychodzące od procesu obsługującego (konieczne będzie użycie funkcji select() lub trybu nieblokującego !!!)
  • przed uruchomieniem bash-a w procesie obsługującym klienta pamiętaj o odpowiednim przeadresowaniu gniazdka (nie tylko desk 0 i 1 ale także 2 !); zwróć uwagę że połączenia TCP są dwukierunkowe (w przeciwieństwie do łączy)
  • nie będzie wyrafinowanej edycji linii poleceń gdyż bash "widzi" że deskryptory 0,1,2 nie dotyczą terminala i używa ich jakby dotyczyły plików zwykłych (tak więc terminal będzie pracował w trybie kanonicznym)
  • UWAGA: pamiętaj o niebezpieczeństwie: nasz serwer telnetu nie sprawdza tożsamości klientów !

     

    Zadanie 68 (*) [tego zadania nie należy robić !!!]
    (TELNET z wyrafinowaną edycją linii poleceń ...)
    Napisz własną wersję serwera i klienta usługi TELNET (patrz też poprzednie zadanie).
  • Serwer powinien dla każdego klienta uruchamiać program "bash" z opcją "-i"<=>"interaktywny" (opcja ta jest konieczna, gdyż program "bash" sprawdza czy jego stdin i stdout to prawdziwy terminal - jeśli tak nie jest to zachowuje się "nieinteraktywnie", np nie wyświetla promptu !).
  • Pamiętaj o przeadresowaniu desk 0,1,2 w procesach obsługujących klientów.
  • Aby skorzystać z możliwości wyrafinowanej edycji linii poleceń jaką daje powłoka "bash", klient powinien włączyć tryb niekanoniczny terminala, wyłączyć "echo", oraz wykonać "c_cc[VMIN]=1". Klient nie może używać "aktywnego czekania" (musisz odpowiednio wykorzystać funkcję select()).
  • Kończenie działania klienta może nastąpić po naciśnięciu Ctrl-D (w trybie niekanonicznym oznacza to przeczytanie znaku o kodzie 4).
  • UWAGA 1: pamiętaj o niebezpieczeństwie: nasz serwer telnetu nie sprawdza tożsamości klientów !
  • UWAGA 2: ponieważ to zadanie zależy silnie od wersji powłoki "bash" więc może się nie udać ...