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:
- serva più client contemporaneamente, con un processo figlio per ogni connessione (
fork); - i processi terminati non restino zombie (gestore di
SIGCHLD); - supporti le connessioni persistenti di HTTP/1.1: più richieste sulla stessa connessione, con
Content-Lengthin ogni risposta,Connection: closequando richiesto, e persistenza di default solo per HTTP/1.1 (per 1.0 solo conConnection: keep-alive); - chiuda una connessione inattiva dopo 5 secondi e comunque dopo 100 richieste;
- risponda
400a una richiesta HTTP/1.1 senzaHoste chiuda dopo ogni richiesta malformata; - 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):
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.- Decide se la connessione resta aperta (
keep): HTTP/1.1 salvoConnection: close; HTTP/1.0 solo conConnection: keep-alive; mai oltreMAX_REQUESTS. - Se la richiesta ha un corpo (
Content-Length), lo scarta (rb_skip), perchéGETeHEADnon lo usano ma i suoi byte restano nel socket. Un corpochunkedin ingresso non è gestito:501e chiusura. serve: invia la risposta conContent-Length(obbligatorio: è l'unico modo con cui il client sa dove finisce) e l'headerConnection: keep-aliveoclose.- Se
keepvale 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
/* 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 ./wwwConnessione 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-aliveIl 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, intattaIl 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, ...)conSA_RESTART | SA_NOCLDSTOP:SA_RESTARTfa ripartireacceptinterrotta dal segnale;SA_NOCLDSTOPevita il segnale quando un figlio si ferma soltanto.on_sigchldsalva e ripristinaerrnoe chiamawaitpid(-1, NULL, WNOHANG)in un ciclo: più figli possono terminare insieme e i segnali ravvicinati si fondono.acceptche fallisce conEINTR→continue; con un altro errore (per esempioEMFILE) il server non muore: stampa e continua. Seforkfallisce (pid < 0) chiudecfde scarta il client.- Nel figlio:
close(lfd),serve_connection,close(cfd),_exit(0)(se il figlio tornasse nel ciclo farebbeacceptanche 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 perHTTP/; se non èHTTP/1.→505.rq->minorvale 1 perHTTP/1.1(e superiori), 0 perHTTP/1.0.- Per ogni header si registrano
Host,Connection(cercandocloseekeep-alive),Content-LengtheTransfer-Encoding; gli altri si ignorano. Un header senza:→400. Host: una richiesta HTTP/1.1 senzaHost→400.
La decisione keep.
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
cfdo il figlio non chiudelfd(descrittori che si accumulano, connessioni che non si chiudono). - Nessun gestore di
SIGCHLD, owaitpidsenza ciclo: zombie. - Il figlio non termina (manca
_exit) e riprende ad accettare connessioni. - Mancanza di
Content-Lengthnelle risposte: il client non sa quando la risposta finisce e resta bloccato. - Dimenticare
rb_skipper un corpo non letto: la richiesta successiva è corrotta. - Creare un nuovo
rbufper 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
acceptoforkinvece di continuare.
Varianti per esercitarsi
- Limitare il numero di figli attivi (contatore aggiornato nel gestore di
SIGCHLD) e rispondere503oltre il limite. - Pre-fork: creare
Nprocessi che chiamano tuttiacceptsullo stesso socket di ascolto. - Passare ai thread (
pthread_create) o a un ciclo di eventi conpoll. - Supportare i corpi
chunkedin ingresso con il decoder di Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1). - Aggiungere
ETage304(Esercizio - Server con autenticazione Basic, ETag e GET condizionale (sul modello della prova pratica)).
Versione ripasso
- Testo. Server HTTP/1.1 concorrente (
forkper connessione), niente zombie (SIGCHLD), connessioni persistenti (Content-Lengthsempre; 1.1 persistente di default, 1.0 solo conkeep-alive), timeout di 5 s, massimo 100 richieste,400senzaHost, corpo in ingresso scartato. - Padre.
acceptin ciclo (EINTR->continue; altri errori: continua);fork:pid < 0->close(cfd); padreclose(cfd). Figlio:close(lfd),serve_connection,close(cfd),_exit(0). - SIGCHLD.
sigactionconSA_RESTART | SA_NOCLDSTOP; gestore: salvaerrno,while (waitpid(-1, NULL, WNOHANG) > 0);, ripristinaerrno. serve_connection.setsockopt(SO_RCVTIMEO, 5 s);struct rbufunico per la connessione; percount = 1..MAX_REQUESTS:
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(altrimenti400); versione nonHTTP/1.->505;Hostobbligatorio in 1.1 (400);rb_line:-1EOF/timeout,-2riga lunga (414/431).- Risposte.
Content-Lengthsempre (anche perHEADe per gli errori) eConnection: keep-alive|close: un client 1.0 conkeep-alivelo 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 erichiesta 1,richiesta 2;printf 'POST ... Content-Length: 5 ... helloGET ...' | nc->405poi200(il corpohelloscartato);ncfermo -> chiusura dopo 5 s; cinquecurl &insieme;ps --ppidsenzaZ. - Codice essenziale (le funzioni centrali, senza commenti):
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;
}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/lfdnon chiusi; nienteSIGCHLDowaitpidsenza ciclo; niente_exitnel figlio; risposte senzaContent-Length;rb_skipdimenticato; nuovorbufa ogni richiesta; nessun timeout; persistenza confusa fra 1.0 e 1.1; si continua dopo un errore di sintassi.
Esercizi su questo argomento
- Esercizio - Proxy HTTP con filtro degli host (sul modello della prova pratica)
- Esercizio - Server con autenticazione Basic, ETag e GET condizionale (sul modello della prova pratica)
- Esercizio - Server HTTP che risponde in chunked con trailer (sul modello della prova pratica)
- Esercizio - Server HTTP con CGI (sul modello della prova pratica)
- Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica)
- Esercizio - Servizio REST con JSON (sul modello della prova pratica)