inf sc w4


Interfejs gniazd.

Gniazda TCP.

Gniazda (sockets) to abstrakcyjne mechanizmy umożliwiające wykonywanie
systemowych funkcji wejścia
wyjścia w odniesieniu do sieci. Gniazda zostały
zaprojektowane w Berkeley na potrzeby Unix BSD. Istnieje grupa funkcji
systemowych obsługujących gniazda, funkcje te stanowią API (Application Program
Interface).
Gniazda umożliwiają między innymi przesyłanie danych między procesami
działającymi na komputerach w sieci z wykorzystaniem połączeń TCP lub protokołu
UDP, przy czym same operacje wysyłania i odbierania danych przypominają zwykłe
operacje zapisywania i odczytu z pliku.


Gniazdowe struktury adresowe.

W wielu funkcjach operujących na gniazdach należy podać wskaźnik do struktury
adresowej. Dla różnych rodzin protokołów zdefiniowano różne struktury
adresowe.

Dla Ipv4 struktura ta nazywa siÄ™ sockaddr_in.

struct sockaddr_in {
uint8_t sin_len; // długość struktury (16)
sa_family_t sin_family; // rodzina adresow: AF_INET
u_int16_t sin_port; // nr portu w tzw. sieciowej kolejności // bajtów
struct in_addr sin_addr; // 32 bitowy adres IP w sieciowej // kolejności
bajtów
char sin_zero[8] // nieużywane
};

Adres intrernetowy jest właściwie w strukturze:
struct in_addr {
u_int32_t s_addr; adres IP
};


W różnych systemach definicje mogą się różnić od powyższej. W linuxie sprobuj
man 7 ip do wyswietlenia opisu.

Ogólna struktura adresowa gniazda (zdefiniowana w pliku nagłówkowym
:

struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};

Założono wykorzystywanie gniazd dla dowolnej rodziny protokołów obsługiwanej
przez system operacyjny, dlatego funkcjom przekazuje się wskaźnik do
odpowiedniej struktury rzutowany na sockaddr. Funkcje gniazd powstały przed
wprowadzeniem do standardu ANSI C void *.

Funkcje przekształcania adresu: inet_aton(), inet_ntoa(), inet_pton(),
inet_ntop().

int inet_aton(const char *strptr, struct in_addr *addrptr);

Przekształca adres w notacji "kropkowej" (np. "149.156.65.43") zapisany jako
napis w C na liczbę 32 bitową w sieciowej kolejności bajtów.
Funkcja inet_aton() zwraca 1 jeśli napis był poprawny, 0 jeśi wystąpił błąd.

char *inet_ntoa(struct in_addr inaddr);

Zwraca wskaźnik do napisu w notacji “kropkowej".

Podobnie działają funkcje inet_pton(), inet_ntop() (patrz man).

Kolejność bajtów sieciowa i hosta.
Kolejności:
little endian - pierwszeństwo bajtu mniej znaczącego,
big endian
pierwszeństwo bajtu bardziej znaczącego.

Funkcje htons(), ntohs(), htonl(), ntohl().

(Host
to
Net, Net
to Host połączone z short lub long).













































Schemat typowej komunikacji między klientem a serwerem
w TCP dla serwera iteracyjnego.




Schemat działania serwera iteracyjnego:

s=socket();

connect();

bind(s,...);

listen(s,...);


while (1)
{
t=accept(s,...);
Obsluguj_klienta(t,);
close(t);
}


Schemat działania serwera współbieżnego:

s=socket();

connect();

bind(s,...);

listen(s,...);

while (1)
{
t=accept(s,...); // blokujące oczekiwanie na połączenie
if ( ! (pid=fork()) ){
close(s);
Obsluguj_klienta(t,);
close(t);
}
else close(t);
}









Przykładowy klient usługi whois.
/*Przyjmujemy arbitralny numer portu 51900 dla tej uslugi (standardowo w Unixie
jest to 43) */
#include
#include
#include
#include
#include

main(int argc, char *argv[])
{
int s; // Deskryptor gniazda
int len; // Dlugosc odebranych danych (w bajtach)
struct sockaddr_in sa; /* Internetowa struktura adresu gniazda (IPv4)
Jej postac moze sie roznic w roznych systemach.
W linuxie sprobuj man 7 ip do wyswietlenia opisu.
Przykladowo:
struct sockaddr_in {
sa_family_t sin_family; rodzina adresow: AF_INET
u_int16_t sin_port; nr portu
struct in_addr sin_addr;
};

Adres intrernetowy:
struct in_addr {
u_int32_t s_addr; adres IP
};
Typy: u_int16_t (16 bitowa liczba calkowita bez znaku)
u_int32_t (32 bitowa liczba calkowita bez znaku) sa
zdefiniowane w
Typ sa_family_t jest zdefiniowany w */

struct hostent *hp; /* struktura przechowujaca informacje o komputerze:
nazwa, adresy IP. Definicja w netdb.h
Wskaznik do struktury jest zwracany np. przez
funkcje gethostbyname.
Sprawdz man gethostbyname dla opisu struktury
hostent i funkcji gethostbyname.
Przykladowo w linuxie:
struct hostent{
char * h_name; nazwa hosta
char ** h_aliases; lista aliasow
int h_addrtype; typ adresu
int h_length; dlugosc adresu
char ** h_addr_list; lista adresow IP
}
Uwaga! We wczesnych implementacjach zamiast
listy adresow IP byl pojedynczy adres
char * h_addr. Dla kompatybilnosci w netdb.h
dodaje sie #define h_addr h_addr_list
*/
char buf[BUFSIZ+1]; // Bufor
char *progname; // Podstawiamy wskaznik do nazwy programu (argv[0]
char *host; // Wskaznik do nazwy komputera odleglego
char *user; // Wskaznik do napisu okreslajacego nazwe konta
progname=argv[0];

// Sprawdz, czy program uruchomiono z dwoma argumentami
if(argc!=3){
fprintf(stderr,"Uzycie:%s maszyna uzytkownik\n",progname);
exit(1);
}

host=argv[1];
user=argv[2];

// Ustal adres (i inne dane)komputera odleglego. Funkcja zwraca wskaznik do
// struktury hostent (patrz wyzej)
if ((hp=gethostbyname(host))==NULL){
fprintf(stderr,"%s: nie znalazlem komputera %s\n",progname,host);
exit(1);
}

// Skopiuj adres IP z hp->h_addr do sa.sin_addr
memcpy(&sa.sin_addr, hp->h_addr , hp->h_length);

sa.sin_family=hp->h_addrtype;

// Ustal numer portu. Htons powoduje zamiane na porzadek sieci
sa.sin_port=htons(51900);

// ntohs zamienia zapis liczby na porzadek hosta
printf("\nPort:%d\n",ntohs(sa.sin_port));

// Otworz gniazdo. Funkcja socket zwraca deskryptor gniazda
// lub -1 w razie bledu. Sprawdz man socket.
if((s=socket(hp->h_addrtype,SOCK_STREAM,0))<0){
perror("socket");
exit(1);
}

// Podlacz gniazdo do serwera. Sprawdz man connect.
// Standardowo wymaga rzutowania na typ wskazujacy na
// strukture struct sockaddr
if(connect(s,(struct sockaddr *) &sa, sizeof(sa))<0){
perror("connect");
exit(1);
}

// Wyslij zapytanie do serwera. Jesli nie uda sie wyslac
// do gniazda tylu bajtow ile wynosi dlugosc napisu *user
// to blad zapisu.
if (write(s,user,strlen(user)) != strlen(user)){
fprintf(stderr, "%s: blad zapisu",progname);
exit(1);
}

// Dopoki serwer nie zakonczyl pisania czytaj dane do bufora.
// Odpowiedz serwera moze dojsc "w kawalkach".
// Deskryptor pliku 1 uzyty w funkcji write oznacza
// standardowe wyjscie.
while ((len=read(s,buf,BUFSIZ))>0)
write(1,buf,len);

// Zamknij gniazdo
close(s);

// Zakoncz z kodem bledu 0
exit(0);

}


Przykładowy serwer usługi whois.

/* Przyjeto arbitralny numer portu 51900
(standardowo w Unixie jest to 43). Serwer obsluguje jednoczesnie
jednego klienta. Ewentualne odwolania innych klientow ustawiane sa
w kolejce o dlugosci zdefiniowanej przez DL_KOLEJKI*/
#include
#include
#include
#include
#include
#include

#define DL_KOLEJKI 5
#define MAXHOSTNAME 32

int main(int argc, char * argv[])
{
int s,t; // Deskryptory gniazd
int i; // Zmienna robocza
struct sockaddr_in sa, isa; // Internetowa struktura adresowa gniazda
// Opis w programie klient
struct hostent *hp; // Struktura przechowujaca dane o komputerze
// Opis w programie klient
char *progname; // wskaznik do nazwy programu
char localhost[MAXHOSTNAME+1];// nazwa lokalnego komputera jako napis

progname=argv[0];

// Odczytaj nazwe komputera lokalnego
gethostname(localhost,MAXHOSTNAME);
// Odczytaj dane o kpmputerze lokalnym (m.in. numer IP)
if((hp=gethostbyname(localhost))==NULL){
fprintf(stderr, "%s: Nie znalazlem komputera %s\n",
progname);
exit(1);
}

// Ustal numer portu
sa.sin_port = htons(1900);

// Skopiuj adres IP z hp->h_addr do sa.sin_addr
memcpy(&sa.sin_addr,hp->h_addr, hp->h_length);
//Mozna to zrobic inaczej:
//bcopy((char *)hp->h_addr, (char *)&sa.sin_addr, hp->h_length);

sa.sin_family=hp->h_addrtype;

// Otworz gniazdo. Funkcja socket zwraca deskryptor gniazda
// lub -1 w razie bledu. Sprawdz man socket.
if ((s=socket(hp->h_addrtype, SOCK_STREAM,0))<0){
perror("socket");
exit(1);
}

// Zwiaz gniazdo z numerem IP i numerem portu (przypisanie
// lokalnego adresu protokolowego. Wymaga rzutowania drugiego
// argumentu na wskaznik do struktury struct sockaddr. Zwraca
// zero jesli wszystko ok, -1 w przeciwnym wypadku.
if ( ( bind(s,(struct sockaddr *) &sa,sizeof(sa))<0)){
perror("bind");
exit(1);
}

// Ustaw gniazdo w stan nasluchiwania (gniazdo bierne)
// i ustal maksymalna liczbe polaczen, ktore jadro systemu powinno
// ustawic w kolejce do tego gniazda (DL_KOLEJKI)
listen(s,DL_KOLEJKI);

// Wejdz w nieskonczona petle w oczekiwaniu na polaczenia.
while(1){
i=sizeof isa;
// Zawieszenie na funkcji accept w oczekiwaniu na klientow
// (uspienie procesu). Patrz man 2 accept. Funkcja accept tworzy
// nowe gniazdo dla polaczenia z klientem i obslugi klienta.
// Gniazdo nasluchujace dalej sluzy tylko do nasluchu.
if((t=accept(s,(struct sockaddr *) &isa,&i))<0){
perror("accept");
exit(1);
}
whois(t); // Wykonanie uslugi whois (patrz nizej)
close(t);
}
}


whois(int sock)
{
struct passwd *p; // struktura przechowujaca dane z linijki w /etc/passwd
// Patrz man getpwnam.
char buf[BUFSIZ+1];
int i;

// Odczytaj zapytanie od klienta
if ((i=read(sock,buf,BUFSIZ))<=0)
return;

// Zakoncz napis standardowym znakiem \0
buf[i]='\0';

// getpwnam zwraca wskaznik do struktury zawierajacej
// pola linijki z /etc/passwd, ktĄra odpowiada
// uzytkownikowi o nazwie przekazanej jako parametr.
if((p=getpwnam(buf))==NULL)
strcpy(buf,"Nie ma takiego uzytkownika\n");
else
sprintf(buf,"%s: %s\n",p->pw_name, p->pw_gecos);

// Wpisz bufor do gniazda (wyslij dane do klienta)
write(sock,buf,strlen(buf));

return;
}



Ważne zagadnienia objaśniane na podstawie przykładowych programów.

Działanie funkcji fork().

Obsługa sygnałów.

Sygnał jest informacją dla procesu, że wystąpiło jakieś zdarzenie. Sygnały
nazywane są też przerwaniami programowymi. Sygnały są na ogół wysyłane
asynchronicznie.

Sygnały są wysyłane z procesu do procesu (również siebie samego) oraz z jądra
do procesu.

Gdy kończy się proces potomny, wówczas jądro systemu przesyła do procesu
macierzystego sygnał SIGCHLD. Proces potomny staje się zombie (defunct),
umożliwiając procesowi macierzystemu pobranie informacji o rozmiarach zasobów
użytych przez proces potomny. Aby nie był tworzony zombie należy wywołać
funkcję wait() w funkcji obsługującej sygnał SIGCHLD.

signal(SIGCHLD, sig_chld);
.
void sig_chld(int signo)
{
pid_t pid;
int stat;

pid = wait(&stat);
return;
}

Takie rozwiązanie może się jednak zakończyć przerwaniem wykonywania programu
(procesu macierzystego), jeśli sygnał nadszedł w momencie blokowania na funkcji
accept(). Nie we wszystkich systemach jÄ…dro wznawia przerwane funkcje
systemowe. Funkcja może być zakończona z kodem błędu EINTR. Zatem należy dodać
obsługę tego błedu, np.
if ( t = accept() < 0){
if (errno == EINTR)
continue; // powrót do pętli, w której wywoływana jest f. accept();
else
perror("accept");
}

Najprostszy sposób (niestety nie działający we wszystkich systemach) na
uniknięcie kłopotów to zlecenie ignorowania sygnału SIGCHLD. Robi się to z
użyciem funkcji signal():

signal(SIGCHLD, SIG_IGN);






Gniazda UDP.






































Schemat typowej komunikacji między klientem a serwerem w UDP.


Przypisanie numeru portu lokalnego (efemerycznego) następuje przy pierwszym
wywołaniu funkcji sendto().
Klient nie ustanawia połączenia z serwerem, jedynie wysyła datagramy. Serwer
odbiera datagramy od dowolnych klientów. Funkcje recfrom() zwraca adres
klienta, zatem serwer może przesłać odpowiedź.
#include

ssize_t recvfrom(int socket, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);

ssize_t sendto(int socket, const void *buff, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t addrlen);

Funkcje te zwracają liczbę odesłanych albo pobranych bajtów lub
1 jeśli
wystąpił błąd.

Przykładowy klient UDP
klient echa.

#include
#define MAXLINE ....

int main (int argc, char ** argv)
{
int s;
struct sockaddr_in servaddr;
int n;
char sendbuff[MAXLINE], recvbuff[MAXLINE+1];

if ( argc != 2){
printf(“Uzycie: klient adresIP");
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons( 51000 );

// wstawienie numeru IP do servaddr
inet_pton ( AF_INET, argv[1], &servaddr.sin_addr);

s = socket (AF_INET, SOCK_DGRAM, 0);
if (p < 0 ){
perror("socket");
exit(1);
}

// fgets wczytuje linię ze standardowego wejścia
// fputs wypisuje linię na standardowe wyjście
while (fgets (sendbuff, MAXLINE, stdin) != NULL) {
sendto(s, sendbuff, strlen(sendbuff),0,&servaddr, sizeof(servaddr));
/* Poniżej NULL oznacza, że klient odbiera odpowiedź od dowolnego serwera */
n = recvfrom(s, recvbuff, MAXLINE, 0, NULL, NULL);
recvbuff[n] = 0; // zakończenie napisu
fputs(recvbuff, stdout);
}
}







Fragment klient echa
wersja z zarządzaniem czasem oczekiwania przez sygnał
SIGALRM.
...
// ustanowienie procedury obslugi sygnalu SIGALRM
signal(SIGALRM, sig_alrm);

while (fgets (sendbuff, MAXLINE, stdin) != NULL) {
sendto(s, sendbuff, strlen(sendbuff),0,&servaddr, sizeof(servaddr));
// ustawienie 5-cio sekundowego czasu oczekiwania
alarm(5);

/* Poniżej NULL oznacza, że klient odbiera odpowiedź od dowolnego serwera */
n = recvfrom(s, recvbuff, MAXLINE, 0, NULL, NULL);

if (n < 0)
if (errno == EINTR)
fprintf(stderr, "Przekroczony czas oczekiwania\n“);
else
perror("Blad gniazda");
else {
alarm(0);
recvbuff[n] = 0; // zakończenie napisu
fputs(recvbuff, stdout);
}
}

// Procedura obslugi sygnalu SIGALRM
void sig_alrm( int signo )
{
// Tylko przerywamy wykonywanie funkcji recvfrom()
return;
}

Przykładowy serwer UDP
serwer echa.
#include

int main (int argc, char ** argv)
{
int s;
struct sockaddr_in servaddr, cliaddr;
int n;
int len;
char buf[MAXLINE];

s = socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(51000);

bind(s, (sockaddr *) &servaddr, sizeof(servaddr));

for ( ; ; ){
n = recvfrom(s, buf, MAXLINE, 0, &cliaddr, &len);
sendto(s, buf, n, 0, cliaddr, len);
}
}


Większość serwerów UDP to serwery iteracyjne. Można jednak tworzyć współbieżne
(patrz Stevens).

Gniazda UDP niepołączone i połączone.

Gniazdo niepołączone to takie, jak powyżej.
Gniazdo połączone powstaje w wyniku wywołania funkcji connect() dla gniazda
UDP. Dla gniazda połączonego:
Dla operacji wyjścia adres docelowy jest już zapamiętany i nie można go
zmienić. W szczególności zamiast funkcji sendto() należy używać write() (lub
send).
Podobnie nie stosuje siÄ™ funkcji recvfrom(), tylko read().





Literatura:
W.R. Stevens: Unix
programowanie usług sieciowych, tom 1, API: gniazda. WNT,
Warszawa 2000.


Wyszukiwarka

Podobne podstrony:
inf sc w2
inf sc w1
inf sc w5
inf sc w3
PÄ…czki twarogowe
inf rak mutg
Panasonic SC HT1000
inf kolo1
AiSD w4 sortowanie2
F2 W4 dielektryki
inf stos) 4
w4
ML1 W4 1 (2)
OPEL sc 303
W4 MECH EN

więcej podobnych podstron