* The Linux
Programmer's
Guide:
Gniazda sieciowe - podstawy
Zaawansowane programowanie gniazd
UWAGA: to jest bardzo dobra
dokumentacja
fun sys Unixa/Linuxa w języku polskim !
--> "gniazdko" to końcowy punkt kanału komunikacyjnego,
przez który proces może wysyłać lub
odbierać dane
--> gniazdka mogą pracować w następujących dziedzinach
:
* internetowej (AF_INET)
- umozliwiaja tworzenie aplikacji klient/serwer
ktorych procesy pracuja na różnych maszynach
* uniksowej (AF_UNIX)
- umozliwiaja tworzenie aplikacji klient/serwer
ktorych procesy pracuja na jednej maszynie
(można to zrobić także przy pomocy łączy nazwanych, lecz jest to
uciążliwe
!)
- obsluguje sie je dokładnie tak samo jak w dziedzinie AF_INET
- gniazdko jest (moze byc) widoczne jako plik w strukturze katalogow
--> są dwa typy gniazdek :
* datagramowe (SOCK_DGRAM)
- zaimplementowane w dziedzinie AF_INET przez protokol UDP
- przesyła się komunikaty (=datagramy)
- nie nawiązuje się połączenia
- nie ma gwarancji że dane dotrą do miejsca przeznaczenia
* strumieniowe (SOCK_STREAM)
- zaimplementowane w dziedzinie AF_INET przez protokol TCP
- nawiązuje się połączenie
- przesyła się dowolne ciągi bajtow (w obie strony)
- gniazda strumieniowe zachowują się zupełnie jak łącza !
- jest automatyczne potwierdzanie dostarczenia danych
- istnieja bufory na obu końcach polaczenia, gromadzące dane
- nie mylić pojęć "gniazdka strumieniowe" i "gniazdka zrobione ze
strumieni"
UWAGA: gniazdka obojga typów występują w obu dziedzinach !
--> powiązanie, adresowanie oraz inne pojęcia ...
* "powiązanie" w dziedzinie
AF_INET
jest zdefiniowane przez:
1) protokol transportu: TCP lub UDP
2) adres_internetowy_1, nr_portu_1
// to jest 1 koniec powiazania (można go utożsamiać z 1 gniazdkiem)
3) adres_internetowy_2, nr_portu_2
// to jest 2 koniec powiazania (można go utożsamiać z 2 gniazdkiem)
* adresowanie w dziedzinie AF_INET
(czyli jak się
definiuje "jeden koniec powiązania")
struct sockaddr_in adres;* do czego służy nr_portu w dziedzinie AF_INET ???
// w tej strukturze przechowuje sie adres i port
// jest ona zdefiniowana w pliku "netinet/in.h"
memset(&adres,0,sizeof(adres));
// wyzerowanie
adres.sin_family=AF_INET;
adres.sin_addr.s_addr=inet_addr("1.2.3.4");
// funkcja "inet_addr (char *)"
// przeksztalca string z adresem "1.2.3.4"
// (musi to byc adres kropkowy/cyfrowy !)
// na odpowiedni "long int"
adres.sin_port=htons(7000);
// "htons()" uniezaleznia nas od sposobu reprezentacji
// "short" na naszej maszynie (ktory to sposob moze byc
// inny na odleglej maszynie !)
* adresowanie w dziedzinie AF_UNIX
struct sockaddr_un adres;
memset(&adres,0,sizeof(adres));
adres.sun_family=AF_UNIX;
strcpy(adres.sun_path,"gniazdko");
// ten plik będzie widoczny dla innych procesów
--> co powinien robić klient, a co serwer
(omawiamy tutaj dziedzinę INTERNETOWĄ,
gniazdka STRUMIENIOWE)
*** klient
***
int sock;
sock=socket (AF_INET,SOCK_STREAM,0);
2) connect() // podlacza sie do serwera
// operacja moze blokowac !!!
struct sockaddr_in adres;
memset(&adres,0,sizeof(adres));
adres.sin_family=AF_INET;
adres.sin_addr.s_addr=inet_addr("1.2.3.4");
adres.sin_port=htons(7000);
// to jest oczywiscie adres serwera !!
connect(sock,(struct sockaddr *)&adres,sizeof(adres));
3) write(sock, ...), read(sock, ...), ...
// uzywa gniazdka do komunikacji z serwerem
// połączenie jest dwukierunkowe !
// moze blokowac !!!
4) close(sock) // zamyka gniazdko
int sock;
sock=socket (AF_INET,SOCK_STREAM,0);
2) bind() // definiuje adres (jesli to konieczne)
// oraz numer portu (koniecznie) na ktorym bedzie oczekiwal na klientow
struct sockaddr_in adres;
memset(&adres,0,sizeof(adres));
adres.sin_family=AF_INET;
adres.sin_addr.s_addr=INADDR_ANY;
// tak mozna zrobic jesli jest jedna karta sieciowa !!!
adres.sin_port=htons(7000);
bind(sock,(struct sockaddr *)&adres,sizeof(adres));
3) listen() // serwer staje sie gotowy do przyjmowania klientow
listen(sock,5); // gniazdko staje się passywne
// liczba oznacza maksymalna ilosc klientow ktorzy moga
// czekac na obsluge
4) accept() // czeka na klientow
// dla kazdego klienta funkcja zwraca NOWE gniazdko (aktywne)
// służące do komunikacji z tym klientem
// (powinno sie utworzyc proces potomny, ktory będzie obsługiwał
klienta)
// moze blokowac !!!
int nowy_sock=accept(sock,NULL,NULL);
5) write(nowy_sock,...), read(nowy_sock,...), ...
// uzywa nowego gniazdka aby komunikowac sie z klientem
// moze blokowac !!!
6) close(nowy_sock) // zamyka gniazdko (NOWE !)
Uwaga: gniazdka strumieniowe doskonale pasują do modelu klient/serwer ... |
--> funkcje pomocnicze
// konwersja zapisu
liczb calkowitych z formatu "maszynowego" na "sieciowy"
// i odwrotnie
htons(), ntohs() // konwersja "unsigned
short"
htonl(), ntohl() // konwersja "unsigned long"
// z maszynowego do sieciowego "hton" = "host to net"
// z sieciowego do maszynowego "ntoh" = "net to host"
unsigned int inet_addr (char *string) ;
// konwersja adresu
typu "1.2.3.4" na "unsigned long"
// (a wlasciwie
"unsigned
int"; zdaje sie ze tutaj "unsigned int" ma 32 bity !?)
char *inet_ntoa (struct in_addr net_addr);
// wykonuje odwrotna
czynnosc
struct hostent *gethostbyname(char * hostname);
// hostname to nazwa typu
"main.amu.edu.pl"
// funkcja ta tworzy
strukture
"hostent"
// zawierajaca adresy
maszyny
w postaci 32-bitowej
// przyklad zastosowania w pliku "sock01.cc"
// w funkcji "AdresCyfrowyKropkowy()"
struct hostent {
char * h_name; /*
official name of host */
char ** h_aliases; /* alias list */
short h_addrtype; /* host address
type
*/
short h_length; /*
length
of address */
char ** h_addr_list; /* list of addresses */
};
getsockname();
// pobiera adres z gniazdka
getpeername();
// pobiera adres z gniazdka po
przeciwnej
stronie
getsockopt(); // konfigurowane gniazdek
setsockopt();
//
odczytywanie dlugosci bufora "nadawczego" i "odbiorczego"
//
(AF_INET, gniazda strumieniowe)
int optval,
optlen=sizeof(optval);
getsockopt(sock,
SOL_SOCKET, SO_SNDBUF, &optval, &optlen);
getsockopt(sock,
SOL_SOCKET, SO_RCVBUF, &optval, &optlen);
/* SOL_SOCKET oznacza, że chodzi o opcje tzw "warstwy gniazd";
mozna także czytac/pisac opcje innych "warstw"
*/
fcntl(sock, F_SETFL,
O_NONBLOCK|fcntl(sock,F_GETFL,0));
/*gniazdko mozna przelaczyc w tryb NIEblokujacy
tak jak w przypadku innych deskryptorow;
mozna takze w tradycyjny sposob uzywac funkcji "select()"
*/
--> struktury: "in_addr", "sockaddr_in", "sockaddr_un", ...
/*
* Internet address
*/
struct in_addr {
union { // chodzi o dostep do poszczegolnych
// czesci adresu internetowego !!!
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
};
#define s_addr S_un.S_addr
#define s_host S_un.S_un_b.s_b2
#define s_net S_un.S_un_b.s_b1
#define s_imp S_un.S_un_w.s_w2
#define s_impno S_un.S_un_b.s_b4
#define s_lh S_un.S_un_b.s_b3
/*
* Socket address, internet style.
*/
struct sockaddr_in {
short sin_family; /* AF_INET */
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
/*
* Socket address, unix style.
*/
struct sockaddr_un {
short sun_family; /* AF_UNIX */
char sun_path[104]; /* sciezka
do pliku */
}
Uwaga: programy używające gniazd w dziedzinie INTERNETOWEJ i w dziedzinie UNIKSOWEJ różnią się jedynie sposobem określania adresu ... |
--> najprostszy przykład użycia gniazdek: sock01.cc(+unix.h)
Uwaga: przy wielokrotnym włączaniu programu sock01 trzeba chwilę odczekać; protokoły sieciowe są tak zaprojektowane aby złośliwy użytkownik nie mógł się "podszyć" pod serwer w razie jego nagłego wyłączenia ...
Zadanie 60
Zadanie 61
--> tutaj omawiamy dziedzine INTERNETOWA, gniazdka DATAGRAMOWE
(2 procesy "proces nr 1" i "proces nr
2" zamierzaja sie
komunikowac; co powinny zrobic ???)
UWAGA: unikam okreslen klient/serwer
poniewaz gniazdka datagramowe
nie narzucaja tego
modelu (tak jak to
robily gniazdka strumieniowe);
komunikujace sie
procesy sa tutaj równorzędne
!
"proces nr 1" zachowuje się jak klient
(tj wysyla dane do "proc nr 2" i oczekuje odpowiedzi)
"proces nr 2" zachowuje się jak serwer
(pobiera dane i zaraz je odsyla do nadawcy)
*** proces nr 1
***
1) socket()
// tworzy gniazdko
int sock;
sock=socket (AF_INET,SOCK_DGRAM,0);
2) bind() // definiuje adres oraz numer portu
// (jest to
konieczne jesli zamierza odbierac dane !!!)
struct sockaddr_in adres;
memset(&adres,0,sizeof(adres));
adres.sin_family=AF_INET;
adres.sin_addr.s_addr=INADDR_ANY; // adres i port wybierane przez system
adres.sin_port=htons(0);
bind(sock,(struct sockaddr *)&adres,sizeof(adres));
3) sendto(), recvfrom() // wysyla datagramy i je odbiera
// "recvfrom()" moze blokowac !!!
UWAGA: nalezy podac do kogo komunikat ma byc wysylany
z tego samego gniazdka
mozna wysylac datagramy
do roznych adresatow
!!!
struct sockaddr_in odlegly; // tu bedzie adres odbiorcy datagramu
int odlegly_len=sizeof(odlegly);
memset(&odlegly,0,sizeof(odlegly));
odlegly.sin_family=AF_INET;
odlegly.sin_addr.s_addr=inet_addr(AdresCyfrowyKropkowy("venus"));
odlegly.sin_port=htons(7000);
/*
int sendto (
int socket,
char *message_addr,
int length,
int flags,
struct sockaddr *dest_addr,
int dest_len );
*/
char *c="1234567890";
i=sendto(sock,c,strlen(c),0,(struct sockaddr *)&odlegly,odlegly_len);
if(i==-1) { // obsluga bledow
}
/* int recvfrom(
int socket,
char *buffer,
int length,
int flags,
struct sockaddr *address,
int *address_len) ;
*/
char buf[100];
i=recvfrom(sock,buf,99,0,(struct sockaddr *)&odlegly,&odlegly_len);
// w strukt. odlegly dostaniemy "nadawce" datagramu !!!
// (w tym wypadku ta informacja nie ma znaczenia)
// -> pocztkowo odlegly_len==sizeof(odlegly)
// -> po wywolaniu recvfrom() odlegly_len zawiera
// rzeczywista dlugosc adresu ...
if(i==-1) { // obsluga bledow
}
4) close() // zamyka gniazdko
int sock;
sock=socket (AF_INET,SOCK_DGRAM,0);
2) bind() // definiuje adres oraz numer portu
// (jest to
konieczne jesli zamierza odbierac dane)
struct sockaddr_in adres;
memset(&adres,0,sizeof(adres));
adres.sin_family=AF_INET;
//adres.sin_addr.s_addr=inet_addr(AdresCyfrowyKropkowy("venus"));
adres.sin_addr.s_addr=INADDR_ANY;
adres.sin_port=htons(7000);
// serwer czeka na datagramy z docelowym nr portu= 7000
bind(sock,(struct sockaddr *)&adres,sizeof(adres));
3) recvfrom(), sendto() // odbiera datagramy i je wysyla
// "recvfrom()" moze blokowac !!!
UWAGA: po odebraniu datagramu przez
"recvfrom()"
mozna odczytac
od kogo ten datagram pochodzi;
informacje te
mozna wykorzystac aby odeslac
odpowiedz !!!
/* int recvfrom(
int socket,
char *buffer,
int length,
int flags,
struct sockaddr *address,
int *address_len) ;
*/
struct sockaddr_in odlegly;
int odlegly_len=sizeof(odlegly);
char buf[100];
i=recvfrom(sock,buf,99,0,(struct sockaddr *)&odlegly,&odlegly_len);
// w strukt. odlegly dostaniemy "nadawce" datagramu !!!
// -> pocztkowo odlegly_len==sizeof(odlegly)
// -> po wywolaniu recvfrom() odlegly_len zawiera
// rzeczywista dlugosc adresu ...
if(i==-1) { // obsluga bledow
}
/* int sendto (
int socket,
char *message_addr,
int length,
int flags,
struct sockaddr *dest_addr,
int dest_len );
*/
j=sendto(sock,buf,i,0,(struct sockaddr *)&odlegly,odlegly_len);
if(j==-1) { // obsluga bledow
}
4) close() // zamyka gniazdko
Zadanie 63
Zadanie 64
.......................................................................
Można zmieniać pewne własności terminala czyli go "konfigurować" przy pomocy funcji ioctl() oraz struktury termio lub termios :
struct termio t1, t2;
ioctl(0,TCGETA,&t1); // odczytanie parametrów terminala
// znajdą się one w strukturze termio
t2=t1;
t1.c_lflag &= ~ECHO;
// wyłączenie echa (podczas wpisywania znaków)
t1.c_lflag &= ~ICANON;
// wyłączenie tzw trybu kanonicznego (wyjaśnione niżej)
ioctl(0,TCSETA,&t1); // zmiana parametrów terminala
char c;
while( read(0,&c,1)==1 ) write(1,&c,1);
// jakieś operacje na terminalu ...
ioctl(0,TCSETA,&t2); // przywrócenie początkowych parametrów
Struktura "termio" ma następującą budowę:
struct termio {
unsigned short c_iflag; /* Input modes */
unsigned short c_oflag; /* Output modes */
unsigned short c_cflag; /* Control modes */
unsigned short c_lflag; /* Line discipline modes */
char c_line; /* Line discipline */
unsigned char c_cc[NCC]; /* Control characters */
};
Informacje o tym co można "zrobić" z terminalem, w jakich trybach może się on znajdować itp, można uzyskać przy pomocy polecenia:
man termios
# dotyczy Linux-a
man 7 tty
# dotyczy "Digital Unix"-a
W pliku tty7.txt znajduje się zawartość wyciągnięta z manuala.
Krótki opis terminala tekstowego:
Tryb kanoniczny terminala polega głównie na tym, że dane z terminala są wczytywane "liniami". Dopiero po wprowadzeniu linii i po wykonaniu ewentualnych modyfikacji (można kasować znaki z prawej strony) oraz po naciśnięciu klawisza ENTER funkcja read(0,...) przestanie blokować i może odczytywać znaki linii w jej ostatecznej postaci !. W trybie niekanonicznym znaki wpisywane na terminalu są dostępne natychmiast. [Dokładniejszy opis znajduje się w książce Bacha, str 354, rozdział 10.3]
Wyłączenie echa powoduje, że znaki które wpisujemy z klawiatury nie pojawiają się automatycznie na terminalu. Zmienna c_cc[VMIN] w trybie niekanonicznym oznacza minimalną liczbę znaków, które należy wpisać, aby stały się dostępne dla funkcji read() (jeśli c_cc[VMIN]==1 to znaki są dostępne od razu).
Przykład tty02.cc to eksperymenty z wyłączaniem trybu kanonicznego oraz echa. Przykład ten powinien działać z każdym typem terminala (także "xterm"), w każdej wersji Unixa, kompatybilnej z SVR4 (System V Release 4).
Zauważmy, że wyłączenie echa i trybu kanonicznego pozwoliło by nam
na zaprogramowanie "historii poleceń" oraz wygodnej "edycji
wprowadzanych komend" w naszej mini-powłoce !.
Przypuśćmy, że mamy dwa (otwarte) deskryptory x1 i y1, oraz zamierzamy napisać program przepisujący dane z x1 do y1. Oczywiście program taki powinen zawierać następujący kod:
char c;
while( read(x1, &c, 1)==1 ) write(y1, &c, 1);
Co zrobić jeśli mamy deskryptory x1, x2 i y1, y2 oraz zamierzamy (w jednym procesie) przepisywac dane z x1 do y1 oraz z x2 do y2 ? Jednym z możliwych rozwiązań jest przełączenie tych deskryptorów w tryb "nieblokujący", przy pomocy funkcji fcntl():
fcntl(x1,F_SETFL, fcntl(x1,F_GETFL)|O_NONBLOCK );
...
Stała F_GETFL oznacza odczytywanie "file status flag" (tych samych, które podaje sie przy otwieraniu pliku funkcją open()), natomiast F_SETFL służy do zmiany tych flag. Flaga O_NONBLOCK wprowadza deskryptor w tzw tryb nieblokujący: jeśli wywołanie jakiejść funkcji systemowej z tym deskryptorem mogłoby blokować proces, to w trybie nieblokującym funkcja po prostu zwróci błąd (errno==EAGAIN).
Po wprowadzeniu wszystkich deskryptorów w tryb nieblokujący moglibyśmy zaprogramować pętle, która by sprawdzała czy nie da się jakiś danych przepisać. Oczywiście jest to raczej złe rozwiązanie, gdyż bazuje na aktywnym czekaniu !.
fcntl(x1,F_SETFL, fcntl(x1,F_GETFL)|O_NONBLOCK );
fcntl(x2,F_SETFL, fcntl(x2,F_GETFL)|O_NONBLOCK );
// zakładamy że pisanie jest zawsze możliwe !
while(1) {
i=read(x1, &c, 1);
// nigdy nie blokuje procesu ...
if(i==1) write(y1, &c, 1);
if(i==-1 && errno!=EAGAIN) { perror("read(x1)"); break; }
// jesli errno==EAGAIN to znaczy że trzeba jeszcze raz spróbować ...
i=read(x2, &c, 1);
if(i==1) write(y2, &c, 1);
if(i==-1 && errno!=EAGAIN) { perror("read(x2)"); break; }
}
Lepsze rozwiązanie stanowi funkcja select() oraz towarzyszące jej makra:
int select(int nfds,
fd_set *readfds, fd_set
*writefds,
fd_set *exceptfds, struct
timeval *timeout) ;
void FD_CLR(int fd, fd_set
*fdset);
int FD_ISSET(int fd,
fd_set *fdset);
void FD_SET(int fd,
fd_set *fdset);
void FD_ZERO(fd_set *fdset);
Funkcja ta pozwala czekać na możliwość zapisu lub odczytu na zbiorze deskryptorów. Zbiory deskryptorów to zmienne typu fd_set. Makra służą do manipulowania tymi zbiorami. Makro FD_SET() wstawia deskryptor do zbioru, FD_ISSET() testuje czy deskryptor jest w zbiorze, FD_ZERO() zeruje cały zbiór. Po zakończeniu działania funkcji select() w zbiorach pozostają tylko te deskryptory, na których można wykonać operację.
Dokładniejsze informacje są w pliku select2.txt.
// Przepisywania x1, x2 na y1, y2 przy pomocy funkcji select()
// UWAGA: zakładamy że do y1 i y2 można zawsze pisać !
fd_set Read, Read0;
FD_ZERO(&Read0);
FD_SET(x1, &Read0);
FD_SET(x2, &Read0);
while(1) {
Read=Read0;
// przy każdym wywołaniu select() trzeba inicjować Read
select(FD_SETSIZE, &Read, NULL, NULL, 0);
if( FD_ISSET(x1,&Read) ) {
read(x1,&c,1); write(y1,&c,1);
}
if( FD_ISSET(x2,&Read) ) {
read(x2,&c,1); write(y2,&c,1);
}
}
A oto dwa przykłady: są to programy do "rozmowy z modemem". Można
wydawać komendy i otrzymywać odpowiedzi od modemu, np:
at
OK
at&v1
TERMINATION REASON.......... LOCAL REQUEST
LAST TX data rate........... 300 BPS
HIGHEST TX data rate........ 300 BPS
LAST RX data rate........... 300 BPS
HIGHEST RX data rate........ 300 BPS
Error correction PROTOCOL... LAPM
Data COMPRESSION............ V42Bis
Line QUALITY................ 000
Receive LEVEL............... 053
Highest SPX Receive State... 00
Highest SPX Transmit State.. 00
OK
Pierwszy z przykładów tty03.cc, używa trybu
nieblokującego. Drugi przykład tty03a.cc używa
funkcji select(), dzięki czemu w znacznie mniejszym stopniu obciąża
procesor. (Oczywiście oba przykłady można tylko oglądać, jeśli nie mamy
modemu i przywilejów root-a - plik /dev/ttyS3 jest dostępny tylko dla
root-a).
potok4 cat \| cat \| cat \| cat
1) W programie "potok4" jest coś nie w porządku, zużywa on zbyt wiele czasu procesora, jak na program w którym nie ma aktywnego czekania, o czym można się przekonać wydając polecenie:
ps -O ppid,pcpu -t [termial z którego uruchomiono potok4]
Twoim zadaniem jest wykryć na czym polega problem i naprawić go.
2) Program "potok4" przepisuje dane po
jednym bajcie, zastanów się czy jest możliwe przepisywanie większymi
porcjami (np po 10 bajtów). Będzie potrzebna dokładna
dokumentacja funkcji read() i write() w odniesieniu do łączy, dostępna
oczywiście w manualu, ale na wszelki wypadek umieszczam pliki: read2.txt i
write2.txt.
.................................................................
c.d. zadań z gniazdkami STRUMIENIOWYMI w dziedzinie INTERNETOWEJ
Zadanie 67Zadanie 68 (*) [tego zadania nie należy robić !!!]