next up previous contents
Next: SOCK_RAW i PF_PACKET Up: Gniazda sieciowe - podstawy Previous: SOCK_DGRAM (UDP)   Contents

PF_UNIX

Zajmiemy się teraz drugą (po PF_INET) domenę adresową - PF_UNIX. Adresy tej domeny mają po prostu format standardowych ścieżek do plików w systemie UNIX. Z domeny PF_UNIX korzysta m.in. demon syslogd. Gniazda typu PF_UNIX mają swoją reprezentację w systemie plików. Popatrzmy, czego używa syslogd:
  srw-rw-rw-   1 root     root            0 Jan  6 13:09 /dev/log
  
Litera 's' na początku to skrót od socket (gniazdo). Pliki te są tworzone w momencie przypisywania gniazdu adresu poprzez funkcję bind(). Trzeba pamiętać, że funkcja bind() zwróci błąd, jeśli nie mamy prawa do utworzenia pliku na podanej ścieżce.

Ogólny sposób korzystania z gniazd PF_UNIX tylko nieznacznie odbiega od konwencji przyjętych wcześniej. Stworzymy prościutki program aby przyswoić sobie niuanse dotyczące takich gniazd. Program umożliwi komunikację między uzytkownikami zalogowanymi na tym samym systemie - coś w stylu write(1) ale oparte na gniazdach PF_UNIX:

  #include <sys/types.h>
  #include <sys/socket.h>         /* socket(), connect(), recv(), send() */
  #include <linux/un.h>           /* struct sockaddr_un                  */
  #include <stdio.h>              /* fgets()                             */
  #include <unistd.h>             /* chdir(), unlink()                   */
  #include <signal.h>             /* signal()                            */
  #include <sys/stat.h>           /* chmod()                             */
  
  #define CHATDIR       "/tmp/"   /* katalog, w ktorym zakładane będą sockety */
  
Pliki reprezentujące gniazda zazwyczaj tworzy się w katalogu /tmp. Nic nie stoi na przeszkodzie aby był to inny katalog ale w przypadku tego programu muszą mieć do niego dostęp wszyscy użytkownicy, którzy zamierzają się komunikować.
  main (int argc, char *argv[])
  {
    int                   sd, /* deskryptor lokalnego gniazda     */
                          cd, /* deskryptor zdalnego gniazda      */
                         len, /* wielkość adresu gniazda          */
                         ret; /* pomocnicza                       */
    struct sockaddr_un saddr, /* adres lokalnego gniazda          */
                       caddr; /* adres zdalnego gniazda           */
    char            buf[256], /* bufor na wiadomości przychodzące */
                   buf1[256]; /* bufor na wiadomości wychodzące   */
    fd_set               fds; /* zbiór deskryptorów               */
  
    if (argc != 2)
      {
        printf ("Uzycie:\n%s nazwa_uzytkownika\n", argv[0]);
  exit (1);
      }
  
    sd = socket (PF_UNIX, SOCK_DGRAM, 0);
    if (sd < 01)
      {
        perror ("socket()");
        exit (1);
      }
  
Tworzymy gniazdo PF_UNIX (nazwa domeny może być również podana, jako PF_LOCAL i PF_FILE). Typ gniazda ustawiamy na SOCK_DGRAM ale możliwe jest także stworzenie gniazda SOCK_STREAM. Wybraliśmy pierwszą możliwość ze względu na specyfikę działania programu. Umożliwia on bowiem odbieranie komunikatów od kilku różnych użytkowników naraz i dlatego bardzo niewygodne byłoby tworzenie osobnego połączenia dla każdego z użytkowników.
    chdir (CHATDIR);
  
    saddr.sun_family = PF_UNIX;
    strncpy (saddr.sun_path, getlogin (), UNIX_PATH_MAX);
  
Powyżej widzimy, czym różni się gniazdo PF_UNIX od np. PF_INET. Różni się sposobem adresowania. Struktura sockaddr_un wygląda tak (linux/un.h):
  #define UNIX_PATH_MAX   108
  struct sockaddr_un {
          sa_family_t sun_family;         /* AF_UNIX          */
          char sun_path[UNIX_PATH_MAX];   /* ścieżka do pliku */
  };
  
Pole sun_family musi zawierać wartość AF_UNIX. Pole sun_path to ciąg znaków, który musimy wypełnić ścieżką do pliku reprezetującego gniazdo.

W przypadku tego programu gniazdo zostanie utworzone w katalogu CHATDIR (chdir (CHATDIR)) i będzie miało taką, nazwę, jak użytkownika go używający (getlogin()).

    if (bind
        (sd, (struct sockaddr *) &saddr,
         sizeof (saddr.sun_family) + strlen (saddr.sun_path)) < 0)
      {
        perror ("bind()");
        exit (1);
      }
  
Przypisujemy gniazdu nazwę aby inni użytkownicy mogli wysyłać do nas wiadomośći. Jak widać zmienił się sposób obliczania długości adresu: dodajemy do siebie wielkość pola sun_family i długość ciągu sun_path.
    if (chmod (saddr.sun_path, S_IRUSR | S_IWUSR | S_IWOTH) < 0)
      {
        perror ("chmod()");
        exit (1);
      }
  
Ustawiamy odpowiednie prawa dostępu do nowoutworzonego pliku i zarazem do gniazda. Sobie dajemy prawa do odczytu (S_IRUSR) i zapisu (S_IWUSR), a reszcie użytkowników tylko prawo do zapisu (S_IWOTH). Dzięki temu inni użytkownicy będą mogli wysyłać wiadomości na adres naszego gniazda. Gdybyśmy korzystali z gniazda SOCK_STREAM musielibyśmy plikowi nadać również prawo do odczytu aby możliwe było połączenie się z nami (connect());
    signal (SIGINT, &sig\_handle);
  
Przejmujemy obsługę SIGINT. Oto funkcja sig_handle():
  void
  sig_handle (int signum)
  {
    if (unlink (getlogin ()) < 0)
      {
        perror ("unlink()");
        exit (1);
      }
    exit (0);
  }
  
Musimy pamiętać o usunięciu pliku reprezentującego gniazdo. Jeśli nie zrobimy tego samodzielnie to plik pozostanie w systemie nawet po zamknięciu samego gniazda. Do tego służy powyższa funkcja.
    caddr.sun_family = PF_UNIX;
    strncpy (caddr.sun_path, argv[1], UNIX_PATH_MAX);
  
Wypełniamy strukturę reprezentującą adres gniazda użytkownika, do którego będziemy wysyłać wiadomości. W lini komend, jako argument dla programu podajemy właśnie nazwę tego użytkownika.

Przechodzimy do głównej pętli:

    while (1)
      {
        FD_ZERO (&fds);
        FD_SET (0, &fds);
        FD_SET (sd, &fds);
  
        ret = select (sd + 1, &fds, NULL, NULL, NULL);
  
Już na początku widzimy nieznane makra. Są to makra pomocnicze dla funkcji select(). Funkcja ta oczekuje aż podane deskryptory zmienią swój stan, tzn. będą gotowe do zapisu lub do odczytu. Najpierw makrem FD_ZERO() czyścimy cały nasz zbiór deskryptorów. Następnie dzięki FD_SET() dodajemy do tego zbioru deskryptor opisujący stdin oraz deskryptor zdalnego gniazda. Po tych czynnościach pozostaje tylko wywołanie funkcji select() (man 2 select):
  #include <sys/time.h>
  #include <sys/types.h>
  #include <unistd.h>
  
  int  select(int  n,  fd_set  *readfds,  fd_set  *writefds,
  fd_set *exceptfds, struct timeval *timeout);
  
  n          - deskryptor o najwyższym numerze (w zbiorze) zwiększony o 1
  *readfds   - zbiór deskryptorów, który będzie monitorowany na możliwość odczytu
  *writefds  - to samo dla zapisu ...
  *exceptfds - ... i dla tzw. wyjątków
  *timeout   - wskaźnik do struktury opisującej limit czasowy, przez jaki funkcja
               będzie czekała na zmianę stanu któregoś z deskryptorów
  
Na razie brzmi to pewnie mało zrozumiale. Zaraz się wyjaśni. Wywołując select() tak, jak w programie, czekamy nieograniczoną ilość czasu (ostatni parametr NULL) aż, któryś z dwóch deskryptorów (stdin i gniazdo) będzie gotowy do odczytu (będą nowe dane do pobrania).
        if (ret < 0)
          {
            perror ("select()");
            exit (1);
          }
  
Jeśli doszliśmy do tego momentu to znaczy, że funkcja select() zwróciła sterowanie i że nadeszły jakieś dane do odczytu. Wywołanie select() zwraca albo ilość deskryptorów, które zmieniły stan albo 0 (upłynął limit czasowy) albo liczbę ujemną (błąd).
        if (FD_ISSET (0, &fds))
          {
            bzero (buf1, sizeof (buf1));
            fgets (buf1, sizeof (buf1), stdin);
            len = sizeof (caddr.sun_family) + strlen (caddr.sun_path);
            sendto (sd, buf1, strlen (buf1), 0, (struct sockaddr *) &caddr,
                    len);
          }
  
Po sprawdzeniu, czy nie pojawił się jakiś błąd wiemy już, że powinniśmy obsłużyć któryś z deskryptorów. Do sprawdzenia, który konkretnie obsłużyć używamy makra FD_ISSET(). Zwróci ono prawdę jeśli podany deskryptor znajduje się w zbiorze (funkcja select() zostawia w zbiorze tylko te deskryptory, które zmieniły stan, resztę czyści). Sprawdziliśmy więc, czy jest coś do odczytania ze standardowego wejścia, czyli mówiąc wprost, czy użytkownik chce wpisać jakiś komunikat do wysłania. W takim przypadku wywołujemy fgets(), które zapisze do bufora buf1 całą linijkę wpisaną przez użytkownika. Następnie tą linijkę wysyłamy do użytkownika, którego podaliśmy w linni komend.
        if (FD_ISSET (sd, &fds))
          {
            len = sizeof (saddr.sun_family) + strlen (saddr.sun_path);
            bzero (buf, sizeof (buf));
            recvfrom (sd, buf, sizeof (buf), 0,
                      (struct sockaddr *) &saddr, &len);
            printf ("[%s] %s", saddr.sun_path, buf);
  }
      }
  }
  
Pozostało jeszcze sprawdzić, czy równocześnie do nadeszły jakieś wiadomość od zdalnych użytkowników (czyli z gniazda sd). Jeśli tak to wczytujemy je do bufora buf, a następnie wyświetlamy na ekranie poprzedzając wiadomość nazwą użytkownika, który ją do nas wysłał.

Jak widać nie ma "normalnego" sposobu na wyjście z programu. Trzeba go zabić albo wcisnąć po prostu C.


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