next up previous contents
Next: SOCK_DGRAM (UDP) Up: Gniazda sieciowe - podstawy Previous: Podstawowe funkcje   Contents

TCP: SOCK_STREAM

Po przyswojeniu tych wszystkich wiadomości teoretycznych czas na sprawdzenie tego w praniu. Pierwszym programem, który napiszemy korzystając z dotychczasowej wiedzy o gniazdach będzie prosty klient usługi POP3 (ang. Post Office Protocol 3, protokół przesyłu poczty dla użytkowników końcowych).

Najpierw potrzebne pliki nagłówkowe:

  #include <sys/types.h>
  #include <sys/socket.h>         /* socket(), connect(), recv(), send() */
  #include <linux/in.h>           /* struct sockaddr_in                  */
  #include <sys/stat.h>           /* S_IRUSR, S_IWUSR */
  #include <fcntl.h>              /* open() */
  #include <unistd.h>             /* close(), write() */
  #include <stdio.h>              /* perror() */
  #include <netdb.h>              /* getservbyname() */
  #include <string.h>             /* strncmp() */
  
Teraz kilka makr, z których skorzystamy w programie. Zastępują one po prostu tablicę argumentów argv[]:
  #define USERNAME  "zygfryd"     /* nazwa użytkownika POP3 */
  #define PASSWORD  "sikret"      /* hasło do skrzynki pocztowej */
  #define POPSERV   "serwer.pl"   /* serwer POP3 */
  #define MAILFILE  "mailbox"     /* ścieżka do pliku przechowującego pocztę */
  #define SERVICE   "pop-3"       /* korzystamy z POP3 */
  #define KEEPM                   /* jeśli zdefiniowane to nie kasujemy
                                     wiadomości z serwera; lepiej mieć to
                                     włączone podczas eksperymentowania z
                                     tym programem ... */
  
Czas na cream-de-la-cream:
  int
  main ()
  {
    int                   fd, /* deskryptor pliku                           */
                          sd, /* deskryptor gniazda                         */
                         ret, /* kod powrotu funkcji                        */
                   nrmsg = 0, /* ilość nowych wiadomości w skrzynce POP3    */ 
                           n; /* zmienna pomocnicza                         */
    long int         msgsize, /* łączna wielkość nowych wiadomości          */
                       msize; /* wielkość pojedynczej wiadomości            */
    struct sockaddr_in saddr; /* adres gniazda serwera POP3                 */
    struct servent   *srvent; /* struktura zawierająca numer portu POP3     */
    struct hostent     *sent; /* z tej struktury odczytamy adres IP serwera */
    char        msgbuf[1024], /* bufor na wiadomosci */
                    buf[256]; /* bufor na komendy/odpowiedzi protokołu POP3 */
  
    sd = socket (PF_INET, SOCK_STREAM, 0);
    if (sd < 0)
      {
        perror ("socket()");
        exit (1);
      }
  
Powyższy fragment tworzy gniazdo do komunikacji strumieniowej w domenie Internetowej. Innymi słowy zamierzamy korzystać z pary protokołów: IP oraz TCP.
  srvent = getservbyname (SERVICE, "tcp");
  if (!srvent)
    {
      perror ("getservbyname()");
      exit (1);
    }
  
Użyliśmy pomocniczej funkcji getservbyname(). Zwraca ona wskaźnik do struktury servent, z której później odczytamy numer portu dla usługi SERVICE ("pop-3") korzystającej z protokołu "tcp". Fukcja ta opiera swoje działanie na zawartości pliku /etc/services.
  printf ("Probuje znalezc adres IP maszyny %s...\n", POPSERV);
  sent = gethostbyname (POPSERV);
  if (!sent)
    {
      herror ("gethostbyname()");
      exit (1);
    }
  else printf ("Adres %s to %s\n", POPSERV, inet_ntoa (*sent->h_addr));
  
W tym fragmencie znajdziemy dwie nieznane jeszcze funkcje. Pierwsza z nich, gethostbyname() zwraca wskaźnik do struktury hostent. Na podstawie jedynego argumentu (w tym przypadku - POPSERV) wypełnia ona wspomnianą strukturę, a wszystko to abyśmy mogli zamienić pełną nazwę domenową (FQDN) serwera POP3 na jego 32-bitowy adres IP. Omawiana funkcja jest częścią tzw. biblioteki resolvera, który korzysta m.in. z plików: /etc/resolv.conf, /etc/host.conf oraz /etc/hosts .

Druga funkcja - inet_ntoa(), zamienia 32-bitowy adres IP na jego odpowiednik w notacji xxx.xxx.xxx.xxx .

Przejdźmy dalej:

  saddr.sin_family = sent->h_addrtype;
  saddr.sin_port = srvent->s_port;
  bcopy (sent->h_addr, (char *) &saddr.sin_addr, sent->h_length);
  
Nadszedł czas na wypełnienie struktury sockaddr_in ponieważ już niedługo wywołamy funkcję connect(), która jak pamiętamy oczekuje tej struktury, jako jednego z argumentów. Aby wiedzieć w pełni, co się dzieje w powyższym fragmencie, zajrzymy do pliku netdb.h i zaznajomimy się ze strukturami servent (getservbyname()) oraz hostent (gethostbyname()):
  struct servent {
            char    *s_name;        /* oficjalna nazwa usługi    */
            char    **s_aliases;    /* lista nazw alternatywnych */
            int     s_port;         /* numer portu               */
            char    *s_proto;       /* używany protokół          */
       }
   
  struct hostent {
            char    *h_name;        /* oficjalna nazwa hosta        */
            char    **h_aliases;    /* lista nazw alternatywnych    */
            int     h_addrtype;     /* typ adresu (domena adresowa) */
            int     h_length;       /* długość adresu               */
            char    **h_addr_list;  /* lista adresów                */
        }
  #define h_addr  h_addr_list[0]  /* pierwszy z listy adresów     */
  
Od tej chwili omawiane trzy linijki kodu powinny być zrozumiałe. Zanim zaczniemy analizować dalszą część kodu zatrzymamy się jeszcze na jakiś czas tutaj. Struktura sockaddr_in oczekuje wartości w tzw. porządku sieciowym (ang. network byte order), sieć jest "maszyną" typu big endian. Inaczej sprawa wygląda inaczej w przypadku komputerów opartych na procesorach serii x86 - są one maszynami typu little endian i przechowują wartości w innym porządku (ang. host byte order). Przyjrzyjmy się bliżej, jak jest ułożona wartość typu short (2 bajty) w pamięci, z której korzysta procesor x86:
  0x00        0x0f (adresy w pamięci)
  |              |
  bbbbbbbbaaaaaaaa                  bbbbbbbb - LSB (ang. least significant byte -
                                                    mniej znaczący bajt)
                                    aaaaaaaa - MSB (ang. most significant byte -
                                                    bardziej znaczący bajt)
  
Odwrotnie jest na systemach big endian. Ta sama wartość będzie tam ułożona tak:
  0x00        0x0f  
  |              |
  aaaaaaaabbbbbbbb
  
Tak więc zawsze, kiedy wypełniamy struktury, które będą potem używane w sieci musimy zadbać o odpowiedni porządek bajtów. Mamy do dyspozycji cztery funkcje do konwersji:
  /* ang. host to network long, czyli konwersja wartości typu long (4 bajty) z 
     formatu hosta (LE) na format sieci (BE) */ 
  unsigned long int htonl(unsigned long int hostlong);
  /* wartość typu short (a bajty) z formatu LE na BE*/
  unsigned short int htons(unsigned short int hostshort);
  /* wartość long z BE na LE */
  unsigned long int ntohl(unsigned long int netlong);
  /* wartość short z BE na LE */
  unsigned short int ntohs(unsigned short int netshort);
  
Problemu tego nie mają użytkownicy np. maszyn z procesorem Alpha ponieważ ich systemy, podobnie jak sieć, układają wartości w porządku big endian.

Ktoś może się spytać, dlaczego nie użyliśmy konwersji wartości podczas wypełniania struktury sockaddr_in z przytoczonego fragmentu kodu. Otóż nie zrobiliśmy tego ponieważ funkcje gethostbyname(), oraz getservbyname() zwracają wyniki od razu przekonwertowane do odpowiedniego formatu (network order), a wartości którymi wypełniliśmy strukturę pochodzą od tychże funkcji. Trzeba dodać, ze znakomita większość funkcji "sieciowych" dla wygody zwraca wartości od razu w odpowiednim formacie. Trzeba by natomiast pamiętać o konwersji gdybyśmy podawali wartości "z palca", np. tak:

  saddr.sin\_port = htons(110);
  
Jako, że saddr.sin_port jest wartością typu short (dwubajtową) musieliśmy dokonać ręcznej konwersji z LE na BE. Jest jeszcze drugi przypadek, kiedy będziemy zmuszeni do ręcznej konwersji - nie obędzie się bez niej, kiedy INTERPRETUJEMY wartości, które przywędrowały z sieci. Mniej więcej taki właśnie przypadek zachodzi w kolejnej części kodu:
    printf ("Łączę się z %s:%i\n", inet_ntoa (*sent->h_addr), ntohs (srvent->s_port));
  
Zwróćmy uwagę na zapis ntohs(srvent->s_port). Konwertujemy wartość BE na format używany przez nasz lokalny system (LE) ponieważ wartość ta jest argumentem funkcji printf, która z kolei jest wykonywana przez procesor oczekujący wartości a porządku LE. W ramach eksperymentów można pominąć wywołanie ntohs() i sprawdzić, co uzyskamy na ekranie, jeśli spróbujemy bezpośrednio wyświetlić wartość srvent->s_port.

Kontynuujmy analizę kodu:

  ret = connect (sd, (struct sockaddr *) &saddr, sizeof (saddr));
  if (ret < 0)
     {
       perror ("connect()");
       exit (1);
     }
    else printf ("Polaczony.\n");
  
Wywołaliśmy funkcję connect() aby połączyć się ze zdalnym procesem. W użytej składni nie ma nic specjalnego poza zamianą struktury, na jaką wskazuje zmienna saddr. Pamiętajmy, że funkcja connect() oczekuje w tym miejscu wskaźnika do struktury sockaddr, a zmienna saddr jest wskaźnikiem do struktury sockaddr_in. Musimy więc odpowiednio zmienić typ (ang. cast) zmiennej saddr. Co prawda możnaby pominąć ten krok i wszystko powinno działać ale prawdopodobnie nasz kompilator wyświetli ostrzeżenie o niezgodności typów.

Koniecznie, należy sprawdzić, czy connect() nie zwróciło błędu. Najczęstszymi przyczynami błędu są: nieobecność żadnego procesu nasłuchującego na wybranym porcie zdalnego hosta, brak uprawnień do połączenia się ze zdalnym serwisem oraz wyczerpanie limitu czasowego na połączenie.

Kolejne linijki kodu:

    bzero (buf, sizeof (buf));
    recv (sd, buf, sizeof (buf), 0);
    vrfy_ans (buf);
  
    printf ("Loguje sie jako uzytkownik: %s\n", USERNAME);
  sprintf (buf, "USER %s\n", USERNAME);
  send (sd, buf, strlen (buf), 0);
  
    bzero (buf, sizeof (buf));
    recv (sd, buf, sizeof (buf), 0);
    vrfy_ans (buf);
  
    sprintf (buf, "PASS %s\n", PASSWORD);
  send (sd, buf, strlen (buf), 0);
  
    bzero (buf, sizeof (buf));
    recv (sd, buf, sizeof (buf), 0);
    vrfy_ans (buf);
    printf ("Zalogowany.\n", USERNAME);
  
    printf ("Sprawdzam, czy sa nowe wiadomosci ...\n");
    sprintf (buf, "STAT\n");
    send (sd, buf, strlen (buf), 0);
  
    bzero (buf, sizeof (buf));
    recv (sd, buf, sizeof (buf), 0);
    vrfy_ans (buf);
    sscanf (buf, "+OK %i %i", &nrmsg, &msgsize);
  
Zrozumienie tego fragmentu nie powinno stworzyć żadnych problemów. Poza kilkoma standardowymi funkcjami libc korzystamy z wywołań send() oraz recv() aby wymieniać informację ze zdalnym procesem (serwerem POP3). Wszystko odbywa się zgodnie z protokołem POP3 (RFC 1081). Funkcja vrfy_ans() sprawdza, czy serwer zwrócił odpowiedź '+OK' na wysłane przez nas polecenie i wygląda tak:
  int
  vrfy_ans (char *b)
  {
    if (!strncmp (b, "+OK", 3))
      return 1;
    else
      {
        printf ("\nBlad! Serwer zwrocil odpowiedz:\n%s\nKoncze ...\n", b);
  exit (2);
      }
  }
  
Jeśli w którymkolwiek momencie naszej sesji z serwerem otrzymaliśmy odpowiedź zawierającą na początku ciąg inny niż '+OK' to oznacza, że coś poszło nie tak i nie pozostaje nic innego, jak zakończyć nasz program. Nie omawiamy tutaj szczegółowo protokołu POP3 ponieważ nie jest to istotne dla zrozumienia mechanizmu gniazd. Zainteresowani powinni zajrzeć do dokumentu RFC 1081.

Jeśli do tej pory wszystko poszło dobrze to znaczy, że jesteśmy zalogowani na serwerze i że możemy przystąpić do pobierania poczty:

  if (nrmsg > 0)
    {
      printf ("Nowych wiadomosci: %i . Calkowity rozmiar: %i.\n\n", nrmsg, msgsize);
  
      fd = open (MAILFILE, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
      if (fd == -1)
          {
            perror ("open()");
            exit (1);
          }
  
Najpierw sprawdziliśmy, czy są dla nas jakieś nowe wiadomości oraz otworzyliśmy plik, w którym je zapiszemy. Poniżej znajduje się już główna pętla odpowiedzialna za pobranie tych wiadomości:
        for (n = 1; n <= nrmsg; n++)
          {
            sprintf (buf, "RETR %i\n", n);
  send (sd, buf, strlen (buf), 0);
  
            bzero (buf, sizeof (buf));
            recv (sd, buf, sizeof (buf), MSG_PEEK);
            vrfy_ans (buf);
  
Jak widać użyliśmy flagi MSG_PEEK w funkcji recv(). Po wydaniu polecenia 'RETR nr_wiadomości' serwer jest zobowiązany do odpowiedzi w formacie '+OK długość_wiadomości', a bezpośrednio po tym do transmisji wybranej wiadomości. Z tego powodu najpierw "podpatrujemy", czy rzeczywiście w odpowiedzi zawarty jest ciąg '+OK' (vrfy_ans()) a dopiero potem przechodzimy do kolejnych czynności:
            sscanf (buf, "+OK %i", &msize);
  printf ("Pobieram wiadomosc nr. %i (rozmiar: %i) ... ", n, msize);
  fflush (stdout);
            bzero (msgbuf, sizeof (msgbuf));
  
            while (recv (sd, msgbuf, sizeof (msgbuf), 0) > 0)
              {
                char *str = msgbuf, *end = msgbuf + strlen (msgbuf);
                int msg_end = 0;
  
                if (!strncmp (msgbuf, "+OK", 3))
                  str = strchr (msgbuf, '\12') + 1;
                if (!strncmp (msgbuf + strlen (msgbuf) - 5, "\15\12.\15\12", 5))
                  {
                    end = strrchr (msgbuf, '\12') - 4;
                    msg_end = 1;
                  }
  
Jesteśmy w pętli while, która pobiera kolejne paczki transmitowanej wiadomości do bufora msgbuf. Widzimy też dwa warunki if. Służą one do odpowiedniego ustawienia zmiennych str, end oraz msg_end. Wszystko dlatego aby wyciąć z wiadomości komunikaty, które nie są jej częścią ('+OK' i sekwencja CRLF.CRLF). Zmienną msg_end ustawiamy na 1 jeśli buforze msgbuf zawarta jest ostatnia część transmitowanej wiadomości (czyli na końcu msgbuf jest tylko CRLF.CRLF).
                ret = write (fd, str, end - str);
                if (ret < 0)
                  {
                    perror ("write()");
                    exit (3);
                  }
  
Zapisujemy "czysty" kawałek wiadomości ...
                bzero (msgbuf, sizeof (msgbuf));
                if (msg_end)
                  {
                    write (fd, "\n\n", 2);
                    break;
                  }
              }
            printf ("OK\n");
  
... i wychodzimy z pętli jeśli pobraliśmy już kompletną wiadomość (msg_end==1).
  #ifndef KEEPM
            printf ("Usuwam wiadomosc nr. %i ... ", n);
  fflush (stdout);
            sprintf (buf, "DELE %i\n", n);
  send (sd, buf, strlen (buf), 0);
            recv (sd, buf, sizeof (buf), 0);
            vrfy_ans (buf);
            printf ("OK\n");
  #endif
          }
  
        printf ("\nWszystkie wiadomosci pobrane. Koncze ...\n");
        close (fd);
      }
    else
      printf ("Nie ma nowych wiadomosci. Koncze ...\n");
    sprintf (buf, "QUIT\n");
    send (sd, buf, strlen (buf), 0);
    recv (sd, buf, sizeof (buf), 0);
    vrfy_ans (buf);
  
    return 0;
  }
  
Ostatni fragment kodu kasuje pobraną wiadomość (jeśli nie jest zdefiniowane makro KEEPM), wydaje serwerowi POP3 komendę 'QUIT' aby zakończyć sesję oraz kończy działanie programu.

Po przykładzie programu-klienta działającego w oparciu o protokół połączeniowy (SOCK_STREAM, TCP) czas najwyższy na przykład prostego serwera. Bez obaw - nie będzie to serwer POP3 ;) Spróbujemy skonstruować tylko uniwersalny szkielet serwera. Zanim zajmiemy się praktyką poświęćmy się teorii aby zapoznać się z funkcjami potrzebnymi do stworzenia serwera.

Jak pamiętamy programy klienckie korzystają z gniazd aktywnych natomiast serwery z pasywnych. Kiedy tworzone jest nowe gniazdo nie ma ono od razu przypisanego swojego adresu. Program kliencki z kolei musi posiadać adres gniazda, z którym zamierza się połączyć (struct sockaddr). Wynika z tego, że proces-serwer musi przede wszystkim przypisać swojemu gniazdu adres aby klient był stanie go odnaleźć w sieci. Do przypisania adresu dla gniazda służy funkcja bind (man 2 bind):

  #include <sys/types.h>
  #include <sys/socket.h>
  
  int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
  
Parametry są identyczne, jak dla funkcji connect(), zmienia się tylko ich znaczenie. Struktura sockaddr w wywołaniu connect() określała adres gniazda, z którym się łączymy, natomiast w przypadku funkcji bind() ta sama struktura odnosi się do naszego lokalnego gniazda. Pamiętamy, że w poprzednim programie tworzyliśmy gniazdo wywołując socket(), a bezpośrednio po tym dokonywaliśmy połączenia. Prawda jest taka, że i wtedy przypisywany był adres dla lokalnego gniazda tylko, że działo się to automatycznie z wnętrza funkcji connect(). Automatycznie odnajdywany był adres lokalnej maszyny i także automatycznie wybierany był port lokalnego gniazda. Co do tego ostatniego, działo się to w pewnym stopniu losowo ponieważ system sam wybierał jeden z dostępnych obecnie portów. Na taką automatyzację nie może pozwolić sobie proces-serwer. Jak wiemy różne serwery powinny nasłuchiwać na pewnych ściśle określonych portach (ang. well known ports). Dlatego też w ich przypadku konieczne jest "ręczne" przypisanie adresu do gniazda. Funkcja bind zwraca 0 lub -1 (błąd).

Kiedy przypisaliśmy adres do gniazda możemy przejść do kolejnej czynności. Trzeba teraz kazać serwerowi "nasłuchiwać" na wybranym porcie. Do tego celu używamy funkcji listen() (man 2 listen):

  #include <sys/socket.h>
  
  int listen(int s, int backlog);
  
Standardowo zwracane jest 0 albo -1 (błąd).

Po przełączeniu serwera w tryb oczekiwania przydałoby się sprawdzić w jakiś sposób, czy przypadkiem nikt już nie próbuje się z nami połączyć. Oto, ostatnia, trzecia funkcja specyficzna dla serwerów (man 2 accept):

  #include <sys/types.h>
  #include <sys/socket.h>
  
  int accept(int s, struct sockaddr *addr, int *addrlen);
  
Ponownie mamy do czynienia z tymi samymi trzema argumentami i ponownie zmienia się ich znaczenie. Struktura sockaddr powinna być pusta ponieważ to funkcja accept() zajmie się jej wypełnianiem. Parametr addrlen podawany przez nas do wywołania funkcji powinien wskazywać na wielkość przekazywanej struktury sockaddr. Funkcja accept() jest funkcją blokującą. W tym wypadku zwraca sterowanie do głównego programu tylko wtedy, kiedy jakiś proces próbuje się z nami łączyć. W takim wypadku opisywana funkcja wypełnia strukturę sockaddr danymi zdalnego (klienta) gniazda a pod adresem wskazywanym przez addrlen umieszcza długość tak wypełnionej struktury. Funkcja zwraca deskryptor nowoutworzonego gniazda lub -1 (błąd). Wspomnianego gniazda możemy od tej pory używać do komunikacji z klientem, natomiast "stare" gniazdo nadal istnieje i nic nie stoi na przeszkodzie aby przyjmować na nim kolejnych klientów.

Tak wygląda w skrócie proces tworzenia serwera. Przejdźmy do przykładu:

Nagłówki:

  #include <stdio.h>
  #include <sys/types.h>
  #include <sys/socket.h>         /* bind(), listen(), accept()      */
  #include <linux/in.h>           /* struct sockaddr_in              */
  #include <sys/wait.h>           /* waitpid()                       */
  #include <unistd.h>             /* fork()                          */
  #include <signal.h>             /* signal()                        */
  
  // Makro:
  #define SPORT 10000             /* port serwera                    */
  
  // Właściwy kod:
  
  main ()
  {
    int                   sd, /* lokalne gniazdo serwera        */
                          cd, /* zdalne gniazdo klienta         */
                        alen, /* wielkość struktury sockaddr_in */
                         pid; /* PID potomka                    */
    struct sockaddr_in saddr, /* adres lokalnego gniazda        */ 
                       caddr; /* adres zdalnego gniazda         */   
  
    sd = socket (PF_INET, SOCK_STREAM, 0);
    if (sd < 0)
      {
        perror ("socket()");
        exit (1);
      }
  
    saddr.sin_family = PF_INET;
    saddr.sin_port = htons (SPORT);
    saddr.sin_addr.s_addr = INADDR_ANY;
  
Kod do momentu wypełniania struktury sockaddr_in (saddr) powinien być już zrozumiały. Nowością jest tajemnicze makro INADDR_ANY wstawione w pole saddr.sin_addr.s_addr. Jest ono zdefiniowane w pliku linux/in.h:

/* Adres do odbierania wszystkich połączeń przychodzących. */

  #define INADDR_ANY              ((unsigned long int) 0x00000000)
  
Jak widzimy INADDR_ANY przełożone na notację xxx.xxx.xxx.xxx da nam 0.0.0.0. Adres taki oznacza, że deklarujemy chęć odbierania połączeń na wszystkich adresach IP, których używa nasz host. INADDR_ANY używa się najczęściej na hostach, które posiadają tylko jeden adres IP. Hosty posiadające kilka interfejsów sieciowych (podłączone do kilku sieci) chciałyby raczej same decydować, z której sieci (na który adres IP) przyjmować połączenia. W takim przypadku należy samodzielnie przeliczyć odpowiedni adres IP z postaci xxx.xxx.xxx.xxx na liczbę 32-bitową. Do tego celu mamy do dyspozycji dwie funkcje inet_aton() oraz inet_addr(). Co prawda podręczniki odradzają już używania inet_addr() (dlaczego tak jest można dowiedzieć się z man'a) ale korzystanie z tej funkcji jest dużo wygodniejsze niż w przypadku jej następczyni i dlatego zapoznamy się właśnie z nią (man 3 inet_addr):
  #include <sys/socket.h>
  #include <netinet/in.h>
  #include <arpa/inet.h>
  
  unsigned long int inet_addr(const char *cp);
  
Jako parametr przyjmuje ona adres IP w notacji xxx.xxx.xxx.xxx, zwraca natomiast 32-bitową liczbę w formacie BE (gotowym do wstawienia do odpowiedniej struktury).

Dotarliśmy do ważnej funkcji bind():

    if (bind (sd, (struct sockaddr *) &saddr, sizeof (saddr)))
      {
        perror ("bind()");
        exit (1);
      }
  
Dzięki temu wywołaniu określiliśmy, że serwer będzie używał portu SPORT do "nasłuchu" i że będzie przyjmował połączenia na wszystkie nasze adresy IP.
    if (listen (sd, 5))
      {
        perror ("listen()");
        exit (1);
      }
  
Od tej chwili serwer nasłuchuje na wcześniej zdefiniowanym porcie. Długość kolejki połączeń ustawiliśmy na 5. Maksymalna wartość parametru backlog to SOMAXCONN, którego wartość znajduje się w linux/socket.h.
    signal (SIGCHLD, sigchld\_handler);
  
Nasz serwer będzie tworzył procesy potomne. Musimy więc zadbać o odpowiednią obsługę sygnału SIGCHLD aby uniknąć mnożenia się tzw. procesów zombie. Funkcja obsługująca wspomniany sygnał wygląda tak:
  void sigchld_handler (int sig)
  {
    int pid;
  
    while ((pid = waitpid (-1, NULL, WNOHANG)) > 0);
  }
  
Często popełnianym błedem jest tylko jednokrotne wywołanie waitpid() wewnątrz funkcji. Trzeba pamiętać, że sygnały w systemach unixowych nie są ustawiane w kolejce (wyjątkiem są sygnały czasu rzeczywistego), a więc kiedy wywoływana jest funkcja sigchld_handler() to wiemy tylko tyle, że zakończył się CO NAJMNIEJ jeden potomek. Wywołanie waitpid() wykonywane w pętli zagwarantuje nam usunięcie wszystkich aktualnie istniejących procesów zombie.

W ten sposób doszliśmy do głównej pętli serwera. Z analogiczną konstrukcją można spotkać się w kodzie wielu różnych serwerów:

    while (1)
      {
  // Jak widać nigdy nie opuszczamy pętli.
  
        alen = sizeof (saddr);
        cd = accept (sd, (struct sockaddr *) &caddr, &alen);
        if (cd < 0)
          {
            perror ("accept()");
            exit (1);
          }
  
Funkcja accept() blokuje wykonywanie programu do momentu aż jakiś klient zgłosi chęć połączenia z serwerem. W takim przypadku accept() wypełni strukturę wskazywaną przez caddr danymi klienta, a w zmiennej cd zwróci deskryptor gniazda, którego będziemy używać do komunikacji z tym konkretnym klientem.
        printf ("Polaczenie od klienta %s:%i\n", inet_ntoa (caddr.sin_addr),
  ntohs (caddr.sin\_port));
  
Trochę informacji o kliencie ... Zwróćmy ponownie uwagę na konieczność zastosowania ntohs().
        if ((ret = fork ()) == 0)
          {
  
Zaraz po przyjęciu połączenia tworzymy nowy proces, który będzie je obsługiwał. Funkcja fork() zwraca rodzicowi PID potomka natomiast z punktu widzenia potomka funkcja ta zwraca 0. Powyższy warunek if tworzy więc potomka i wszystkie instrukcje, które znajdują się w tym bloku if będą wykonywane przez potomka.
            /* Jesteśmy dzieckiem */
            dup2 (cd, 0);
            dup2 (cd, 1);
            dup2 (cd, 2);
  
Funkcja dup2(oldfd, newfd) powoduje, że deskryptor newfd staje się kopią deskryptora oldfd, a ten ostatni jest zamykany. Trzy wywołania dup2() powyżej ustawiają deskryptory 0 (stdin), 1 (stdout) i 2 (stderr) tak aby wskazywały na gniazdo, za pomocą którego komunikujemy się z klientem. Po co to robimy dowiemy się za chwile.
            close (sd);
  
Zamykamy gniazdo sd, które dla nas jest niepotrzebne.
            printf ("\nProsze czekac. Uruchamiam powloke ...\n\n");
  //        send (cd, "\nProsze czekac. Uruchamiam powloke ...\n\n", 40, 0);
  
W tym miejscu możemy dowiedzieć się po co używaliśmy wcześniej dup2(). Dzięki temu, że standardowe wejście/wyjście przekierowaliśmy do gniazda połączonego z klientem możemy używać standardowych funkcji do operacji na strumieniach we/wy (printf(), scanf(), getchar() itp.). Zakomentowana jest alternatywna linijka zawierająca wywołanie send() - również tego wywołania (plus recv()) możemy używać do komunikacji z klientem ale czyż rodzina funkcji printf/scanf nie jest wygodniejsza ... ?

Właściwie już teraz mamy szkielet serwera. Utworzyliśmy łącze z klientem, nic nie stoi na przeszkodzie aby wymieniać dowolne informacje. Postaramy się jednak aby ten nasz przykładowy serwer robił coś konkretnego:

            if (execl ("/bin/bash", "bash", "-i", NULL) < 0)
              {
                perror("execl()");
                exit(1);
              }
  

Wcześniejsze rzekierowanie standardowych strumieni nie było podyktowane tylko wygodą. Zrobiliśmy to z myślą o programie, który jest uruchamiany przez powyższe polecenie. Dzięki temu przekierowaniu uruchomiona właśnie powłoka nie będzie oczekiwała poleceń z klawiatury komputera, na którym jest uruchomiona ale z komputera, na którym działa klient. To samo tyczy się standardowego wyjścia (ekranu).

          }
        else if (ret < 0)
          {
            perror ("fork()");
            exit (1);
          }
  
  // To jeszcze sprawdzenie, czy udało się stworzyć potomka.
  
        /* Jesteśmy rodzicem */
        close (cd);
      }
  }
  
A powyżej ostatnie polecenie w pętli while wykonywane przez rodzica. Zamykamy niepotrzebne gniazdo cd ponieważ to nie my obsługujemy klienta. Po wykonaniu ostatniej instrukcji w pętli program (zgodnie z oczekiwaniami ;) wraca do jej początku, gdzie ponownie uruchamia funkcję accept() i czeka na następnego klienta.


next up previous contents
Next: SOCK_DGRAM (UDP) Up: Gniazda sieciowe - podstawy Previous: Podstawowe funkcje   Contents
Paweł Niewiadomski
2000-10-17