Salta al contenuto
Note per Studenti Esercizio - Client HTTP 0.9 e 1.0 che scarica una pagina (slide HTTP 0.9 e 1.0)

Esercizio - Client HTTP 0.9 e 1.0 che scarica una pagina (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; l'estensione con il salvataggio su file è l'«Esercizio 1» delle slide).

  1. Scrivere un client HTTP/0.9 in C che si collega a un server sulla porta data, invia GET /percorso e stampa quello che riceve fino alla chiusura della connessione.
  2. Scrivere un client HTTP/1.0 che invia una richiesta completa (Request-Line, Host, User-Agent), legge e analizza la risposta separando status-line, header e corpo, mostra stato e header, e salva il corpo in un file.
  3. Il client 1.0 deve accorgersi se la risposta è stata troncata (confrontando Content-Length con i byte ricevuti).

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 →, 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 →, 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 →, 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 →.


Idea

Il client HTTP è un client TCP (Esercizio - Echo TCP e UDP, client e server (sul modello della prova pratica)) che parla un protocollo testuale: apre la connessione, scrive la richiesta, legge la risposta.

HTTP/0.9. La richiesta è GET /percorso\r\n; la risposta è solo il documento. Non c'è niente da analizzare: si legge fino a read == 0 (il server chiude).

HTTP/1.0. Richiesta e risposta hanno la struttura riga iniziale, header, riga vuota, corpo (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 →). Il client:

  1. costruisce la richiesta con snprintf (ogni riga termina con \r\n, e una riga vuota chiude gli header);
  2. legge la status-line con read_line (un byte alla volta, fino a \n) ed estrae versione e codice con sscanf;
  3. legge gli header riga per riga, finché la riga è vuota, spezzando ogni riga al primo : e togliendo gli spazi;
  4. legge il corpo fino alla chiusura della connessione e lo scrive su file o su stdout;
  5. confronta, se c'è, Content-Length con i byte ricevuti.

La lettura un byte alla volta è lenta ma sicura: non si legge mai oltre la riga vuota, quindi tutto ciò che resta nel socket è il corpo. (Un server o una connessione persistente richiedono un buffer di lettura: Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica).)

Codice

Client HTTP/0.9

c
/* client09.c - client HTTP/0.9: una sola riga di richiesta, risposta = solo il documento.
 * Compilare:  gcc -Wall -Wextra -o client09 client09.c
 * Usare:      ./client09 host porta percorso      es. ./client09 127.0.0.1 8000 /index.html
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

/* Risolve host:porta con getaddrinfo (IPv4 o IPv6) e si connette al primo indirizzo che risponde. */
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;       /* IPv4 o IPv6, come capita */
    hints.ai_socktype = SOCK_STREAM;   /* stream = TCP */
    int rc = getaddrinfo(host, port, &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
        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;                      /* connesso */
        close(fd);
        fd = -1;
    }
    freeaddrinfo(res);
    if (fd < 0)
        fprintf(stderr, "connect a %s:%s fallita\n", host, port);
    return fd;
}

/* write puo' scrivere meno byte del richiesto: si ripete finche' non e' tutto inviato. */
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;
}

int main(int argc, char **argv)
{
    if (argc != 4) {
        fprintf(stderr, "uso: %s host porta percorso\n", argv[0]);
        return 1;
    }
    int fd = connect_to(argv[1], argv[2]);
    if (fd < 0)
        return 1;

    /* La richiesta HTTP/0.9 e' SOLO "GET percorso" + CRLF: niente versione, niente header. */
    char req[1024];
    int n = snprintf(req, sizeof req, "GET %s\r\n", argv[3]);
    if (n < 0 || (size_t)n >= sizeof req) {
        fprintf(stderr, "percorso troppo lungo\n");
        return 1;
    }
    if (write_all(fd, req, (size_t)n) < 0) {
        perror("write");
        return 1;
    }

    /* La risposta e' il documento e basta; finisce quando il server chiude (read restituisce 0). */
    char buf[4096];
    ssize_t r;
    size_t tot = 0;
    while ((r = read(fd, buf, sizeof buf)) > 0) {
        fwrite(buf, 1, (size_t)r, stdout);
        tot += (size_t)r;
    }
    if (r < 0)
        perror("read");
    fprintf(stderr, "\n[%zu byte ricevuti]\n", tot);
    close(fd);
    return 0;
}

Client HTTP/1.0

c
/* client10.c - client HTTP/1.0: GET con status-line, header e corpo; salva il corpo su file.
 * Compilare:  gcc -Wall -Wextra -o client10 client10.c
 * Usare:      ./client10 host porta percorso [file_uscita]
 *             es. ./client10 www.example.com 80 / example.html
 */
#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_HEADERS 64
#define MAX_LINE 4096

struct header {
    char *name;     /* "Content-Length" */
    char *value;    /* "1256"           */
};

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;
    int rc = getaddrinfo(host, port, &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
        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;
}

/* Legge UNA riga un byte alla volta (cosi' non si legge mai oltre la riga vuota finale:
 * quello che segue e' il corpo). Toglie CRLF o LF. Torna la lunghezza, -1 se EOF o riga troppo lunga. */
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;
}

/* Spezza "Nome:  valore  " in nome e valore (senza spazi ai bordi). Torna 0 se la riga e' valida. */
static int parse_header(char *line, struct header *h)
{
    char *colon = strchr(line, ':');
    if (colon == NULL || colon == line)
        return -1;                       /* niente ':' oppure nome vuoto */
    *colon = '\0';
    char *v = colon + 1;
    while (*v == ' ' || *v == '\t')
        v++;                             /* salta gli spazi dopo ':' */
    size_t len = strlen(v);
    while (len > 0 && (v[len - 1] == ' ' || v[len - 1] == '\t'))
        v[--len] = '\0';                 /* toglie gli spazi in coda */
    h->name = strdup(line);
    h->value = strdup(v);
    return 0;
}

/* I nomi degli header NON distinguono maiuscole e minuscole. */
static const char *header_get(const struct header *hs, int n, const char *name)
{
    for (int i = 0; i < n; i++)
        if (strcasecmp(hs[i].name, name) == 0)
            return hs[i].value;
    return NULL;
}

int main(int argc, char **argv)
{
    if (argc < 4 || argc > 5) {
        fprintf(stderr, "uso: %s host porta percorso [file_uscita]\n", argv[0]);
        return 1;
    }
    const char *host = argv[1], *port = argv[2], *path = argv[3];

    int fd = connect_to(host, port);
    if (fd < 0) {
        fprintf(stderr, "connessione fallita\n");
        return 1;
    }

    /* Full-Request: Request-Line, header, riga vuota. Host non e' obbligatorio in 1.0, ma quasi tutti i server lo vogliono. */
    char req[2048];
    int n = snprintf(req, sizeof req,
                     "GET %s HTTP/1.0\r\n"
                     "Host: %s\r\n"
                     "User-Agent: client10/1.0\r\n"
                     "\r\n",
                     path, host);
    if (n < 0 || (size_t)n >= sizeof req) {
        fprintf(stderr, "richiesta troppo lunga\n");
        return 1;
    }
    if (write_all(fd, req, (size_t)n) < 0) {
        perror("write");
        return 1;
    }

    /* 1) Status-Line: "HTTP/1.0 200 OK" */
    char line[MAX_LINE];
    if (read_line(fd, line, sizeof line) < 0) {
        fprintf(stderr, "nessuna status-line\n");
        return 1;
    }
    int major, minor, status;
    if (sscanf(line, "HTTP/%d.%d %d", &major, &minor, &status) != 3) {
        fprintf(stderr, "non e' una risposta HTTP/1.x: '%s'\n", line);
        return 1;
    }
    const char *reason = strchr(line, ' ');
    reason = reason ? strchr(reason + 1, ' ') : NULL;
    printf("status: %d (%s)  versione %d.%d\n", status, reason ? reason + 1 : "", major, minor);

    /* 2) Header, fino alla riga vuota */
    struct header hs[MAX_HEADERS];
    int nh = 0;
    for (;;) {
        int len = read_line(fd, line, sizeof line);
        if (len < 0) {
            fprintf(stderr, "risposta interrotta negli header\n");
            return 1;
        }
        if (len == 0)
            break;                       /* riga vuota: finiscono gli header */
        if (nh < MAX_HEADERS && parse_header(line, &hs[nh]) == 0) {
            printf("header: %s = %s\n", hs[nh].name, hs[nh].value);
            nh++;
        }
    }

    /* 3) Corpo: in HTTP/1.0 finisce quando il server chiude la connessione (read restituisce 0). */
    FILE *out = (argc == 5) ? fopen(argv[4], "wb") : stdout;
    if (out == NULL) {
        perror("fopen");
        return 1;
    }
    char buf[8192];
    ssize_t r;
    long total = 0;
    while ((r = read(fd, buf, sizeof buf)) > 0) {
        fwrite(buf, 1, (size_t)r, out);
        total += r;
    }
    if (out != stdout)
        fclose(out);
    close(fd);

    /* 4) Se c'e' Content-Length si puo' controllare che il corpo non sia stato troncato. */
    const char *cl = header_get(hs, nh, "Content-Length");
    if (cl != NULL && atol(cl) != total)
        fprintf(stderr, "ATTENZIONE: Content-Length=%s ma ricevuti %ld byte (risposta troncata?)\n", cl, total);
    fprintf(stderr, "[corpo: %ld byte]\n", total);

    for (int i = 0; i < nh; i++) {
        free(hs[i].name);
        free(hs[i].value);
    }
    return 0;
}

Compilare e provare

Per avere un server a cui collegarsi basta quello di Python (risponde a HTTP/1.0 per default):

$ mkdir www && echo '<html><body><h1>Ciao</h1></body></html>' > www/index.html
$ (cd www && python3 -m http.server 8000 --bind 127.0.0.1) &
$ gcc -Wall -Wextra -o client10 client10.c
$ ./client10 127.0.0.1 8000 /index.html pagina.html
status: 200 (OK)  versione 1.0
header: Server = SimpleHTTP/0.6 Python/3.12.10
header: Date = Sun, 11 Oct 2026 20:00:36 GMT
header: Content-type = text/html
header: Content-Length = 40
header: Last-Modified = Sun, 11 Oct 2026 20:00:25 GMT
[corpo: 40 byte]
$ cat pagina.html
<html><body><h1>Ciao</h1></body></html>
$ ./client10 127.0.0.1 8000 /nonesiste
status: 404 (File not found)  versione 1.0
...

Con un server vero: ./client10 www.example.com 80 / example.html (l'indirizzo si può ottenere con nslookup www.example.com; getaddrinfo lo fa da solo).

Il client 0.9: gcc -Wall -Wextra -o client09 client09.c && ./client09 127.0.0.1 8080 /index.html. Attenzione: molti server non rispondono alla richiesta a una riga. Il server di Python, per esempio, dopo GET /index.html aspetta ancora le righe degli header e il client resta in attesa; funziona invece con il server dell'esercizio Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica), che accetta il formato HTTP/0.9. Con nc si vede lo stesso scambio a mano:

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

Spiegazione dei punti chiave

connect_to (stessa funzione dei client successivi).

  • memset(&hints, 0, sizeof hints) e hints.ai_family = AF_UNSPEC, ai_socktype = SOCK_STREAM: IPv4 o IPv6, solo TCP.
  • getaddrinfo restituisce una lista; il ciclo prova gli indirizzi uno dopo l'altro finché connect riesce; freeaddrinfo libera la lista. Gli errori di getaddrinfo si leggono con gai_strerror, non con errno.
  • Le slide usano AF_INET e inet_addr: funziona ma è legato a IPv4 e a un indirizzo numerico. Con getaddrinfo il programma accetta anche il nome del server.

write_all. write può scrivere meno byte del richiesto: il ciclo avanza il puntatore (buf += w) e diminuisce n finché è tutto inviato.

La richiesta HTTP/1.0 (snprintf).

GET /index.html HTTP/1.0\r\n
Host: 127.0.0.1\r\n
User-Agent: client10/1.0\r\n
\r\n

snprintf non scrive mai più di sizeof req byte e restituisce la lunghezza che avrebbe voluto: se è maggiore o uguale a sizeof req la richiesta sarebbe stata troncata. Host non esiste in HTTP/1.0 ma i server veri lo richiedono (ospitano più siti).

read_line.

  • Legge un byte per volta in c; se read restituisce <= 0 (connessione chiusa o errore) ritorna -1.
  • Al \n toglie un eventuale \r che lo precede (così funziona sia con CRLF sia con LF), termina la stringa e restituisce la lunghezza: 0 per una riga vuota, che è il segnale "fine degli header".
  • Se la riga riempie il buffer ritorna -1: si rifiuta una riga più lunga del limite invece di scrivere oltre il buffer.

La status-line. sscanf(line, "HTTP/%d.%d %d", &major, &minor, &status) deve restituire 3; altrimenti la risposta non è HTTP/1.x (potrebbe essere una risposta 0.9, senza status-line, e il client lo segnala). La reason phrase è il testo dopo il secondo spazio e serve solo per stampare.

parse_header.

  • strchr(line, ':'): il nome è prima del primo :, il valore dopo. Se non c'è il : o il nome è vuoto la riga non è valida.
  • Si saltano gli spazi e le tabulazioni dopo il : e si tolgono quelli in coda: la sintassi di HTTP/1.0 ha un solo spazio dopo i due punti, ma i server veri non sempre lo rispettano, e così il client funziona con tutti.
  • strdup copia nome e valore perché line viene riusato per l'header successivo; alla fine si fa free.

header_get confronta i nomi con strcasecmp: i nomi degli header non distinguono maiuscole e minuscole (Content-Length, content-length, Content-length sono lo stesso header; il server di Python manda Content-type).

Il corpo. Il ciclo while ((r = read(fd, buf, sizeof buf)) > 0) copia tutto con fwrite(buf, 1, r, out) (non fputs o printf("%s"): il corpo può contenere byte zero, come un'immagine) e conta i byte. Con fopen(..., "wb") il file è binario. In HTTP/1.0 il corpo finisce quando il server chiude: read restituisce 0.

Il controllo di integrità. Se è presente Content-Length e il totale ricevuto è diverso, la risposta è stata troncata (la connessione è caduta). Senza questo controllo un download interrotto sembra un successo.

Errori tipici

  • Dimenticare la riga vuota finale della richiesta (\r\n dopo l'ultimo header): il server aspetta altri header e non risponde.
  • Scrivere \n invece di \r\n nella richiesta (molti server tollerano, altri no).
  • Leggere la risposta con una sola read: i dati arrivano a pezzi.
  • Usare printf("%s") o strlen sul corpo: si perde tutto dopo il primo byte zero (immagini, PDF); usare fwrite con la lunghezza.
  • Cercare l'header con strcmp: manca il confronto senza distinguere maiuscole e minuscole.
  • Assumere due caratteri esatti dopo il : (": "): un server che omette lo spazio fa leggere un valore spostato.
  • Non controllare il codice di stato: un 404 ha comunque un corpo (una pagina di errore) che non è il documento richiesto.
  • Non controllare le lunghezze: righe lunghe senza limite o troppi header.
  • Dimenticare close(fd) e free.

Varianti per esercitarsi

Versione ripasso

  • Testo. Client HTTP/0.9 (GET /percorso\r\n, si legge fino a read == 0); client HTTP/1.0 che invia Request-Line + Host + User-Agent, separa status-line, header e corpo, salva il corpo su file, segnala una risposta troncata (Content-Length diverso dai byte ricevuti).
  • Struttura. getaddrinfo (IPv4/IPv6) con ciclo socket+connect; write_all (ciclo: write può scrivere meno); richiesta con snprintf (controllare n >= sizeof req); lettura della risposta; free/close.
  • Richiesta HTTP/1.0. GET /index.html HTTP/1.0\r\nHost: <host>\r\nUser-Agent: client10/1.0\r\n\r\n. La riga vuota chiude gli header. Host non è in 1.0 ma i server lo vogliono.
  • read_line (un byte alla volta: nessuna lettura oltre la riga vuota):
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;                                   /* riga troppo lunga */

Ritorna 0 per la riga vuota = fine degli header.

  • Status-line. sscanf(line, "HTTP/%d.%d %d", &major, &minor, &status) == 3, altrimenti non è HTTP/1.x (forse 0.9). La frase dopo il secondo spazio serve solo da testo.
  • Header. Primo : -> nome / valore; saltare gli spazi dopo : e togliere quelli in coda (RFC 1945 prevede un solo spazio, i server veri no); strdup di nome e valore; header_get con strcasecmp.
  • Corpo. while ((r = read(fd, buf, sizeof buf)) > 0) fwrite(buf, 1, r, out): fwrite con la lunghezza (il corpo può avere byte zero), file aperto "wb"; fine = chiusura del server. Controllo: Content-Length presente e diverso dal totale = risposta troncata.
  • Prove. python3 -m http.server 8000 come server; ./client10 127.0.0.1 8000 /index.html pagina.html -> status: 200 (OK) versione 1.0, header, [corpo: 40 byte]. Il server di Python con una richiesta 0.9 aspetta altri header: usare il server dell'esercizio o nc.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
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;
    int rc = getaddrinfo(host, port, &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
        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 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;
}

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;
}

static const char *header_get(const struct header *hs, int n, const char *name)
{
    for (int i = 0; i < n; i++)
        if (strcasecmp(hs[i].name, name) == 0)
            return hs[i].value;
    return NULL;
}
  • Errori tipici: manca \r\n\r\n finale; \n al posto di \r\n; una sola read; printf("%s")/strlen sul corpo; strcmp sui nomi degli header; ": " supposto fisso; codice di stato ignorato; righe e header senza limiti; mancano close e free.

Esercizi su questo argomento

Teoria collegata