Salta al contenuto
Note per Studenti Esercizio - Server HTTP che risponde in chunked con trailer (sul modello della prova pratica)

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:

  1. GET /stream: invia cinque righe, una per chunk, a intervalli di tempo (il client deve poterle vedere man mano che arrivano), poi un trailer X-Totale-Byte con il numero totale di byte di dati inviati; l'header Trailer deve annunciare il trailer;
  2. GET /lorem: invia un testo spezzato in chunk da 16 byte (l'ultimo più corto), per vedere bene la struttura;
  3. ogni altro percorso: 404 con Content-Length (qui la lunghezza è nota, quindi niente chunked);
  4. scrivere le funzioni send_chunk e end_chunks che 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> CRLF

e la chiusura del corpo:

0 CRLF
[righe di trailer, ciascuna "Nome: valore" CRLF]
CRLF

Il 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 con snprintf e 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): scrive 0\r\n, i trailer (già terminati da \r\n), e la riga vuota finale.

Codice

c
/* 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): %zx stampa un size_t in esadecimale minuscolo; len = 16 diventa 10, len = 12 diventa c. Leggere questa riga come decimale è l'errore più comune (10 è 16, non 10).
  • Tre scritture: intestazione, dati, \r\n finale. I dati sono esattamente len byte; possono contenere qualunque cosa.
  • Se len == 0 la funzione non fa nulla: un chunk vuoto si legge come 0\r\n ed 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: chunked e non Content-Length. Trailer: X-Totale-Byte annuncia 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 trailer X-Totale-Byte: 60.
  • Se send_chunk fallisce (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-Length e Transfer-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 \r e \n: permette di iniettare header o un'altra risposta (response splitting).
  • Usare il chunked con client HTTP/1.0.

Varianti per esercitarsi

Versione ripasso

  • Testo. GET /stream: 5 righe, una per chunk, con pausa; trailer X-Totale-Byte (annunciato con Trailer). GET /lorem: testo in chunk da 16 byte. Altro: 404 con Content-Length. Funzioni send_chunk e end_chunks.
  • Perché chunked. La lunghezza non è nota prima degli header (Content-Length impossibile): 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, mai Content-Length; Trailer: X-Totale-Byte.
c
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. Per i in 1..5: "riga %d di 5\n" = 12 byte (c in esadecimale) come chunk, usleep(300000), total += len; poi end_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):
c
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-Length insieme a chunked; trailer non annunciato o con campi vietati; CR/LF dall'utente negli header (response splitting); chunked verso client 1.0.

Esercizi su questo argomento

Teoria collegata