Programmazione socket
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 stream-oriented 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 socket
I due tipi principali di socket SOCK_STREAM TCP affidabile ordine dati garantito orientato alla connessione bidirezionale SOCK_DGRAM UDP inaffidabile nessuna garanzia su ordine dati nessuna nozione di “connessione” – l’applicazione indica la destinazione di ogni pacchetto può inviare o ricevere App socket 3 2 1 Dest. App socket 3 2 1 D1 D3 D2
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!
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)
Quando usare il bind SOCK_DGRAM: SOCK_STREAM: 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)
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
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 Active 1 Active 2 socket socket
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
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
Sending / Receiving Data Con connessione (SOCK_STREAM): int count = send (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 int count = recv (sock, &buf, len, flags); count: Num. byte ricevuti (-1 se errore) buf: void[ ], immagazzina i byte ricevuti len: intero, lunghezza buffer (in byte) Le chiamate sono blocking [ritornano solo dopo che i dati sono inviati (al socket buf) / ricevuti]
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]
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
Client/server socket interaction: TCP crea socket socket() crea socket socket() associa port bind () aspetta richieste listen () TCP connection setup richiedi connessione connect () accetta richieste accept () manda dati send () ricevi dati recv () manda dati send () ricevi dati recv () chiudi socket close () chiudi socket close ()
Client/server socket interaction: UDP crea socket socket() crea socket socket() associa port bind () manda dati sendto () ricevi dati recvfrom () manda dati sendto () ricevi dati recvfrom () chiudi socket close () chiudi socket close ()
La struct sockaddr Generica: sa_family Specifica Internet: 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 };
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 Big-Endian machine Little-Endian machine SBAGLIATO 12.40.119.128 128.119.40.12 128 119 40 12 128 119 40 12
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?
Funzioni di byte-ordering u_long htonl(u_long x); u_short htons(u_short x); u_long ntohl(u_long x); u_short ntohs(u_short x); Sulle macchine big-endian, queste routine non fanno nulla Sulle macchine little-endian, invertono il byte order Lo stesso codice funziona indipendentemente dal tipo di “endian” della macchina Big-Endian machine Little-Endian machine 128 119 40 12 128.119.40.12 128 119 40 12 128.119.40.12 htonl ntohl 128 119 40 12 128 119 40 12
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)
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));
Server: Creazione della coda server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons(atoi(argv[1])); 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)); struttura per il bind dimensiono la coda di backlog
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”); close(sock); printf("server: ho chiuso il socket\n"); } /* fine della funzione main */ qui ci va il codice che presta il servizio (segue)
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 */
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));
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)); printf("client: connesso a %s, porta %d\n", argv[1], ntohs(server.sin_port));
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)); printf("client: ricevo dal server %s\n", result); printf("client: num. o \"quit\"? "); close(sock); printf("client: ho chiuso il socket\n"); } /* fine della funzione main */
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
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
select function call int status = select (nfds, &readfds, &writefds, &exceptfds, &timeout); status: # 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 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 )
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”
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);