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).
- 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.
- 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.
- 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 diaccept; per ogni client si fareadewritefinchéreadrestituisce0(il client ha chiuso), poiclosedel 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 direcvfrom+sendtoverso il mittente. Nientelisten, nienteaccept, niente connessione. - Client UDP:
getaddrinfo(tipoSOCK_DGRAM),socket,setsockopt(SO_RCVTIMEO), ciclo disendtoerecvfrom.
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
/* 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
/* 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
/* 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
/* 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:51514Controlli utili:
ss -ltnp | grep 7777mostra il socket inLISTENe il processo;ss -tan | grep 7777gli stati delle connessioni (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 →).nc 127.0.0.1 7777è un client già pronto: serve per provare il server senza il proprio client.- Test del limite dell'iterativo: aprire due
nc 127.0.0.1 7777. Il primo viene servito; il secondo si collega (laconnectriesce, ci pensa il kernel) ma non riceve nulla finché il primo non chiude: il server è occupato nelreaddel primo client. - Versione UDP:
./echo_udp_server 7777 &, poi./echo_udp_client 127.0.0.1 7777oppureecho ciao | nc -u -w1 127.0.0.1 7777. Se si ferma il server e si scrive nel client, dopo 2 secondi appare(nessuna risposta: datagramma perso?).
Spiegazione dei punti chiave
Server TCP.
signal(SIGPIPE, SIG_IGN): se il client sparisce mentre il server scrive, il kernel inviaSIGPIPEche di default uccide il processo. Ignorandolo lawriterestituisce-1conerrno = EPIPEewrite_allritorna 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 inTIME_WAIT; senza l'opzione, riavviare il server subito dàAddress already in use.memset(&addr, 0, sizeof addr): azzera anche il campo di riempimentosin_zeroche il kernel si aspetta a zero.addr.sin_port = htons(atoi(argv[1])): la porta va nell'ordine di rete; senzahtonsil 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;lfdresta in ascolto.inet_ntopentohs(cli.sin_port): da binario in ordine di rete a testo e numero.while ((n = read(cfd, buf, sizeof buf)) > 0):readrestituisce0quando il client chiude (EOF),-1in caso di errore; il ciclo termina in entrambi i casi.close(cfd)chiude il socket del client, nonlfd.
Client TCP.
getaddrinfo(argv[1], argv[2], &hints, &res): risolve nome e porta in una lista di indirizzi, IPv4 o IPv6. Il ciclofor (p = res; ...)provasocketeconnectsu ognuno;freeaddrinfolibera la lista.read_exact(fd, reply, n): il client ha inviatonbyte e l'echo li restituisce uguali, ma TCP può consegnarli in piùread. Una singolareadpotrebbe restituire meno byte e stampare una risposta troncata. Il ciclo accumula finché hanbyte.fgetsconserva il\n: la riga inviata include il newline, così il server la restituisce intera eprintf("echo: %s")non aggiunge un a-capo.close(fd)alla fine invia il FIN: il server vedereadrestituire0.
Server e client UDP.
SOCK_DGRAM: UDP. Il server fa solobind: 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 acliconsendto.- Il client non fa
connectnébind: il kernel assegna una porta effimera al primosendto. SO_RCVTIMEOconstruct timeval: senza timeout, se il datagramma o la risposta si perde,recvfromresta bloccata per sempre (UDP non ritrasmette). Con il timeout scaduto la chiamata fallisce conEAGAINoEWOULDBLOCK.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
htonssulla porta (si ascolta sulla porta sbagliata) omemsetsullasockaddr_in. bindfallisce con "Address already in use": mancaSO_REUSEADDRoppure un altro processo usa la porta.- Chiudere
lfdal posto dicfddentro il ciclo: il server smette di accettare. - Nel client TCP usare una sola
reade supporre che arrivino tutti i byte; stamparereplysenza'\0'. - Nel client UDP non impostare il timeout: il programma si blocca alla prima perdita.
- Confondere il server UDP con uno TCP: chiamare
listenoacceptsu un socket UDP dà errore. - Lasciare attivo
SIGPIPEnel 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.
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+ ciclosocket/connect; per ogni riga:write_all, poiread_exact(n)(TCP è stream: unareadpuò dare meno);fgetsconserva il\n;closeinvia FIN. - Server UDP.
SOCK_DGRAM, solobind;recvfrom= un datagramma + indirizzo del mittente;sendtoversocli. Buffer troppo piccolo: il resto si perde. - Client UDP.
getaddrinfoconSOCK_DGRAM,SO_RCVTIMEO(struct timeval2 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 =read0. UDP: nessuna connessione, un datagramma per chiamata, perdite possibili (timeout nel client), mittente noto darecvfrom. - Prove.
ss -ltnp | grep 7777;nc 127.0.0.1 7777; con duencil secondo aspetta (iterativo);nc -u. Si compila congcc -Wall -Wextra. - Codice essenziale (le funzioni centrali, senza commenti):
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 != 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);
}
}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
htonsomemset; nienteSO_REUSEADDR(EADDRINUSE);close(lfd)al posto diclose(cfd); una solareadnel client TCP; niente timeout nel client UDP;listen/acceptsu UDP;SIGPIPEattivo.