uni
librerie:

#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

compilazione

  • usiamo GCC
    • gcc -Wall -c myfile1.c myfile2.c compilazione no linking
      • -Wall NON ignora i warning
    • gcc -o myprogram myfile1.c myfile2.c linking

Cooperazione tra processi

  • Due processi possono cooperare attraverso:
    • Sincronizzazione (Es. semafori)
    • Comunicazione, cioè scambio di informazioni
      • Memoria condivisa
      • Chiamate a procedura remota
      • Scambio di messaggi
  • due processi possono cooperare
    • sulla stessa macchina
    • su macchine diverse (sistema distribuito)
      • come avviene la comunicazione? tramite scambio di messaggi per cui è stata definita l’astrazione dei socket

Socket

Un socket è un meccanismo per la comunicazione tra processi, sulla stessa macchina o su macchine differenti.

Un socket è identificato da un indirizzo:

  • indirizzo host: TCP/IP: indirizzo IP
  • indirizzo processo: TCP/IP: numero di porta

Un socket è un’estremità di un canale di comunicazione

È un’astrazione implementata dal sistema operativo, che ci rende disponibile delle primitive (system calls) per:
– Creare un socket
– Assegnargli un indirizzo e una porta
– Connettersi ad un altro socket
– Accettare una connessione
– Inviare e ricevere dati attraverso i socket

strutture per gli indirizzi

/* Endpoint IPv4in netinet/in.h */
struct sockaddr_in {
	sa_family_t sin_family; /* address family: AF_INET */
	in_port_t sin_port; /* port in network byte order */
	struct in_addr sin_addr; /* internet address */
};
/* Internet address in netinet/in.h */
struct in_addr {
	uint32_t s_addr; /* address in network byte order */
};
  • Attenzione: esiste anche la struttura struct sockaddr usata da alcune funzioni descritte più avanti ma non la useremo direttamente.

Formato degli indirizzi

  • formato numerico (n): 32 bit usato dal computer
  • formato presentazione(p): stringa in notazione decimale puntata
int inet_pton(int af, const char *src, void *dst);
  • af: (address family) famiglia (AF_INET)
  • src: stringa del tipo “ddd.ddd.ddd.ddd”
  • dst: puntatore a una struct in_addr
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
  • af: famiglia (AF_INET)
  • src: puntatore a una struct in_addr
  • dst: puntatore a un buffer di caratteri lungo size
  • size: deve valere almeno INET_ADDRSTRLEN

Endianess

big endian

Per primo il MSB
indirizzo piccolo - byte grande

LA RETE USA BIG ENDIAN

little endian

Per primo il LSB
Indirizzo piccolo - byte piccolo

funzioni di conversione

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // host to network long
uint16_t htons(uint16_t hostshort); // host to network short
uint32_t ntohl(uint32_t netlong); // network to host long
uint16_t ntohs(uint16_t netshort); // network to host short

unit16_t e uint32_t sono definiti nell’Header file <stdint.h>

Primitive

socket()

#incldue <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
 
int socket(int domain, int type, int protocol);
  • domain: famiglia di protocolli da utilizzare
    • AF_LOCAL : comunicazione locale
    • AF_INET : protocolli IPv4, TCP e UDP
  • type:
    • SOCK_STREAM: TCP
    • SOCK_DGRAM: UDP
  • protocol: sempre a
  • la funzione restituisce:
    • un descrittore di file che rappresenta il socket e servirà a manipolare il socket attraverso altre primitive
    • se essore
      nota che il socket non è ancora associato ad un indirizzo IP o ad una porta.

close()

Chiude un socket: non può più essere usato per inviare o ricevere dati.

#include <unistd.h>
int close(int fd);
  • int fd: descrittore del processo
  • restituisce 0 se ha successo, -1 su errore

chi chiama la close() manda zero all’altro endpoint e la connessione viene chiusa: l’host remoto riceverà 0 dalla recv()

Lato server

bind()

Assegna un socket ad un indirizzo (IP+porta). Di solito il cliente non ha bisogno di eseguire una bind().

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: descrittore del socket (socket file descriptor)
  • addr: puntatore alla struttura di tipo struct sockaddr
    • Visto che usiamo struct sockaddr_in bisogna convertire il puntatore
  • addrlen: dimensione di addr
  • La funzione restituisce 0 se ha successo, -1 su errore
ret = bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr));

listen()

Imposta il socket in modalità passiva, di ascolto. Il socket verrà usato per ricevere richieste di connessione. Si possono mettere in attesa solo i socket SOCK_STREAM.

int listen(int sockfd, int backlog);
  • sockfd: descrittore del socket
  • backlog: dimensione della coda, ovvero quante richieste da client possono rimanere in attesa
  • restituisce 0 se ha successo, -1 altrimenti

La richiesta (da parte di un client) di connessione al socket del server arriva al sistema operativo:

  • Il kernel la mette in attesa in una coda, finché il server non chiama accept() per prenderla in carico.
  • Se arrivano più richieste contemporanee, vengono accodate.

accept()

Accetta una richiesta di connessione pervenuta sul socket (solo su SOCK_STREAM).

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd: descrittore socket
  • addr: puntatore ad una struttura (vuota) di tipo struct sockaddr, inizializzare una nuova struttura e inserirla nella accept, sarà la sockaddr del client che si connette
  • addrlen: dimensione di addr
  • restituisce il descrittore di un nuovo socket che verrà usato per la comunicazione, -1 se errore
  • questa funzione è bloccante, il programma si ferma finché non arriva una richiesta
struct sockaddr_in cl_addr;
int len = sizeof(cl_addr);
new_sd = accept(sd, (struct sockaddr*)&cl_addr, &len);

Lato Client

Connect()

Connette il socket locale ad un socket remoto.

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: descrittore del socket locale
  • addr: puntatore alla struttura contenente l’indirizzo del server
  • addrlen
  • restituisce 0 se ha successo, -1 se errore
  • questa funzione è bloccante: il programma si ferma finché la richiesta di connessione non è stata accettata
ret = connect(sd, (struct sockaddr*) &sv_addr, sizeof(sv_addr));

Scambio dati

Send()

invia un messaggio attraverso un socket connesso

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd
  • buf: puntatore al buffer contenente il messaggio da inviare
  • len: dimensione in byte del messaggio
  • flags: per settare opzioni, noi lo lasciamo a 0
  • la funzione restituisce il numero di byte inviati se ha successo, altrimenti -1
  • la funzione è bloccante: il programma si ferma finché non ha inviato tutto il messaggio

send() non spedisce pacchetti: copia i dati nel buffer del kernel, che poi li frammenta in pacchetti TCP

recv()

Preleva un messaggio da un socket connesso.

ssize_t recv(int sockfd, const void *buf, size_t len, int flags);
  • sockfd
  • buf: puntatore al buffer in cui salvare il messaggio
  • len (in byte) del messaggio
  • flags
    • MSG_WAITALL
  • restituisce il numero di byte ricevuti, -1 per errore, 0 se il socket remoto si è chiuso
  • funzione bloccante: si ferma finché non ha letto qualcosa

Gestione degli errori

Le primitive viste restituiscono -1 quando concludono in errore.
Inoltre settano una variabile, errno (in errno.h), che può essere letta per scoprire il motivo dell’errore.
Nel manuale di ogni funzione c’è l’elenco degli errori possibili.

#include <errno.h>
//…
ret = bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if (ret == -1) {
		if (errno == EADDRINUSE) {/* Gestisci errore */}
		if (errno == EINVAL) {/* Gestisci errore */}
		//…
}

a volte vogliamo solo sapere l’errore e uscire: perror("Error: "); legge errno e stampa l’errore su schermo in forma leggibile

Errori possibili

ValoreNome costanteSignificatoQuando capita tipicamente
9EBADFBad file descriptorIl socket non è valido o è stato già chiuso.
13EACCESPermission deniedPorta privilegiata (<1024) senza privilegi root.
98EADDRINUSEAddress already in usebind() su una porta già occupata.
99EADDRNOTAVAILCannot assign requested addressIP non valido o non configurato sull’host.
111ECONNREFUSEDConnection refusedIl server non è in ascolto sulla porta indicata.
113EHOSTUNREACHNo route to hostNessuna rotta verso l’host remoto.
110ETIMEDOUTConnection timed outNessuna risposta dal server entro il timeout.
32EPIPEBroken pipeScrittura (send) su un socket chiuso dall’altro lato.
104ECONNRESETConnection reset by peerL’altro lato ha chiuso in modo “brusco” la connessione.
105ENOBUFSNo buffer space availableBuffer di rete saturi, sistema sotto pressione.
115EINPROGRESSOperation now in progressIn socket non-bloccanti durante connect().
11EAGAIN/EWOULDBLOCKResource temporarily unavailableNessun dato pronto in socket non-bloccante (o coda piena in send()).
4EINTRInterrupted system callLa recv()/send() è stata interrotta da un segnale.

esempio 1

#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main () {
	/* Creazione socket */
	int sd = socket(AF_INET, SOCK_STREAM, 0);
	/* Creazione indirizzo */
	struct sockaddr_in my_addr;
	memset(&my_addr, 0, sizeof(my_addr); // Pulizia
	my_addr.sin_family = AF_INET ;
	my_addr.sin_port = htons(4242);
	inet_pton(AF_INET, "192.168.4.5", &my_addr.sin_addr);
}

Esempio di connessione

Server Concorrente

Creazione di Processo Figlio

Come li distinguo?

p = fork()
  • il padre vede p = PID figlio
  • il figlio vede p = 0

Uso di Fork

Il figlio deve cancellare il socket in ascolto.
Il padre deve cancellare il socket per la comunicazione.

Server multi-processo

#include
int main () {
int ret, sd, new_sd, len;
struct sockaddr_in my_addr, cl_addr;
//...
pid_t pid;
sd = socket(AF_INET, SOCK_STREAM, 0);
ret = bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr));
ret = listen(sd, 10);
len = sizeof(cl_addr);
//…
while (1) {
	new_sd = accept(sd, (struct sockaddr*)&cl_addr,&len);
	pid = fork();
	if (pid == -1){/*Gestione errore*/}
	if (pid == 0) {
	// Sono nel processo figlio. Chiudo socket in ascolto
		close(sd);
		// Servo la richiesta con new_sd
		//…
		close(new_sd);
		exit(0); // Il figlio termina
	}
	// Sono nel processo padre. Chiudo socket connesso al client
	close(new_sd);
}

Uso dei thread

#include <pthread.h>

e per compilare

gcc <opzioni> file.c -pthread

Tipi e funzioni da usare:

pthread_t
 
pthread_mutex_t // semaforo per proteggere le risorse condivise
 
int pthread_create(pthread_t*thread, const pthread_attr_t*attr, void*(*start_routine)(void*),void*arg) // crea un nuovo thread che parte eseguendo start_routine(void*)
 
int pthread_join(pthread_t thread, void** retval) // un thread può bloccarsi in attesa della terminazione di un altro thread
 
void pthread_exit(void* retval) //per terminare un thread e mettere a disposizione un suo risultato (retval)

Socket Bloccanti e non bloccanti

Socket bloccante

Di default un socket è bloccante, tutte le operazioni su di esso fermano l’esecuszione del processo in attesa del risultato:

  • connect
  • accept
  • send
  • recv: si blocca finché non c’è un qualche dato disponibile
    • ritorna 0 finchè tutto il messaggio richiesto non è disponibile, con il flag MSG_WAITALL

Socket non bloccante

Un socket può essere settato come non bloccante:

socket(AF_INET,SOCK_STREAM|SOCK_NONBLOCK,0);

le operazioni non attendono risultati:

  • connect: se non può connettersi subito restituisce -1 e setta errno a EINPROGRESS
  • accept: se non si sono richieste restituisce -1 e setta errno a EWOULDBLOCK
  • send: se non riesce ad inviare tutto il messaggio subito (il buffer è pieno), restituisce -1 e setta errno a EWOULDBLOCK
  • recv: se non ci sono messaggi restituisce -1 e setta errno a EWOULDBLOCK

I/O multiplexing

Esistono multiplexing sincrono e asincrono

Multiplexing sincrono

Problema:

  • voglio controllare più socket contemporaneamente
  • se faccio operazioni su un socket bloccante, non posso controllarne altri
    Soluzioni:
  • multiplexing con la primitiva select()
  • esamina più socket contemporaneamente, il primo pronto viene usato
    • sincrono: il programma rimane comunque in attesa che qualche descrittore sia pronto

select()

Controlla più socket contemporaneamente

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
  • nfds: numero del descrittore più alto tra quelli da controllare, +1
  • readfds: lista di descrittori da controllare per la lettura
  • writefds: lista di descrittori da controllare per la scrittura
  • exceptfds: lista di descrittori da controllare per le eccezioni (non ci interessa)
  • timeout: intervallo di timeout
  • la funzione restituisce il numer odi descrittori pronti, -1 su errore
  • la funzione è bloccante, si blocca finché un descrittore tra quelli controllati diventa pronto, oppure finché il timeout non scade. Se scade il timeout ritorna 0 e non errore!

Descrittori pronti

select() rileva i socket pronti.
Un socket è pronto in lettura se:

  • C’è almeno un byte da leggere
  • Il socket è stato chiuso (read() restituirà 0)
  • È un socket in ascolto e ci sono connessioni effettuate
  • C’è un errore (read() restituirà -1)

Un socket è pronto in scrittura se:

  • C’è spazio nel buffer per scrivere
  • C’è un errore (write() restituirà -1)
    • Se il socket è chiuso, errno = EPIPE

Struttura del timeout

#include <sys/socket.h>
#include <netinet/in.h>
struct timeval {
	long tv_sec; /* seconds */
	long tv_usec; /* microseconds */
};
  • timeout = NULLL attesa indefinita, fino a quando il processore è pronto
  • timeout = { 10 ; 5 }: attesa massima di 10secondi e 5 microsecondi
  • timeout = { 0 ; 0 }: attesa nulla, controlla i descrittori ed esce immediatamente (polling)

Insieme di descrittori

Un descrittore è un int che va da 0 a FD_SETSIZE (di solito 1024) (questo è il numero massimo di socket che posso gestire con l'IO multiplexing, e dipende dal sistema).
Un insieme di descrittori si rappresenta con una variabile di tipo fd_set:

  • si manipola con macro simili a funzioni
/* Rimuovere un descrittore dal set */
void FD_CLR(int fd, fd_set *set);
/* Controllare se un descrittore è nel set */
int FD_ISSET(int fd, fd_set *set);
/* Aggiungere un descrittore al set */
void FD_SET(int fd, fd_set *set);
/* Svuotare il set */
void FD_ZERO(fd_set *set);

Utilizzo di select()

select() modifica i set di descrittori:

  • prima di chiamare select() inserisco nei seti di lettura e di scrittura i descrittori che voglio monitorare
  • dopo select() trovo nei set di lettura e scrittura i descrittori pronti
int main(int argc, char *argv[]){
	fd_set master; // Set principale
	fd_set read_fds; // Set di lettura
	int fdmax; // Numero max di descrittori
	
	struct sockaddr_in sv_addr; // indirizzo server
	struct sockaddr_in cl_addr; // indirizzo client
	int listener; // socket per ascolto
	int newfd;
	char buf[1024]; // Buffer
	int nbytes;
	int addrlen;
	int i;
	/* Azzero i set */
	FD_ZERO(&master);
	FD_ZERO(&read_fds);
	
	listener = socket(AF_INET, SOCK_STREAM, 0);
	sv_addr.sin_family = AF_INET;
	// Mi metto in ascolto su tutte le interfacce (indirizzi IP)
	sv_addr.sin_addr.s_addr = INADDR_ANY;
	sv_addr.sin_port = htons(20000);
	bind(listener, (struct sockaddr*)& sv_addr, sizeof(sv_addr));
	listen(sd, 10);
	FD_SET(listener, &master); // Aggiungo il listener al set
	fdmax = listener; // Tengo traccia del maggiore
		for(;;) {
			read_fds = master; // Copia
			select(fdmax + 1, &read_fds, NULL, NULL, NULL);
			for(i = 0; i <= fdmax; i++) { // Scorro tutto il set
				if(FD_ISSET(i, &read_fds)) { // Trovato un desc. pronto
					if(i == listener) { // È il listener
						addrlen = sizeof(cl_addr);
						newfd = accept(listener,(struct sockaddr *)&cl_addr, &addrlen)
						FD_SET(newfd, &master); // Aggiungo il nuovo socket
						if(newfd > fdmax){ fdmax = newfd; } // Aggiorno max
						} else { // È un altro socket
						nbytes = recv(i, buf, sizeof(buf));
						//… Uso dati
						close(i); // Chiudo socket
						FD_CLR(i, &master); // Rimuovo il socket dal set
					}
				}
			}
		}
	return 0;
}

Socket UDP

Vedi Transport Layer.
La bind() per il client può anche essere opzionale. È necessaria se si vuole usare una porta specifica (per superare restrizioni di firewall o NAT)
Non c’è connessione, quindi non si fanno listen(), connect() o accept(), si utilizza solo socket(), bind() e sendto() e recvfrom().

sendto()

Invia un messaggio attraverso un socket all’indirizzo specificato.

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd: descrittore socket da usare per invio
  • len: dimensione in byte del messaggio
  • flags: per settare opzioni, lo lasciamo a zero
  • dest_addr: puntatore alla struttura in cui è salvato l’indirizzo del destinatario
  • addrlen: lunghezza di dest_addr
  • la funzione restituisce il numero di byte inviati, -1 su errore
  • la funzione è bloccante, si ferma finché non ha scritto tutto il messaggio

recvfrom()

Riceve un messaggio attraverso un socket.

ssize_t recvfrom(int sockfd, const void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t addrlen);
  • sockfd: descrittore del socket
  • buf: puntatore al buffer per ricevere il messaggio
  • len: dimensione in byte del messaggio
  • src_addr: puntatore dove salvare l’indirizzo del mittente
  • addrlen: lunghezza di src_addr
  • la funzione restituisce il numero di byte ricevuti, -1 su errore, 0 se il socket remoto si è chiuso
  • funzione bloccante

Connessione socket UDP

Usando connect() si può associare ad un socket UDP un indirizzo remoto.
Il socket riceverà/invierà pacchetti solo da/a quell’indirizzo, ma attenzione, non è una connessione.
Con un socket connesso si possono usare send() e recv(), evitando di specificare ogni volta l’indirizzo.