Esercizio - GET condizionale con If-Modified-Since e cache su file (slide HTTP 0.9 e 1.0)
In questa pagina 6
Testo (slide HTTP 0.9 e 1.0 del corso di Reti di Calcolatori, Ing. Informatica UniPD, «Esercizio 2»).
- Effettuare una richiesta HTTP/1.0 a un server (le slide usano
www.example.com, con l'indirizzo ottenuto danslookup), includendo l'headerHost. - Salvare la risposta in un file nel formato seguente: prima riga = data di ultima modifica, cioè il valore di
Last-Modifiedo, in sua assenza, diDate; dalla seconda riga il contenuto della risposta (entity-body). - Le chiamate successive devono usare l'header
If-Modified-Sinceper fare GET condizionali. - Se il server risponde con una nuova versione (
200), aggiornare il file e mostrare il contenuto; se risponde304, mostrare il contenuto presente nel file.
Teoria: 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 →, 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 →, Esercizio - Client HTTP 0.9 e 1.0 che scarica una pagina (slide HTTP 0.9 e 1.0).
Idea
È una cache lato client minimale. Il file di cache contiene due cose: la data che il server ha dato alla risorsa e il corpo. Ad ogni esecuzione:
esiste il file di cache?
no -> GET normale
sì -> GET con "If-Modified-Since: <data della prima riga>"
risposta del server:
200 OK -> nuova versione: riscrivi il file (data = Last-Modified oppure Date) e mostra il corpo
304 Not Modified -> nessun corpo: mostra il corpo salvato nel file
altro (404, 500) -> segnala l'errore e non toccare la cacheDue scelte progettuali:
- La data nell'
If-Modified-Sincesi rimanda identica alla stringa ricevuta: non occorre convertirla in numeri (si evitanostrptimee fusi orari) ed è esattamente quello che il server si aspetta. - Se il server non manda
Last-Modifiedsi usaDate: è la data del messaggio, quindi un304non arriverà mai (il server non sa se la risorsa è cambiata rispetto a quel momento); la cache funzionerà ma si riscarica ogni volta. È il comportamento richiesto dal testo.
Codice
/* cget.c - GET condizionata HTTP/1.0 con cache su file.
*
* File di cache: prima riga = data dell'ultima modifica (Last-Modified, o Date se manca),
* dalla seconda riga = corpo della risposta.
* Prima chiamata: GET normale, salva la cache. Chiamate successive: If-Modified-Since.
* 200 -> aggiorna la cache e mostra il corpo; 304 -> mostra il corpo salvato.
*
* Compilare: gcc -Wall -Wextra -o cget cget.c
* Usare: ./cget host porta percorso file_cache
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#define MAX_LINE 4096
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;
}
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;
}
/* Se la riga e' l'header 'name', torna un puntatore al valore (senza spazi iniziali), altrimenti NULL. */
static const char *header_value(const char *line, const char *name)
{
size_t k = strlen(name);
if (strncasecmp(line, name, k) != 0 || line[k] != ':')
return NULL;
const char *v = line + k + 1;
while (*v == ' ' || *v == '\t')
v++;
return v;
}
/* Carica la cache: torna 1 se esiste. date: prima riga; body/len: il resto (allocato con malloc). */
static int load_cache(const char *path, char *date, size_t dsize, char **body, size_t *len)
{
FILE *f = fopen(path, "rb");
if (f == NULL)
return 0;
if (fgets(date, (int)dsize, f) == NULL) {
fclose(f);
return 0;
}
date[strcspn(date, "\r\n")] = '\0';
size_t cap = 4096, n = 0;
char *b = malloc(cap);
size_t r;
while (b != NULL && (r = fread(b + n, 1, cap - n, f)) > 0) {
n += r;
if (n == cap) {
cap *= 2;
b = realloc(b, cap);
}
}
fclose(f);
if (b == NULL)
return 0;
*body = b;
*len = n;
return 1;
}
int main(int argc, char **argv)
{
if (argc != 5) {
fprintf(stderr, "uso: %s host porta percorso file_cache\n", argv[0]);
return 1;
}
const char *host = argv[1], *port = argv[2], *path = argv[3], *cachefile = argv[4];
char cdate[128] = "";
char *cbody = NULL;
size_t clen = 0;
int have_cache = load_cache(cachefile, cdate, sizeof cdate, &cbody, &clen);
int fd = connect_to(host, port);
if (fd < 0) {
fprintf(stderr, "connessione fallita\n");
return 1;
}
/* Richiesta: se abbiamo la cache aggiungiamo If-Modified-Since con la data memorizzata, cosi' com'e'. */
char req[2048];
int n = snprintf(req, sizeof req, "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: cget/1.0\r\n", path, host);
if (have_cache)
n += snprintf(req + n, sizeof req - (size_t)n, "If-Modified-Since: %s\r\n", cdate);
n += snprintf(req + n, sizeof req - (size_t)n, "\r\n");
if (write_all(fd, req, (size_t)n) < 0) {
perror("write");
return 1;
}
/* Status-line */
char line[MAX_LINE];
int status = 0;
if (read_line(fd, line, sizeof line) < 0 || sscanf(line, "HTTP/%*d.%*d %d", &status) != 1) {
fprintf(stderr, "risposta non valida\n");
return 1;
}
/* Header: ci servono solo Last-Modified e Date */
char last_modified[128] = "", date[128] = "";
for (;;) {
int len = read_line(fd, line, sizeof line);
if (len < 0) {
fprintf(stderr, "risposta interrotta\n");
return 1;
}
if (len == 0)
break;
const char *v;
if ((v = header_value(line, "Last-Modified")) != NULL)
snprintf(last_modified, sizeof last_modified, "%s", v);
else if ((v = header_value(line, "Date")) != NULL)
snprintf(date, sizeof date, "%s", v);
}
if (status == 304) {
/* 304 Not Modified: nessun corpo, la copia locale e' ancora buona. */
if (!have_cache) {
fprintf(stderr, "304 ma non ho nessuna cache?\n");
return 1;
}
fprintf(stderr, "[304: uso la cache, modificata il %s]\n", cdate);
fwrite(cbody, 1, clen, stdout);
} else if (status == 200) {
/* Nuova versione: leggo il corpo fino alla chiusura e riscrivo la cache. */
size_t cap = 8192, len = 0;
char *body = malloc(cap);
ssize_t r;
char buf[4096];
while (body != NULL && (r = read(fd, buf, sizeof buf)) > 0) {
if (len + (size_t)r > cap) {
while (len + (size_t)r > cap)
cap *= 2;
body = realloc(body, cap);
if (body == NULL)
break;
}
memcpy(body + len, buf, (size_t)r);
len += (size_t)r;
}
if (body == NULL) {
fprintf(stderr, "memoria esaurita\n");
return 1;
}
const char *stamp = last_modified[0] ? last_modified : date; /* Last-Modified, altrimenti Date */
FILE *f = fopen(cachefile, "wb");
if (f == NULL) {
perror("fopen cache");
return 1;
}
fprintf(f, "%s\n", stamp);
fwrite(body, 1, len, f);
fclose(f);
fprintf(stderr, "[200: nuova versione (%s), %zu byte, cache aggiornata]\n", stamp, len);
fwrite(body, 1, len, stdout);
free(body);
} else {
fprintf(stderr, "[stato %d: cache non modificata]\n", status);
}
close(fd);
free(cbody);
return 0;
}Compilare e provare
$ gcc -Wall -Wextra -o cget cget.c
$ (cd www && python3 -m http.server 8000 --bind 127.0.0.1) &
$ ./cget 127.0.0.1 8000 /index.html cache.txt
[200: nuova versione (Sun, 11 Oct 2026 20:00:25 GMT), 40 byte, cache aggiornata]
<html><body><h1>Ciao</h1></body></html>
$ ./cget 127.0.0.1 8000 /index.html cache.txt
[304: uso la cache, modificata il Sun, 11 Oct 2026 20:00:25 GMT]
<html><body><h1>Ciao</h1></body></html>
$ head -c 60 cache.txt | od -c | head -3 # prima riga = data, poi il corpo
0000000 S u n , 1 1 O c t 2 0 2 6
0000020 2 0 : 0 0 : 2 5 G M T \n < h
...
$ touch www/index.html # il file viene modificato sul server
$ ./cget 127.0.0.1 8000 /index.html cache.txt
[200: nuova versione (Sun, 11 Oct 2026 20:03:26 GMT), 40 byte, cache aggiornata]
...Per vedere i messaggi: curl -v --http1.0 -H 'If-Modified-Since: Sun, 11 Oct 2026 20:00:25 GMT' http://127.0.0.1:8000/index.html risponde HTTP/1.0 304 Not Modified. (Si noti: [...] e le altre diagnostiche sono su stderr, il corpo su stdout.)
Spiegazione dei punti chiave
load_cache.
fopen(path, "rb")fallisce se il file non esiste: è la prima esecuzione,have_cachevale0e la richiesta sarà non condizionale.fgets(date, ...)legge la prima riga (la data) edate[strcspn(date, "\r\n")] = '\0'toglie il fine riga.- Il corpo si legge con
freadin un buffer che si raddoppia quando è pieno (realloc): non si conosce la dimensione in anticipo. Il corpo può contenere byte zero: si conserva la lunghezzaclen, non si usastrlen.
La richiesta. snprintf costruisce la riga di richiesta e gli header fissi; se c'è la cache si aggiunge If-Modified-Since: <data>\r\n; infine \r\n chiude gli header. Il server confronta la data con quella di modifica della risorsa e risponde 304 se non è cambiata.
header_value. Confronta l'inizio della riga con il nome (strncasecmp, senza distinguere maiuscole) e controlla che subito dopo ci sia :; restituisce un puntatore al valore dopo gli spazi. Serve solo per Last-Modified e Date: gli altri header si scartano.
Il ramo 304. Non c'è corpo: la risposta termina con la riga vuota. Il client stampa il corpo salvato (fwrite(cbody, 1, clen, stdout)). Se arrivasse un 304 senza che esista una cache (non potrebbe, perché non avremmo mandato If-Modified-Since) si segnala l'errore.
Il ramo 200. Si legge il corpo fino alla chiusura della connessione (HTTP/1.0) con un buffer che cresce (realloc, controllando che non fallisca). Poi si sceglie la data (Last-Modified se presente, altrimenti Date) e si riscrive il file: prima riga la data, poi il corpo con fwrite (non fputs: byte zero). Infine si stampa il corpo.
Altri codici. Un 404 o 500 non deve cancellare una cache valida: il programma stampa lo stato e lascia il file com'è.
Perché non si confrontano date. Il client non decide nulla: spedisce la data e lascia al server il confronto. Questo evita problemi di fuso e di formato (RFC 9110 ammette tre formati di data, ma la stringa viene dal server stesso).
Errori tipici
- Convertire la data o riscriverla in un formato diverso: il server può non riconoscerla e rispondere sempre
200. - Dimenticare di togliere il
\nletto dafgetsdalla data:If-Modified-Since: Sun, ... GMT\nrompe la riga di richiesta. - Salvare il corpo con
fputsostrlene perdere i byte dopo uno zero; non aprire il file in modo binario ("wb","rb"). - Sovrascrivere la cache anche con
404o500. - Usare
DatecomeLast-Modifiedpensando che dia304: senzaLast-Modifiedil server non può rispondere304. - Aspettare un corpo dopo il
304: in HTTP un304non ha mai corpo, e in HTTP/1.0 lareadtermina comunque alla chiusura. - Non controllare
realloc.
Varianti per esercitarsi
- Usare
ETageIf-None-Match: nella cache si memorizza la stringaETag(virgolette comprese) e la si rimanda uguale (Esercizio - Server con autenticazione Basic, ETag e GET condizionale (sul modello della prova pratica) è il server che li genera). - Memorizzare più risorse: un file di cache per URL (nome ottenuto dall'URL con un hash).
- Passare a HTTP/1.1 con connessione persistente (Esercizio - Client HTTP 1.1 con Content-Length e connessione persistente (slide HTTP 1.1)).
Versione ripasso
- Testo. Client HTTP/1.0 con
Host; file di cache: riga 1 =Last-Modified(oDatese manca), dalla riga 2 il corpo; chiamate successive conIf-Modified-Since;200-> riscrivere il file e mostrare;304-> mostrare il file. - Schema.
file di cache esiste? no -> GET sì -> GET + "If-Modified-Since: <riga 1>"
200 -> salva (data, corpo), mostra 304 -> mostra il corpo salvato altro -> errore, cache intatta- Data. Si rimanda identica come stringa: niente
strptime, il confronto lo fa il server. SenzaLast-Modifiedsi usaDate: non darà mai304(comportamento voluto dal testo). load_cache.fopen("rb")(non esiste = prima esecuzione);fgetsper la data edate[strcspn(date, "\r\n")] = '\0'; corpo confreade buffer raddoppiato conrealloc(lunghezzaclen, maistrlen: byte zero).- Richiesta.
GET path HTTP/1.0\r\nHost: ...\r\nUser-Agent: ...\r\n+ (se c'è cache)If-Modified-Since: <data>\r\n+\r\n. - Risposta. Status-line con
sscanf("HTTP/%*d.%*d %d"); header: soloLast-ModifiedeDate(strncasecmp);304= nessun corpo;200= lettura fino alla chiusura (read == 0) con buffer che cresce, poifprintf(f, "%s\n", stamp)+fwrite(body, 1, len, f), file"wb". - Prove.
python3 -m http.server 8000; 1ª esecuzione[200: nuova versione ..., cache aggiornata]; 2ª[304: uso la cache ...]; dopotouchdel file sul server di nuovo 200. Diagnostica sustderr, corpo sustdout. - Codice essenziale (le funzioni centrali, senza commenti):
static int load_cache(const char *path, char *date, size_t dsize, char **body, size_t *len)
{
FILE *f = fopen(path, "rb");
if (f == NULL)
return 0;
if (fgets(date, (int)dsize, f) == NULL) {
fclose(f);
return 0;
}
date[strcspn(date, "\r\n")] = '\0';
size_t cap = 4096, n = 0;
char *b = malloc(cap);
size_t r;
while (b != NULL && (r = fread(b + n, 1, cap - n, f)) > 0) {
n += r;
if (n == cap) {
cap *= 2;
b = realloc(b, cap);
}
}
fclose(f);
if (b == NULL)
return 0;
*body = b;
*len = n;
return 1;
}
static const char *header_value(const char *line, const char *name)
{
size_t k = strlen(name);
if (strncasecmp(line, name, k) != 0 || line[k] != ':')
return NULL;
const char *v = line + k + 1;
while (*v == ' ' || *v == '\t')
v++;
return v;
}
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;
}- Errori tipici: data riscritta in altro formato;
\ndella data non tolto;fputs/strlensul corpo e file non binario; cache sovrascritta con404/500;Datecreduta valida per304;reallocnon controllato.