Le variabili possono essere definite ovunque, come nel C++.
Le differenze strutturali si notano nella gestione di oggetti, nella memoria dinamica e nelle operaizoni di I/O.
struct Complex{
double Re;
double Im;
};
int main(){
int a = 4;
struct Complex c; //! Va esplicitata
}
In C abbiamo le seguenti caratteristiche:
struct nomeStructpublicC funzionano in C++In C non esiste la keyword new per allocare dinamicamente memoria nello heap.
Per poter fare operazioni sulla memoria dobbiamo includere <stdlib.h> e utilizare:
void* malloc(size) per allocare una zona di memroiavoid free(void*) per deallocare una zona di memoria#include <stdlib.h>
int main(){
int mem_size = 5;
int *ptr;
ptr = (int*)malloc(sizeof(int)*mem_size);
if(ptr == NULL){
// errore nell'allocazione
}
free(ptr);
}
Le operazioni di I/O si basano sulla <stdlib.h>, che fornisce funzioni per leggere e scrivere da/verso:
stdin (tastiera), attraverso la int scanf(char* formato, void& indirizzi). È una funzione rischiosa perché non controlla la lunghezza e può causare buffer overflow. Meglio utilizzare fgets(str, sizeof(str), stdin)stdout (schermo), attraverso la int printf(char* formato, argomenti)file attraverso la fprintf(FILE* f, ...) e la fscanf(FILE* f, ...)#include <stdio.h>
int main(){
char* str = "Hello!\n";
printf(str);
pritnf("str = %s", str);
int i = 5;
printf("i = %d\n", i);
scanf("%d", &i);
printf("i = %d\n", i);
}

Alcuni caratteri epr la manipolazione delle stringhe nelle funzioni.
In C non esiste il tipo nativo stringa, ma è un array di caratteri terminato con il carattere speciale \0:
#include <stdio.h>
char *str1 = "Hello \n"; // Alloco 8 Byte |H|e|l|l|o| |\n|\0|
int len = strlen(str1); // len -> 7
Per confrontare due stringhe alfabeticamente, ovvero secondo l’ordine del dizionario, si utilizza la int strcmp(char* s1, char* s1):
0 se sono identiche<0 se s1 è alfabeticamente minore di s2>0 se s1 è alfabeticamente maggiore di s2Alcune funzioni:
char str[20];
n = sizeof(str);
strncpy(str, "Hello \n", n); // 8 è minore di 20, quindi tutt'ok
char *str2 = "World!\n";
n2 = sizeof(str2);
strcat(str, str2, n2);
/*
* considera str come la lunghezza senza \0, mentre in str2 lo considera
* produce quindi "Hello \nWorld!\n\0"
Ogni file aperto è rappresentato da un puntatore di tipo FILE* definito in <stdio.h>
#include <stdio.h>
FILE* fd;
fd = fopne("path/file.txt", "r");
if(fd == NULL){
// Gestione errore
}
| Modificatore | Significato | File inestistente |
|---|---|---|
r |
Sola lettura | segmentation fault |
w |
Sola scrittura | crea il file |
r+ |
lettura e scrittura | crea il file |
a |
append | crea il file |
a+ |
lettura e append | crea il file |
Per leggere/scrivere si un file si utilizzano fscanf() e fprintf() che funzionano esattamente come scanf e printf.
#include <stdio.h>
// Lettura file
int ret, n;
FILE *fd1;
fd1 = fopen("/tmp/foo.txt", "r");
if (fd1 == NULL) {
// Gestione errore
}
ret = fscanf(fd1, "%d", &n);
// Scrittura file
char *str = "Hello!\n";
FILE *fd2;
fd2 = fopen("/tmp/bar.txt", "w");
if (fd2 == NULL) {
// Gestione errore
}
ret = fprintf(fd2, "%s", str);
Per ottenere informazioni sul file:
#include <stdio.h>
#include <sys/stat.h>
// Dimensione
int ret, size;
struct stat info;
ret = stat("/tmp/foo.txt", &info);
size = info.st_size;
Per la compilazione si creano i file oggetto a partire dai file sorgente:
# Generiamo un file oggetto per ogni sorgente
gcc -Wall -c myfile1.c myfile2.c
# `-Wall` non ignora i warning
# `-c` specifica che sono file `.c`
Per il linking dei vari file oggetto in un unico eseguibile:
gcc -o myProgram myFile1.o myFile2.o
Oppure in un unica linea:
gcc -Wall -c myFile1.c myFile2.c -o myProgram
Di seguito un esempio di stampa di data e ora:
time_t rawtime;
struct tm * timeinfo;
/* Ottieni l'ora in formato POSIX */
time(&rawtime);
timeinfo = localtime (&rawtime);
/* La struttura timeinfo non va deallocata (è allocata staticamente dal
sistema operativo, viene sovrascritta ad ogni invocazione */
/* Stampa l'ora */
// ctime() trasforma l'ora in stringa
printf("%s\n", ctime(&rawtime));
/* Formattare data/ora in stringa secondo un formato specificato */
strftime(buffer, BUFFER_SIZE, "%Y-%m-%d %H:%M:%S", timeinfo);
I vari formati sono:
| Modificatore | Descrizione | Esempio |
|---|---|---|
%A |
Nome completo del giorno della settimana | Wednesday |
%d |
Giorno del mese, con due cifre | 25 |
%B |
Nome completo del mese | September |
%Y |
Anno con quattro cifre | 2024 |
%H |
Ora con due cifre nel formato 24 ore | 15 |
%M |
Minuti con due cifre | 30 |
%S |
Secondi con due cifre | 45 |
%a |
Nome abbreviato del giorno della settimana | Mon, Tue |
%b |
Nome abbreviato del mese | Jan, Feb |
%I |
Ora con due cifre nel formato 12 ore | 03 |
%p |
AM/PM (usato con il formato 12 ore) | / |
%m |
mese | 01-12 |
La struct tm invece:
struct tm{
int tm_sec // Seconds [0,60].
int tm_min // Minutes [0,59].
int tm_hour // Hour [0,23].
int tm_mday // Day of month [1,31].
int tm_mon // Month of year [0,11].
int tm_year // Years since 1900.
int tm_wday // Day of week [0,6] (Sunday =0).
int tm_yday // Day of year [0,365].
int tm_isdst // Daylight Savings flag.
};
Due processi possono cooperare attraverso:
Due processi possono cooperare:
Un paradigma basato su scambio di messaggi, utilizzato principalmente per sistemi distribuiti, è il modello Client/Server.


Il protocollo dei messaggi è il TCP oppure l’UDP.
Si aprono atraverso i socket.
Un socket è l’astrazione di un canale di comunicazione tra processi. Fornisce un interfaccia unica per operare con diversi protocolli di rete, nascondendo i dettagli dei livelli sottostanti.
Un socket è identificato da un indirizzo:
TCP/IP: indirizzo IP)TCP/IP: numero porta)Nelle comunicazioni un scket è l’estremità del canale di comunicazione:

L’astrazione del socket è implementata dal sistema operativo. Quest’ultimo infatti mette a disposizioen delle primitive (system calls) che permettono di:
Per la creazione di un socket dobbiamo includere tre librerie e utilizzare la primitiva socket():
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
/**
* Permette la creazione di un socket. Non lo associa ad un indirizzo IP o ad una porta
* @param domain è la famisglia di protocolli da utilizzare, può valere
* - AF_LOCAL: comunicazione locale
* - AF_INET: protocolli IPv4, TCP e UDP
* @param type specifica la tipologia di socket
* - SOCK_STREAM: connessione affidabile, bidireionale (TCP)
* - SOCK_DGRAM: invio di pacchetti senza connessione (UDP)
* @param protocol va impostato sempre a 0
* @return un descrittore di file che rappresenta il socket e servità a manipolare il socket attraverso le altre primitive, `-1` su errore
*/
int socket(int domain, int type, int protocol);
Alcune strutture dati utili per la gestione {degli indirizzi:
/* Endpont IPv4 in `netinet/in.h`*/
struct sockaddr_in{
sa_family_t sin_family; /* address family: AP_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*/
}
Esiste anche una struct sockaddr utilizzata da alcune funzioni, ma che non non manipoleremo mai direttamente.
Negli esempi è possibile notare la dicitura in network byte order. Questo è sepcificata perché calcolatori diversi possono usare modalit diverse per ordinare i byte.
Prendiamo per esempio il numero 422990 in 32bit: 0000 0000 0000 0110 0111 0100 0100 1110
Questo può essere memorizzato:
| big-endian | little-endian |
|---|---|
| Per primo il byte più significativo (MSB) | Per primo il byte meno significativo (LSB) |
0000 0000 \| 0000 0110 \| 0111 0100 \| 0100 1110 |
0100 1110 \| 0111 0100 \| 0000 0110 \| 0000 0000 |
Sulla rete viene utilizzata la notazione big-endian, mentre il formato dell’host è variabile.
Sono quindi fornite le seguenti funzioni di conversione:
#include <arpa/inet.h>
#include <stdin.h> // per `uint16_t` e `uint32_t`
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
I formati uint16_t e uint32_t utilizzano sempre rispettivamente 16 e 32 bit, a prescindere del calcolatore utilizzato per compilare.
Noi siamo abituati a vederli nel formato presentazione, in notazione puntata (192.34.5.123)
Gli indirizzi IP seguono però un formato numerico su 32bit utilizzato dal computer. (3223455 099)
Sono quindi necessari delle funzioni di traduzione:
/**
* Permette di tradurre da formato presentazione a formato numerico
* @param af (address family) famiglia (AF_INET)
* @param src stringa del tipo "ddd.ddd.ddd.ddd"
* @param dst puntatore a una struct in_addr
*/
int inet_pton(int af, const char *src, void *dst);
/**
* Permette di tradurre da formato numerico a formato presentazione
* @param af: famiglia (AF_INET)
* @param src: puntatore a una struct in_addr
* @param dst: puntatore a un buffer di caratteri lungo size
* @param size: deve valere almeno INET_ADDRSTRLEN
*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
Un piccolo programma che crea un socket e crea un indirizzo può essere il seguente:
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
int sd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr)); // Pulizia per sicurezza
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(4242);
inet_pton(AF_INET, "192.168.4.5", &my_addr.sin_addr);
}
Le operazioni preliminari che effettua un server per istaurare comunicazioni su socket TCP sono:
socket()bind()listen()accept()/**
* Assegna un socket a un indirizzo (IP+Porta).
* Viene usata dal server per specificare indirizzo e porta sui quali ricevere richieste.
* Di solito, il client non ha bisogno di eseguire questa funzione
* @param sockfd: descrittore del socket (socket file descriptor)
* @param addr: puntatore alla struttura di tipo `struct sockaddr` (Visto che usiamo `struct sockaddr_in` bisogna convertire il puntatore)
* @param addrlen: dimensione di `addr`
* @return `0` se ha successo, `-1` su errore
*/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// Esempio
ret = bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr));
/**
* Specifica che il socket è passivo, verrà quindi utilizzato per ricevere richieste di connessione.
* Possono essere messi in listen solo socket `SOCK_STREAM`
* All'arrivo di una richiesta di conessione il kernel lo mette in attesa in una coda, attendendo la chiamata di accept() per prenderle in carico
* Se arrivano più richieste di `backlog` le nuove richieste verranno rifiutate
* @param sockfd: descrittore del socket
* @param backlog: dimensione della coda, cioè quante richieste dai client possono rimanere in attesa di essere gestite
* @return `0` se ha successo, `-1` su errore
*/
int listen(int sockfd, int backlog);
// Esempio
ret = listen(sd, 10);
/**
* Accetta una richiesta di connessioen perveunta sul socket.
* Ha senso solo sui socket `SOCK_STREAM`
* La funzione è bloccante: il programma si ferma finché non arriva una richiesta
* @param sockfd: descrittore del socket
* @param addr: puntatore a una struttura (vuota) di tipo `struct sockaddr` (Qui ci viene salvato l'indirizzo del client
* @param addrlen: dimensione di addr
* @returns il descrittore di un nuovo socket che verrà usato per la comunicazione, `-1` su errore
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// Esempio
struct sockaddr_in cl_addr;
int len = sizeof(cl_addr);
new_sd = accept(sd, (struct sockaddr*)&cl_addr, &len);
Un esempio di codice lato server può essere il seguente:
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(){
int ret, sd, new_sd, len;
struct sockaddr_in myaddr, cl_addr;
sd = socket(AF_INET, SOCK_STREAM, 0);
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(4242);
inet_pton(AF_INET, "192.168.4.5", &my_addr.sin_addr);
// se vogliamo stare in attesa su tutte le interfacce:
// `my_addr.sin_addr.s_addr = INADDR_ANY`
ret = bind(sd, (struct sockaddr*)&myaddr, sizeof(my_addr));
ret = listen(sd, 10);
int len = sizeof(cl_addr);
new_sd = accept(sd, (struct sockaddr*)&cl_addr, &len);
}
Per connettere un socket locale ad un socket remoto si utilizza:
/**
* Permette di connettere un socket locale ad un socket remoto.
* È una funzione bloccante che ferma il programma finché la richiesta di connessione non viene accetata
* @param sockfd descrittore del socket locale
* @param addr puntatore alla struttura contenente l'indirizzo del server
* @param addrlen dimensione di addr
* @returns `0` se ha successo, `-1 su errore
*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Un esempo di codice:
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main () {
int ret, sd;
struct sockaddr_in sv_addr; // Struttura per il server
/* Creazione socket */
sd = socket(AF_INET, SOCK_STREAM, 0);
/* Creazione indirizzo del server */
memset(&sv_addr, 0, sizeof(sv_addr)); // Pulizia
sv_addr.sin_family = AF_INET ;
sv_addr.sin_port = htons(4242);
inet_pton(AF_INET, "192.168.4.5", &sv_addr.sin_addr);
ret = connect(sd, (struct sockaddr*)&sv_addr, sizeof(sv_addr));
//...
}
Chi vuole inviare messaggi utilizza la seguente primitiva:
/**
* Invia un messaggio attraverso un socket connesso.
* È una funzione bloccante che ferma il programma finché non ha scritto tutto il messaggio.
* La funzione non "spedice pacchetti", ma copia dati nel buffer del kernel. Sarà poi quest'ultimo a frammentarli in pacchetti TCP.
* In caso di buffer saturo si blocca l'esecuzione del programma finché non vi è sufficiente spazio
* @param sockfd descrittore del socket
* @param buf puntatore al buffer contenente il messaggio da inviare
* @param len dimensione in byte del messaggio
* @param flags per settare delle opzioni, lo lascieremo a `0`
* @returns `numero di byte inviati` se ha successo, `-1` su errore
*/
ssize_t send(int sockfd, const void* buf, size_t len, int flags);
Un esempio:
int ret, sd, len;
char buffer[1024];
// ...
strcpy(buffer, "Hello Server!");
len = strlen(buffer);
ret = send(sd, (void*)buffer, len, 0);
if(ret < len){
// Gestione Errore
}
Chi vuole ricevere un messaggio invece:
/**
* Permette di prelevare un messaggio da un socket connesso.
* La funzione è bloccante, ferma il programma finché non ha letto qualcosa
* @param sockfd descrittore del socket
* @param buf puntatore al buffer in cui salvare il messaggio
* @param len dimensione in byte del messaggio desiderato
* @param flags per settare delle opzioni
* @returns `numero di byte ricevuti`, `-1` su errore, `0` se il socket remoto si è
chiuso
*/
ssize_t recv(int sockfd, const void *buf, size_t len, int flags);
Vediamo un esempio:
int ret, sd, bytes_needed;
char buffer[1024];
// ...
bytes_needed = 20;
ret = recv(sd, (void*)buffer, bytes_needed, 0);
// Adesso 0 < ret <= bytes_needed
if (ret < bytes_needed) {
// Gestione errore
}
// OPPURE, per avere la certezza che `ret == bytes_needed`
ret = recv(sd, (void*)buffer, bytes_needed, MSG_WAITALL);
Se volessimo chiudere un socket, questo non può più essere usato per inviare o ricevere dati:
#include <unistd.h>
/**
* Permette di chiudere un socket.
* Invia un messaggio di `0` all'host remoto
* @param fd descrittore del socket
* @returns `0` se ha successo, `-1` su errore
*/
int close(int fd);
Uno schema può essere il seguente:

Abbiamo visto che le primitive fin’ora restituiscono -1 in caso di errore.
Oltre ad avere questo ritoro, le primitive settano una variabile errno che può essere letta per scoprire il motivo dell’errore.
#include <errno.h>
ret = bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr));
if (ret == -1) {
switch(errno){
case EADDRINUSE: /* Gestisci errore */
case EINVAL: /* Gestisci errore */
//...
}
}
Di seguito si può vedere una breve lista, non completa, di errori rilevanti:

Per vedere l’errore a schermo è sufficiente utilizzare un altra funzione:
#include <stdio.h>
// ...
ret = bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr));
if (ret == -1) {
perror("Error: ");
exit(1);
}
// ...
Quando creiamo un server, tendenzialmente vogliamo che sia in grado di accettare più connessioni mentre ne gestisce altre.
Per fare ciò vi sono diversi metodi:
#include <unistd.h>
pid_t fork(void);
La primitiva fork() duplica un processo restituendo:
PID del figlio0Quando un processo viene duplicato, il padre e il figlio si ritrovano gli stessi descrittori duplicati.
Ogniuno deve quindi chiudere il descrittore che non usa, per evitare di mantenere aperte risorse non necessarie. In particolare:
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);
}
pthreadUn thread è identificato dal tipo pthread_t, e protegge le risorse condivise attraverso pthread_mutex_t
#include <pthread.h>
/**
* crea un nuovo processo che partirà eseguendo start_routine(void)
*/
int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void* (*start_routine)(void), void* arg);
/**
* blocca un thread in attesa della terminazione di un altro
*/
int pthread_join(pthread_t thread, void** retval);
/**
* per terminare un thread e mettere a disposizione un suo risultato
*/
void pthread_exit(void* retval);
Di default un socket è bloccante.
Tutte le operazioni su di esso fermano l’esecuzione del processo in attesa del risultato. Questa modalità è sepmlice da usare e va bene per applicazioni in cui i blocchi temporanei non sono problematici o in cui viene utilizzato il multithreading per gestire la concorrenza.
I socket possono però essere settati anche come non bloccanti.
In questo caso le operazioni non attendono i risultati:
connect(): se non può connettersi subito restituisce -1 e setta errno = EINPROGRESSaccept(): se non ci sono richieste restituisce -1 e setta errno = EWOULDBLOCKsend(): se non riesce a inviare tutto il messaggio subito (il buffer è pieno) restituisce -1 e setta errno = EWOULDBLOCKrecv(): se non ci sono messaggi restituisce -1 e setta errno = EWOULDBLOCKPer cerare un socket non bloccante:
int host_fd = socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK, 0);
Altri errori che ci possono essere sono quelli nell’immagine già presentata sopra.
La modalità non bloccante invece è utile in applicazioni che devono gestire molte connessioni simultaneamente e dove bloccare il programma per un singolo socket è inefficiente.
La modalità non blocante è utilizzata insieme a tecniche di multiplexing di IO, con chiamate come select(), poll() e epoll() per monitorare più socket, e agire quando uno o più di questi diventano pronti per operazioni di IO.
#include <unistd.h>
#include <fcntl.h>
/**
* Server per manipolare un descrittore di file o di socket
* @param fd il descrittore
* @param cmd il comando da passare. Alcuni esempo
* - F_GETFL: ottieni i flag di stato
* - F_SETFL: imposta i flag di stato
* @param other terzo argomento facoltativo necessario per alcuni comandi
* @returns dipende dal comando, `-1` e errno su errore
*/
int fcntl(int fd, int cmd, int other);
Un esempio di utilizzo:
int sd, ret;
sd = socket(/**/);
/* Ottieni i flag attuali */
ret = fcntl(sd, F_GETFL, NULL);
/* Setto il flag non bloccanto, MANTENENDO GLI ALTRI*/
fcntl(sd, F_SET, ret | O_NONBLOCK);
Permette di controllare più descrittori/socket (non bloccanti) nello stesso momento, attraverso la primitiva select().
Può essere:
selectAbbiamo già detto che permette di controllare più socket contemporaneamente:
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
/**
* Rileva i socket pronti
* È una funzione bloccante. Si sblocca o quando un socket controllato diventa pronto o quando scade il timeout.
* @param nfds numero del descrittore più alto tra quelli da controllare + 1
* @param readfds lista di descrittori da controllare per la lettura
* @param writefds lista di descrittori da controllare per la scrittura
* @param exceptfds lista di descrittori da controllare per le eccezioni (non lo utilizzeremo)
* @param timeout intervallo di timeout
* @return restituisce il numero di descrittori pronti, `0` se scade il timeout, `-1` su errore
*/
int select(int nfds, fd_set* readfds, fd_set* writefds_fd, fd_set* exceptfds, struct timeval *timeout);
Un socket si dice pronto in lettura se:
read() == 0)read() == -1)Un socket si dice pronto in scrittura se:
write() == -1), se il socket viene chiuso viene settato errno = EPIPE#include <sys/socket.h>
#include <netinet/in.h>
struct timeval{
long tv_sec; // secondi
long tv_usec; // microsecondi
};
struct timeval timeout = NULL; // attesa indefinita
timeout = {10; 5}; // attesa massima di 10 secondi e 5 microsecondi
timeout = {0; 0}; // attesa nulla, controlla ed esce (polling)
Un descrittore è un int che va da 0 a FD_SETSIZE (tipicamente 1024).
Un insieme di descrittori si rappresenta con una variabile di tipo fd_set, che manipoleremo con delle macro simili a funzioni:
/* Rimuove 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);
/* Aggiunge un descrittore al set */
void FD_SET(int fd, fd_set *set);
/* Svuotare il set */
void FD_ZERO(fd_set *set);
select()Vediamo un esempio di utilizzo di select:
int main(int argc, char** argv){
fd_set master; // Set principale
fd_set read_fds; // Set di lettura
int fdmax; // Numero massimo di descrittori
struct sockaddr_in sv_addr; // Indirizzo server
struct sockaddr_in cl_addr; // Indirizzo client
int listener; // Socket per l'ascolto
int newfd;
char buf[1024];
int nbytes;
int addrlen;
int i;
// Azzero i set
FD_ZERO(&master);
FD_ZERO(&read_fds);
listener = socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK, 0);
sv_addr.sin_family = AF_INET;
sv_addr.sin_port = hton(20000);
sv_addr.sin_addr.s_addr = INADDR_ANY;
bind(listener, (struct sockaddr*)&sv_addr, sizeof(sv_addr));
listen(listener, 10);
FD_SET(listener, &master); // Aggiungo il listener al set
fdmax = listener;
for(;;){
read_fds = master; // copio il set
select(fdmax + 1, &read_fds, NULL, NULL, NULL);
for(int i = 0; i <= fd_max; ++i){ // scorro tutto il set
if(FD_ISSET(i, &read_fds)){ // se trovo un descrittore pronto
if(i == listener){ // è il listener
addrlen = sizeof(cl_addr);
newfd = accept(listener, (struct sockaddr_in*)&cl_addr, &addrlen);
FD_SET(newfd, &master); // aggiungo il nuovo socket al set
if(newfd > fdmax){
fdmax = newfd; // aggiorno max
}
}
else{
// Se è un nuovo socket
nbytes = recv(i, buf, sizeof(buf));
// ... uso i dati
close(i);
FD_CLR(i, &master); // rimuovo il socket dal set
}
}
}
}
}
A differenza dei socket TCP che instaurano una connessione, richiedendo operazioni preliminari per instaurare il canale virtuale, i socket UDP sono connection-less.
Questa differenza comporta la perdita dell’affidabilità, ottenedo però connessioni più rapide
sendto() e revcfrom()/**
* È una funzione bloccante
* @param sockfd descrittore del socket
* @param buf buffer contenente il messaggio da inviare
* @param len lunghezza del messaggio in byte
* @param flags per settare opzioni, lo lasceremo a `0`
* @param dest_addr puntatore alla struttura che contiene l'indirizzo del destinatario
* @param addrlen lunghezza di `dest_addr`
* @returns il numero di byte inviati, `-1` su errore
*/
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
/**
* È una funzione bloccante
* @param sockfd descrittore del socket
* @param buf buffer contenente il messaggio da ricevere
* @param len lunghezza del messaggio in byte
* @param flags per settare opzioni
* @param src_addr puntatore alla struttura vuota dove salveremo l'indirizzo del mittente
* @param addrlen lunghezza di `src_addr`
* @returns il numero di byte ricevuti, `-1` su errore, `0` se il socket remoto si è chiuso
*/
ssize_t recv_from(itn sockfd, const void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t addrlen);
Possiamo vedere un esempio di codice:
Server
int main () {
int ret, sd, len;
char buf[BUFLEN];
struct sockaddr_in my_addr, cl_addr;
int addrlen = sizeof(cl_addr);
/* Creazione socket */
sd = socket(AF_INET, SOCK_DGRAM, 0);
/* Creazione indirizzo */
memset(&my_addr, 0, sizeof(my_addr)); // Pulizia
my_addr.sin_family = AF_INET ;
my_addr.sin_port = htons(4242);
my_addr.sin_addr.s_addr = INADDR_ANY;
ret = bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr));
while(1) {
len = recvfrom(sd, buf, BUFLEN, 0,(struct sockaddr*)&cl_addr, &addrlen);
//...
}
}
Client
int main() {
int ret, sd, len;
char buf[BUFLEN];
struct sockaddr_in my_addr, sv_addr; // Struttura per il server
sd = socket(AF_INET, SOCK_DGRAM, 0); /* Creazione socket */
if (sd < 0) {
perror("Errore nella creazione del socket");
exit(1);
}
memset(&my_addr, 0, sizeof(my_addr)); /* Creazione proprio indirizzo*/
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = INADDR_ANY;
my_addr.sin_port = htons(5432);
if (bind(sd, (struct sockaddr*)&my_addr, sizeof(my_addr)) < 0) {
perror("Errore nel bind");
close(sd);
exit(1);
}
memset(&sv_addr, 0, sizeof(sv_addr)); /* Creazione indirizzo del server */
sv_addr.sin_family = AF_INET ;
sv_addr.sin_port = htons(4242);
inet_pton(AF_INET, "192.168.4.5", &sv_addr.sin_addr);
while(1) {
len = sendto(sd, buf, BUFLEN, 0,(struct sockaddr*)&sv_addr,sizeof(sv_addr));
//...
}
}
Di seguito è proposto un esempio di client che non utilizza bind:
int main(){
int ret, sd, len;
char buf[BUFLEN];
struct sockaddr_in sv_addr;
/* Creazione Socket */
sd = socket(AF_INET, SOCK_DGRAM, 0);
/* Creazione indirizzo del server */
memset(&sv_addr, 0, sizeof(sv_addr)); // Pulizia
sv_addr.sin_family = AF_INET ;
sv_addr.sin_port = htons(4242);
inet_pton(AF_INET, "192.168.4.5", &sv_addr.sin_addr);
while(1){
len = sendto(sd, buf, BUFLEN, 0, (struct sockaddr*)&sv_addr, sizeof(sv_addr));
// ...
}
}
Di seguito è proposto uno schema di comunicazione attraverso socket UDP:

Attraverso un socket connesso tramite connect() è possibile utilizzare send() e recv() senza specificare ogni volta l’indirizzo.
Questo non è tuttavia necessario.
Molti protocolli a livello applicativo inviano messaggi di testo (text protocols) invece di inviare direttamente i dati delle strutture dati (binary protocols).
I dati da inviare sono tradotti in stringhe (con una lunghezza massima) e inviati nel socket come testo, descrivendo i campo attraverso keywords.
Alcuni esempi di Text Protocols sono HTTP, SMTP, FTP, …
Questi protocolli hanno come pro:
Come contro:
Immaginiamo di avere la seguente struttura:
struct tmp{
int a;
char b;
} t;
Possiamo pensare di convertirla in testo:
/* invio */
sprintf(buffer, "%d %c", t.a, t.b);
/* ricezione */
sscanf(buffer, "%d %c", &t.a, &t.b);
Il Binary Protocol utilizza dati codificati direttamente nel formato binario nativo, non leggibile dall’uomo.
Si definiscono messaggi con struttura prefissata, composti da campi che rappresentano l’informazione da scambiare. L’informazione 12345 verrà convertita in binario a 32bit e inviati come tale, occupando solo 4Byte invece di 5Byte
Ciascun campo avrà una lunghezza e un tipo associate. Nei rari casi in cui ci potrebbero essere campi a lunghezza variabile, questi hanno comunque una dimensione massima.
L’invio di strutture senza controlli:
sendto(sock, &t, sizeof(struct tmp), 0, &server, length);
È da evitare in quanto è una brutta idea. Questo infatti non tiene conto di:
Le strutture dati devono essere quindi progettato con dei tipi che possono essere trasferiti.
struct temp{
uint32_t a;
uint8_t b;
} t;
//...
t.a = htonl(t.a); // Convertire il formato prima dell’invio
ret = send(new_sd, (void*)&t.a, sizeof(uint32_t), 0);
t.b = htonl(t.b);
ret = send(new_sd, (void*)&t.b, sizeof(uint8_t), 0);
Così che il ricevitore:
struct temp t;
ret = recv(new_sd, (void *)&t.a, sizeof(uint32_t), 0);
if (ret < sizeof(uint32_t)) {
// Gestione errore
}
t.a = ntohl(t.a); // Convertire in host order il campo ‘a’
ret = recv(new_sd, (void *)&t.b, sizeof(uint8_t), 0);
if (ret < sizeof(uint8_t)) {
// Gestione errore
}
// ...
Il tipo uint32_t è un tipo di dato con una dimensione fissa di 32bit su tutte le piattaforme.
Questo standard è fodamentale per i protocolli binari, dove è necessario sapere esattamente quanti byte vengono trasmessi e ricevuti.
Infatti, le dimensioni del tipo int possono variare dall’architettura o dal sistema operativo:
32bit un int è di 4Byte64bit un int può essere di 4Byte, ma anche di 8ByteLa variabilità rende l’uso del tipo int poco sicuro nei protocolli binari.
Poiché la rappresentazione binaria dei dati può variare tra le diverse piattaforme, è necessario gestire l’endianess e la serializzazione/deserializzazione.
Per garantire la corrente interpretazione dei dati tra sistemi con endianess diverso si utilizzano funzioni tipo htonl() (Host to Network Long) e nothl() (Network to Host Long) per convertire tra host byte order e network byte order.
La serializzazione è il processo di trasformazione di una struttura complessa in una sequenza di byte, mentre la deserializzazione fa il contrario, trasformando sequenze di byte in strutture dati.
Il protocollo binario ha diversi vantaggi:
Tuttavia, anche in questo caso abbiamo dei contro:
htonl() e ntohl() per la conversione.