srw-rw-rw- 1 root root 0 Jan 6 13:09 /dev/logLitera '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.