Wprowadzenie do programowania w sieci Internet – cz. 2 Komunikacja międzyprocesowa w układzie klient-serwer. Cechy układu klient-serwer : niesymetria relacji klient serwer dobrze znany adres/port usługodawcy uzgodniony protokół komunikacji.
Połączenie jest określone jednoznacznie przez asocjację: protokół + adres (IP+port) klienta + adres (IP+port) serwera
Interfejs gniazd (sockets) wykorzystuje i rozszerza model dostępu do obiektów w systemie plików, posługujący się deskryptorami plików (open, close, read, write). Dwa podstawowe tryby komunikacji za pomocą gniazd: Tryb bezpołączeniowy (datagramowy) Tryb połączeniowy (strumieniowy) Systemy operacyjne – materiały pomocnicze
1
oprac.: L.J. Opalski 2009-2017
Prosta komunikacja (pytanie-odpowiedź) – interfejs gniazd Komunikacja połączeniowa (TCP) Serwer socket() bind() listen() accept() ew. blokowanie do chwili uzyskania nawiązanego połączenia z klientem read()
Klient socket() | | | | <--------------
Otwieranie połączenia klient <--> serwer
<------------> zlecenie <-------------(request)
write()
wykonywanie usługi
| | |
write()
connect()
odpowiedź --------------> (reply)
read()
2 * close()
Systemy operacyjne – materiały pomocnicze
Sesja komunikacji klient <--> serwer
close()
2
oprac.: L.J. Opalski 2009-2017
Prosta komunikacja (pytanie-odpowiedź) – interfejs gniazd Komunikacja bezpołączeniowa (datagramowa) Serwer
Klient
socket() bind() recvfrom() blokowanie do chwili otrzymania danych (zlecenia) od klienta wykonywanie usługi
socket() bind() |
sendto()
zlecenie <-------------(request)
| | odpowiedź --------------> (reply)
close()
Systemy operacyjne – materiały pomocnicze
sendto()
recvfrom() close()
3
oprac.: L.J. Opalski 2009-2017
Tworzenie gniazd #include
#include int sock = socket(int family, int type, int protocol); family = AF_UNIX
- dla domeny UNIX (połączenia lokalne, identyfikacja przez ścieżkę w systemie plików)
family = AF_INET - dla domeny INTERNET (połączenia odległe, identyfikacja przez adres Internetowy i numer portu)
type = SOCK_STREAM - komunikacja za pomocą strumienia type = SOCK_DGRAM - komunikacja za pomocą datagramów type = SOCK_RAW
- bezpośredni dostęp do transportu
protocol = 0 - automatyczny wybór protokołu na podstawie type protocol > 0 - użycie wskazanego protokołu (patrz /etc/protocols) sock - deskryptor gniazdka; jeśli sock<0, to błąd, a kod błędu jest w zmiennej globalnej errno
Systemy operacyjne – materiały pomocnicze
4
oprac.: L.J. Opalski 2009-2017
Adresowanie gniazd Jawne adresowanie gniazda: int ret = bind( int sock,/* deskryptor gniazda */ const struct sockaddr *addr,/* wsk. na str. adr. */ int addr_len /* wykorzystywany rozmiar struktury adresującej */ );
Ogólna postać struktury adresującej struct sockaddr addr{ u_short sa_family; /* rodzina adresowania AF_xxx */ char sa_data[ ]; /* adres (długość pola zależy od domeny) */ };
Systemy operacyjne – materiały pomocnicze
5
oprac.: L.J. Opalski 2009-2017
Adresowanie gniazd – c.d. Struktura adresująca w domenie Internet ( /usr/include/netinet/in.h) : struct sockaddr_in { short sin_family; /* rodzina adresowa AF_INET */ u_short sin_port; /* numer portu w porządku sieci */ struct in_addr sin_addr; /* adres IP */ char sin_zero[8]; /* wypełnienie */ };
struct in_addr { union { 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; } Systemy operacyjne – materiały pomocnicze
6
oprac.: L.J. Opalski 2009-2017
Adresowanie gniazd – c.d. Struktura adresowa dla protokołu IPv6 (netinet/in.h) struct in6_addr {uint8_t s6_addr[16];}; /* 128-bitowy adres IPv6 */ struct sockaddr_in6 {/* Struktura ogólna dla IPv6 */ sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port[14]; /* 16b. numer portu (uint16_t) */ uint32_t sin6_flowinfo; /* et. przepływu i priorytet */ struct in6_addr sin6_addr; /* adres IPv6 */ }
Struktura dla rodziny adresowej UNIX (AF_UNIX) (sys/un.h) struct sockaddr_un{ /* Struktura dla domeny UNIX */ sa_family_t sun_family; /* AF_LOCAL */ char sun_path[]; /* nazwa ścieżkowa, długość nieokr. (POSIX), typowa długość: 92 do 108 */ }
Systemy operacyjne – materiały pomocnicze
7
oprac.: L.J. Opalski 2009-2017
Funkcje do zamiany kolejności bajtów Porządek sieciowy bajtów w liczbach wielobajtowych: pierwszy bajt jest najbardziej znacząc (big-endian). Np. liczba dwubajtowa 320 jest Nr kolejny bitu reprezentowana następująco --->
0 1 2 3 4 5 6 7 8 9 A B C D E F
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |0 0 0 0 0 0 0 1|0 1 0 0 0 0 0 0| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Do zmiany porządku bajtów służą w oprogramowaniu sieciowym funkcje htons, htonl, ntohs, ntohl Typ danej w porządku stacji (host ordered)
Funkcja konwersji
Dana w porządku sieciowym
uint16_t
-------> htons ------>
uint_16_t
uint16_t
<------- ntohs <------
uint_16_t
uint32_t
-------> htonl ------>
uint32_t
uint32_t
<------- ntohl <------
uint32_t
8
oprac.: L.J. Opalski 2009-2017
Konwersja adresów IP z/do postaci kropkowej Funkcje konwersji adresów bez obsługi błędów #include in_addr_t inet_addr(const char *p);/* konwersja adresu kropkowego z p na adres IP w postaci binarnej w sieciowej kolejności bajtów; dla błędu zwracana jest stała INADDR_NONE (zazwyczaj same jedynki) */ char *inet_ntoa(struct in_addr in); /* przekształca adres in (w sieciowej kolejności bajtów) w napis w notacji kropkowo dziesiętnej; funkcja zwraca adres napisu przechowywanego w buforze statycznym funkcji. */
Funkcja konwersji z obsługą błędów #include int inet_aton(const char *p, struct in_addr *addrptr);/* konwersja adresu kropkowego z p na adres IP w postaci binarnej w sieciowej kolejności bajtów – zapamiętany pod adresem addrptr; funkcja zwraca 1 dla poprawnej konwersji i 0 dla błędu */ Systemy operacyjne – materiały pomocnicze
9
oprac.: L.J. Opalski 2009-2017
Konwersja adresów - POSIX
Funkcje POSIX
do konwersji adresów IPv4 i IPv5, z obsługą błędów
#include const char *inet_ntop(int af, /* rodzina adresowania */ const void *restrict src, /* wskaźnik na bufor z adresem (IPv4 jeśli af==AF_INET */ char *restrict dst, /* wskaźnik na bufor w którym funkcja zapisze postać tekstową adresu */ socklen_t size /* rozmiar bufora wskazywanego przez dst */ ); /* Funkcja przeprowadza konwersję z postaci binarnej (porządek sieciowy) na postać prezentacyjną (ASCII); w przypadku sukcesu zwraca wskaźnik bufora, przy błędzie zwraca NULL (kod błędu w errno) */ #include int inet_pton(int af, /* rodzina adresowania */ const char *restrict src, /* wskaźnik na bufor z adresem w postaci prezentacyjnej (tekstowej) */ void *restrict dst
/* wskaźnik na bufor w którym funkcja zapisze postać binarną adresu (w porządku sieciowym), Długość bufora: 32b dla af==AF_INET, 128b dla af==AF_INET6 */
); /* Dla adresu IPv4 funkcja przeprowadza konwersję adresu z postaci kropkowej (ddd.ddd.ddd.ddd) na binarną; w przypadku sukcesu zwraca 1, dla błędnej postaci adresu: 0; dla nieznanego af zwraca -1 ustawiając errno na EAFNOSUPPORT Funkcja przyjmuje też adresy IPv6 o postaci x:x:x:x:x:x:x:x (patrz man inet_ntop Systemy operacyjne – materiały pomocnicze
10
oprac.: L.J. Opalski 2009-2017
Adresowanie gniazda #include int bind{ int
sockfd; /* deskryptor gniazda */
const struct sockaddr *addr; /* wskaźnik na strukturę adresową */ socklen_t addrlen; /* długość struktury adresowej (w bajtach) */ }
Funkcja bind przypisuje gniazdu adres protokołowy zwracając normalnie wartość 0; w przypadku błędu zwraca -1 po ustawieniu errno (EADDRINUSE, EINVAL,... patrz man bind).
Jeśli port=0, to system wybiera automatycznie port efemeryczny.
Modele systemu końcowego: słaby i silny (różnica dla stacji wielosieciowych). Model silny akceptuje tylko datagramy przybywające do tego interfejsu, który jest związany z tym samym adresem co podany w przysłanym datagramie (RFC1122).
Użycie adresu uogólnionego (numer IPv4 ma wartość INADDR_ANY, albo numer IPv6 ma wartość in6addr_any), powoduje, że dla stacji wielosieciowej (multihomed host) oprogramowanie zwleka z wyborem lokalnego adresu aż: gniazdo będzie połączone (TCP), albo będzie wysłany datagram (UDP)
Systemy operacyjne – materiały pomocnicze
11
oprac.: L.J. Opalski 2009-2017
Adresowanie gniazd – przykład
Tworzenie gniazda i przydział adresu serwera w domenie Internet: struct sockaddr_in addr; /* struktura adresowa */ int sockfd; /* deskryptor gniazda */
/* Tworzenie gniazda UDP w domenie Internet */ if((sockfd = socket(AF_INET,SOCK_DGRAM,0)) < 0){ /* błąd */ } bzero((char *) &addr, sizeof(addr));/* zerowanie struktury adresującej */ addr.sin_family = AF_INET; /* domena Internet */ addr.sin_addr.s_addr = htonl(INADDR_ANY); /* odbiór na wszystkich interfejsach serwera: */ /* Alternatywa: odbiór na konkretnym interfejsie, np. o numerze IP 127.0.0.1 */ /* addr.sin_addr.s_addr = inet_addr(”127.0.0.1”); */ addr.sin_port = htons(9000);/* „dobrze znany” port w porządku sieciowym */
/* Jawne wiązanie adresu z gniazdem */ if(bind(sockfd,(struct sockaddr *) &addr, sizeof(addr))<0) { /* błąd wiązania adresu */ } Systemy operacyjne – materiały pomocnicze
12
oprac.: L.J. Opalski 2009-2017
Korzystanie z nazw stacji Struktura adresów stacji () struct hostent { char * h_name;/* oficjalna (kanoniczna) nazwa stacji */ char ** h_aliases; /* tablica pseudonimów, zakończona NULL */ int h_addrtupe; /* typ adresu (AF_INET lub AF_INET6) */ int h_length; /* długość adresu (4 lub 16) */ char ** h_addr_list; /* wskaźnik do tablica wskaźników z adresami IPv4 i IPv6, zakończona NULL */ }; #define h_addr
h_addr_list[0] /* pierwszy adres w tablicy adresów */
Funkcje do transformacji adresów stacji struct hostent* gethostbyname(const char *nazwa/* nazwa symboliczna stacji */ );/* Funkcja zwraca wskaźnik na strukturę adresów stacji o danej nazwie, albo NULL (ustawiając zmienną h_errno, zdefiniowaną w ) */ struct hostent* gethostbyaddr(const char *adres, /* wskaźnik na adres IP w postaci binarnej(struct in_addr, albo struct in6_addr) */ int len, /* długość adresu */ int typ /* typ adresu (np. AF_INET) */ ); Systemy operacyjne – materiały pomocnicze
13
oprac.: L.J. Opalski 2009-2017
Korzystanie z nazw stacji - przykład int main(int argc, char *argv[]){ char *host; int sockfd, port=10000; struct sockaddr_in serv_addr; if ( argc>=2){ host=argv[1]; if ( argc==3 ) port=atoi(argv[2]); } else host=”127.0.0.1”; fprintf(stderr,"%s: server %s, TCP port %d\n", argv[0],host,port); bzero((char *) &serv_addr, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; if (!isdigit(host[0]) || inet_aton(host,&serv_addr .sin_addr)==0){ struct hostent *hp=gethostbyname(host); if(hp==NULL){ fprintf(stderr,"%s: host %s not found\n",argv[0],host); exit(1); } serv_addr.sin_addr.s_addr = ((struct in_addr *)(hp>h_addr))->s_addr; serv_addr.sin_port = htons(port); } .................. } Systemy operacyjne – materiały pomocnicze
14
oprac.: L.J. Opalski 2009-2017
Korzystanie z nazw stacji: POSIX #include #include #include /* Given node and service, which identify an Internet host and a service, getaddrinfo() returns one or more addrinfo structures, each of which contains an Internet address that can be specified in a call to bind(2) or connect(2). */ int getaddrinfo (const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); void freeaddrinfo(struct addrinfo *res); addrinfo */
/* zwalnianie pamięci struct
const char *gai_strerror(int errcode);/* */
tłumaczenie kodu błędu na komunikat
/* converts a socket address to a corresponding host and/or service, in a protocol-independent manner. */ int getnameinfo (const struct sockaddr *sa, socklen_t salen,/* socket addr */ char *host, socklen_t hostlen, /* host name buffer */ char *serv, socklen_t servlen, /* service n.buffer */ int flags /* modifies function behawior */ );
Patrz przykład użycla: cl_getaddrinfo.c, sv_getaddrinfo.c Systemy operacyjne – materiały pomocnicze
15
oprac.: L.J. Opalski 2009-2017
Oczekiwanie na połączenie struct sockaddr_in
cli_addr;
int newsockfd, clilen=sizeof(cli_addr);
listen(sockfd, 5); /* ustanowienie kolejki oczekujących */ newsockfd = accept( /* oczekiwanie na klientów */ sockfd, /* deskryptor gniazda nasłuchu */ (struct sockaddr *) &cli_addr,/* struktura na adres */ &clilen);/* na wejściu długość struktury/na wyjściu – zajętość struktury przez adres klienta */ if (newsockfd < 0){ /* błąd */ } Uwaga: deskryptor sockfd może być użyty do dalszego nasłuchu i pobierania nawiązanych połączeń, a newsockfd do wymiany danych => możliwy wybór:
-
odbieranie następnych połączeń po zakończeniu nowo utworzonego (serwer iteracyjny)
-
współbieżna realizacja (za pomocą wątków, podprocesów, czy przy wykorzystaniu select()) nasłuchu połączeń (i otwierania nowych) oraz obsługi już otwartych połączeń (serwer współbieżny)
Systemy operacyjne – materiały pomocnicze
16
oprac.: L.J. Opalski 2009-2017
Tworzenie połączenia (strona klienta) #define SERV_HOST_ADDR .... /* adres IP serwera */ #define SERV_TCP_PORT .... /* port TCP serwera */ int sockfd; /* deskryptor gniazda komunikacji z serwerem */ struct sockaddr_in addr; /* struktura adresująca */ bzero((char *) &addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr =inet_addr(SERV_HOST_ADDR); addr.sin_port = htons(SERV_TCP_PORT); if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){ /* błąd tworzenia gniazda*/ } /* Połącz z serwerem */ if(connect(sockfd,(struct sockaddr *)&addr,sizeof(addr)) < 0){ /* błąd */ }
/* Tu deskryptor sockfdmoże być użyty do komunikacji z serwerem */
Systemy operacyjne – materiały pomocnicze
17
oprac.: L.J. Opalski 2009-2017
Pozyskiwanie adresów końców (partnerów) aktywnego połączenia #include int getsockname(int sock, struct sockaddr *name, socklen_t *namelen);
pobiera do struktury wskazywanej przez name ( o maks. długości podanej przed wywołaniem przez *namelen) adres lokalny połączonego gniazda sock. #include int getpeername(int sock, struct sockaddr *name, socklen_t *namelen); pobiera do struktury wskazywanej przez name ( o maks. długości podanej przed wywołaniem przez *namelen) adres hosta partnera połączonego z gniazdem sock. Obydwie funkcje zwracają 0 w przypadku poprawnego wyjścia, a –1 w przypadku błędu (po ustawieniu wartości kodu błędu w errno). Systemy operacyjne – materiały pomocnicze
18
oprac.: L.J. Opalski 2009-2017
Wymiana danych przez połączenie gniazdowe #include ssize_t len = read(int sock, void *buf, size_t bufsz); ssize_t len = write(int sock, const void *buf, size_t bufsz);
Alternatywa: ssize_t len = send(int sock, const void *buf, size_t bufsz, int flags); ssize_t len = recv(int sock, void *buf, size_t bufsz, int flags); flagi: MSG_OOB, MSG_PEEK, MSG_WAITALL (recv),
MSG_DONTROUTE (send) Systemy operacyjne – materiały pomocnicze
19
oprac.: L.J. Opalski 2009-2017
Przykład – realizacja strumieniowej usługi echo int tcpserv(int fd){/* fd: deskryptor gniazda uzyskanego z wywołania accept */ char buf[64000]; int nin; while((nin=read(fd,buf,sizeof(buf)))>0){ char *p=buf; int nout, towrite=nin; if(nin<0){ if(errno==EINTR) continue; perror("tcpserv:1"); return 1; } while(towrite>0){ nout=write(fd,p,towrite); if(nout<0){ if(errno==EINTR) continue; perror("tcpserv:2"); return 2; } towrite-=nout; p+=nout; } } return 0; } Systemy operacyjne – materiały pomocnicze
20
oprac.: L.J. Opalski 2009-2017
Kończenie połączenia int
ret=shutdown(
int sockfd, /* deskryptor zamykanego gniazda */ int how /* SHUT_RD (0) – czytanie */ /* SHUT_WR (1) – pisanie */ /* SHUT_RDWR (2) – obydwa kierunki */
); /* funkcja zwraca normalnie 0, albo –1 dla niepowodzenia */
int
ret=close( int sockfd );
Jeśli nie ustawiono opcji SO_LINGER dla gniazda, to close() natychmiast wraca (nie czekając na wysłanie ew. danych z gniazda wysyłkowego).
Systemy operacyjne – materiały pomocnicze
21
oprac.: L.J. Opalski 2009-2017
Komunikacja bez połączenia - przesyłanie datagramów
ssize_t
ret=recvfrom(int sockfd, void *buf, size_t bufsz, int flags, struct sockaddr *from, socklen_t *p_addrlen);
Odbiera datagram do wskazanego bufora (buf) o długości bufsz, a ret>=0 przyjmuje wartość długości odebranego datagramu.
Jeśli from!=NULL, to struktura wskazana przez from (o długości określonej przez wartość *p_addrlen) odbiera adres nadawcy, a długość adresu jest wpisana do *p_addrlen.
ssize_t
ret=sendto(int sockfd, const void *buf, size_t bufsz, int flags, const struct sockaddr *to, socklen_t addrlen);
Wysyła bufsz bajtów bufora buf pod adres określony przez to (długość adresu: addrlen).
Uwaga: sendto() nie wymaga uprzedniego jawnego zaadresowania gniazda, ale należy użyć bind() przed sendto() jeśli będzie oczekiwana odpowiedź (datagram) przez wywołanie recvfrom() flags jak dla send/recv (dla potrzeb laboratorium przedmiotu: flags==0) Systemy operacyjne – materiały pomocnicze
22
oprac.: L.J. Opalski 2009-2017
Przykład – realizacja datagramowej usługi echo int udpserv(int fd){/* fd: deskryptor zaadresowanego gniazda UDP */ char buf[64000]; int n; socklen_t clilen; struct sockaddr_in sa; clilen=sizeof(sa);/* określenie maks. długości adresu */ n=recvfrom(fd,buf,sizeof(buf),0, (struct sockaddr *)&sa,&clilen); if(n<0){ perror("udpserv:1"); return 1; } /* Uwagi: 1) zawartość sa określa nadawcę wiadomości => umożliwia odesłanie wiadomości zwrotnej (tu kopii wiadomości odebranej) 2) wartość clilen może być zmieniona przez wywołanie recvfrom() */ fprintf(stderr,”IP nadawcy: %s, port=%d\n”, inet_ntoa(sa. sin_addr), ntohs(sa.sin_port)); if(sendto(fd,buf,n,0,(struct sockaddr *)&sa,clilen)!=n){ perror("udpserv:2"); return 2; } return 0; } Systemy operacyjne – materiały pomocnicze
23
oprac.: L.J. Opalski 2009-2017