Esercizio - Server HTTP che risponde in chunked con trailer (sul modello della prova pratica)
In questa pagina 6
Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD).
Scrivere in C un server HTTP/1.1 che produce risposte di lunghezza non nota in anticipo con Transfer-Encoding: chunked:
GET /stream: invia cinque righe, una per chunk, a intervalli di tempo (il client deve poterle vedere man mano che arrivano), poi un trailerX-Totale-Bytecon il numero totale di byte di dati inviati; l'headerTrailerdeve annunciare il trailer;GET /lorem: invia un testo spezzato in chunk da 16 byte (l'ultimo più corto), per vedere bene la struttura;- ogni altro percorso:
404conContent-Length(qui la lunghezza è nota, quindi niente chunked); - scrivere le funzioni
send_chunkeend_chunksche producono il formato corretto.
Teoria: HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encodingHTTP/1.1 (oggi RFC 9110 e 9112) rende la connessione persistente di default (si chiude solo con "Connection: close"), rende obbligatorio l'header Host (virtual hosting) e introduce i nuovi metodi PUT, DELETE, OPTIONS, TRACE, Expect: 100-continue, richieste di intervalli (206) e Transfer-Encoding: chunked; con la connessione persistente il client deve sapere dove finisce ogni risposta: lunghezza del corpo nell'ordine HEAD/1xx/204/304 senza corpo, Transfer-Encoding chunked, Content-Length, altrimenti fino alla chiusura; il chunked divide il corpo in blocchi preceduti dalla lunghezza in esadecimale, termina con un chunk 0 e un trailer facoltativo, e si decodifica contando i byte dichiarati (non cercando CRLF).HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding →, Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1), Client e server TCP in C - bind, listen, accept e processi concorrentiUn client TCP in C fa getaddrinfo, socket, connect, poi write e read; un server fa socket, setsockopt(SO_REUSEADDR), bind, listen e un ciclo di accept. Un server iterativo serve un client alla volta e un client lento blocca tutti; un server concorrente con fork crea un processo figlio per ogni connessione: il figlio chiude il socket di ascolto e serve la connessione, il padre chiude il socket di connessione e torna ad accept. I figli terminati diventano zombie finche' il padre non li raccoglie con waitpid, di solito in un gestore di SIGCHLD con WNOHANG in un ciclo; i descrittori non chiusi tengono viva la connessione (nessun FIN) o esauriscono EMFILE; uno stato CLOSE_WAIT che si accumula indica una close mancante. Alternative: thread, pre-fork, I/O multiplexing con poll o epoll.Client e server TCP in C - bind, listen, accept e processi concorrenti →.
Idea
Un server che genera la risposta mentre la produce (output di un programma, un log in diretta, un risultato che richiede tempo) non può scrivere Content-Length negli header, che vengono prima: non conosce ancora la lunghezza. Le alternative sono accumulare tutto in memoria per misurarlo (e il client aspetta la fine), oppure il chunked: ogni pezzo è preceduto dalla sua dimensione, e la fine è un chunk di lunghezza zero. Il client riceve e mostra i dati subito (streaming) e la connessione può restare persistente.
Il formato di un chunk (RFC 9112 par. 7.1) è:
<dimensione in esadecimale> CRLF
<dati> CRLFe la chiusura del corpo:
0 CRLF
[righe di trailer, ciascuna "Nome: valore" CRLF]
CRLFIl trailer serve a inviare metadati che si conoscono solo alla fine (qui il totale dei byte, in pratica un hash o un conteggio). Va annunciato negli header con Trailer: X-Totale-Byte.
Le funzioni:
send_chunk(fd, data, len): scrive"%zx\r\n"(la lunghezza in esadecimale consnprintfe il formato%zx), poi i dati, poi\r\n. Un chunk di lunghezza zero non va mai inviato da qui: significherebbe "fine del corpo".end_chunks(fd, trailers): scrive0\r\n, i trailer (già terminati da\r\n), e la riga vuota finale.
Codice
/* chunk_server.c - server HTTP/1.1 che risponde con Transfer-Encoding: chunked (lunghezza non nota in anticipo).
* GET /stream : cinque righe generate una alla volta, con un trailer finale X-Totale-Byte
* GET /lorem : un testo spezzato in chunk da 16 byte (comodo per vedere la struttura)
* altro : 404 con Content-Length
*
* Compilare: gcc -Wall -Wextra -o chunk_server chunk_server.c
* Avviare: ./chunk_server 8080
* Provare: curl -i http://127.0.0.1:8080/lorem (curl decodifica i chunk)
* curl --raw -i http://127.0.0.1:8080/lorem (--raw mostra i chunk cosi' come viaggiano)
* curl -N http://127.0.0.1:8080/stream (-N: stampa man mano che arriva)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static int write_all(int fd, const char *buf, size_t n)
{
while (n > 0) {
ssize_t w = write(fd, buf, n);
if (w < 0) {
if (errno == EINTR)
continue;
return -1;
}
buf += w;
n -= (size_t)w;
}
return 0;
}
/* Un chunk: <lunghezza in esadecimale> CRLF <dati> CRLF. Un chunk di lunghezza 0 NON va mandato qui:
* e' il last-chunk e chiude il corpo (lo fa end_chunks). */
static int send_chunk(int fd, const char *data, size_t len)
{
if (len == 0)
return 0;
char hdr[32];
int n = snprintf(hdr, sizeof hdr, "%zx\r\n", len);
if (write_all(fd, hdr, (size_t)n) < 0 || write_all(fd, data, len) < 0 || write_all(fd, "\r\n", 2) < 0)
return -1;
return 0;
}
/* last-chunk "0", poi i trailer (righe "Nome: valore"), poi la riga vuota finale. */
static int end_chunks(int fd, const char *trailers)
{
if (write_all(fd, "0\r\n", 3) < 0)
return -1;
if (trailers != NULL && write_all(fd, trailers, strlen(trailers)) < 0)
return -1;
return write_all(fd, "\r\n", 2);
}
/* Legge la richiesta fino alla riga vuota, un byte per volta; copia la prima riga in first. */
static int read_request(int fd, char *first, size_t size)
{
char line[1024];
size_t n = 0;
int nline = 0;
char c;
for (;;) {
if (read(fd, &c, 1) <= 0)
return -1;
if (c != '\n') {
if (n + 1 < sizeof line)
line[n++] = c;
continue;
}
if (n > 0 && line[n - 1] == '\r')
n--;
line[n] = '\0';
if (nline++ == 0)
snprintf(first, size, "%s", line);
else if (n == 0)
return 0; /* riga vuota: richiesta completa */
n = 0;
}
}
static void handle(int fd)
{
char first[1024], method[16], target[512];
if (read_request(fd, first, sizeof first) < 0 || sscanf(first, "%15s %511s", method, target) != 2) {
close(fd);
return;
}
if (strcmp(method, "GET") == 0 && strcmp(target, "/stream") == 0) {
const char *head =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Transfer-Encoding: chunked\r\n"
"Trailer: X-Totale-Byte\r\n" /* annuncia che alla fine arrivera' questo trailer */
"Connection: close\r\n"
"\r\n";
write_all(fd, head, strlen(head));
size_t total = 0;
for (int i = 1; i <= 5; i++) {
char msg[64];
int len = snprintf(msg, sizeof msg, "riga %d di 5\n", i);
if (send_chunk(fd, msg, (size_t)len) < 0)
break;
total += (size_t)len;
usleep(300000); /* il client riceve ogni riga subito, senza aspettare la fine */
}
char trailer[64];
snprintf(trailer, sizeof trailer, "X-Totale-Byte: %zu\r\n", total);
end_chunks(fd, trailer);
} else if (strcmp(method, "GET") == 0 && strcmp(target, "/lorem") == 0) {
const char *text = "Il chunked permette di mandare un corpo senza conoscerne la lunghezza.\n";
const char *head =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"Transfer-Encoding: chunked\r\n"
"Connection: close\r\n"
"\r\n";
write_all(fd, head, strlen(head));
size_t len = strlen(text);
for (size_t off = 0; off < len; off += 16) /* l'ultimo chunk puo' essere piu' corto */
send_chunk(fd, text + off, len - off < 16 ? len - off : 16);
end_chunks(fd, NULL);
} else {
const char *resp =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 10\r\n" /* qui la lunghezza e' nota: niente chunked */
"Connection: close\r\n"
"\r\n"
"Not Found\n";
write_all(fd, resp, strlen(resp));
}
close(fd);
}
int main(int argc, char **argv)
{
if (argc != 2) {
fprintf(stderr, "uso: %s porta\n", argv[0]);
return 1;
}
signal(SIGPIPE, SIG_IGN);
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0) {
perror("socket");
return 1;
}
int yes = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons((unsigned short)atoi(argv[1]));
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0 || listen(lfd, 8) < 0) {
perror("bind/listen");
return 1;
}
for (;;) {
int cfd = accept(lfd, NULL, NULL); /* NULL: non ci serve l'indirizzo del client */
if (cfd < 0) {
if (errno == EINTR)
continue;
perror("accept");
break;
}
handle(cfd);
}
return 0;
}Compilare e provare
$ gcc -Wall -Wextra -o chunk_server chunk_server.c
$ ./chunk_server 8080 &
$ curl -i http://127.0.0.1:8080/lorem # curl decodifica i chunk
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: close
Il chunked permette di mandare un corpo senza conoscerne la lunghezza.Con --raw curl non decodifica e mostra il formato come viaggia sulla rete (CRLF visibili con cat -A):
$ curl --raw -i http://127.0.0.1:8080/lorem | cat -A
HTTP/1.1 200 OK^M$
Content-Type: text/plain; charset=utf-8^M$
Transfer-Encoding: chunked^M$
Connection: close^M$
^M$
10^M$
Il chunked perme^M$
10^M$
tte di mandare u^M$
10^M$
n corpo senza co^M$
10^M$
noscerne la lung^M$
7^M$
hezza.$
^M$
0^M$
^M$Quattro chunk da 10 (esadecimale = 16 byte) e uno da 7 byte (hezza. e il \n finale); il chunk 0 chiude. Per /stream:
$ curl -N --raw -i http://127.0.0.1:8080/stream
HTTP/1.1 200 OK^M$
Content-Type: text/plain^M$
Transfer-Encoding: chunked^M$
Trailer: X-Totale-Byte^M$
Connection: close^M$
^M$
c^M$
riga 1 di 5$
^M$
c^M$
riga 2 di 5$
^M$
...
c^M$
riga 5 di 5$
^M$
0^M$
X-Totale-Byte: 60^M$
^M$(-N disattiva il buffer di curl: le righe compaiono a intervalli di 0,3 s.) Ogni riga riga N di 5\n è lunga 12 byte, in esadecimale c; il totale è 5 × 12 = 60 byte, ed è quello che il trailer comunica. Il client dell'esercizio Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1) lo mostra: ./client11_chunked 127.0.0.1 8080 /stream stampa chunk 1: 12 byte, ..., trailer: X-Totale-Byte: 60.
Spiegazione dei punti chiave
send_chunk.
snprintf(hdr, sizeof hdr, "%zx\r\n", len):%zxstampa unsize_tin esadecimale minuscolo;len = 16diventa10,len = 12diventac. Leggere questa riga come decimale è l'errore più comune (10è 16, non 10).- Tre scritture: intestazione, dati,
\r\nfinale. I dati sono esattamentelenbyte; possono contenere qualunque cosa. - Se
len == 0la funzione non fa nulla: un chunk vuoto si legge come0\r\ned equivale alla fine del messaggio, quindi non può uscire per sbaglio (per esempio da un file vuoto).
end_chunks. 0\r\n, poi trailers (stringhe Nome: valore\r\n), poi \r\n. Senza la riga vuota finale il client resta in attesa perché non vede la fine dei trailer.
/stream.
- Gli header comprendono
Transfer-Encoding: chunkede nonContent-Length.Trailer: X-Totale-Byteannuncia che alla fine arriverà quel campo. - Il ciclo genera una riga, la invia come chunk e dorme 300 ms con
usleep: il client riceve ogni riga appena inviata, senza aspettare la fine. - Il totale dei byte si accumula (
total += len) e finisce nel trailerX-Totale-Byte: 60. - Se
send_chunkfallisce (client sparito,EPIPE) il ciclo si interrompe.
/lorem. Il testo è spezzato in blocchi da 16 byte con un ciclo for (off = 0; off < len; off += 16): l'ultimo chunk è più corto (len - off). Mostra chiaramente che le dimensioni dei chunk non hanno rapporto con il contenuto.
read_request. Legge la richiesta un byte alla volta fino alla riga vuota; memorizza solo la prima riga. È la versione minima: per un server vero si usa il lettore a buffer di Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica).
Il 404. Qui la lunghezza è nota (Content-Length: 10 per Not Found\n), quindi non si usa il chunked: il chunked serve quando la lunghezza non è nota, non è obbligatorio per tutte le risposte.
Un caso da non dimenticare: il chunked si può usare solo se il client parla HTTP/1.1 (un client HTTP/1.0 non lo capisce): un server serio controlla la versione della richiesta e per un client 1.0 risponde chiudendo la connessione e delimitando il corpo con la chiusura.
Errori tipici
- Scrivere la dimensione in decimale (
%zu) invece che in esadecimale (%zx). - Inviare un chunk di lunghezza zero (per esempio per un file vuoto) a metà della risposta: il client considera finito il corpo.
- Dimenticare il CRLF dopo i dati di un chunk, o la riga vuota finale dopo
0\r\n. - Mandare insieme
Content-LengtheTransfer-Encoding: chunked: i due header sono incompatibili (vale il chunked e un intermediario può rifiutare il messaggio, RFC 9112 par. 6.3). - Inviare il trailer senza averlo annunciato con
Trailer, o mettere nel trailer campi non consentiti (Content-Length,Host). - Mettere nei trailer o negli header valori provenienti dall'utente senza togliere
\re\n: permette di iniettare header o un'altra risposta (response splitting). - Usare il chunked con client HTTP/1.0.
Varianti per esercitarsi
- Inviare un file a chunk da 4 KiB senza conoscere la dimensione (
readdal file esend_chunkper ogni blocco), con trailerX-Checksum. - Aggiungere le estensioni di chunk (
10;nome=valore). - Servire
/streamcon connessione persistente: dopoend_chunkssi può leggere la richiesta successiva (Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica)). - Rendere il server concorrente (un client lento di
/streamblocca tutti gli altri).
Versione ripasso
- Testo.
GET /stream: 5 righe, una per chunk, con pausa; trailerX-Totale-Byte(annunciato conTrailer).GET /lorem: testo in chunk da 16 byte. Altro:404conContent-Length. Funzionisend_chunkeend_chunks. - Perché chunked. La lunghezza non è nota prima degli header (
Content-Lengthimpossibile): si invia a pezzi, il client mostra man mano (streaming), la connessione può restare persistente. - Formato. Chunk:
<dimensione HEX> CRLF <dati> CRLF. Fine:0 CRLF+ trailer (Nome: valore CRLF) +CRLF. Header:Transfer-Encoding: chunked, maiContent-Length;Trailer: X-Totale-Byte.
static int send_chunk(int fd, const char *data, size_t len)
{
if (len == 0) return 0; /* il chunk vuoto = fine del corpo: non si invia da qui */
char hdr[32]; int n = snprintf(hdr, sizeof hdr, "%zx\r\n", len); /* ESADECIMALE */
if (write_all(fd, hdr, n) < 0 || write_all(fd, data, len) < 0 || write_all(fd, "\r\n", 2) < 0) return -1;
return 0;
}
static int end_chunks(int fd, const char *trailers)
{ write_all(fd, "0\r\n", 3); if (trailers) write_all(fd, trailers, strlen(trailers));
return write_all(fd, "\r\n", 2); } /* riga vuota finale *//stream. Periin 1..5:"riga %d di 5\n"= 12 byte (cin esadecimale) come chunk,usleep(300000),total += len; poiend_chunks(fd, "X-Totale-Byte: 60\r\n")./lorem: blocchi da 16 byte (10), l'ultimo da 7 (hezza.\n). Corpo di/lorem: 16+16+16+16+7 = 71 byte.- Prove.
curl -i http://127.0.0.1:8080/lorem(decodificato);curl --raw -i ... | cat -A(formato sul filo con^M$);curl -N --raw -i .../stream(a intervalli). Client C:./client11_chunked 127.0.0.1 8080 /stream. 404.Content-Length: 10(Not Found\n): il chunked si usa solo se la lunghezza non è nota; solo per client HTTP/1.1.- Codice essenziale (le funzioni centrali, senza commenti):
static void handle(int fd)
{
char first[1024], method[16], target[512];
if (read_request(fd, first, sizeof first) < 0 || sscanf(first, "%15s %511s", method, target) != 2) {
close(fd);
return;
}
if (strcmp(method, "GET") == 0 && strcmp(target, "/stream") == 0) {
const char *head =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Transfer-Encoding: chunked\r\n"
"Trailer: X-Totale-Byte\r\n"
"Connection: close\r\n"
"\r\n";
write_all(fd, head, strlen(head));
size_t total = 0;
for (int i = 1; i <= 5; i++) {
char msg[64];
int len = snprintf(msg, sizeof msg, "riga %d di 5\n", i);
if (send_chunk(fd, msg, (size_t)len) < 0)
break;
total += (size_t)len;
usleep(300000);
}
char trailer[64];
snprintf(trailer, sizeof trailer, "X-Totale-Byte: %zu\r\n", total);
end_chunks(fd, trailer);
} else if (strcmp(method, "GET") == 0 && strcmp(target, "/lorem") == 0) {
const char *text = "Il chunked permette di mandare un corpo senza conoscerne la lunghezza.\n";
const char *head =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"Transfer-Encoding: chunked\r\n"
"Connection: close\r\n"
"\r\n";
write_all(fd, head, strlen(head));
size_t len = strlen(text);
for (size_t off = 0; off < len; off += 16)
send_chunk(fd, text + off, len - off < 16 ? len - off : 16);
end_chunks(fd, NULL);
} else {
const char *resp =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 10\r\n"
"Connection: close\r\n"
"\r\n"
"Not Found\n";
write_all(fd, resp, strlen(resp));
}
close(fd);
}- Errori tipici: dimensione in decimale (
%zu); chunk di lunghezza zero a metà risposta; manca il CRLF dopo i dati o la riga vuota finale;Content-Lengthinsieme a chunked; trailer non annunciato o con campi vietati; CR/LF dall'utente negli header (response splitting); chunked verso client 1.0.