Programmazione socket 2-1 Programmazione socket Obiettivo: imparare a costruire applicazioni client/server che comunicano tramite socket Socket API introdotte in UNIX BSD4.1, 1981 create, utilizzate e rilasciate esplicitamente dalle applicazioni paradigma client/server due tipi di servizi di trasporto via API socket: unreliable datagram reliable, byte streamoriented socket un’interfaccia situata nell’host, creata dall’applicazione e controllata dal SO attraverso la quale un processo applicativo può sia inviare che ricevere messaggi a/da un altro processo applicativo situato in un altro host 2-2 I due tipi principali di socket SOCK_STREAM SOCK_DGRAM TCP affidabile ordine dati garantito orientato alla connessione bidirezionale App 3 2 1 UDP inaffidabile nessuna garanzia su ordine dati nessuna nozione di “connessione” – l’applicazione indica la destinazione di ogni pacchetto può inviare o ricevere App 3 socket 2 D1 2 1 1 Dest. 3 2 1 socket D2 3 D3 2-3 Creazione di socket in C: socket int s = socket (domain, type, protocol); s: socket descriptor, un intero (come un file-handle) domain: intero, dominio di comunicazione • es., AF_INET (IPv4 protocol) – usato di solito type: tipo di comunicazione • SOCK_STREAM: affidabile, 2-vie, connection-based • SOCK_DGRAM: inaffidabile, connectionless • altri valori: servono permessi root, usati raramente o obsoleti protocol: specifica il protocollo, di solito settato a 0 (vedere file /etc/protocols per una lista di opzioni) NOTA: Una chiamata socket non specifica da dove verranno i dati o dove andranno, crea solamente un’interfaccia! 2-4 La funzione bind associa e riserva un port alla socket int status = bind (sockid, &addrport, size); status: error status, = -1 se il bind fallisce sockid: intero, socket descriptor addrport: struct sockaddr, l’indirizzo (IP) e il port della macchina • es. indirizzo: INADDR_ANY sceglie l’indirizzo locale • es. port: 0 lascia al SO il compito di stabilire il port size: la dimensione (in byte) della struttura addrport usato dal server (opzionalmente dal client) 2-5 Quando usare il bind SOCK_DGRAM: in trasmissione il bind non è necessario. Il SO trova un port ogni volta che la socket manda un pacchetto in ricezione il bind è necessario SOCK_STREAM: la destinazione è determinata durante il setup di connessione non occorre conoscere il port attraverso cui vengono inviati i dati (durante il setup di connessione l’estremità ricevente è informata sul port del mittente) 2-6 Connection Setup (SOCK_STREAM) Ricordare: nessun connection setup per il SOCK_DGRAM I partecipanti alla connessione sono di due tipi: passivo: aspetta che un partecipante attivo richieda la connessione attivo: inizia la richiesta di connessione verso il lato passivo Una volta che la connessione è stabilita, i partecipanti attivi e passivi sono “simili” entrambi possono mandare e ricevere dati ognuno può terminare la connessione 2-7 Connection setup (cont.) Participante passivo (es. server) step 1: listen (per arrivo di richieste) step 3: accept (una richiesta) step 4: trasferimento dati La connessione viene accettata su una nuova socket La vecchia socket continua ad aspettare la connessione di nuovi partecipanti Three way handshaking Participante attivo (es. client) step 2: richiede & stabilisce connection step 4: trasferimento dati Passive Participant a-sock-1 l-sock a-sock-2 socket socket Active 1 Active 2 2-8 Connection setup: listen & accept Usate dal partecipante passivo (server) int status = listen (sock, queuelen); status: 0 se si mette in ascolto, -1 se dà errore sock: intero, socket descriptor queuelen: intero, numero di partecipanti attivi che possono “aspettare” per una connessione listen è non-blocking: ritorna immediatamente int s = accept (sock, &name, namelen); s: intero, la nuova socket (usata per il trasferimento dati) sock: intero, la socket originale, usata come prototipo per s name: struct sockaddr, indirizzo del partecipante attivo namelen: sizeof(name): valore/risultato • deve essere settato in maniera appropriata prima della chiamata • aggiustato dal SO quando la funzione ritorna accept è blocking: aspetta una connessione prima di ritornare 2-9 connect call Usata dal partecipante attivo (client) int status = connect (sock, &name, namelen); status: 0 se connessione OK, -1 altrimenti sock: intero, socket da essere utilizzata nella connessione name: struct sockaddr: indirizzo del partecipante passivo namelen: intero, sizeof(name) connect è blocking 2-10 Sending / Receiving Data Con connessione (SOCK_STREAM): int count = send (sock, &buf, len, flags); • • • • int count = recv (sock, &buf, len, flags); • • • • count: Numero byte trasmessi (-1 se errore) buf: char[ ], buffer da trasmettere len: intero, lunghezza buffer (in byte) da trasmettere flags: intero, opzioni speciali, di solito settate a 0 count: Num. byte ricevuti (-1 se errore) buf: void[ ], immagazzina i byte ricevuti len: intero, lunghezza buffer (in byte) flags: intero, opzioni speciali, di solito settate a 0 Le chiamate sono blocking [ritornano solo dopo che i dati sono inviati (al socket buf) / ricevuti] 2-11 Sending / Receiving Data (cont.) Senza connessione (SOCK_DGRAM): int count = sendto (sock, &buf, len, flags, &addr, addrlen); • count, sock, buf, len, flags: stesse di send • addr: struct sockaddr, indirizzo della destinazione • addrlen: sizeof(addr) int count = recvfrom (sock, &buf, len, flags, &addr, addrlen); • count, sock, buf, len, flags: stesse di recv • name: struct sockaddr, indirizzo della sorgente • namelen: sizeof(name): valore/risultato Le chiamate sono blocking [ritornano solo dopo che i dati sono inviati (al socket buf) / ricevuti] 2-12 close Quando si finisce di utilizzare una socket, la socket dovrebbe essere chiusa: status = close (s); status: 0 se OK, -1 se errore s: il socket descriptor (della socket da chiudere) Chiusura di una socket Chiude una connessione (per SOCK_STREAM) Libera il port utilizzato dalla socket 2-13 Client/server socket interaction: TCP Server Client crea socket socket() crea socket socket() associa port bind () aspetta richieste listen () accetta richieste accept () TCP connection setup richiedi connessione connect () manda dati send () ricevi dati recv () manda dati send () chiudi socket close () ricevi dati recv () chiudi socket close () 2-14 Client/server socket interaction: UDP Server Client crea socket socket() crea socket socket() associa port bind () manda dati sendto () ricevi dati recvfrom () manda dati sendto () chiudi socket close () ricevi dati recvfrom () chiudi socket close () 2-15 La struct sockaddr Generica: struct sockaddr { u_short sa_family; char sa_data[14]; }; sa_family • specifica quale famiglia di indirizzi deve essere usata • determina come i 14 byte rimanenti saranno utilizzati Specifica Internet: struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; }; sin_family = AF_INET sin_port: port # (0-65535) sin_addr: IP address sin_zero: non utilizzato //Structure per ragioni storiche struct in_addr { u_long s_addr; //32-bit long }; 2-16 Byte-ordering di indirizzo e port Indirizzo e port sono memorizzati come interi u_short sin_port; (16 bit) in_addr sin_addr; (32 bit) Problema: Macchine/SO usano differenti modalità per memorizzare i dati • little-endian: lower bytes first • big-endian: higher bytes first Queste macchine devono poter comunicare l’una con l’altra attraverso la rete 128.119.40.12 128 Big-Endian machine 119 40 12 Little-Endian machine 128 119 12.40.119.128 40 12 2-17 Soluzione: Network Byte-Ordering Definizioni: Host Byte-Ordering: il byte ordering usato dall’host (big o little) Network Byte-Ordering: il byte ordering usato dalla rete – sempre big-endian Ogni word inviata attraverso la rete dovrebbe essere convertita in Network Byte-Order prima della trasmissione (e viceversa in Host Byte-Order una volta riceuta) D: Le socket devono effettuare la conversione automaticamente? D: Dato che per le macchine big-endian non servono routine di conversione e per le macchine little-endian sì, come si può evitare di scrivere due versioni di codice? 2-18 Funzioni di byte-ordering u_long htonl(u_long x); u_long ntohl(u_long x); u_short htons(u_short x); u_short ntohs(u_short x); Sulle macchine big-endian, queste routine non fanno nulla Sulle macchine little-endian, invertono il byte order Big-Endian Little-Endian12 40 128.119.40.12 119 128 machine 128 119 40 12 128.119.40.12 machine 119 40 12 128 119 40 ntohl 128 12 Lo stesso codice funziona indipendentemente dal tipo di “endian” della macchina 2-19 Altre funzioni utili atoi (char* s): converte la stringa s in un intero bcopy (void* s, void* d, int n): copia n byte di s in d bzero (char* c, int n): pone n byte a 0 a partire dal valore puntato da c gethostname (char *name, int len): ritorna il nome dell’host sui cui il processo risiede gethostbyname (char *name): converte l’hostname in una struttura (hostent) contenente l’indirizzo IP (utilizzando il servizio di DNS) 2-20 Server : Inizializzazione #include <sys/types.h> […altri include…] #define MAX_CODA 5 /* massimo backlog */ main(int argc, char* argv[]) /* prende in input la porta */ { int sock; /* socket in attesa */ int sockmsg; /* socket servente */ struct sockaddr_in server; if ( argc != 2 ) { printf("uso: %s <numero-della-porta>\n", argv[0]); exit(EXIT_FAILURE); } sock = socket(AF_INET,SOCK_STREAM,0); /* socket prototipo */ if( sock <0 ) { printf("server: errore %s nella creazione del socket\n", strerror(errno)); exit(EXIT_FAILURE); } 2-21 Server: Creazione della coda server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons(atoi(argv[1])); struttura per il bind if( bind(sock, (struct sockaddr *)&server, sizeof(server)) ) { printf("server: bind fallita\n"); exit(EXIT_FAILURE); } printf("server: rispondo sulla porta %d\n", ntohs(server.sin_port)); if( listen(sock, MAX_CODA) <0 ) { printf("server: errore %s nella listen\n", strerror(errno)); exit(EXIT_FAILURE); } dimensiono la coda di backlog 2-22 Server: Gestione delle connessioni int totale=0; char input[256]; sockmsg = accept(sock, 0, 0); if( sockmsg <0 ) { printf("errore %s nella accept\n", strerror(errno)); exit(EXIT_FAILURE); } printf("server: accetto una nuova connessione\n”); qui ci va il codice che presta il servizio (segue) } close(sock); printf("server: ho chiuso il socket\n"); /* fine della funzione main */ 2-23 Server: Gestione del client { /* questo e’ il codice di servizio del server */ int len; printf("server %d: iniziato \n", getpid() ); while( len = recv(sockmsg, input, sizeof(input), 0) ) { int numero; char tot[256]; input[len]='\0'; /* termina la stringa*/ numero = atoi(input); /* converti in intero */ printf("server: arrivato il numero: %d\n", numero); totale=totale+numero; /* calcolo totale*/ sprintf(tot, "%d", totale); /* prepara la stringa */ send(sockmsg, tot, sizeof(tot), 0); /* invia la stringa */ } close(sockmsg); /* prima di uscire chiudi il socket */ printf("server: socket chiuso\n”); exit(EXIT_SUCCESS); /* connessione terminata */ } 2-24 Client: Inizializzazione #include <stdio.h> […altri include…] main(int argc, char* argv[]) { int sock; /* descrittore del socket */ struct sockaddr_in server; struct hostent *hp; char input[256]; if(argc!=3) { printf("uso: %s <host> <numero-della-porta>\n", argv[0]); exit(1); } sock = socket(AF_INET, SOCK_STREAM, 0); if( sock < 0 ) { printf("client: errore %s nella creazione del socket\n", strerror(errno)); exit(1); } 2-25 Client: Connessione col server hp = gethostbyname(argv[1]); if( hp == NULL ){ printf("client: l'host %s non e' raggiungibile.\n", argv[1]); exit(1); } server.sin_family = AF_INET; bcopy(hp->h_addr, &server.sin_addr, hp->h_length); server.sin_port = htons(atoi(argv[2])); if( connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0 ) { printf("client: errore %s durante la connect\n", strerror(errno)); exit(1); } printf("client: connesso a %s, porta %d\n", argv[1], ntohs(server.sin_port)); 2-26 Client: Gestione messaggi printf("client: num. o ‘quit’? "); scanf("%s",&input); while( strcmp(input,"quit") != 0 ) { char result[256]; if( send (sock, (char *)&input, strlen(input), 0) <0) { printf("errore %s durante la write\n", strerror(errno)); exit(1); } if( recv(sock,(char *)&result, sizeof(result), 0) < 0 ) { printf("errore %s durante la read\n", strerror(errno)); exit(1); } printf("client: ricevo dal server %s\n", result); printf("client: num. o \"quit\"? "); scanf("%s",&input); } close(sock); printf("client: ho chiuso il socket\n"); } /* fine della funzione main */ 2-27 Gestione del blocco delle funzioni Molte delle funzioni esaminate si bloccano finchè accade un determinato evento accept: fino all’arrivo di una connessione connect: fino a quando la connessione non è stabilita recv, recvfrom: fino a quando un pacchetto (di dati) non è ricevuto send, sendto: fino a quando i dati non vengono messi nel buffer della socket Per semplici programmi il blocco è conveniente Cosa accade ai programmi più complessi? connessioni multiple invio e ricezione contemporaneo necessità di eseguire in contemporanea codice non legato alla rete 2-28 Gestione blocco delle funzioni (cont.) Opzioni: creazione di codice multi-process o multi-threaded “eliminazione” del blocking (es., usando la funzione di controllo del file descriptor fcntl) uso della funzione select Cosa fa la select? si può bloccare permanentemente, per un intervallo limitato o non bloccarsi input: un set di file-descriptor output: info sullo stato dei file-descriptor cioè, può identificare le socket che sono “pronte all’uso”: le funzioni che coinvolgono quelle socket ritornano immediatamente 2-29 select function call int status = select (nfds, &readfds, &writefds, &exceptfds, &timeout); # di oggetti pronti, -1 se errore nfds: 1 + il numero del più grande file descriptor da controllare readfds: lista dei descrittori “pronti alla lettura” writefds: lista dei descrittori “pronti alla scrittura” exceptfds: lista dei descrittori che registrano un’eccezione status: timeout: intervallo dopo il quale la select ritorna, anche se non c’è niente di pronto – può essere tra 0 e (settare il parametro timeout a NULL per ) 2-30 Da utilizzare con la select select utilizza una struct fd_set per le liste dei descrittori è un vettore di bit se il bit i è settato in [readfds, writefds, exceptfds], select controllerà che il file descriptor (cioè la socket) i è pronta per [reading, writing, exception] Prima di chiamare select: FD_ZERO (&fdvar): azzera la struttura FD_SET (i, &fdvar): aggiunge il file descriptor i alla lista FD_CLR (i, &fdvar): rimuove il file descriptor i dalla lista Dopo aver chiamato select: int FD_ISSET (i, &fdvar): booleano ritorna TRUE iff i è “pronto” 2-31 Rilascio dei port Qualche volta un’uscita “rude” da un programma (es. ctrl-c) non rilascia il port correttamente In ogni caso il port dovrebbe essere rilasciato dopo alcuni minuti Per ridurre la probabilità di questo inconveniente, includere il codice seguente: #include <signal.h> void cleanExit(){exit(0);} nel codice della socket: signal(SIGTERM, cleanExit); signal(SIGINT, cleanExit); 2-32