Salta al contenuto
Note per Studenti HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsing

HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsing

In questa pagina 7

HTTP (HyperText Transfer Protocol) è il protocollo del livello applicazione del Web (Livello applicazione - HTTPIl livello applicazione è il più alto della pila: offre servizi all'utente con una connessione logica tra le due applicazioni e riceve servizi solo dal trasporto (DNS, HTTP, e-mail, FTP). Il Web (WWW, nato al CERN nel 1989) è un servizio client-server distribuito di pagine collegate da ipertesti; ogni pagina ha un URL protocollo://host:porta/percorso. HTTP: il client manda una richiesta, il server una risposta, su TCP (server sulla porta 80, client su una porta temporanea); senza stato. Messaggi di testo (riga di richiesta o di stato, intestazioni, riga vuota, corpo), metodi GET, POST, HEAD, PUT, DELETE, codici di stato 2xx-5xx. Una pagina con N oggetti incorporati richiede 2(N+1) RTT con connessioni non persistenti e (N+2) RTT con connessione persistente (trascurando la trasmissione). I cookie danno memoria al protocollo: Set-Cookie nella risposta, Cookie nelle richieste, file nel browser e base di dati nel sito. Un proxy (web cache) tiene le copie delle risposte recenti: meno carico sul server, meno traffico, meno ritardo.Livello applicazione - HTTP →). Questa nota segue le prime due versioni, che sono le più semplici e permettono di capire come si costruiscono e si leggono i messaggi in un programma C: HTTP/0.9 e HTTP/1.0 (RFC 1945). Sono testuali, senza stato, e viaggiano su TCP (porta 80) come visto in 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 →.

HTTP/0.9

Nasce nel 1991 con il primo Web (Tim Berners-Lee): serve solo a ottenere documenti HTML. Il protocollo è minimo.

Definizione (Simple-Request e Simple-Response, RFC 1945 par. 4.1). Simple-Request = "GET" SP Request-URI CRLF; Simple-Response = [ Entity-Body ]. La richiesta è una sola riga; la risposta è soltanto il documento.

Funzionamento: il client apre la connessione TCP, invia GET /pagina.html seguito da CRLF, il server risponde con il contenuto della pagina e chiude la connessione. Lo si può provare a mano (se il server accetta ancora questo formato):

$ printf 'GET /index.html\r\n' | nc 127.0.0.1 8080
<html><body><h1>Ciao</h1></body></html>

I limiti sono evidenti:

  • il client non può mandare nulla oltre al percorso: niente dati, niente informazioni sul client;
  • nessun codice di stato: non si distingue il documento vuoto da un errore, né un documento inesistente da un errore di generazione;
  • nessuna autenticazione;
  • solo HTML: senza Content-Type il client non sa se riceve testo, immagini o altro (RFC 1945 par. 4.1: il formato semplice «impedisce al server di identificare il tipo di contenuto»);
  • la fine della risposta si capisce solo dalla chiusura della connessione: se la connessione cade a metà, il client non può accorgersene.

Un client C per HTTP/0.9 è il minimo indispensabile: socket, connect, write("GET /pagina\r\n"), read in ciclo fino a 0. Il codice è in Esercizio - Client HTTP 0.9 e 1.0 che scarica una pagina (slide HTTP 0.9 e 1.0). Un server moderno risponde a questo formato solo se ha il supporto esplicito: molti rispondono con un errore.

HTTP/1.0

HTTP/1.0 (RFC 1945, 1996) è la prima versione formalmente standardizzata (documento informativo). Sul funzionamento di base non cambia: una connessione TCP per ogni richiesta, il server chiude dopo la risposta. Introduce:

  • header nelle richieste e nelle risposte, per descrivere meglio ciò che viene scambiato;
  • codici di stato: una risposta dice se è andata bene;
  • nuovi metodi: oltre a GET, HEAD e POST;
  • contenuti non HTML: immagini, testo, dati binari, con Content-Type.

La richiesta diventa una Full-Request e la risposta una Full-Response. Il formato generale dei messaggi (RFC 1945 par. 4.1):

Full-Request   = Request-Line
                 *( General-Header | Request-Header | Entity-Header )
                 CRLF
                 [ Entity-Body ]

Full-Response  = Status-Line
                 *( General-Header | Response-Header | Entity-Header )
                 CRLF
                 [ Entity-Body ]

Un messaggio HTTP è quindi: una riga iniziale, zero o più header (uno per riga), una riga vuota (solo CRLF) che separa gli header dal corpo, e l'eventuale corpo (entity body). Ogni riga termina con CRLF (\r\n, byte 0x0D 0x0A).

La richiesta

http
GET /index.html HTTP/1.0
User-Agent: client10/1.0
From: studente@example.com
If-Modified-Since: Sun, 11 Oct 2026 20:00:25 GMT

Request-Line: Method SP Request-URI SP HTTP-Version CRLF, tre campi separati da un solo spazio.

metodo significato corpo nella richiesta
GET richiede la risorsa no
HEAD come GET ma la risposta contiene solo gli header, nessun corpo no
POST invia dati al server (un modulo, un file) perché li elabori sì

HEAD serve a conoscere dimensione, tipo e data di modifica senza scaricare il corpo, o a controllare che un link esista. Il Request-URI è di norma il percorso assoluto (/index.html, con l'eventuale query ?a=1); la sintassi degli URI è in Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →.

La risposta

http
HTTP/1.0 200 OK
Date: Sun, 11 Oct 2026 20:05:00 GMT
Server: mini-http/1.0
Last-Modified: Sun, 11 Oct 2026 20:00:25 GMT
Content-Type: text/html
Content-Length: 40

<html><body><h1>Ciao</h1></body></html>

Status-Line: HTTP-Version SP Status-Code SP Reason-Phrase CRLF. Il codice (3 cifre) è quello che il programma legge; la frase è solo testo per le persone. Il corpo qui è di 40 byte (39 caratteri più il \n finale), e Content-Length lo dice.

Le categorie dei codici (la prima cifra) sono cinque:

classe significato esempi in HTTP/1.0
1xx informativo (non usato in HTTP/1.0)
2xx successo 200 OK, 201 Created, 202 Accepted, 204 No Content
3xx reindirizzamento: servono altre azioni 301 Moved Permanently, 302 Moved Temporarily, 304 Not Modified
4xx errore del client 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
5xx errore del server 500 Internal Server Error, 501 Not Implemented, 502 Bad Gateway, 503 Service Unavailable

Il client deve ragionare sulla classe quando incontra un codice sconosciuto (un 299 si tratta come un 200, un 499 come un 400).

Esempi di risposte non 200:

http
HTTP/1.0 301 Moved Permanently
Location: http://www.example.com/nuova.html
http
HTTP/1.0 304 Not Modified
Date: Sun, 11 Oct 2026 20:06:00 GMT
http
HTTP/1.0 404 Not Found
Content-Type: text/html
Content-Length: 59

<html><body><h1>404 - pagina non trovata</h1></body></html>

Il 304 non ha mai un corpo: dice che la copia in possesso del client è ancora valida (vedi sotto).

Gli header

Un header è Nome: valore seguito da CRLF. I nomi non distinguono maiuscole e minuscole (content-length è uguale a Content-Length). In HTTP/1.0 gli header si dividono in quattro categorie:

categoria applicati a header (RFC 1945)
generali richieste e risposte Date (data del messaggio), Pragma (no-cache: non usare copie in cache)
di richiesta richieste User-Agent (software del client), Referer (URI della pagina di provenienza), From (e-mail di chi richiede), Authorization (credenziali), If-Modified-Since (GET condizionale)
di risposta risposte Location (dove si trova la nuova risorsa, per i reindirizzamenti), Server (software del server), WWW-Authenticate (sfida di autenticazione)
di entità descrivono il corpo Allow, Content-Encoding, Content-Length (byte del corpo), Content-Type, Expires, Last-Modified

La data deve avere un formato fisso e sempre in GMT (RFC 1945 par. 3.3): Sun, 06 Nov 1994 08:49:37 GMT è il formato preferito (derivato dalla RFC 1123); un ricevente deve accettare anche i formati obsoleti Sunday, 06-Nov-94 08:49:37 GMT e Sun Nov 6 08:49:37 1994.

In una richiesta valida di HTTP/1.0 gli header sono facoltativi. L'header Host, che dice a quale sito della macchina ci si rivolge, non fa parte di HTTP/1.0 ma quasi tutti i server di oggi lo pretendono (è obbligatorio in HTTP/1.1, 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 →); un client scritto per un server reale lo manda sempre.

Dove finisce il corpo

Il punto più delicato di HTTP/1.0 è sapere quanti byte compongono il corpo:

  • richieste GET e HEAD: non hanno corpo; la richiesta termina con la riga vuota;
  • richieste POST: il corpo c'è e la sua lunghezza deve essere dichiarata con Content-Length (RFC 1945 par. 8.3: «un Content-Length valido è obbligatorio per tutte le POST»; un server che non riesce a determinarla risponde 400);
  • risposte: il server chiude la connessione quando ha finito; il client legge fino a quando read restituisce 0. Il Content-Length è facoltativo ma, se c'è, permette di accorgersi di una risposta troncata (se read restituisce 0 prima di aver ricevuto Content-Length byte, la risposta è incompleta).

Esempio di POST con un modulo:

http
POST /iscrizione HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 17

nome=Mario&eta=30

nome=Mario&eta=30 ha 17 caratteri, e Content-Length: 17 permette al server di sapere quando smettere di leggere senza attendere la chiusura della connessione (che il client non può fare, perché dopo la richiesta attende la risposta).

GET condizionale

Per non riscaricare ciò che il client ha già, il client memorizza la risposta insieme alla data Last-Modified e alla richiesta successiva aggiunge If-Modified-Since con quella data. Se la risorsa non è cambiata il server risponde 304 Not Modified senza corpo; altrimenti 200 con il corpo nuovo.

 1a richiesta:  GET /index.html
                200 OK, Last-Modified: Sun, 11 Oct 2026 20:00:25 GMT, corpo        (il client salva data e corpo)
 2a richiesta:  GET /index.html + If-Modified-Since: Sun, 11 Oct 2026 20:00:25 GMT
                304 Not Modified  (nessun corpo: si usa la copia salvata)
 3a richiesta (file modificato):  200 OK + corpo nuovo + Last-Modified nuovo

La data va rispedita così com'è (come stringa): non serve convertirla. L'esercizio Esercizio - GET condizionale con If-Modified-Since e cache su file (slide HTTP 0.9 e 1.0) implementa proprio questo schema, con la cache in un file.

Provare HTTP/1.0 a mano

Essendo testo, HTTP si prova con nc (netcat) o telnet:

$ printf 'GET / HTTP/1.0\r\nHost: www.example.com\r\n\r\n' | nc www.example.com 80
HTTP/1.0 200 OK
...
$ curl -v --http1.0 http://www.example.com/        # mostra richiesta e risposta

Per ottenere l'indirizzo di un server da usare nei programmi: nslookup www.example.com o dig www.example.com (Livello applicazione - DNSIl DNS (Domain Name System) traduce i nomi (www.amazon.com) negli indirizzi IP, perché le persone preferiscono i nomi e i protocolli TCP/IP usano gli indirizzi. È un database distribuito e gerarchico: albero rovesciato con radice, domini di primo livello e sottodomini (al più 128 livelli); le informazioni sono su tanti server (13 server radice) e i nuovi domini si registrano presso un registrar accreditato ICANN. Ogni ISP ha un DNS locale, il cui indirizzo l'host riceve con DHCP: l'host (resolver) gli manda la richiesta, di solito su UDP, e il DNS locale interroga radice, dominio di primo livello e server dell'organizzazione. Ogni server che impara un'associazione la tiene in cache, marcando la risposta non autoritativa, e la scarta dopo il TTL. Record (nome, tipo, valore, classe, TTL). Con il NAT il DNS deve restituire l'indirizzo pubblico. Quattro attacchi: macchina compromessa, risposta falsa all'host, avvelenamento della cache del DNS locale, server DNS malevolo.Livello applicazione - DNS →).

Il parsing dei messaggi

Leggere un messaggio da un socket significa riconoscere le sue parti, un byte dopo l'altro, sapendo che i dati arrivano a pezzi (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 →). L'algoritmo ha tre fasi.

  1. Riga iniziale. Si legge fino al primo \n (togliendo il \r che lo precede). In una risposta si estraggono versione, codice, frase con sscanf(riga, "HTTP/%d.%d %d", ...); la frase è il resto della riga, dopo il secondo spazio. In una richiesta: metodo, URI, versione.
  2. Header. Una riga alla volta: ogni riga ha la forma nome: valore. Si cerca il primo :, il nome è quello che precede, il valore è quello che segue senza gli spazi iniziali e finali. Una riga vuota (lunghezza 0 dopo aver tolto il CRLF) segnala la fine degli header.
  3. Corpo. Quello che segue la riga vuota. In HTTP/1.0: fino alla chiusura della connessione per le risposte, Content-Length byte per le richieste con corpo.
 stato 1: leggo la riga iniziale  --(CRLF)-->  stato 2
 stato 2: leggo una riga          --(riga vuota)--> stato 3 (corpo)
                                  --(riga "nome: valore")--> si memorizza e resta in stato 2
 stato 3: leggo il corpo

Esempio di lettura un byte alla volta (come nelle slide del corso): non si rischia mai di leggere oltre la riga vuota, perché ciò che segue è il corpo (o, con le connessioni persistenti, la risposta successiva), ed è per questo che il codice è semplice:

c
static int read_line(int fd, char *line, size_t max)
{
    size_t n = 0;
    char c;
    while (n + 1 < max) {
        if (read(fd, &c, 1) <= 0)
            return -1;                         /* connessione chiusa o errore */
        if (c == '\n') {
            if (n > 0 && line[n - 1] == '\r')  /* si toglie il CR prima del LF */
                n--;
            line[n] = '\0';
            return (int)n;
        }
        line[n++] = c;
    }
    return -1;                                 /* riga troppo lunga: si rifiuta */
}

Il costo è una system call per byte: va bene per esercizi, nei server veri si usa un buffer di lettura (Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica)). Il codice completo di un client che esegue le tre fasi è in Esercizio - Client HTTP 0.9 e 1.0 che scarica una pagina (slide HTTP 0.9 e 1.0); la separazione in funzioni di parsing, con test, è in Esercizio - Parsing di request line, header e URI (sul modello della prova pratica).

Un parser robusto

Le regole che separano un parser che funziona sui propri esempi da uno che funziona con i server veri:

Nota sul codice delle slide. La lettura degli header che pone il valore a due caratteri dopo il : funziona con la sintassi di RFC 1945 (": ") ma non con un server che omette lo spazio; ed è dimensionata con un buffer fisso da 100000 byte senza controlli. Va bene per capire il meccanismo, non per un programma robusto.

Esercizi collegati

Errori tipici

  • Dimenticare la riga vuota (\r\n\r\n) alla fine della richiesta: il server aspetta altri header e non risponde mai.
  • Usare \n al posto di \r\n nelle richieste scritte da un client.
  • Leggere la risposta con una sola read e supporre di avere tutto: servono un ciclo fino a 0 e un buffer abbastanza grande (o un salvataggio su file).
  • Stampare il corpo con printf("%s") senza il '\0', o con strlen, perdendo i byte dopo uno zero (immagini).
  • Cercare gli header con strcmp (maiuscole e minuscole contano) invece di strcasecmp.
  • Credere che Reason-Phrase abbia significato per il programma: conta solo il codice.
  • Trattare qualunque risposta come successo senza controllare il codice (404 e 500 hanno un corpo che è una pagina di errore, non il documento richiesto).
  • In una POST, non mandare Content-Length o mandarlo sbagliato (un valore troppo piccolo tronca il corpo, uno troppo grande fa attendere il server).

Domande d'esame

1. Elencare le differenze fra HTTP/0.9 e HTTP/1.0 e scrivere un esempio di richiesta e di risposta HTTP/1.0. Traccia. 0.9: una sola riga GET /percorso, risposta = solo il documento, niente header né codici di stato, solo HTML, connessione chiusa alla fine. 1.0: Request-Line con versione, Status-Line con codice, header generali, di richiesta, di risposta e di entità, metodi HEAD e POST, contenuti di qualsiasi tipo (Content-Type), Content-Length; ancora una connessione per richiesta. Esempio: GET /index.html HTTP/1.0 + riga vuota; HTTP/1.0 200 OK, Content-Type: text/html, Content-Length: 40, riga vuota, 40 byte di corpo.

2. Come fa un client HTTP/1.0 a capire dove finisce la risposta? E un server dove finisce una richiesta POST? Traccia. Il client legge il corpo finché il server chiude la connessione (read restituisce 0); se c'è Content-Length può verificare di non aver ricevuto una risposta troncata. Una POST ha un corpo che il client non può delimitare chiudendo (aspetta la risposta): per questo Content-Length è obbligatorio e il server legge esattamente quei byte; GET e HEAD finiscono con la riga vuota.

3. Un client invia GET /a.html HTTP/1.0 con If-Modified-Since: <data>. Quali risposte possibili e cosa fa il client? Traccia. 304 Not Modified senza corpo: il client mostra la copia salvata. 200 OK con il corpo nuovo e un nuovo Last-Modified: il client sostituisce la copia. Altri codici (404, 500): il client non tocca la copia e segnala l'errore.

Versione ripasso

  • HTTP/0.9 (1991). Simple-Request = "GET" SP Request-URI CRLF; Simple-Response = [Entity-Body]. Risposta = solo il documento, il server chiude. Nessun header, nessun codice, solo HTML, nessuna autenticazione; il client non può mandare dati.
  • HTTP/1.0 (RFC 1945). Una connessione TCP per richiesta. Novità: header, codici di stato, metodi HEAD e POST, contenuti non HTML (Content-Type).
  • Messaggio. Riga iniziale, header Nome: valore, riga vuota, corpo; righe terminate da CRLF.
http
GET /index.html HTTP/1.0            HTTP/1.0 200 OK
User-Agent: client10/1.0            Content-Type: text/html
If-Modified-Since: <data>           Content-Length: 40
                                    <40 byte di corpo>
c
static int read_line(int fd, char *line, size_t max)
{
    size_t n = 0;
    char c;
    while (n + 1 < max) {
        ssize_t r = read(fd, &c, 1);
        if (r <= 0)
            return -1;
        if (c == '\n') {
            if (n > 0 && line[n - 1] == '\r')
                n--;
            line[n] = '\0';
            return (int)n;
        }
        line[n++] = c;
    }
    return -1;
}
c
static int parse_header(char *line, struct header *h)
{
    char *colon = strchr(line, ':');
    if (colon == NULL || colon == line)
        return -1;
    *colon = '\0';
    char *v = colon + 1;
    while (*v == ' ' || *v == '\t')
        v++;
    size_t len = strlen(v);
    while (len > 0 && (v[len - 1] == ' ' || v[len - 1] == '\t'))
        v[--len] = '\0';
    h->name = strdup(line);
    h->value = strdup(v);
    return 0;
}
  • Errori tipici: manca \r\n\r\n finale; \n invece di \r\n; una sola read; printf("%s") su dati binari; strcmp sui nomi; credere che la frase conti; ignorare il codice; POST senza Content-Length.

Esercizi su questo argomento

Teoria collegata