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 | | aaaaaaaabbbbbbbbTak 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);
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.