Introduzione ai socket
Un socket è un canale di comunicazione asimmetrico tra due processi in esecuzione sullo stesso calcolatore o su due computer diversi interconnessi da un rete di comunicazione.
I socket sono stati progettati con in mente una architettura client-server, intrinsecamete asimmetrica.
- Per server si intende un processo che fornisce un servizio, e.g., server web, server smtp, etc.
- Per client si intende un processo che usufruisce di un servizio, e.g., browser web, client di posta, etc.
L’asimmetria si riflette anche nella diversa sequenza di operazioni che client e server devono eseguire per stabilire la connessione.
I socket vengono identificati attraverso un file descriptor che viene ritornato e/o processato da opportune system call.
- Consente di utilizzare in modo trasparente l’interfaccia del virtual file system per le comunicazioni tra processi, i.e., una volta operativo, è possibile utilizzare le system call classiche per l’I/O di basso livello read, write, close, etc, per interagire con i socket.
Esistono diversi tipi di socket
- socket locali (“File” di tipo Socket): Il file system garantisce la corretta consegna dei messaggi.
- socket TCP “con connessione”: Lo strato di rete del S.O. garantisce la corretta consegna dei messaggi.
- socket UDP “senza connessione”: Nessuna garanzia sulla consegna dei messaggi.
- …
I socket: il server
In una architettura client-server è necessario associare ad ogni servizio un indirizzo univoco.
Esistono diversi tipi di indirizzamento dei servizi, in funzione del tipo di rete di comunicazione utilizzata per la connessione.
L’indirizzo del servizio deve essere noto al client affinchè sia possibile stabilire la connessione.
Tipicamente, il server ottiene l’indirizzo del client solo dopo la connessione.
I ruoli di client e server possono essere dinamici; Ad esempio, il processo “client” A invia al processo “server” B l’indirizzo del processo “client” C. Il “server” B può ora diventare “client” del processo C.
Il server deve attendere la connessione di un client. Un server esegue la seguente sequenza di operazioni:
- Operazione – system call;
- Crea il socket – socket;
- Gli assegna un indirizzo – bind;
- Rende il servizio raggiungibile – listen;
- Accetta nuove connessioni – accept;
- Interagisce con il client;
- Chiude il socket – close;
- Se il socket è locale: cancella il file corrispondente – unlink.
Creazione di un socket
La creazione di un socket avviene attraveso l’invocazione della seguente system call:
int socket(int famiglia,
int tipo, int protocollo);
Ritorna il socket descriptor o -1 in caso di errore.
Il primo parametro definisce la famiglia cui appartiente il protocollo
- PF_LOCAL: Socket locale
- PF_INET: Socket di rete
Il secondo parametro definisce il tipo di connessione:
- SOCK_STREAM: Socket con connessione (locale o TCP)
- SOCK_DGRAM: Socket senza connesione (UDP).
L’ultimo parametro è tipicamente non definito e posto a 0.
Creazione di un socket (a) locale, (b) TCP, (c) UCP.
Associare un indirizzo ad un socket
L’associazione di un indirizzo ad un socket avviene attraverso la seguente system call:
int bind(int sockfd,
const struct sockaddr *my_addr,
socklen_t addrlen);
Assegna l’indirizzo my_addr al socket sockfd.
Il tipo effettivo del secondo argomento dipende dalla famiglia del socket
- socket locali: un indirizzo è, sostanzialmente, il nome di un file; bind fallisce se il file esiste già;
- socket TCP/UDP: indirizzo IP + numero di porta del servizio; bind fallisce se la porta è già in uso.
Il terzo argomento è pari alla dimensione del secondo argomento
- Calcolata, ad es., utilizzando la funzione sizeof().
Restituisce 0 se l’operazione è avvenuta con successo oppure -1 in caso di errore.
Nota: Ogni porta può essere associata al più ad un servizio.
La struttura utilizzata per il naming di socket locali definita in sys/un.h.
Esempio di naming per un socket locale.
Indirizzamento su reti TCP-UDP/IP
Un indirizzo per una connessione TCP/IP o UDP/IP è una coppia:
- Indirizzo IP: Intero senza segno a 32 bit.
- Numero di porta: Intero senza segno a 16 bit (compresso nell’intervallo da 0 a 65535).
L’indirizzo IP identifica “univocamente” il computer all’interno di una rete locale/geografica.
- La rappresentazione “puntata” degli indirizzi IP consiste di 4 interi compresi tra 0 e 255 divisi da tre punti.
- Esistono tecniche che consentono di associare più computer ad un unico indirizzo IP. La trattazione di queste tecniche esula dallo scopo di questo corso.
- Ogni computer ha sempre almeno due indirizzi IP, il 127.0.0.1 o “localhost” ed un IP associato ad ogni interfaccia di rete.
- Esempio: Un PC con una scheda di rete ed una interfaccia Wifi ha 3 indirizzi IP, 127.0.0.1, un IP associato alla scheda di rete ed un IP associato all’interfaccia Wifi.
Indirizzamento su reti TCP-UDP/IP (segue)
Il numero di porta viene utilizzato per offrire diversi servizi dallo stesso indirizzo IP.
- Porte comprese tra 0 e 1023 sono riservate a processi dell’utente root.
- E.g., 21 ftp, 22 ssh, 80 http.
- Un sottoinsieme delle porte comprese tra 1024 e 49152 sono utilizzabili dai processi utente. Tra queste alcune sono registrate presso la IANA.
- Porte comprese tra 49152 e 65525, anche dette “effimere”, utilizzate dal sistema operativo per connessioni dinamiche.
Ordine dei Byte
Architetture diverse memorizzano gli interi in modo diverso:
- Consideriamo l’intero 32769=215 + 1=00000000 00000000 10000000 00000001
- Le architetture Big-endian memorizzano l’intero a partire dal byte più significativo
- In memoria: 00000000 00000000 10000000 00000001
- Le architetture Little-endian memorizzano l’intero a partire dal byte meno significativo
- In memoria: 00000001 10000000 00000000 00000000
L’invio di un intero da una architettura big-endian ad una architettura little-endian (o viceversa) “senza conversione” risulterebbe in un errore di interpretazione.
Ordine dei Byte (segue)
TCP definisce come “network order” la codifica Big-endian; è necessario, quindi:
- Convertire qualsiasi intero in “network order” prima di inviarlo su rete;
- Convertire l’intero in “host order” una volta ricevuto dalla rete.
Funzioni di conversione: n=network, h=host, s=short, l=long
- Host to Network:
uint32_t htonl(uint32_t x); uint16_t htons(uint16_t x)
- Network to Host:
uint32_t ntohl(uint32_t x); uint16_t ntohs(uint16_t x)
- Ad es. La conversione da host order a network di un long (32 bit) è effettuata tramite htonl().
Nota: Ogni messaggio in transito contiene sempre IP e numero di porta sorgente e destinazione.
Indirizzamento su reti TCP-UDP/IP
La struttura sockaddr_in consente di definire le informazioni necessarie per identificare univocamente un servizio di rete.
I campi della struttura:
- sin_family: definisce la famiglia del protocollo (AF_INET per I protocoli di rete).
- sin_port: contiene il numero di porta, in network order, del servizio;
- sin_addr: contiene una struttura composta da un solo intero a 32 bit in network order, che identifica l’indirizzo IP del servizio.
- Lato client: definisce l’IP del servizio.
- La funzione inet_aton consente di convertire un indirizzo in notazione “dotted” nella sua rappresentazione a 32 bit in network order.
La struttura sockaddr_in utilizzata per il naming di socket di rete.
Esempio di utilizzo della struttura sockaddr_in.
Associare un indirizzo ad un servizio
Come detto in precedenza, ogni sistema possiede sempre almeno due interfacce, identificate da localhost e dall’indirizzo IP.
Quando un client si connette ad un server, la richiesta di connessione giunge sempre ad una sola interfaccia.
La system call bind consente di associare un servizio ad una coppia (IP, porta). Su macchine con più interfacce è possibile:
- Consentire che il servizio sia raggiungibile da tutte le interfacce, specificando INADDR_ANY nel campo sin_addr della struttura sockaddr_in;
- Restringere l’accesso al servizio alle connessioni proveniente da una interfaccia specifica, indicando l’indirizzo IP corrispondente nel campo sin_addr della struttura sockaddr_in.
Associa il alla porta 5200 ed accetta connessioni da tutte le interfacce presenti.
Attivare il servizio
Una volta eseguita l’associazione del servizio alla porta locale, il server deve eseguire la system call:
int listen(int sockfd, int lunghezza_coda);
Il primo parametro indica il socket da attivare:
- Sockfd deve essere stato precedentemente processato con successo dalla system call bind, i.e., a sockfd deve essere associata una porta.
Il secondo argomento specifica quante connessioni possono essere in attesa di essere accettate:
- Se il numero di connessioni in attesa è al più pari al secondo parametro, il processo client resta in attesa di connessione fino al raggiungimento di un time-out.
- Se il numero di connessioni in attesa supera il secondo parametro, il client riceve immediatamente “connection refused”.
La listen indica al sistema operativo di monitorare le richieste in arrivo su socket identificato da sockfd:
- Il socket è detto in attesa di nuove connessioni o in ascolto.
- Dal punto di vista del client, il servizio risulta inesistente fino a quando il processo server non esegue la system call listen.
È possibile eseguire la listen solo per socket SOCK_STREAM e SOCK_SEQPACKET.
Accettare una connessione
L’accettazione di una connessione viene effettuata attraverso la seguente system call:
int accept(int sockfd, struct sockaddr *indirizzo_client,
socklen_t *dimensione_indirizzo);
Il primo parametro indica un socket in ascolto.
Il secondo ed il terzo parametro vengono ritornati dalla accept e contengono informazioni sul client (indirizzo IP, numero di porta da cui il client si connette).
La accept restituisce un nuovo descrittore! (oppure -1 in caso di errore)
- Crea un nuovo socket, su una porta effimera libera, dedicato a questa connessione. Questo socket costituisce il canale di comunicazione con il client connesso.
- Il vecchio socket resta in ascolto. I.e., il sistema operativo torna a monitorare il socket in ascolto.
Il comportamento di default della accept è bloccare il processo che la invoca se non vi sono connessioni in attesa
- ll socket può essere marcato non-blocking.
Server: riepilogando
Un server fornisce un servizio ad un insieme di client. Un server che utilizza socket locali o TCP deve:
- Creare un socket
- socket(famiglia, tipo, protocollo).
- Assegnare un indirizzo al socket
- bind(fd, indirizzo, dimensione_indirizzo);
- Per i socket locali, l’indirizzo è il nome di un “file” di tipo socket;
- Per il socket TCP, l’indirizzo è una coppia (IP/INADDR_ANY,numero di porta).
- Porre il socket in ascolto
- listen(fd, lunghezza_coda);
- Rende il servizio raggiunggibile dai client.
- Accettare una nuova connessione
- accept(fd, indirizzo_client, dimensione_ind);
- System call, di default, bloccante;
- Ritorna il socket descriptor che indica la connessione con il client.
- Chiudere/cancellare il socket
- close (ed unlink per i socket locali).
I socket: Il client
In una architettura client-server, il client è l’attore che, tipicamente, stabilisce la connessione.
È necessario che il client conosca, a priori, l’indirizzo del servizio di cui vuole usufruire.
L’indirizzo del server è costituito dalla coppia (IP, porta).
Un client esegue la seguente sequenza di operazioni:
- Crea il socket – socket;
- Si connette al server – connect;
- Interagisce con il server;
- Chiude il socket – close;
- Se il socket è locale: cancella il file corrispondente – unlink;
Connettersi al server
La connessione al server avviene utilizzando la seguente system call:
int connect(int sockfd, const struct sockaddr *serv_addr,
socklen_t addrlen);
Il parametro sockfd identifica il socket descriptor ritornato dalla system call socket.
Il secondo parametro punta ad una struttura sockaddr_in/un contenente l’indirizzo del server.
Il terzo parametro contiene la dimensione el secondo parametro
- Calcolato utilizzando la funzione sizeof().
La system call connect ritorna 0, se la connessione ha avuto successo, oppure -1 in caso di errore.
- A differenza della accept, NON ritorna il descrittore del canale di comunicazione!
- Il canale di comunicazione è identificato da sockfd.
- La variabile errno può esere utilizzata per identificare la causa dell’errore ed informare l’utente (connection refused, network unreachable, connection timeout, etc.)
Struttura del client
Schema di una connessione. La connect associa automaticamente Il socket del client vad una porta effimera, nell'esempio 45000.
I/O su socket
Per le operazioni di lettura/scrittura su socket è possibile utilizzare le system call di I/O di base, read e write.
È necessario stabilire un protocollo applicativo tra client e server in modo che ogni sequenza di byte scritta sul socket venga prelevata per intero dall’altro end-point della connessione.
Comportamento di default per la lettura sul socket:
- Come per l’I/O su standard input, la lettura su socket è bloccante se non vi sono dati in attesa.
- La read ritorna zero quando non vi sono dati in attesa ed il socket è stato chiuso dall’altro end-point della connessione.
- Essendo la lettura da socket una operazione “lenta”, è possible che la read ritorni un numero di byte inferiore a quello attesi. In questo caso è sufficiente leggere dal socket i soli byte restanti.
Comporamento di default per la scrittura sul socket:
- Come per la read, è possibile che la write venga interrotta durante la scrittura. Anche in questo caso è sufficiente scrivere i soli byte restanti.
- La scrittura su un socket chiuso dall’altro end-point implica la ricezione del segnale SIGPIPE, la cui azione di default è la terminazione del processo.
- Se il segnale SIGPIPE viene ignorato esplicitamente (signal(SIGPIPE, SIG_IGN)), write restituisce -1 e imposta errno=EPIPE.
- In alternativa è possiile gestire questo evento intercettando SIGPIPE.
Server concorrenti
La gestione di un singolo client per volta rende la progettazione del server estremamente più semplice rispetto ad un server che deve gestire più client contemporaneamente. Questa struttura, però, ha una serie di effetti collaterali:
- Server poco utilizzato: Ad esempio se il client è fermo su I/O da standard input, anche il server è inattivo;
- Altissimo tempo di attesa per i client: se la sessione è lunga, I client connessi non possono essere serviti finchè il server non termina la sessione con il client corrente.
Per aumentare le prestazioni, preservando una relativa semlicità architetturale, un server può generare un nuovo figlio per ogni nuova connessione.
Il server esegue una fork dopo ogni accept
- il padre torna subito ad eseguire una nuova accept
- il figlio gestisce la connessione con un solo client e poi termina
- come vedremo, in alternativa alla programmazione multi-processo, è possibile utilizzare una programmazione multi-thread.
Nota: la concorrenza o meno del server è trasparente al client, fintanto che il protocollo applicativo implemetato è lo stesso.
Nomi di dominio
Gli utenti tendono a ricordare più semplicemente stringhe del tipo www.federica.unina.it piuttosto che sequenze apparentemente arbitrarie di 4 interi come 143.225.172.146.
Per consetire una maggiore semplicità di memorizzazione, ad ogni server viene solitamente assegnato, oltre all’indirizzo IP, anche almeno un nome di dominio (domain name).
Il servizio di rete chiamato DNS (Domain Name Service) converte i nomi di dominio in indirizzi IP (e viceversa).
- www.federica.unina.it→ 143.225.172.146
- questa conversione prende il nome di “risoluzione del nome di dominio”, dall’inglese “domain name resolution”
- Non descriveremo ulteriormente le regole che sottendono l’assegnazione e la traduzione di indirizzi IP/nomi di dominio.
È possibile interrogare il DNS da shell utilizzando, ad es., il comando host
- host <nome>/<IP>
- esempio: host www.unina.it
Nomi di dominio (segue)
In una applicazione in linguaggio C, l’accesso al servizio DNS può essere effettuato attraverso la system call gethostbyname:
- Prende in input una stringa contenente l’IP in notazione puntata o il nome mnenomico del server.
- Essendo il DNS un servizio di rete, è da ricordare che l’IP del server che fornisce tale servizio è memorizzato come parametro del sistema operativo ed il numero di porta è standard.
- Ad ogni nome mnemonico possono essere associati più IP e viceversa.
- Il DNS riempie la struttura hostent.
- Gli indirizzi IP associati sono contenuti in h_addr_list in network order. Attenzione alla conversione tra tipi (char *->uint32_t *).
- Il nome “canonico” dell’host è contenuto nel campo h_name.
- Gli alias (o “altri nomi” cui l’host è associato) sono contenuti in h_aliases.
La struttura hostent utilizzata da gethostbyname.
Nomi di dominio (segue)
La system call gethostbyname richiede al DNS le informazioni relative ad un nome mnemonico o un IP in notazione puntata.
Ritorna NULL su errore. La variabile errno può essere utilizzata per verificare o meno l’esistenza del nome richiesto.
La struttura ritornata può essere riutilizzata da invocazioni successive della stessa system call (non thread-safe).
Per convertire un indirizzo IP da network order (intero a 32 bit o uint32_t) a notazione puntata è possibile utilizzare:
char *inet_ntoa(struct in_addr in);
La stringa ritornata viene sempre sovrascritta dalle successive invocazioni (non thread-safe).
Nota: La funzione inet_ntoa NON prende input un uint32_t ma una struttura in_addr
La struttura hostent rappresentata graficamente.
Utilizzo di gethostbyname.
Comandi utili
netstat [-t] [-all] [-p] [-n]
- elenca tutti i socket di rete del sistema (non riguarda i socket locali)
- “-t” mostra solo i socket TCP, cioè quelli con famiglia=PF_INET e tipo=SOCK_STREAM
- “-all” (oppure “-a”) mostra anche i socket in ascolto
- “-p” specifica il pid del processo che ha creato ciascun socket
- “-n” mostra gli indirizzi e le porte in formato numerico (invece di simbolico)
- ad es., netstat -t -a -p -n mostra tutti i socket TCP aperti, indicando il PID del processo corrispondente e mostrando gli indirizzi in formato numerico
Comandi utili (segue)
Esempio di output di netstat.
Comandi utili (segue)
- telnet
- crea un socket e lo collega all’indirizzo remoto dato
- esempio: “telnet www.unina.it 80″ si mette in comunicazione diretta con il server http dell’università
- quello che si digita da terminale viene mandato sul socket
- quello che proviene dal socket viene stampato su stdout
- telnet può quindi essere considerato come un “client generico”
- /sbin/ifconfig
- mostra l’indirizzo IP della macchina corrente.