Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endianness
In questa pagina 10
Un programma di rete in C non scambia "oggetti": scambia byte. Quello che il kernel consegna con read è una sequenza di byte di cui si conosce solo la lunghezza; quello che si spedisce con write è la memoria così com'è. Per scrivere client e server che funzionano su macchine diverse bisogna quindi sapere esattamente come il C dispone i dati in memoria. Questa nota richiama solo ciò che serve alla parte di rete; per il resto si rimanda a Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →, Strutture (struct) in Cstruct per raggruppare campi di tipo diverso, typedef, accesso con punto e freccia, struct come parametri e valori di ritorno, array di struct, struct allocate dinamicamente e struct autoreferenziali.Strutture (struct) in C →, Array e stringhe in CArray di dimensione fissa in memoria contigua, inizializzazione, nessun controllo sugli indici, passaggio a funzioni con la lunghezza, matrici; stringhe come array di char terminati da '\0' e funzioni di string.h.Array e stringhe in C → e Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →.
La memoria di un processo
Quando il sistema operativo avvia un programma gli dà uno spazio di indirizzi virtuale (Memoria virtualeIndirizzi virtuali e fisici, pagine e frame; traduzione con la tabella delle pagine e calcolo dei campi; page fault e sostituzione delle pagine; TLB e tempo di accesso effettivo; protezione e condivisione; confronto con la cache.Memoria virtuale →) diviso in segmenti:
| segmento | contenuto | esempio in un server |
|---|---|---|
| codice (text) | le istruzioni macchina, in sola lettura | main, handle_client |
| dati statici (data, bss) | variabili globali e static, stringhe letterali |
static char root[256] |
| heap | memoria allocata con malloc, cresce verso l'alto |
il buffer di una risposta di dimensione variabile |
| stack | variabili locali e indirizzi di ritorno, cresce verso il basso | char line[1024] dentro una funzione |
indirizzi alti +----------------+
| stack | <- variabili locali, un blocco per ogni chiamata
| | |
| v |
| |
| ^ |
| | |
| heap | <- malloc / free
+----------------+
| dati statici | <- globali, static, stringhe letterali
+----------------+
| codice |
indirizzi bassi +----------------+Due conseguenze pratiche per i programmi di rete:
- Un array locale vive nello stack e sparisce al ritorno della funzione: restituire
lineda una funzione che lo ha dichiarato locale è un errore (puntatore pendente). Un buffer che deve sopravvivere va nello heap o nel chiamante. - Lo stack è piccolo (di solito 8 MB):
char buf[10000000];locale manda il programma in errore. Un buffer di qualche kilobyte è invece normale.
Con fork (System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →) il figlio riceve una copia di tutto lo spazio di indirizzi: una variabile modificata dal figlio non cambia nel padre.
Tipi e dimensioni
Le dimensioni di int e long dipendono da macchina e compilatore. Su Linux a 64 bit (modello LP64) valgono char 1 byte, short 2, int 4, long 8 e un puntatore 8. Nei protocolli le dimensioni devono essere esatte: si usano i tipi di <stdint.h>.
| tipo | significato | uso tipico |
|---|---|---|
uint8_t |
intero senza segno a 8 bit | un byte di un buffer |
uint16_t |
senza segno a 16 bit | numero di porta, lunghezza |
uint32_t |
senza segno a 32 bit | indirizzo IPv4, timestamp |
uint64_t |
senza segno a 64 bit | lunghezza estesa di un frame WebSocket |
size_t |
dimensione di un oggetto (senza segno) | risultato di sizeof, strlen, argomento di read |
ssize_t |
come size_t ma con segno |
valore di ritorno di read e write (-1 = errore) |
Il punto delicato è ssize_t: read restituisce il numero di byte letti, 0 a fine flusso e -1 in caso di errore. Salvarlo in un size_t trasforma -1 in un numero enorme e il controllo if (n < 0) non scatta mai.
ssize_t n = read(fd, buf, sizeof buf); /* giusto: n puo' valere -1 */
if (n < 0) { perror("read"); }char può avere o non avere il segno a seconda della piattaforma: un byte 0xE8 letto in un char può diventare -24. Per trattare byte si usa unsigned char o uint8_t; è indispensabile con isxdigit, con gli indici di tabelle e con i confronti.
Byte, buffer e stringhe
Una stringa C è una sequenza di caratteri terminata da un byte '\0': strlen, strcpy, printf("%s") si fermano a quel byte. Un buffer è una zona di memoria con una lunghezza nota a parte. I dati letti da un socket sono un buffer, non una stringa:
- non c'è nessun
'\0'finale:readnon lo aggiunge; - possono contenere byte
0in mezzo (un file PNG, un frame WebSocket); - possono arrivare a pezzi: una
readrestituisce quello che c'è, non "un messaggio" (System call POSIX, file descriptor e API delle socketLe system call sono le funzioni con cui un programma in user space chiede servizi al kernel (open, read, write, close, fork, pipe, dup2, socket, bind, listen, accept, connect); restituiscono -1 e impostano errno in caso di errore; un file descriptor e' un intero che indicizza la tabella dei file aperti del processo (0 stdin, 1 stdout, 2 stderr) e vale per file, pipe e socket; read e write possono trasferire MENO byte del richiesto (e sui socket TCP non c'e' nessun confine fra i messaggi), quindi servono cicli write_all e read_exact; l'API delle socket crea un punto finale di comunicazione (socket), lo lega a indirizzo e porta (bind), lo rende passivo (listen), accetta connessioni (accept) o si connette (connect); getaddrinfo traduce nomi e porte in indirizzi (IPv4 e IPv6); UDP usa sendto e recvfrom e conserva i confini dei datagrammi, TCP e' uno stream affidabile.System call POSIX, file descriptor e API delle socket →).
Regole per non sbagliare:
- Prima di usare
strlen,strcmp,strstroprintf("%s")su dati ricevuti, terminare a mano il buffer:buf[n] = '\0', riservando un byte in più (read(fd, buf, sizeof buf - 1)). - Per i dati binari usare le funzioni
mem*, che prendono la lunghezza:memcpy,memmove,memcmp,memset,memchr. memcpynon gestisce aree che si sovrappongono: per spostare dati dentro lo stesso buffer si usamemmove.strncpynon aggiunge il'\0'se la sorgente è lunga quanto il limite. Per copiare in sicurezza si usasnprintf(dst, sizeof dst, "%s", src), che termina sempre e tronca.snprintfrestituisce la lunghezza che la stringa avrebbe avuto senza troncamento: se il valore è maggiore o uguale asizeof dst, la stringa è stata tagliata.
char req[256];
int n = snprintf(req, sizeof req, "GET %s HTTP/1.0\r\nHost: %s\r\n\r\n", path, host);
if (n < 0 || (size_t)n >= sizeof req) { /* richiesta troppo lunga: errore */ }
write(fd, req, (size_t)n); /* si scrivono n byte, non strlen: qui coincidono, ma con dati binari no */Il terminatore di riga dei protocolli testuali come HTTP è \r\n (CR LF, byte 0x0D 0x0A), non il solo \n.
Puntatori e aritmetica dei puntatori
Un puntatore contiene un indirizzo (Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →). Con un puntatore a T, sommare n significa avanzare di n * sizeof(T) byte: con char * e uint8_t * si avanza di n byte, ed è per questo che i buffer si indicano così.
Il modello di lettura "a pezzi" ricorrente nei client è:
char buf[65536];
size_t tot = 0;
ssize_t n;
while ((n = read(fd, buf + tot, sizeof buf - 1 - tot)) > 0) /* si continua a scrivere dopo quanto gia' letto */
tot += (size_t)n;
buf[tot] = '\0';buf + tot è l'indirizzo del primo byte libero; sizeof buf - 1 - tot è lo spazio rimasto (meno un byte per il '\0'). Se non si controlla lo spazio residuo si ha un buffer overflow.
Altri usi che compaiono nel codice di rete:
char **argv: array di puntatori a stringa,argv[1]è il primo argomento (di solito l'host o la porta).void *: puntatore senza tipo, restituito damalloce accettato daread,write,memcpy.- Cast di puntatori a struct:
bindvuole unstruct sockaddr *, ma si passa l'indirizzo di unastruct sockaddr_in:bind(fd, (struct sockaddr *)&addr, sizeof addr). È lecito perché le due strutture iniziano con lo stesso camposa_family. const:const char *spromette di non modificare i caratteri puntati; è il tipo giusto per le stringhe letterali ("GET"), che stanno nei dati statici in sola lettura.
Struct, allineamento e padding
Una struct mette i campi uno dopo l'altro, ma il compilatore rispetta l'allineamento: un tipo da k byte viene messo a un indirizzo multiplo di k (per int e uint32_t: multiplo di 4; per double e uint64_t: multiplo di 8), perché così la CPU lo legge in un accesso solo. Per ottenerlo inserisce byte di riempimento, il paddingbyte inutilizzati inseriti dal compilatore fra un campo e il successivo, oppure in coda, per rispettare l'allineamento. Anche la dimensione totale diventa un multiplo dell'allineamento più grande.
struct sparso { char c; int i; char d; }; /* c@0, 3 byte di padding, i@4, d@8, 3 byte di padding in coda: sizeof = 12 */
struct compatto { int i; char c; char d; }; /* i@0, c@4, d@5, 2 byte di padding in coda: sizeof = 8 */Il programma endian_demo.c (in fondo alla nota) stampa proprio queste dimensioni: 12 per sparso, 8 per compatto, 6 per la versione __attribute__((packed)). Regole utili:
- ordinando i campi dal più grande al più piccolo il padding diminuisce;
offsetof(struct sparso, i)(da<stddef.h>) dà la posizione di un campo;__attribute__((packed))(estensione di GCC e Clang) elimina il padding ma rende lenti, e su alcune CPU impossibili, gli accessi non allineati;- una struct non si spedisce con
write(fd, &s, sizeof s): il padding contiene byte casuali, l'allineamento cambia da compilatore a compilatore e l'ordine dei byte dei campi dipende dalla macchina. Il formato sul filo si definisce campo per campo. - Per lo stesso motivo una
struct sockaddr_insi azzera conmemset(&addr, 0, sizeof addr)prima di riempirla: contiene un campo di riempimento (sin_zero) che il kernel si aspetta a zero.
Endianness e ordine dei byte di rete
Un numero a più byte va scritto in memoria in qualche ordine (Codifiche binarie e informazione non numericaBit, byte e multipli (potenze di 2 e di 10); codici BCD e Gray; caratteri ASCII, Unicode e UTF-8; ordine dei byte (little e big endian); bit di parità e codice di Hamming per rilevare e correggere errori.Codifiche binarie e informazione non numerica →). Per uint32_t x = 0x12345678 i quattro byte sono 12 34 56 78:
| ordine | primo byte in memoria | macchine |
|---|---|---|
| big endian | il più significativo: 12 34 56 78 |
protocolli di rete, SPARC, PowerPC storici |
| little endian | il meno significativo: 78 56 34 12 |
x86, x86-64, ARM nella configurazione abituale |
Definizione (network byte order). Tutti i campi numerici a più byte dei protocolli Internet (porte, lunghezze, indirizzi IPv4, campi di TCP, UDP, DNS) viaggiano in big endian, detto network byte order. Il formato della macchina è l'ordine host.
Le funzioni di <arpa/inet.h> convertono:
| funzione | conversione | tipo |
|---|---|---|
htons |
host to network, short | uint16_t |
htonl |
host to network, long | uint32_t |
ntohs |
network to host, short | uint16_t |
ntohl |
network to host, long | uint32_t |
Esempio. La porta 8080 è 0x1F90. Su una macchina little endian la variabile uint16_t host = 8080 contiene in memoria i byte 90 1F; htons(8080) restituisce un valore che in memoria ha i byte 1F 90, cioè l'ordine di rete: ecco perché si scrive addr.sin_port = htons(8080). Su una macchina big endian htons non fa nulla, ma il codice resta uguale e quindi portabile.
0x12345678 in memoria: 78 56 34 12 (little endian)
porta 8080 (host): 90 1f
porta 8080 (rete): 1f 90
0x12345678 sul filo: 12 34 56 78Quando serve la conversione e quando no:
- serve per ogni campo numerico a 2, 4 o 8 byte che entra o esce da un messaggio binario:
sin_port,sin_addr.s_addr, un campo di lunghezza di un frame WebSocket, gli identificatori di un messaggio DNS; - non serve per i singoli byte (
uint8_t) né per il testo: una stringa ASCII o UTF-8 ha lo stesso ordine ovunque, e l'HTTP/1.x è testo; - non si converte due volte:
htons(htons(x))torna axe produce un errore silenzioso.
Per i formati binari è spesso più chiaro serializzare a mano con shift e maschere, che non dipendono dalla macchina:
uint8_t buf[4];
buf[0] = (uint8_t)(x >> 24); /* il byte piu' significativo per primo: big endian */
buf[1] = (uint8_t)(x >> 16);
buf[2] = (uint8_t)(x >> 8);
buf[3] = (uint8_t)x;
uint32_t y = (uint32_t)buf[0] << 24 | (uint32_t)buf[1] << 16 | (uint32_t)buf[2] << 8 | buf[3]; /* lettura inversa */Il cast (uint32_t) prima dello shift è necessario: buf[0] << 24 è un'operazione su int e, con buf[0] >= 0x80, sposta un bit nel segno (comportamento non definito).
Bit, flag e maschere
I campi più piccoli di un byte (flag) si estraggono con AND e shift. Nell'intestazione di un messaggio DNS il campo flags a 16 bit vale per esempio 0x8180:
| bit | nome | valore in 0x8180 |
significato |
|---|---|---|---|
| 15 | QR | 1 | 1 = risposta, 0 = domanda |
| 8 | RD | 1 | ricorsione desiderata |
| 7 | RA | 1 | ricorsione disponibile |
| 3..0 | RCODE | 0 | codice di errore (0 = nessuno, 3 = nome inesistente) |
uint16_t flags = 0x8180;
int qr = (flags >> 15) & 1; /* 1 */
int rcode = flags & 0x000F; /* 0 */Un programma di prova
Il programma seguente stampa tutto quello che è stato descritto sopra (compilare con gcc -Wall -Wextra -o endian_demo endian_demo.c).
/* endian_demo.c - ordine dei byte, allineamento e padding delle struct.
* Compilare e provare: gcc -Wall -Wextra -o endian_demo endian_demo.c && ./endian_demo
*/
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <arpa/inet.h> /* htons, htonl, ntohs, ntohl */
struct sparso { char c; int i; char d; }; /* int vuole indirizzi multipli di 4 */
struct compatto { int i; char c; char d; }; /* stessi campi, riordinati */
struct __attribute__((packed)) impacchettato { char c; int i; char d; }; /* niente padding (estensione GCC/Clang) */
static void dump(const char *label, const void *p, size_t n)
{
const unsigned char *b = p; /* unsigned char: si legge il singolo byte senza segno */
printf("%-22s", label);
for (size_t i = 0; i < n; i++)
printf(" %02x", b[i]);
printf("\n");
}
int main(void)
{
/* 1) Come e' scritto in memoria un intero a 32 bit */
uint32_t x = 0x12345678;
dump("0x12345678 in memoria:", &x, sizeof x); /* little endian (x86, ARM): 78 56 34 12 */
uint16_t host = 8080, rete = htons(host); /* 8080 = 0x1F90 */
dump("porta 8080 (host):", &host, sizeof host); /* little endian: 90 1f */
dump("porta 8080 (rete):", &rete, sizeof rete); /* network byte order: 1f 90 */
uint8_t first = *(uint8_t *)&x; /* test dell'endianness a run-time */
printf("questa macchina e' %s endian\n", first == 0x78 ? "little" : "big");
/* 2) Serializzare a mano, senza dipendere dall'endianness della macchina */
uint8_t buf[4];
buf[0] = (uint8_t)(x >> 24); /* il byte piu' significativo per primo = big endian */
buf[1] = (uint8_t)(x >> 16);
buf[2] = (uint8_t)(x >> 8);
buf[3] = (uint8_t)x;
dump("0x12345678 sul filo:", buf, sizeof buf); /* 12 34 56 78 su QUALSIASI macchina */
uint32_t back = (uint32_t)buf[0] << 24 | (uint32_t)buf[1] << 16 | (uint32_t)buf[2] << 8 | buf[3];
printf("riletto: 0x%08x\n", (unsigned)back);
/* 3) Allineamento e padding */
printf("sizeof(struct sparso) = %zu\n", sizeof(struct sparso));
printf(" offset c=%zu i=%zu d=%zu\n", offsetof(struct sparso, c), offsetof(struct sparso, i), offsetof(struct sparso, d));
printf("sizeof(struct compatto) = %zu\n", sizeof(struct compatto));
printf("sizeof(struct impacchettato) = %zu\n", sizeof(struct impacchettato));
/* 4) Il motivo per cui una struct NON si spedisce con write(fd, &s, sizeof s) */
struct sparso s;
memset(&s, 0xAA, sizeof s); /* il padding contiene spazzatura finche' non lo si azzera */
s.c = 'A';
s.i = 1;
s.d = 'B';
dump("struct sparso in memoria:", &s, sizeof s);/* 41 aa aa aa 01 00 00 00 42 aa aa aa */
return 0;
}Errori tipici
- Trattare un buffer ricevuto come stringa senza terminarlo con
'\0':printf("%s")legge oltre il buffer. - Salvare il risultato di
readin unsize_te perdere il controllo dell'errore (-1). - Usare
sizeof(p)su un puntatore pensando di avere la dimensione dell'array: dà 8, non la lunghezza del buffer.sizeofdà la dimensione dell'array solo sulla variabile array stessa (char buf[256]). - Spedire una
structconwriteesizeof: padding casuale e ordine dei byte della macchina finiscono sul filo. - Dimenticare
htonssulla porta:sin_port = 8080collega alla porta0x901F= 36895. - Applicare
htonsa un campo che non è un numero a più byte, oppure convertire due volte. - Fare shift su
charouint8_tsenza cast auint32_t: il risultato è uninte può avere segno. - Restituire l'indirizzo di un array locale.
Domande d'esame
1. Una struct { char tipo; uint32_t lunghezza; char flag; } viene spedita con write(fd, &m, sizeof m). Quali problemi ci sono? Come si corregge?
Traccia. Con allineamento a 4 byte la struct occupa 12 byte: tipo a 0, tre byte di padding, lunghezza a 4, flag a 8, altri tre byte di padding. Il padding contiene byte casuali (informazione che esce dal processo e dati diversi a ogni esecuzione), il tipo uint32_t viaggia nell'ordine host (little endian su x86) mentre il ricevente si aspetta big endian, e un altro compilatore potrebbe disporre i campi in modo diverso. Correzione: definire il formato sul filo (1 byte di tipo, 4 byte di lunghezza in big endian, 1 byte di flag = 6 byte) e serializzare campo per campo con htonl o con shift, in un uint8_t buf[6].
2. Un client chiama read(fd, buf, sizeof buf) e poi printf("%s", buf). Cosa può andare storto e come si evita?
Traccia. read non aggiunge '\0', quindi printf può leggere oltre i dati ricevuti (e mostrare spazzatura o terminare con un errore); read può restituire meno byte del previsto e -1 in caso di errore. Si salva il risultato in un ssize_t n, si controlla n < 0, si legge al massimo sizeof buf - 1 byte e si scrive buf[n] = '\0' prima di stampare; per dati che possono contenere byte 0 si usa fwrite(buf, 1, n, stdout).
Versione ripasso
Memoria di un processo. Codice (sola lettura), dati statici (globali,
static, stringhe letterali), heap (malloc, cresce verso l'alto), stack (locali, cresce verso il basso). Un array locale sparisce al ritorno: non restituirlo. Stack piccolo (circa 8 MB). Dopoforkil figlio ha una copia dello spazio di indirizzi (Memoria virtualeIndirizzi virtuali e fisici, pagine e frame; traduzione con la tabella delle pagine e calcolo dei campi; page fault e sostituzione delle pagine; TLB e tempo di accesso effettivo; protezione e condivisione; confronto con la cache.Memoria virtuale →, Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →).Tipi esatti.
<stdint.h>:uint8_t,uint16_t(porta),uint32_t(IPv4),uint64_t.size_tsenza segno (sizeof,strlen);ssize_tcon segno (read,write:-1errore,0fine flusso). Mai salvarereadin unsize_t. Per i byteunsigned char/uint8_t, nonchar(può avere segno).Buffer e stringhe. I dati di un socket sono un buffer con una lunghezza, non una stringa: nessun
'\0', byte0possibili, arrivo a pezzi. Terminare a mano (buf[n] = '\0', leggeresizeof buf - 1), per i binari usarememcpy,memmove(aree sovrapposte),memcmp,memchr.strncpynon termina: megliosnprintf(dst, sizeof dst, "%s", src).snprintftorna la lunghezza voluta: se>= sizeof dstè stato troncato. Fine riga HTTP:\r\n.Puntatori.
p + navanza din * sizeof(T)byte. Lettura a pezzi:read(fd, buf + tot, sizeof buf - 1 - tot)etot += n.char **argv,void *, cast(struct sockaddr *)&addr(stesso primo camposa_family),const char *per i letterali (Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →).Allineamento e padding. Un tipo da
kbyte sta a un indirizzo multiplo dik;sizeofè multiplo dell'allineamento maggiore.struct { char c; int i; char d; }= 12 byte (c@0, i@4, d@8); riordinata{ int i; char c; char d; }= 8;packed= 6. Campi dal più grande al più piccolo;offsetof. Non spedire una struct conwrite(&s, sizeof s): padding casuale, ordine dei byte dell'host, layout dipendente dal compilatore (Strutture (struct) in Cstruct per raggruppare campi di tipo diverso, typedef, accesso con punto e freccia, struct come parametri e valori di ritorno, array di struct, struct allocate dinamicamente e struct autoreferenziali.Strutture (struct) in C →).memset(&addr, 0, sizeof addr)prima di riempire unasockaddr_in.Endianness.
0x12345678: big endian12 34 56 78, little endian78 56 34 12(x86, ARM). La rete usa big endian (network byte order).htons/ntohs(16 bit),htonl/ntohl(32 bit). Porta 8080 =0x1F90: host little endian90 1F, rete1F 90. Si convertono solo i numeri a più byte, non il testo né i singoli byte; mai due volte (Codifiche binarie e informazione non numericaBit, byte e multipli (potenze di 2 e di 10); codici BCD e Gray; caratteri ASCII, Unicode e UTF-8; ordine dei byte (little e big endian); bit di parità e codice di Hamming per rilevare e correggere errori.Codifiche binarie e informazione non numerica →).funzione conversione tipo quando htonshost -> rete uint16_tporta in sin_port, lunghezze a 16 bithtonlhost -> rete uint32_tindirizzo IPv4, lunghezze a 32 bit ntohsrete -> host uint16_tporta letta da sockaddr_inntohlrete -> host uint32_tnumero a 32 bit letto da un messaggio Dimensioni su Linux a 64 bit (LP64):
char1,short2,int4,long8, puntatore 8 byte.Serializzare a mano.
buf[0] = x >> 24; buf[1] = x >> 16; buf[2] = x >> 8; buf[3] = x;e lettura(uint32_t)buf[0] << 24 | ...: indipendente dalla macchina; il cast auint32_tprima dello shift evita il segno.Flag.
(flags >> 15) & 1,flags & 0x000F. Esempio DNS0x8180: QR=1 (risposta), RD=1, RA=1, RCODE=0; stesso schema per i frame di WebSocket, QUIC e HTTP-3HTTP e' richiesta e risposta, quindi poco adatto a notifiche e dati in tempo reale (polling, long polling, SSE); WebSocket (RFC 6455) apre con un handshake HTTP/1.1 Upgrade (Sec-WebSocket-Key a 16 byte casuali in Base64, risposta 101 con Sec-WebSocket-Accept = Base64 di SHA-1 di chiave piu' GUID fisso) un canale bidirezionale persistente sulla stessa connessione TCP, con frame di 2-14 byte di intestazione (FIN, opcode, MASK, lunghezza su 7, 16 o 64 bit, chiave di mascheramento obbligatoria dal client al server) e messaggi di testo, binari, close, ping e pong. QUIC (RFC 9000) e' un protocollo di trasporto sopra UDP con TLS 1.3 integrato, flussi indipendenti (niente head-of-line blocking fra flussi), connection ID che permettono di cambiare rete, handshake in 1 RTT e ripresa in 0 RTT; HTTP/3 (RFC 9114) mappa HTTP su QUIC con un flusso per richiesta e QPACK per gli header, ed e' annunciato con Alt-Svc.WebSocket, QUIC e HTTP-3 →.Schema di lettura a pezzi:
char buf[65536]; size_t tot = 0; ssize_t n;
while ((n = read(fd, buf + tot, sizeof buf - 1 - tot)) > 0)
tot += (size_t)n; /* n == 0: fine flusso; n < 0: errore */
buf[tot] = '\0'; /* solo ora e' una stringa */- Schema di scrittura sicura di una richiesta:
int n = snprintf(req, sizeof req, "GET %s HTTP/1.0\r\n\r\n", path); if (n < 0 || (size_t)n >= sizeof req) errore; write(fd, req, (size_t)n); - Layout della struct d'esame
{ char tipo; uint32_t lunghezza; char flag; }: 12 byte (tipo@0, padding 3, lunghezza@4, flag@8, padding 3). Sul filo: 1 byte di tipo, 4 byte di lunghezza in big endian, 1 byte di flag = 6 byte, costruiti in unuint8_t buf[6]conhtonlo shift. read+printf("%s"): manca il'\0',readpuò dare meno byte o-1: salvare inssize_t, controllaren < 0, terminare a mano, per dati binarifwrite(buf, 1, n, stdout).- Errori tipici: buffer non terminato passato a
%s;readin unsize_t;sizeofdi un puntatore (dà 8, non la lunghezza); struct spedita conwrite; porta senzahtons(8080diventa la porta 36895);htonssu testo o doppia conversione; shift di unuint8_tsenza cast; restituire un array locale.
Esercizi su questo argomento
- Esercizio - Client DNS con query e risposta costruite a mano (sul modello della prova pratica)
- Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1)
- Esercizio - Client WebSocket con handshake, SHA-1 e Base64 (sul modello della prova pratica)
- Esercizio - Echo TCP e UDP, client e server (sul modello della prova pratica)
- Esercizio - Parsing di request line, header e URI (sul modello della prova pratica)
- Esercizio - URI, scomposizione, percent-encoding e riferimenti relativi (sul modello della prova pratica)