Esercizio - Client HTTP 1.1 con Content-Length e connessione persistente (slide HTTP 1.1)
In questa pagina 6
Testo (slide HTTP 1.1 del corso di Reti di Calcolatori, Ing. Informatica UniPD: «Client HTTP/1.1: Content-Length»).
Modificare il client HTTP/1.0 (Esercizio - Client HTTP 0.9 e 1.0 che scarica una pagina (slide HTTP 0.9 e 1.0)) in modo che:
- invii richieste HTTP/1.1 con l'header
Host; - invii più richieste sulla stessa connessione TCP (persistente) e legga ogni risposta senza aspettare la chiusura della connessione, usando
Content-Lengthper sapere dove finisce il corpo; - rispetti le regole sulla lunghezza del corpo: nessun corpo per la risposta a
HEADe per gli stati1xx,204,304, anche se c'èContent-Length; - dichiari
Connection: closenell'ultima richiesta; - se il server chiude la connessione (header
Connection: closenella risposta, o risposta HTTP/1.0, o corpo terminato dalla chiusura), riapra la connessione per le richieste seguenti.
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 →, HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsingHTTP/0.9 (1991) ha una sola riga di richiesta "GET /percorso" e la risposta e' solo il documento HTML, senza codici di stato ne' header, e la connessione si chiude alla fine; HTTP/1.0 (RFC 1945) aggiunge Request-Line con versione, Status-Line con codice, header (generali, di richiesta, di risposta, di entita'), i metodi HEAD e POST, tipi di contenuto diversi dall'HTML; un messaggio e' riga iniziale, header "Nome: valore" a righe CRLF, riga vuota, corpo; in HTTP/1.0 il corpo della risposta finisce quando il server chiude la connessione, mentre per POST serve Content-Length; il parsing legge la prima riga, poi gli header fino alla riga vuota, poi il corpo; i nomi degli header non distinguono maiuscole e minuscole, e un client robusto accetta LF semplice e spazi variabili.HTTP 0.9 e 1.0 - struttura dei messaggi, header e parsing →, 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 →.
Idea
Con HTTP/1.0 il client sapeva che la risposta era finita perché il server chiudeva. Con la connessione persistente la chiusura non arriva: il client deve contare. La regola di RFC 9112 par. 6.3 (nell'ordine, per le risposte che interessano qui):
| ordine | condizione | corpo |
|---|---|---|
| 1 | risposta a HEAD, stato 1xx, 204, 304 |
nessuno |
| 2 | Transfer-Encoding: chunked |
decodifica dei chunk (Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1)) |
| 3 | Content-Length: n |
esattamente n byte |
| 4 | nessuno dei due | fino alla chiusura della connessione (e la connessione non si riusa) |
Il programma implementa 1, 3, 4 e segnala chiaramente il caso 2. Un'altra novità rispetto a HTTP/1.0 è la decisione di riuso della connessione dopo ogni risposta:
- risposta HTTP/1.1: riusabile salvo
Connection: close; - risposta HTTP/1.0: riusabile solo con
Connection: keep-alive; - mai se il corpo è stato delimitato dalla chiusura del socket.
Se non è riusabile e restano richieste da fare, il client chiude e riapre.
La lettura continua a essere un byte alla volta per le righe (read_line) e a blocchi per il corpo (read con al più i byte mancanti), così il client non legge mai un byte oltre la risposta corrente: i byte successivi, se ci sono, sono l'inizio della risposta seguente.
Codice
/* client11_cl.c - client HTTP/1.1 con connessione persistente e corpo delimitato da Content-Length.
* Manda piu' richieste sulla STESSA connessione TCP; se il server chiude, riapre.
*
* Compilare: gcc -Wall -Wextra -o client11_cl client11_cl.c
* Usare: ./client11_cl host porta percorso1 [percorso2 ...]
* un percorso preceduto da "HEAD:" usa il metodo HEAD (es. HEAD:/index.html)
* es. ./client11_cl 127.0.0.1 8000 /index.html HEAD:/index.html /a.txt
* Un corpo chunked NON e' gestito qui: vedi il client dell'esercizio sul chunked.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#define MAX_LINE 4096
struct response {
int major, minor; /* versione della risposta */
int status;
long content_length; /* -1 se assente */
int chunked;
int conn_close; /* "Connection: close" presente */
int conn_keepalive; /* "Connection: keep-alive" presente */
};
static int connect_to(const char *host, const char *port)
{
struct addrinfo hints, *res, *p;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host, port, &hints, &res) != 0)
return -1;
int fd = -1;
for (p = res; p != NULL; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd < 0)
continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
return fd;
}
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;
}
/* Una riga alla volta, un byte per read: non si legge mai un byte oltre la riga, quindi il corpo
* (e la risposta successiva su una connessione persistente) resta intatto nel socket. */
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;
if (c == '\n') {
if (n > 0 && line[n - 1] == '\r')
n--;
line[n] = '\0';
return (int)n;
}
line[n++] = c;
}
return -1;
}
/* Vero se 'hay' contiene 'needle' senza distinguere maiuscole e minuscole. */
static int ci_contains(const char *hay, const char *needle)
{
size_t k = strlen(needle);
for (; *hay; hay++)
if (strncasecmp(hay, needle, k) == 0)
return 1;
return 0;
}
/* Legge status-line e header. Torna 0 se tutto bene. */
static int read_head(int fd, struct response *rs)
{
char line[MAX_LINE];
memset(rs, 0, sizeof *rs);
rs->content_length = -1;
if (read_line(fd, line, sizeof line) < 0 || sscanf(line, "HTTP/%d.%d %d", &rs->major, &rs->minor, &rs->status) != 3)
return -1;
for (;;) {
int len = read_line(fd, line, sizeof line);
if (len < 0)
return -1;
if (len == 0)
return 0; /* riga vuota: fine degli header */
char *colon = strchr(line, ':');
if (colon == NULL)
continue;
*colon = '\0';
const char *v = colon + 1;
while (*v == ' ' || *v == '\t')
v++;
if (strcasecmp(line, "Content-Length") == 0)
rs->content_length = atol(v);
else if (strcasecmp(line, "Transfer-Encoding") == 0 && ci_contains(v, "chunked"))
rs->chunked = 1;
else if (strcasecmp(line, "Connection") == 0) {
if (ci_contains(v, "close"))
rs->conn_close = 1;
if (ci_contains(v, "keep-alive"))
rs->conn_keepalive = 1;
}
}
}
/* Quanti byte ha il corpo? Si seguono le regole di RFC 9112 par. 6.3 (nell'ordine). Il corpo viene
* stampato in anteprima e scartato. *close_delimited vale 1 se il corpo e' finito con la chiusura del socket.
* Torna la lunghezza del corpo, -1 su errore, -2 se e' chunked. */
static long read_body(int fd, const struct response *rs, int head_request, int *close_delimited)
{
*close_delimited = 0;
/* 1) HEAD, 1xx, 204, 304: MAI un corpo, anche se c'e' Content-Length (e' la lunghezza che avrebbe avuto il GET) */
if (head_request || rs->status / 100 == 1 || rs->status == 204 || rs->status == 304)
return 0;
if (rs->chunked)
return -2;
char buf[4096];
long total = 0;
if (rs->content_length >= 0) {
/* 2) Content-Length: si leggono esattamente quei byte, ne' uno di piu' ne' uno di meno */
long left = rs->content_length;
while (left > 0) {
size_t want = left < (long)sizeof buf ? (size_t)left : sizeof buf;
ssize_t r = read(fd, buf, want);
if (r < 0 && errno == EINTR)
continue;
if (r <= 0)
return -1; /* il server ha chiuso prima: risposta incompleta */
if (total == 0)
printf(" anteprima: %.*s\n", r < 60 ? (int)r : 60, buf); /* buf non e' terminato da '\0' */
total += r;
left -= r;
}
return total;
}
/* 3) Niente lunghezza: il corpo finisce quando il server chiude la connessione (stile HTTP/1.0) */
ssize_t r;
while ((r = read(fd, buf, sizeof buf)) > 0)
total += r;
*close_delimited = 1;
return total;
}
int main(int argc, char **argv)
{
if (argc < 4) {
fprintf(stderr, "uso: %s host porta percorso1 [percorso2 ...]\n", argv[0]);
return 1;
}
const char *host = argv[1], *port = argv[2];
int fd = connect_to(host, port);
if (fd < 0) {
fprintf(stderr, "connessione fallita\n");
return 1;
}
int nconn = 1;
for (int i = 3; i < argc; i++) {
int last = (i == argc - 1);
const char *method = "GET", *path = argv[i];
if (strncmp(path, "HEAD:", 5) == 0) {
method = "HEAD";
path += 5;
}
/* In HTTP/1.1 la connessione e' persistente di default: Connection: close solo nell'ultima richiesta. */
char req[2048];
int n = snprintf(req, sizeof req,
"%s %s HTTP/1.1\r\n"
"Host: %s:%s\r\n"
"User-Agent: client11_cl/1.0\r\n"
"%s"
"\r\n",
method, path, host, port, last ? "Connection: close\r\n" : "");
if (write_all(fd, req, (size_t)n) < 0) {
perror("write");
return 1;
}
struct response rs;
if (read_head(fd, &rs) < 0) {
fprintf(stderr, "risposta non valida o connessione chiusa\n");
return 1;
}
int cd;
long len = read_body(fd, &rs, strcmp(method, "HEAD") == 0, &cd);
if (len == -2) {
fprintf(stderr, "corpo chunked: non gestito da questo client\n");
return 1;
}
if (len < 0) {
fprintf(stderr, "corpo incompleto\n");
return 1;
}
printf("%s %s -> %d, Content-Length %ld, corpo letto %ld byte (connessione n. %d)\n",
method, path, rs.status, rs.content_length, len, nconn);
/* La connessione si puo' riusare? 1.1: si', salvo "Connection: close"; 1.0: solo con "keep-alive";
* e mai se il corpo e' finito con la chiusura del socket. */
int reusable = (rs.major == 1 && rs.minor >= 1) ? !rs.conn_close : rs.conn_keepalive;
if (cd)
reusable = 0;
if (!reusable && !last) {
printf(" il server chiude la connessione: ne apro una nuova\n");
close(fd);
fd = connect_to(host, port);
if (fd < 0) {
fprintf(stderr, "riconnessione fallita\n");
return 1;
}
nconn++;
}
}
close(fd);
return 0;
}Compilare e provare
Un server HTTP/1.1 persistente di prova (il server di Python con il protocollo 1.1):
$ python3 -c "import http.server as h; h.test(h.SimpleHTTPRequestHandler, protocol='HTTP/1.1', port=8000, bind='127.0.0.1')" &
$ gcc -Wall -Wextra -o client11_cl client11_cl.c
$ ./client11_cl 127.0.0.1 8000 /index.html HEAD:/index.html /a.txt
anteprima: <html><body><h1>Ciao</h1></body></html>
GET /index.html -> 200, Content-Length 40, corpo letto 40 byte (connessione n. 1)
HEAD /index.html -> 200, Content-Length 40, corpo letto 0 byte (connessione n. 1)
anteprima: riga1
riga2
GET /a.txt -> 200, Content-Length 12, corpo letto 12 byte (connessione n. 1)Tutte e tre le richieste usano la connessione n. 1. Controllare con ss -tan | grep 8000 durante l'esecuzione, o con strace -e trace=connect ./client11_cl ... (una sola connect). Con il server dell'esercizio Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica), che manda Connection: close a ogni risposta, il client riapre la connessione:
$ ./client11_cl 127.0.0.1 8080 /index.html /a.txt
anteprima: <html><body><h1>Ciao</h1></body></html>
GET /index.html -> 200, Content-Length 40, corpo letto 40 byte (connessione n. 1)
il server chiude la connessione: ne apro una nuova
anteprima: riga1
riga2
GET /a.txt -> 200, Content-Length 12, corpo letto 12 byte (connessione n. 2)Con HEAD: davanti al percorso si prova il caso 1: senza la regola sulla HEAD il client resterebbe bloccato in attesa dei 40 byte che il server non invia.
Spiegazione dei punti chiave
La richiesta.
GET /index.html HTTP/1.1\r\n
Host: 127.0.0.1:8000\r\n
User-Agent: client11_cl/1.0\r\n
\r\n (nell'ultima richiesta anche Connection: close\r\n)Hostè obbligatorio in HTTP/1.1: un server risponde400se manca.- Nessun
Connection: keep-alive: in HTTP/1.1 la persistenza è il comportamento predefinito. Solo l'ultima richiesta diceConnection: close, così il server chiude dopo la risposta e il client non resta con una connessione aperta.
struct response. Dopo read_head contiene: versione (major, minor), status, content_length (-1 se assente), chunked, conn_close e conn_keepalive. L'header Connection può contenere più parole (keep-alive, Upgrade): ci_contains cerca la sottostringa senza distinguere maiuscole.
read_head. Come nel client 1.0 (read_line byte per byte, status-line con sscanf, header spezzati al primo :), ma raccoglie solo i quattro header che servono. I nomi si confrontano con strcasecmp: content-length e Content-Length sono lo stesso header (il server di Python manda Content-type).
read_body.
- Caso 1 (
head_request,status/100 == 1,204,304): restituisce0senza leggere. È il punto più delicato: la risposta aHEADhaContent-Length: 40ma nessun corpo. - Caso 2 (
chunked): il client segnala e termina (nel prossimo esercizio lo si decodifica). - Caso 3 (
Content-Length):leftconta i byte mancanti; ognireadchiede al piùmin(left, sizeof buf);r <= 0prima della fine significa che il server ha chiuso: corpo incompleto (errore, non una risposta valida). Chiedere solo i byte mancanti è ciò che impedisce di consumare l'inizio della risposta successiva. - Caso 4 (niente lunghezza):
readfino a0;*close_delimited = 1avvisa il chiamante che la connessione non è riusabile.
La stampa dell'anteprima usa %.*s con la lunghezza letta: il buffer non è terminato da '\0' e printf("%s") leggerebbe oltre i dati ricevuti.
La decisione di riuso (nel main).
int reusable = (rs.major == 1 && rs.minor >= 1) ? !rs.conn_close : rs.conn_keepalive;
if (cd) reusable = 0; /* corpo delimitato dalla chiusura */
if (!reusable && !last) { close(fd); fd = connect_to(host, port); nconn++; }Se la risposta è HTTP/1.1 la connessione resta valida finché non c'è Connection: close; se è 1.0 serve keep-alive esplicito. Ogni riapertura incrementa nconn, che il programma stampa a ogni richiesta.
Quello che il programma non fa (e un client vero deve fare): ritentare automaticamente una richiesta se il server chiude una connessione inattiva (solo per richieste idempotenti), gestire il pipelining, gestire 100 Continue, i timeout, e il chunked.
Errori tipici
- Aspettare il corpo dopo una risposta a
HEAD,204o304: il programma si blocca. - Leggere "fino alla chiusura" con una connessione persistente:
readnon restituisce mai0finché il server non chiude. - Leggere con un buffer fisso più grande dei byte mancanti: si consumano byte della risposta successiva e si perde la sincronizzazione (con
readdi blocchi si limitawantaleft). - Non distinguere il fine corpo (
Content-Length) dalla fine della risposta quando il server chiude prima: corpo incompleto. - Dimenticare
Hostin HTTP/1.1, o mandareConnection: keep-alivecredendolo necessario. - Non controllare
Connection: closenella risposta: la richiesta successiva va su un socket chiuso (erroreEPIPEo risposta vuota). - Confrontare
Content-Lengthconstrcmpinvece distrcasecmp. - Stampare il buffer con
%ssenza terminatore.
Varianti per esercitarsi
- Aggiungere la ripetizione automatica di una
GETse la connessione è stata chiusa dal server prima di ricevere qualunque byte della risposta. - Salvare ogni corpo in un file
risposta-N.bin. - Implementare il pipelining: inviare tutte le richieste insieme e leggere poi le risposte nello stesso ordine.
- Unificare con il client del chunked: Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1).
Versione ripasso
- Testo. Client HTTP/1.1 con
Host; più richieste sulla stessa connessione, risposte delimitate daContent-Length; nessun corpo perHEADe per1xx/204/304;Connection: closenell'ultima richiesta; riapertura se il server chiude. - Lunghezza del corpo (RFC 9112 par. 6.3). (1)
HEAD,1xx,204,304: nessun corpo; (2)Transfer-Encoding: chunked: chunk; (3)Content-Length: n: esattamentenbyte; (4) altrimenti fino alla chiusura (connessione non riusabile). - Riuso della connessione. 1.1: riusabile salvo
Connection: close; 1.0: solo conkeep-alive; mai se il corpo è finito con la chiusura. Se non riusabile e restano richieste:close+connect_to,nconn++. - Richiesta.
GET path HTTP/1.1\r\nHost: host:porta\r\nUser-Agent: ...\r\n+ (solo l'ultima)Connection: close\r\n+\r\n.Hostobbligatorio; nientekeep-alive. read_head.read_linea un byte alla volta;sscanf("HTTP/%d.%d %d"); header:Content-Length(atol),Transfer-Encoding(contienechunked),Connection(lista:close,keep-alive) constrcasecmpeci_contains.read_body.
if (head_request || status / 100 == 1 || status == 204 || status == 304) return 0; /* nessun corpo */
if (chunked) return -2;
if (content_length >= 0) { long left = content_length;
while (left > 0) { size_t want = left < sizeof buf ? left : sizeof buf; /* al più i byte MANCANTI */
ssize_t r = read(fd, buf, want);
if (r <= 0) return -1; /* chiuso prima: incompleto */
left -= r; } }
else /* fino alla chiusura */ { while (read(fd, buf, sizeof buf) > 0) ...; *close_delimited = 1; }- Prove. Server di prova:
python3 -c "import http.server as h; h.test(h.SimpleHTTPRequestHandler, protocol='HTTP/1.1', port=8000, bind='127.0.0.1')":./client11_cl 127.0.0.1 8000 /index.html HEAD:/index.html /a.txt-> tutte e tre sulla connessione n. 1. Con il server dell'esercizio (Connection: close) il client riapre (connessione n. 2).HEAD:prova la regola dellaHEAD. - Anteprima.
%.*scon la lunghezza letta: il buffer non è terminato da'\0'. - Non gestito. Ritentativo automatico, pipelining,
100 Continue, timeout, chunked. - Codice essenziale (le funzioni centrali, senza commenti):
static int read_head(int fd, struct response *rs)
{
char line[MAX_LINE];
memset(rs, 0, sizeof *rs);
rs->content_length = -1;
if (read_line(fd, line, sizeof line) < 0 || sscanf(line, "HTTP/%d.%d %d", &rs->major, &rs->minor, &rs->status) != 3)
return -1;
for (;;) {
int len = read_line(fd, line, sizeof line);
if (len < 0)
return -1;
if (len == 0)
return 0;
char *colon = strchr(line, ':');
if (colon == NULL)
continue;
*colon = '\0';
const char *v = colon + 1;
while (*v == ' ' || *v == '\t')
v++;
if (strcasecmp(line, "Content-Length") == 0)
rs->content_length = atol(v);
else if (strcasecmp(line, "Transfer-Encoding") == 0 && ci_contains(v, "chunked"))
rs->chunked = 1;
else if (strcasecmp(line, "Connection") == 0) {
if (ci_contains(v, "close"))
rs->conn_close = 1;
if (ci_contains(v, "keep-alive"))
rs->conn_keepalive = 1;
}
}
}
static long read_body(int fd, const struct response *rs, int head_request, int *close_delimited)
{
*close_delimited = 0;
if (head_request || rs->status / 100 == 1 || rs->status == 204 || rs->status == 304)
return 0;
if (rs->chunked)
return -2;
char buf[4096];
long total = 0;
if (rs->content_length >= 0) {
long left = rs->content_length;
while (left > 0) {
size_t want = left < (long)sizeof buf ? (size_t)left : sizeof buf;
ssize_t r = read(fd, buf, want);
if (r < 0 && errno == EINTR)
continue;
if (r <= 0)
return -1;
if (total == 0)
printf(" anteprima: %.*s\n", r < 60 ? (int)r : 60, buf);
total += r;
left -= r;
}
return total;
}
ssize_t r;
while ((r = read(fd, buf, sizeof buf)) > 0)
total += r;
*close_delimited = 1;
return total;
}- Errori tipici: corpo atteso dopo
HEAD/204/304; "fino alla chiusura" con connessione persistente;readpiù grande dei byte mancanti (si perde la sincronizzazione); chiusura prima della fine non rilevata;Hostmancante;Connection: closedella risposta ignorato;strcmpsui nomi;%ssu buffer non terminato.