Salta al contenuto
Note per Studenti Esercizio - Echo TCP e UDP, client e server (sul modello della prova pratica)

Esercizio - Echo TCP e UDP, client e server (sul modello della prova pratica)

In questa pagina 6

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

  1. Scrivere in C, per Linux, un server TCP iterativo che accetta connessioni sulla porta data come argomento e rimanda al client tutto ciò che riceve (echo), e un client TCP che legge righe dalla tastiera, le invia al server e stampa la risposta.
  2. Scrivere la versione UDP dei due programmi: il server rimanda ogni datagramma al mittente, il client invia una riga per datagramma e aspetta la risposta per al massimo 2 secondi.
  3. Dire cosa cambia fra le due versioni e perché il client UDP ha bisogno di un timeout.

Teoria: 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 →, TCP - connessione, affidabilità e controllo di flussoTCP (Transmission Control Protocol) è il protocollo di trasporto con connessione e affidabile: trasforma il servizio senza connessione e inaffidabile di IP in un flusso di byte ordinato, senza errori né duplicati. La connessione si apre con l'handshake a tre vie (SYN, SYN+ACK, ACK) e si chiude con tre o quattro segmenti (FIN). I byte sono numerati: il numero di sequenza è quello del primo byte del segmento, il numero di ACK (cumulativo) è il prossimo byte atteso. Il mittente può inviare $\min(\text{rwnd},\text{cwnd})$ byte non ancora confermati; rwnd (finestra del ricevitore, in un campo di 16 bit) è il controllo di flusso. L'errore si gestisce con checksum, ACK, timeout di ritrasmissione (RTO) e ritrasmissione rapida dopo tre ACK duplicati. Per usare tutto il canale la finestra deve valere almeno il prodotto banda-ritardo (BDP); il throughput massimo è $\text{MSS}\cdot W_{\max}/\text{RTT}$.TCP - connessione, affidabilità e controllo di flusso →, Protocollo UDPUDP (User Datagram Protocol) è il protocollo di trasporto senza connessione e inaffidabile: rispetto a IP aggiunge soltanto la comunicazione processo-processo (numeri di porta) e un controllo d'errore facoltativo. L'intestazione è di soli 8 byte (porta sorgente, porta destinazione, lunghezza, checksum). Il checksum copre pseudo-intestazione (indirizzi IP, protocollo 17, lunghezza), intestazione e dati, ed è il complemento a uno della somma a 16 bit; se vale 0 significa "non calcolato", e un risultato 0 si trasmette come 0xFFFF. UDP non ha connessione, numeri di sequenza, controllo di flusso, di errore né di congestione: si sceglie per i messaggi brevi (DNS, DHCP, RIP, SNMP) e per le applicazioni in tempo reale, dove conta non aggiungere ritardo.Protocollo UDP →, Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endiannessIn C un programma di rete maneggia byte, non oggetti: un processo ha codice, dati statici, heap e stack; i tipi hanno dimensioni fisse solo se si usano <stdint.h> (uint8_t, uint16_t, uint32_t); i dati che arrivano da un socket sono un buffer di byte con una lunghezza, NON una stringa C terminata da '\0'; i puntatori e l'aritmetica dei puntatori (buf + totale) permettono di riempire un buffer a pezzi; una struct puo' contenere byte di riempimento (padding) per l'allineamento, quindi non si spedisce con write(&s, sizeof s); sulla rete i numeri a piu' byte viaggiano in big endian (network byte order) e si convertono con htons, htonl, ntohs, ntohl, oppure si serializzano a mano con shift e maschere.Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endianness →.


Idea

Sono i quattro programmi più piccoli che esercitano tutte le chiamate delle socket. Servono da modello per ogni esercizio successivo.

  • Server TCP: socket → setsockopt(SO_REUSEADDR) → bind → listen → ciclo di accept; per ogni client si fa read e write finché read restituisce 0 (il client ha chiuso), poi close del socket del client.
  • Client TCP: getaddrinfo → socket + connect → ciclo "leggi una riga da tastiera, write, leggi esattamente altrettanti byte di risposta, stampa" → close.
  • Server UDP: socket(SOCK_DGRAM) → bind → ciclo di recvfrom + sendto verso il mittente. Niente listen, niente accept, niente connessione.
  • Client UDP: getaddrinfo (tipo SOCK_DGRAM), socket, setsockopt(SO_RCVTIMEO), ciclo di sendto e recvfrom.

Le due differenze da ricordare: TCP è uno stream (la risposta di 20 byte può arrivare in due read e il client deve leggerne esattamente 20), UDP è datagrammi (una sendto = una recvfrom, ma il datagramma può perdersi).

Codice

Server TCP

c
/* echo_tcp_server.c - server TCP iterativo: rimanda indietro tutto quello che riceve.
 * Compilare:  gcc -Wall -Wextra -o echo_tcp_server echo_tcp_server.c
 * Avviare:    ./echo_tcp_server 7777
 * Provare:    nc 127.0.0.1 7777      (si scrive una riga, il server la rimanda)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

/* Scrive tutti i byte: write puo' fermarsi prima (buffer del socket pieno, segnale). */
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 != 2) {
        fprintf(stderr, "uso: %s porta\n", argv[0]);
        return 1;
    }
    signal(SIGPIPE, SIG_IGN);                       /* niente morte improvvisa se il client sparisce */

    /* 1) socket(): crea il "telefono". AF_INET = IPv4, SOCK_STREAM = TCP. */
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd < 0) {
        perror("socket");
        return 1;
    }

    /* Senza SO_REUSEADDR un riavvio subito dopo la chiusura fallisce con "Address already in use" (TIME_WAIT). */
    int yes = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);

    /* 2) bind(): assegna al socket l'indirizzo locale (tutte le interfacce) e la porta. */
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof addr);                  /* azzera anche i campi di riempimento */
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    addr.sin_port = htons((unsigned short)atoi(argv[1]));   /* host order -> network order (big endian) */
    if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) {
        perror("bind");
        return 1;
    }

    /* 3) listen(): il socket diventa "passivo"; il kernel completa gli handshake e mette in coda fino a 5 connessioni. */
    if (listen(lfd, 5) < 0) {
        perror("listen");
        return 1;
    }

    for (;;) {
        /* 4) accept(): blocca finche' arriva una connessione completata e restituisce un NUOVO socket per quel client. */
        struct sockaddr_in cli;
        socklen_t clen = sizeof cli;                /* in ingresso: dimensione del buffer; in uscita: dimensione scritta */
        int cfd = accept(lfd, (struct sockaddr *)&cli, &clen);
        if (cfd < 0) {
            if (errno == EINTR)
                continue;
            perror("accept");
            break;
        }
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof ip);
        printf("connesso %s:%d\n", ip, ntohs(cli.sin_port));

        /* 5) dialogo: read restituisce 0 quando il client chiude (EOF), -1 su errore. */
        char buf[1024];
        ssize_t n;
        while ((n = read(cfd, buf, sizeof buf)) > 0)
            if (write_all(cfd, buf, (size_t)n) < 0)
                break;
        printf("chiuso %s:%d\n", ip, ntohs(cli.sin_port));
        close(cfd);                                 /* si chiude il socket del client, non quello in ascolto */
    }
    close(lfd);
    return 0;
}

Client TCP

c
/* echo_tcp_client.c - client TCP: legge righe dalla tastiera, le manda al server e stampa la risposta.
 * Compilare:  gcc -Wall -Wextra -o echo_tcp_client echo_tcp_client.c
 * Usare:      ./echo_tcp_client 127.0.0.1 7777
 */
#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>

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 ESATTAMENTE n byte (TCP e' uno stream: una read puo' restituirne meno). Torna -1 su errore o EOF anticipato. */
static int read_exact(int fd, char *buf, size_t n)
{
    while (n > 0) {
        ssize_t r = read(fd, buf, n);
        if (r < 0 && errno == EINTR)
            continue;
        if (r <= 0)
            return -1;
        buf += r;
        n -= (size_t)r;
    }
    return 0;
}

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

    /* getaddrinfo traduce nome o IP + porta in una lista di indirizzi pronti per connect (IPv4 e IPv6). */
    struct addrinfo hints, *res, *p;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    int rc = getaddrinfo(argv[1], argv[2], &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) {      /* prova gli indirizzi uno dopo l'altro */
        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;                                  /* three-way handshake completato */
        close(fd);
        fd = -1;
    }
    freeaddrinfo(res);
    if (fd < 0) {
        fprintf(stderr, "impossibile connettersi a %s:%s\n", argv[1], argv[2]);
        return 1;
    }

    char line[1024], reply[1024];
    while (fgets(line, sizeof line, stdin) != NULL) {
        size_t n = strlen(line);
        if (write_all(fd, line, n) < 0) {
            perror("write");
            break;
        }
        /* l'echo restituisce esattamente n byte: li aspettiamo tutti */
        if (read_exact(fd, reply, n) < 0) {
            fprintf(stderr, "il server ha chiuso la connessione\n");
            break;
        }
        reply[n] = '\0';
        printf("echo: %s", reply);
    }
    close(fd);                                      /* invia FIN: il server vede read() == 0 */
    return 0;
}

Server UDP

c
/* echo_udp_server.c - server UDP: rimanda ogni datagramma al mittente. Niente listen, niente accept.
 * Compilare:  gcc -Wall -Wextra -o echo_udp_server echo_udp_server.c
 * Avviare:    ./echo_udp_server 7777
 * Provare:    echo ciao | nc -u -w1 127.0.0.1 7777
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "uso: %s porta\n", argv[0]);
        return 1;
    }
    int fd = socket(AF_INET, SOCK_DGRAM, 0);        /* SOCK_DGRAM = UDP: senza connessione */
    if (fd < 0) {
        perror("socket");
        return 1;
    }
    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(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
        perror("bind");
        return 1;
    }

    char buf[2048];
    for (;;) {
        struct sockaddr_in cli;
        socklen_t clen = sizeof cli;
        /* recvfrom restituisce UN datagramma intero (se il buffer e' piu' piccolo il resto va perso)
         * e dice chi l'ha mandato: UDP non ha connessione, l'indirizzo del mittente arriva con ogni pacchetto. */
        ssize_t n = recvfrom(fd, buf, sizeof buf, 0, (struct sockaddr *)&cli, &clen);
        if (n < 0) {
            perror("recvfrom");
            continue;
        }
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof ip);
        printf("%zd byte da %s:%d\n", n, ip, ntohs(cli.sin_port));
        sendto(fd, buf, (size_t)n, 0, (struct sockaddr *)&cli, clen);   /* risposta allo stesso mittente */
    }
}

Client UDP

c
/* echo_udp_client.c - client UDP: manda un datagramma per riga e aspetta la risposta per 2 secondi.
 * Compilare:  gcc -Wall -Wextra -o echo_udp_client echo_udp_client.c
 * Usare:      ./echo_udp_client 127.0.0.1 7777
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netdb.h>

int main(int argc, char **argv)
{
    if (argc != 3) {
        fprintf(stderr, "uso: %s host porta\n", argv[0]);
        return 1;
    }
    struct addrinfo hints, *res;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;                 /* UDP */
    int rc = getaddrinfo(argv[1], argv[2], &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
        return 1;
    }
    int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (fd < 0) {
        perror("socket");
        return 1;
    }

    /* Se il datagramma (o la risposta) si perde, recvfrom aspetterebbe per sempre: serve un timeout. */
    struct timeval tv = {.tv_sec = 2, .tv_usec = 0};
    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);

    char line[1024], reply[2048];
    while (fgets(line, sizeof line, stdin) != NULL) {
        /* sendto: nessuna connessione, ogni datagramma porta il proprio destinatario */
        if (sendto(fd, line, strlen(line), 0, res->ai_addr, res->ai_addrlen) < 0) {
            perror("sendto");
            break;
        }
        ssize_t n = recvfrom(fd, reply, sizeof reply - 1, 0, NULL, NULL);   /* NULL: non ci interessa il mittente */
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                printf("(nessuna risposta: datagramma perso?)\n");   /* UDP non ritrasmette: decide l'applicazione */
            else
                perror("recvfrom");
            continue;
        }
        reply[n] = '\0';
        printf("echo: %s", reply);
    }
    freeaddrinfo(res);
    close(fd);
    return 0;
}

Compilare e provare

$ gcc -Wall -Wextra -o echo_tcp_server echo_tcp_server.c
$ gcc -Wall -Wextra -o echo_tcp_client echo_tcp_client.c
$ ./echo_tcp_server 7777 &
$ ./echo_tcp_client 127.0.0.1 7777
ciao
echo: ciao
come va?
echo: come va?
^D
connesso 127.0.0.1:51514          <- stampato dal server
chiuso 127.0.0.1:51514

Controlli utili:

Spiegazione dei punti chiave

Server TCP.

  • signal(SIGPIPE, SIG_IGN): se il client sparisce mentre il server scrive, il kernel invia SIGPIPE che di default uccide il processo. Ignorandolo la write restituisce -1 con errno = EPIPE e write_all ritorna l'errore, ma il server continua.
  • socket(AF_INET, SOCK_STREAM, 0): IPv4, TCP, protocollo scelto dal sistema.
  • setsockopt(... SO_REUSEADDR ...): dopo la chiusura la porta resta in TIME_WAIT; senza l'opzione, riavviare il server subito dà Address already in use.
  • memset(&addr, 0, sizeof addr): azzera anche il campo di riempimento sin_zero che il kernel si aspetta a zero.
  • addr.sin_port = htons(atoi(argv[1])): la porta va nell'ordine di rete; senza htons il server ascolterebbe sulla porta sbagliata.
  • INADDR_ANY: accetta connessioni su tutte le interfacce.
  • listen(lfd, 5): il socket diventa passivo; il kernel completa gli handshake e tiene fino a 5 connessioni in coda.
  • accept(lfd, (struct sockaddr *)&cli, &clen): clen è prima la dimensione del buffer e poi quella dell'indirizzo scritto. Restituisce un nuovo descrittore per il client; lfd resta in ascolto.
  • inet_ntop e ntohs(cli.sin_port): da binario in ordine di rete a testo e numero.
  • while ((n = read(cfd, buf, sizeof buf)) > 0): read restituisce 0 quando il client chiude (EOF), -1 in caso di errore; il ciclo termina in entrambi i casi.
  • close(cfd) chiude il socket del client, non lfd.

Client TCP.

  • getaddrinfo(argv[1], argv[2], &hints, &res): risolve nome e porta in una lista di indirizzi, IPv4 o IPv6. Il ciclo for (p = res; ...) prova socket e connect su ognuno; freeaddrinfo libera la lista.
  • read_exact(fd, reply, n): il client ha inviato n byte e l'echo li restituisce uguali, ma TCP può consegnarli in più read. Una singola read potrebbe restituire meno byte e stampare una risposta troncata. Il ciclo accumula finché ha n byte.
  • fgets conserva il \n: la riga inviata include il newline, così il server la restituisce intera e printf("echo: %s") non aggiunge un a-capo.
  • close(fd) alla fine invia il FIN: il server vede read restituire 0.

Server e client UDP.

  • SOCK_DGRAM: UDP. Il server fa solo bind: non c'è connessione da accettare.
  • recvfrom(fd, buf, sizeof buf, 0, &cli, &clen): restituisce un datagramma intero e l'indirizzo del mittente. Se il buffer è più piccolo, il resto del datagramma va perso. Il mittente serve perché UDP non ha connessione: la risposta va a cli con sendto.
  • Il client non fa connect né bind: il kernel assegna una porta effimera al primo sendto.
  • SO_RCVTIMEO con struct timeval: senza timeout, se il datagramma o la risposta si perde, recvfrom resta bloccata per sempre (UDP non ritrasmette). Con il timeout scaduto la chiamata fallisce con EAGAIN o EWOULDBLOCK.
  • recvfrom(..., NULL, NULL): non interessa chi risponde.

Confronto.

TCP UDP
connessione connect/accept, handshake nessuna
unità di dati flusso di byte, senza confini datagramma
una read restituisce quello che è arrivato (può essere meno) un datagramma intero
perdita di dati ritrasmessa dal kernel persa: serve il timeout nel client
fine della comunicazione read restituisce 0 nessuna: ogni datagramma è indipendente
mittente noto da la connessione recvfrom ad ogni datagramma

Errori tipici

  • Dimenticare htons sulla porta (si ascolta sulla porta sbagliata) o memset sulla sockaddr_in.
  • bind fallisce con "Address already in use": manca SO_REUSEADDR oppure un altro processo usa la porta.
  • Chiudere lfd al posto di cfd dentro il ciclo: il server smette di accettare.
  • Nel client TCP usare una sola read e supporre che arrivino tutti i byte; stampare reply senza '\0'.
  • Nel client UDP non impostare il timeout: il programma si blocca alla prima perdita.
  • Confondere il server UDP con uno TCP: chiamare listen o accept su un socket UDP dà errore.
  • Lasciare attivo SIGPIPE nel server TCP.

Per continuare

Il server resta iterativo: per servire più client insieme si aggiunge fork e la gestione di SIGCHLD, come in echo_fork_server.c di 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 →. Lo stesso scheletro, con il protocollo HTTP al posto dell'echo, è nei server successivi (Esercizio - Server HTTP iterativo con GET, HEAD e codici di errore (sul modello della prova pratica)).

Versione ripasso

  • Testo. Server TCP iterativo di echo + client TCP; versione UDP (server che rimanda al mittente, client con timeout di 2 s); differenze fra le due.
  • Server TCP, in sintesi.
c
signal(SIGPIPE, SIG_IGN);
int lfd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes);
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(porta);
bind(lfd, (struct sockaddr *)&addr, sizeof addr);
listen(lfd, 5);
for (;;) { cfd = accept(lfd, (struct sockaddr *)&cli, &clen);
           while ((n = read(cfd, buf, sizeof buf)) > 0) write_all(cfd, buf, n);   /* 0 = il client ha chiuso */
           close(cfd); }
  • Client TCP. getaddrinfo + ciclo socket/connect; per ogni riga: write_all, poi read_exact(n) (TCP è stream: una read può dare meno); fgets conserva il \n; close invia FIN.
  • Server UDP. SOCK_DGRAM, solo bind; recvfrom = un datagramma + indirizzo del mittente; sendto verso cli. Buffer troppo piccolo: il resto si perde.
  • Client UDP. getaddrinfo con SOCK_DGRAM, SO_RCVTIMEO (struct timeval 2 s), sendto, recvfrom(..., NULL, NULL); scaduto il timeout: EAGAIN/EWOULDBLOCK = "datagramma perso?".
  • Differenze. TCP: connessione, stream senza confini (read = quanto arrivato), ritrasmissione dal kernel, fine = read 0. UDP: nessuna connessione, un datagramma per chiamata, perdite possibili (timeout nel client), mittente noto da recvfrom.
  • Prove. ss -ltnp | grep 7777; nc 127.0.0.1 7777; con due nc il secondo aspetta (iterativo); nc -u. Si compila con gcc -Wall -Wextra.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static int read_exact(int fd, char *buf, size_t n)
{
    while (n > 0) {
        ssize_t r = read(fd, buf, n);
        if (r < 0 && errno == EINTR)
            continue;
        if (r <= 0)
            return -1;
        buf += r;
        n -= (size_t)r;
    }
    return 0;
}
c
int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "uso: %s porta\n", argv[0]);
        return 1;
    }
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) {
        perror("socket");
        return 1;
    }
    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(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
        perror("bind");
        return 1;
    }
    char buf[2048];
    for (;;) {
        struct sockaddr_in cli;
        socklen_t clen = sizeof cli;
        ssize_t n = recvfrom(fd, buf, sizeof buf, 0, (struct sockaddr *)&cli, &clen);
        if (n < 0) {
            perror("recvfrom");
            continue;
        }
        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof ip);
        printf("%zd byte da %s:%d\n", n, ip, ntohs(cli.sin_port));
        sendto(fd, buf, (size_t)n, 0, (struct sockaddr *)&cli, clen);
    }
}
c
int main(int argc, char **argv)
{
    if (argc != 3) {
        fprintf(stderr, "uso: %s host porta\n", argv[0]);
        return 1;
    }
    struct addrinfo hints, *res;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;
    int rc = getaddrinfo(argv[1], argv[2], &hints, &res);
    if (rc != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rc));
        return 1;
    }
    int fd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (fd < 0) {
        perror("socket");
        return 1;
    }
    struct timeval tv = {.tv_sec = 2, .tv_usec = 0};
    setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
    char line[1024], reply[2048];
    while (fgets(line, sizeof line, stdin) != NULL) {
        if (sendto(fd, line, strlen(line), 0, res->ai_addr, res->ai_addrlen) < 0) {
            perror("sendto");
            break;
        }
        ssize_t n = recvfrom(fd, reply, sizeof reply - 1, 0, NULL, NULL);
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                printf("(nessuna risposta: datagramma perso?)\n");
            else
                perror("recvfrom");
            continue;
        }
        reply[n] = '\0';
        printf("echo: %s", reply);
    }
    freeaddrinfo(res);
    close(fd);
    return 0;
}
  • Errori tipici: niente htons o memset; niente SO_REUSEADDR (EADDRINUSE); close(lfd) al posto di close(cfd); una sola read nel client TCP; niente timeout nel client UDP; listen/accept su UDP; SIGPIPE attivo.

Esercizi su questo argomento

Teoria collegata