Salta al contenuto
Note per Studenti Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica)

Esercizio - Server HTTP concorrente con fork e connessioni persistenti (sul modello della prova pratica)

In questa pagina 6

Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD).

Estendere il server HTTP dell'esercizio Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica) in modo che:

  1. serva più client contemporaneamente, con un processo figlio per ogni connessione (fork);
  2. i processi terminati non restino zombie (gestore di SIGCHLD);
  3. supporti le connessioni persistenti di HTTP/1.1: più richieste sulla stessa connessione, con Content-Length in ogni risposta, Connection: close quando richiesto, e persistenza di default solo per HTTP/1.1 (per 1.0 solo con Connection: keep-alive);
  4. chiuda una connessione inattiva dopo 5 secondi e comunque dopo 100 richieste;
  5. risponda 400 a una richiesta HTTP/1.1 senza Host e chiuda dopo ogni richiesta malformata;
  6. scarti correttamente il corpo di una richiesta che lo contiene (altrimenti i suoi byte sporcano la richiesta successiva).

Teoria: 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 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 →, 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

Il server ha due livelli.

Il padre fa solo accept. Per ogni connessione accettata crea un figlio con fork:

 padre:  accept() -> cfd --fork--> padre: close(cfd); torna ad accept()
                              \--> figlio: close(lfd); serve_connection(cfd); close(cfd); _exit(0)

Il figlio chiude lfd (non accetta nuove connessioni) e serve tutte le richieste di quella connessione; il padre chiude cfd (altrimenti la connessione non si chiuderebbe mai e i descrittori finirebbero). Un gestore di SIGCHLD raccoglie i figli terminati con waitpid(-1, NULL, WNOHANG) in un ciclo: niente zombie (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 →).

Il figlio esegue serve_connection, un ciclo "leggi una richiesta, rispondi" (for count = 1..MAX_REQUESTS):

  1. read_request: legge la request line e gli header con il lettore a buffer (rb_line). Il buffer è per connessione e deve vivere per tutta la durata: i byte della richiesta seguente possono già trovarsi dentro.
  2. Decide se la connessione resta aperta (keep): HTTP/1.1 salvo Connection: close; HTTP/1.0 solo con Connection: keep-alive; mai oltre MAX_REQUESTS.
  3. Se la richiesta ha un corpo (Content-Length), lo scarta (rb_skip), perché GET e HEAD non lo usano ma i suoi byte restano nel socket. Un corpo chunked in ingresso non è gestito: 501 e chiusura.
  4. serve: invia la risposta con Content-Length (obbligatorio: è l'unico modo con cui il client sa dove finisce) e l'header Connection: keep-alive o close.
  5. Se keep vale 0, esce dal ciclo e il figlio chiude.

Il timeout. setsockopt(SO_RCVTIMEO, 5 s): se il client non invia nulla per 5 secondi la read fallisce con EAGAIN, rb_fill restituisce -1, la richiesta non arriva, e il figlio chiude. Senza questo, ogni connessione inattiva terrebbe vivo un processo per sempre: con migliaia di connessioni aperte e silenziose il server finirebbe le risorse (attacco slowloris).

Codice

c
/* http_server_fork.c - server HTTP/1.1 concorrente: un processo figlio per connessione, connessioni persistenti.
 *
 * Compilare:  gcc -Wall -Wextra -o http_server_fork http_server_fork.c
 * Avviare:    ./http_server_fork 8080 ./www
 * Provare:    curl -v http://127.0.0.1:8080/index.html http://127.0.0.1:8080/a.txt   (UNA connessione, due richieste)
 *             nc 127.0.0.1 8080           (rimanere fermi 5 secondi: il server chiude la connessione inattiva)
 *             for i in 1 2 3 4 5; do curl -s -o /dev/null http://127.0.0.1:8080/ & done; wait   (5 client insieme)
 *             ps aux | grep http_server_fork       (un figlio per connessione aperta; nessuno zombie)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <time.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAX_LINE 2048
#define MAX_REQUESTS 100                           /* richieste servite al massimo su una connessione */
#define IDLE_TIMEOUT 5                             /* secondi di silenzio dopo i quali si chiude */

static const char *root_dir;

/* ---------- utilita' ---------- */
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;
}

struct rbuf {
    int fd;
    char buf[4096];
    size_t pos, len;
};

static int rb_fill(struct rbuf *r)
{
    while (r->pos == r->len) {
        ssize_t k = read(r->fd, r->buf, sizeof r->buf);
        if (k < 0 && errno == EINTR)
            continue;
        if (k <= 0)
            return -1;                             /* EOF, errore o timeout (EAGAIN con SO_RCVTIMEO) */
        r->pos = 0;
        r->len = (size_t)k;
    }
    return 0;
}

/* Una riga con CRLF/LF tolti. Torna la lunghezza, -1 su EOF/timeout, -2 se la riga e' troppo lunga. */
static int rb_line(struct rbuf *r, char *line, size_t max)
{
    size_t n = 0;
    for (;;) {
        if (rb_fill(r) < 0)
            return -1;
        char c = r->buf[r->pos++];
        if (c == '\n') {
            if (n > 0 && line[n - 1] == '\r')
                n--;
            line[n] = '\0';
            return (int)n;
        }
        if (n + 1 >= max)
            return -2;
        line[n++] = c;
    }
}

static int rb_skip(struct rbuf *r, long n)         /* scarta n byte (corpo che non ci interessa) */
{
    while (n > 0) {
        if (rb_fill(r) < 0)
            return -1;
        size_t avail = r->len - r->pos;
        size_t take = ((long)avail < n) ? avail : (size_t)n;
        r->pos += take;
        n -= (long)take;
    }
    return 0;
}

static const char *mime_type(const char *path)
{
    static const struct { const char *ext, *type; } tab[] = {
        {".html", "text/html; charset=utf-8"}, {".htm", "text/html; charset=utf-8"}, {".css", "text/css"},
        {".js", "text/javascript"}, {".json", "application/json"}, {".txt", "text/plain; charset=utf-8"},
        {".png", "image/png"}, {".jpg", "image/jpeg"}, {".gif", "image/gif"}, {".svg", "image/svg+xml"},
        {".pdf", "application/pdf"},
    };
    const char *dot = strrchr(path, '.');
    if (dot != NULL)
        for (size_t i = 0; i < sizeof tab / sizeof tab[0]; i++)
            if (strcasecmp(dot, tab[i].ext) == 0)
                return tab[i].type;
    return "application/octet-stream";
}

static void http_date(time_t t, char *out, size_t size)
{
    struct tm *g = gmtime(&t);
    strftime(out, size, "%a, %d %b %Y %H:%M:%S GMT", g);
}

static const char *reason(int code)
{
    switch (code) {
    case 200: return "OK";
    case 301: return "Moved Permanently";
    case 400: return "Bad Request";
    case 403: return "Forbidden";
    case 404: return "Not Found";
    case 405: return "Method Not Allowed";
    case 414: return "URI Too Long";
    case 431: return "Request Header Fields Too Large";
    case 501: return "Not Implemented";
    case 505: return "HTTP Version Not Supported";
    default:  return "Error";
    }
}

static int hexval(int c)
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

static int url_decode(const char *src, char *dst, size_t dsize)
{
    size_t n = 0;
    for (; *src; src++) {
        int c = (unsigned char)*src;
        if (c == '%') {
            int h = hexval((unsigned char)src[1]);
            int l = (h >= 0) ? hexval((unsigned char)src[2]) : -1;
            if (h < 0 || l < 0)
                return -1;
            c = h * 16 + l;
            src += 2;
            if (c == 0)
                return -1;
        }
        if (n + 1 >= dsize)
            return -1;
        dst[n++] = (char)c;
    }
    dst[n] = '\0';
    return 0;
}

static int path_is_safe(const char *p)
{
    for (const char *s = p; (s = strstr(s, "..")) != NULL; s += 2) {
        int start = (s == p) || s[-1] == '/';
        int end = (s[2] == '\0') || s[2] == '/';
        if (start && end)
            return 0;
    }
    return 1;
}

/* ---------- risposte: keep = 1 se la connessione resta aperta ---------- */
static void send_head(int fd, int code, const char *ctype, long long len, const char *extra, int keep)
{
    char head[1024], date[64];
    http_date(time(NULL), date, sizeof date);
    int n = snprintf(head, sizeof head,
                     "HTTP/1.1 %d %s\r\n"
                     "Date: %s\r\n"
                     "Server: mini-http-fork/1.0\r\n"
                     "Content-Type: %s\r\n"
                     "Content-Length: %lld\r\n"    /* obbligatorio con la connessione persistente: dice dove finisce il corpo */
                     "Connection: %s\r\n"
                     "%s"
                     "\r\n",
                     code, reason(code), date, ctype, len, keep ? "keep-alive" : "close", extra);
    write_all(fd, head, (size_t)n);
}

static void send_error(int fd, int code, const char *extra, int head_only, int keep)
{
    char body[256];
    int blen = snprintf(body, sizeof body, "<html><body><h1>%d %s</h1></body></html>\n", code, reason(code));
    send_head(fd, code, "text/html; charset=utf-8", blen, extra, keep);
    if (!head_only)
        write_all(fd, body, (size_t)blen);
}

/* ---------- una richiesta ---------- */
struct req {
    char method[16], target[MAX_LINE];
    int minor;                                     /* 0 o 1: HTTP/1.minor */
    int has_host, conn_close, conn_keepalive, chunked;
    long clen;
};

/* Legge la richiesta. Torna 0 se ok, -1 se la connessione e' finita (nessuna richiesta), altrimenti il codice d'errore. */
static int read_request(struct rbuf *in, struct req *rq)
{
    char line[MAX_LINE];
    memset(rq, 0, sizeof *rq);
    int len = rb_line(in, line, sizeof line);
    while (len == 0)
        len = rb_line(in, line, sizeof line);      /* RFC 9112 2.2: righe vuote prima della richiesta si ignorano */
    if (len == -1)
        return -1;
    if (len == -2)
        return 414;
    char ver[16], extra[16];
    if (sscanf(line, "%15s %2047s %15s %15s", rq->method, rq->target, ver, extra) != 3)
        return 400;                                /* servono esattamente tre campi */
    if (strncmp(ver, "HTTP/", 5) != 0)
        return 400;
    if (strncmp(ver, "HTTP/1.", 7) != 0)
        return 505;                                /* HTTP/2.0, HTTP/0.9...: versione non supportata */
    rq->minor = atoi(ver + 7) >= 1 ? 1 : 0;        /* 1.x con x >= 1 si tratta come 1.1 */
    int nh = 0;
    for (;;) {
        len = rb_line(in, line, sizeof line);
        if (len == -1)
            return -1;
        if (len == -2 || ++nh > 100)
            return 431;
        if (len == 0)
            break;
        char *colon = strchr(line, ':');
        if (colon == NULL)
            return 400;
        *colon = '\0';
        const char *v = colon + 1;
        while (*v == ' ' || *v == '\t')
            v++;
        if (strcasecmp(line, "Host") == 0)
            rq->has_host = 1;
        else if (strcasecmp(line, "Connection") == 0) {
            if (strstr(v, "close") || strstr(v, "Close"))
                rq->conn_close = 1;
            if (strstr(v, "keep-alive") || strstr(v, "Keep-Alive"))
                rq->conn_keepalive = 1;
        } else if (strcasecmp(line, "Content-Length") == 0)
            rq->clen = atol(v);
        else if (strcasecmp(line, "Transfer-Encoding") == 0)
            rq->chunked = 1;
    }
    if (rq->minor >= 1 && !rq->has_host)
        return 400;                                /* Host obbligatorio in HTTP/1.1 */
    return 0;
}

/* Serve GET/HEAD. Torna lo status inviato. */
static int serve(int fd, const struct req *rq, int keep)
{
    int head_only = strcmp(rq->method, "HEAD") == 0;
    if (strcmp(rq->method, "GET") != 0 && !head_only) {
        if (strcmp(rq->method, "POST") == 0 || strcmp(rq->method, "PUT") == 0 || strcmp(rq->method, "DELETE") == 0) {
            send_error(fd, 405, "Allow: GET, HEAD\r\n", 0, keep);
            return 405;
        }
        send_error(fd, 501, "", 0, keep);
        return 501;
    }
    char target[MAX_LINE], path[MAX_LINE];
    snprintf(target, sizeof target, "%s", rq->target);
    char *q = strchr(target, '?');
    if (q)
        *q = '\0';
    if (target[0] != '/' || url_decode(target, path, sizeof path) < 0) {
        send_error(fd, 400, "", head_only, keep);
        return 400;
    }
    if (!path_is_safe(path)) {
        send_error(fd, 403, "", head_only, keep);
        return 403;
    }
    char full[MAX_LINE * 2];
    size_t plen = strlen(path);
    snprintf(full, sizeof full, "%s%s%s", root_dir, path, path[plen - 1] == '/' ? "index.html" : "");
    struct stat st;
    if (stat(full, &st) == 0 && S_ISDIR(st.st_mode)) {
        char extra[MAX_LINE + 32];
        snprintf(extra, sizeof extra, "Location: %s/\r\n", path);
        send_error(fd, 301, extra, head_only, keep);
        return 301;
    }
    int ffd = open(full, O_RDONLY);
    if (ffd < 0 || fstat(ffd, &st) < 0 || !S_ISREG(st.st_mode)) {
        int code = (ffd < 0 && errno == EACCES) ? 403 : 404;
        send_error(fd, code, "", head_only, keep);
        if (ffd >= 0)
            close(ffd);
        return code;
    }
    send_head(fd, 200, mime_type(full), (long long)st.st_size, "", keep);
    if (!head_only) {
        char buf[8192];
        ssize_t r;
        while ((r = read(ffd, buf, sizeof buf)) > 0)
            if (write_all(fd, buf, (size_t)r) < 0)
                break;
    }
    close(ffd);
    return 200;
}

/* Il ciclo di una connessione persistente: si esegue nel processo figlio. */
static void serve_connection(int fd, const char *peer)
{
    struct timeval tv = {.tv_sec = IDLE_TIMEOUT, .tv_usec = 0};
    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);   /* una read ferma per 5 s fallisce: connessione inattiva */
    struct rbuf in = {.fd = fd};
    struct req rq;
    for (int count = 1; count <= MAX_REQUESTS; count++) {
        int rc = read_request(&in, &rq);
        if (rc < 0)
            break;                                 /* il client ha chiuso o e' rimasto zitto troppo a lungo */
        if (rc > 0) {                              /* richiesta malformata: si risponde e si chiude */
            send_error(fd, rc, "", 0, 0);
            fprintf(stderr, "[%d] %s -> %d (richiesta non valida)\n", (int)getpid(), peer, rc);
            break;
        }
        /* persistente? 1.1: si', salvo "Connection: close"; 1.0: solo con "keep-alive". */
        int keep = (rq.minor >= 1) ? !rq.conn_close : rq.conn_keepalive;
        if (count == MAX_REQUESTS)
            keep = 0;
        if (rq.chunked) {                          /* corpo chunked in ingresso: non lo gestiamo */
            send_error(fd, 501, "", 0, 0);
            break;
        }
        if (rq.clen > 0 && rb_skip(&in, rq.clen) < 0)   /* corpo da scartare, altrimenti sporcherebbe la richiesta dopo */
            break;
        int status = serve(fd, &rq, keep);
        fprintf(stderr, "[%d] %s \"%s %s\" -> %d (richiesta %d, %s)\n", (int)getpid(), peer, rq.method, rq.target, status,
                count, keep ? "keep-alive" : "close");
        if (!keep)
            break;
    }
}

/* Quando un figlio termina il kernel manda SIGCHLD; senza wait() il figlio resterebbe "zombie". */
static void on_sigchld(int sig)
{
    (void)sig;
    int saved = errno;                             /* il gestore non deve alterare errno del codice interrotto */
    while (waitpid(-1, NULL, WNOHANG) > 0)         /* piu' figli possono finire insieme: si raccolgono tutti */
        ;
    errno = saved;
}

int main(int argc, char **argv)
{
    if (argc != 3) {
        fprintf(stderr, "uso: %s porta radice\n", argv[0]);
        return 1;
    }
    root_dir = argv[2];
    signal(SIGPIPE, SIG_IGN);
    struct sigaction sa;
    memset(&sa, 0, sizeof sa);
    sa.sa_handler = on_sigchld;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;       /* SA_RESTART: accept riparte da solo dopo il segnale */
    sigaction(SIGCHLD, &sa, NULL);

    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, 64) < 0) {
        perror("bind/listen");
        return 1;
    }
    fprintf(stderr, "in ascolto sulla porta %s, radice %s\n", argv[1], root_dir);

    for (;;) {
        struct sockaddr_in cli;
        socklen_t clen = sizeof cli;
        int cfd = accept(lfd, (struct sockaddr *)&cli, &clen);
        if (cfd < 0) {
            if (errno == EINTR)
                continue;
            perror("accept");
            continue;                              /* es. troppi file aperti (EMFILE): non si butta giu' il server */
        }
        char peer[INET_ADDRSTRLEN + 8];
        inet_ntop(AF_INET, &cli.sin_addr, peer, sizeof peer);
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork");
            close(cfd);
        } else if (pid == 0) {
            /* FIGLIO: non accetta connessioni, quindi chiude il socket in ascolto (ne ha una copia) */
            close(lfd);
            serve_connection(cfd, peer);
            close(cfd);
            _exit(0);
        } else {
            /* PADRE: la connessione e' del figlio; si chiude la propria copia, altrimenti il socket
             * resterebbe aperto per sempre (il FIN parte solo quando si chiudono TUTTE le copie). */
            close(cfd);
        }
    }
}

Compilare e provare

$ gcc -Wall -Wextra -o http_server_fork http_server_fork.c
$ ./http_server_fork 8080 ./www &
in ascolto sulla porta 8080, radice ./www

Connessione persistente: curl con due URL usa una sola connessione.

$ curl -v http://127.0.0.1:8080/index.html http://127.0.0.1:8080/a.txt 2>&1 | grep -E "Connected|Re-using|Connection:|HTTP/1.1 200"
* Connected to 127.0.0.1 (127.0.0.1) port 8080
< HTTP/1.1 200 OK
< Connection: keep-alive
* Re-using existing connection with host 127.0.0.1
< HTTP/1.1 200 OK
< Connection: keep-alive

Il log del server mostra le due richieste sulla stessa connessione, con lo stesso PID del figlio:

[4121] 127.0.0.1 "GET /index.html" -> 200 (richiesta 1, keep-alive)
[4121] 127.0.0.1 "GET /a.txt" -> 200 (richiesta 2, keep-alive)

Altri casi, con nc e printf:

$ printf 'GET /index.html HTTP/1.0\r\n\r\n' | nc 127.0.0.1 8080      # 1.0 senza keep-alive: Connection: close
$ printf 'GET / HTTP/1.1\r\n\r\n' | nc 127.0.0.1 8080                # senza Host: 400 e chiusura
$ printf 'POST /x HTTP/1.1\r\nHost: x\r\nContent-Length: 5\r\n\r\nhelloGET /index.html HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8080
HTTP/1.1 405 Method Not Allowed ...                                  # la POST (corpo "hello" scartato)
HTTP/1.1 200 OK ...                                                  # e la GET successiva, intatta

Il terzo comando verifica rb_skip: se il corpo hello non venisse scartato, la richiesta successiva comincerebbe con helloGET /index.html ... e sarebbe un 400.

Timeout: nc 127.0.0.1 8080 e restare fermi: dopo 5 secondi il server chiude la connessione. Concorrenza: cinque client insieme (for i in 1 2 3 4 5; do curl -s -o /dev/null http://127.0.0.1:8080/ & done; wait) e, in un altro terminale, ps -o pid,ppid,stat,cmd --ppid <pid del padre> mostra un figlio per connessione aperta e nessuno Z (zombie) dopo la chiusura. Con il server iterativo un nc rimasto aperto bloccherebbe tutti gli altri; qui no.

Spiegazione dei punti chiave

Padre e figlio.

  • sigaction(SIGCHLD, ...) con SA_RESTART | SA_NOCLDSTOP: SA_RESTART fa ripartire accept interrotta dal segnale; SA_NOCLDSTOP evita il segnale quando un figlio si ferma soltanto.
  • on_sigchld salva e ripristina errno e chiama waitpid(-1, NULL, WNOHANG) in un ciclo: più figli possono terminare insieme e i segnali ravvicinati si fondono.
  • accept che fallisce con EINTR → continue; con un altro errore (per esempio EMFILE) il server non muore: stampa e continua. Se fork fallisce (pid < 0) chiude cfd e scarta il client.
  • Nel figlio: close(lfd), serve_connection, close(cfd), _exit(0) (se il figlio tornasse nel ciclo farebbe accept anche lui). Nel padre: close(cfd).

rbuf e rb_line. Come nell'esercizio precedente, ma rb_line restituisce -1 per EOF o timeout e -2 per riga troppo lunga: il chiamante distingue "il client se n'è andato" (si chiude senza rispondere) da "riga troppo lunga" (414 o 431).

read_request.

  • Le righe vuote prima della request line si ignorano (RFC 9112 par. 2.2: è tollerato un CRLF di troppo dopo il corpo di una richiesta precedente).
  • sscanf(line, "%15s %2047s %15s %15s", ...) deve restituire esattamente 3: un quarto campo (extra) indica troppi spazi → 400. La versione deve iniziare per HTTP/; se non è HTTP/1. → 505. rq->minor vale 1 per HTTP/1.1 (e superiori), 0 per HTTP/1.0.
  • Per ogni header si registrano Host, Connection (cercando close e keep-alive), Content-Length e Transfer-Encoding; gli altri si ignorano. Un header senza : → 400.
  • Host: una richiesta HTTP/1.1 senza Host → 400.

La decisione keep.

c
int keep = (rq.minor >= 1) ? !rq.conn_close : rq.conn_keepalive;
if (count == MAX_REQUESTS) keep = 0;

HTTP/1.1: persistente di default; HTTP/1.0: solo se il client lo chiede. La risposta riporta la scelta nell'header Connection: un client HTTP/1.0 che ha chiesto keep-alive lo deve leggere nella risposta per sapere che la connessione resta aperta.

send_head. Tutte le risposte, compresi gli errori, hanno Content-Length (anche per HEAD: il valore è quello che avrebbe il corpo di GET, ma il corpo non si invia) e Connection: keep-alive|close. Senza Content-Length il client non saprebbe dove finisce la risposta e la connessione persistente non funzionerebbe.

Il corpo in ingresso. rb_skip(&in, rq.clen) scarta Content-Length byte (anche se il corpo è già in parte nel buffer). Ignorarlo è un errore grave nelle connessioni persistenti: i byte del corpo verrebbero letti come l'inizio della richiesta successiva (desincronizzazione, e in un intermediario base per il request smuggling). Per Transfer-Encoding si risponde 501 e si chiude (non si può sapere dove finisce il corpo senza decodificarlo).

Errori della richiesta. Per una richiesta malformata (rc > 0): si invia l'errore con keep = 0 e si chiude la connessione: dopo un errore di sintassi non si può più fidarsi della posizione dei messaggi successivi. Per errori di contenuto (404, 405, 403) la richiesta era ben formata e la connessione resta aperta.

serve. È la logica del server precedente (metodi, percorso, stat, open) con keep come argomento. HEAD e GET condividono il codice.

Il timeout vale per ogni read: SO_RCVTIMEO si imposta una volta sul socket, e un client che manda un byte al secondo senza finire mai la richiesta non scatta il timeout. Un server di produzione imporrebbe anche un tempo massimo per ricevere un'intera richiesta.

Errori tipici

  • Il padre non chiude cfd o il figlio non chiude lfd (descrittori che si accumulano, connessioni che non si chiudono).
  • Nessun gestore di SIGCHLD, o waitpid senza ciclo: zombie.
  • Il figlio non termina (manca _exit) e riprende ad accettare connessioni.
  • Mancanza di Content-Length nelle risposte: il client non sa quando la risposta finisce e resta bloccato.
  • Dimenticare rb_skip per un corpo non letto: la richiesta successiva è corrotta.
  • Creare un nuovo rbuf per ogni richiesta: i byte già letti della richiesta successiva si perdono.
  • Non impostare un timeout: un client inattivo tiene occupato un processo per sempre.
  • Supporre che HTTP/1.0 sia persistente di default, o che HTTP/1.1 non lo sia.
  • Continuare a servire dopo una richiesta malformata o dopo un Connection: close.
  • Chiudere il server per un errore di accept o fork invece di continuare.

Varianti per esercitarsi

Versione ripasso

  • Testo. Server HTTP/1.1 concorrente (fork per connessione), niente zombie (SIGCHLD), connessioni persistenti (Content-Length sempre; 1.1 persistente di default, 1.0 solo con keep-alive), timeout di 5 s, massimo 100 richieste, 400 senza Host, corpo in ingresso scartato.
  • Padre. accept in ciclo (EINTR -> continue; altri errori: continua); fork: pid < 0 -> close(cfd); padre close(cfd). Figlio: close(lfd), serve_connection, close(cfd), _exit(0).
  • SIGCHLD. sigaction con SA_RESTART | SA_NOCLDSTOP; gestore: salva errno, while (waitpid(-1, NULL, WNOHANG) > 0);, ripristina errno.
  • serve_connection. setsockopt(SO_RCVTIMEO, 5 s); struct rbuf unico per la connessione; per count = 1..MAX_REQUESTS:
c
int rc = read_request(&in, &rq);
if (rc < 0) break;                                  /* EOF o timeout */
if (rc > 0) { send_error(fd, rc, "", 0, 0); break; }/* malformata: rispondi e chiudi */
int keep = (rq.minor >= 1) ? !rq.conn_close : rq.conn_keepalive;
if (count == MAX_REQUESTS) keep = 0;
if (rq.chunked) { send_error(fd, 501, "", 0, 0); break; }
if (rq.clen > 0 && rb_skip(&in, rq.clen) < 0) break;   /* scarta il corpo */
serve(fd, &rq, keep);  if (!keep) break;
  • read_request. Righe vuote iniziali ignorate; sscanf("%15s %2047s %15s %15s") == 3 (altrimenti 400); versione non HTTP/1. -> 505; Host obbligatorio in 1.1 (400); rb_line: -1 EOF/timeout, -2 riga lunga (414/431).
  • Risposte. Content-Length sempre (anche per HEAD e per gli errori) e Connection: keep-alive|close: un client 1.0 con keep-alive lo legge nella risposta.
  • Errori. Richiesta malformata: si chiude (non ci si può più fidare della sincronizzazione); 404/405/403: la connessione resta aperta.
  • Prove. curl -v URL1 URL2 -> Re-using existing connection; log con lo stesso PID e richiesta 1, richiesta 2; printf 'POST ... Content-Length: 5 ... helloGET ...' | nc -> 405 poi 200 (il corpo hello scartato); nc fermo -> chiusura dopo 5 s; cinque curl & insieme; ps --ppid senza Z.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static int read_request(struct rbuf *in, struct req *rq)
{
    char line[MAX_LINE];
    memset(rq, 0, sizeof *rq);
    int len = rb_line(in, line, sizeof line);
    while (len == 0)
        len = rb_line(in, line, sizeof line);
    if (len == -1)
        return -1;
    if (len == -2)
        return 414;
    char ver[16], extra[16];
    if (sscanf(line, "%15s %2047s %15s %15s", rq->method, rq->target, ver, extra) != 3)
        return 400;
    if (strncmp(ver, "HTTP/", 5) != 0)
        return 400;
    if (strncmp(ver, "HTTP/1.", 7) != 0)
        return 505;
    rq->minor = atoi(ver + 7) >= 1 ? 1 : 0;
    int nh = 0;
    for (;;) {
        len = rb_line(in, line, sizeof line);
        if (len == -1)
            return -1;
        if (len == -2 || ++nh > 100)
            return 431;
        if (len == 0)
            break;
        char *colon = strchr(line, ':');
        if (colon == NULL)
            return 400;
        *colon = '\0';
        const char *v = colon + 1;
        while (*v == ' ' || *v == '\t')
            v++;
        if (strcasecmp(line, "Host") == 0)
            rq->has_host = 1;
        else if (strcasecmp(line, "Connection") == 0) {
            if (strstr(v, "close") || strstr(v, "Close"))
                rq->conn_close = 1;
            if (strstr(v, "keep-alive") || strstr(v, "Keep-Alive"))
                rq->conn_keepalive = 1;
        } else if (strcasecmp(line, "Content-Length") == 0)
            rq->clen = atol(v);
        else if (strcasecmp(line, "Transfer-Encoding") == 0)
            rq->chunked = 1;
    }
    if (rq->minor >= 1 && !rq->has_host)
        return 400;
    return 0;
}

static void serve_connection(int fd, const char *peer)
{
    struct timeval tv = {.tv_sec = IDLE_TIMEOUT, .tv_usec = 0};
    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
    struct rbuf in = {.fd = fd};
    struct req rq;
    for (int count = 1; count <= MAX_REQUESTS; count++) {
        int rc = read_request(&in, &rq);
        if (rc < 0)
            break;
        if (rc > 0) {
            send_error(fd, rc, "", 0, 0);
            fprintf(stderr, "[%d] %s -> %d (richiesta non valida)\n", (int)getpid(), peer, rc);
            break;
        }
        int keep = (rq.minor >= 1) ? !rq.conn_close : rq.conn_keepalive;
        if (count == MAX_REQUESTS)
            keep = 0;
        if (rq.chunked) {
            send_error(fd, 501, "", 0, 0);
            break;
        }
        if (rq.clen > 0 && rb_skip(&in, rq.clen) < 0)
            break;
        int status = serve(fd, &rq, keep);
        fprintf(stderr, "[%d] %s \"%s %s\" -> %d (richiesta %d, %s)\n", (int)getpid(), peer, rq.method, rq.target, status,
                count, keep ? "keep-alive" : "close");
        if (!keep)
            break;
    }
}

static void on_sigchld(int sig)
{
    (void)sig;
    int saved = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
    errno = saved;
}
c
static void send_head(int fd, int code, const char *ctype, long long len, const char *extra, int keep)
{
    char head[1024], date[64];
    http_date(time(NULL), date, sizeof date);
    int n = snprintf(head, sizeof head,
                     "HTTP/1.1 %d %s\r\n"
                     "Date: %s\r\n"
                     "Server: mini-http-fork/1.0\r\n"
                     "Content-Type: %s\r\n"
                     "Content-Length: %lld\r\n"
                     "Connection: %s\r\n"
                     "%s"
                     "\r\n",
                     code, reason(code), date, ctype, len, keep ? "keep-alive" : "close", extra);
    write_all(fd, head, (size_t)n);
}
  • Errori tipici: cfd/lfd non chiusi; niente SIGCHLD o waitpid senza ciclo; niente _exit nel figlio; risposte senza Content-Length; rb_skip dimenticato; nuovo rbuf a ogni richiesta; nessun timeout; persistenza confusa fra 1.0 e 1.1; si continua dopo un errore di sintassi.

Esercizi su questo argomento

Teoria collegata