Sei sulla pagina 1di 54

I Berkley Socket

Corso di Sistemi A.S. 2004/2005


Proff. M. Vailati e D. Tacca
La libreria socket
 I socket sono delle API (Application Programmer
Interface) che consentono ai programmatori di
amministrare comunicazioni tra processi (InterProcess
Communication)
 A differenza degli altri costrutti di comunicazione (pipe,
code di messaggi e memoria condivisa) i socket
consentono il colloquio tra processi che possono risiedere
su macchine diverse, e dunque costituiscono lo strumento
di base per realizzare un servizio di rete
 In pratica, consentono ad un programmatore di effettuare
trasmissioni TCP e UDP senza curarsi dei dettagli che
sono uguali per ogni comunicazione (three-way
handshake, finestre, buffer, ecc)
Perché i socket
 Vogliamo estendere la possibilità di
comunicazione a processi in esecuzione su
macchine diverse ovvero creare applicazioni
client-server di rete.
 Sono necessarie tecniche di programmazione che
rendano la rete "trasparente" ai processi stessi e
quindi ai programmatori che li hanno scritti.
 La libreria socket, nata per Unix, è oggi usata per
la realizzazione delle applicazioni Internet per
quasi tutti i sistemi operativi.
OSI vs. TCP/IP
Application
Telnet, HTTP
Presentation FTP, SMTP
SOCKET NFS, RPC …
Session

Transport TCP / UDP

Network IP

Data Link
Non specificati
Physical
Indirizzi del livello rete (IP)
 In internet
– Indirizzo IP (sequenza di 32 bit)
– Per facilitare la sua memorizzazione da parte
dell’uomo viene indicato in notazione decimale
(valore decimale di ogni byte separato da “.”)
es. 10.0.1.4 corrisponde all’indirizzo IP
00001010 | 00000000 | 00000001 | 0000100
Indirizzi del livello Trasporto
(TCP/UDP)
 In internet
– coppie <indirizzo IP, porta>
– esempio:
» 158.110.1.2:23 (porta 23: telnet)
 Definiscono i punti di accesso presso cui i
processi possono attendere le connessioni
Come conoscerli?
 Per alcuni processi sono noti a priori
(“well know ports” di TCP/UDP) (/etc/services)

processo processo
client applicazioni server server
(telnet) (telnet) FTP

livello TCP/UDP 23 21
SAP

livelli 1, 2, 3
Well Know Ports
echo 7/tcp telnet 23/tcp
echo 7/udp smtp 25/tcp
discard 9/tcp time 37/tcp
systat 11/tcp time 37/udp
daytime 13/tcp nameserver 42/tcp
daytime 13/udp whois 43/tcp
netstat 15/tcp tftp 69/udp
ftp 21/tcp gopher 70/tcp
fsp 21/udp gopher 70/udp
ssh 22/tcp www 80/tcp
ssh 22/udp www 80/udp
Fasi di una comunicazione TCP
 dichiarazione al sistema operativo che si intende
instaurare nel seguito una connessione (specifica
delle caratteristiche)
 apertura della connessione
– diversa nel server e nel client:
» il server assume di definire la connessione prima del client, e
rimane in attesa che il client si connetta alla porta specificata
» il client assume che il server sia già attivo e prova a connettersi
specificando indirizzo e porta del server
 scambio di dati bidirezionale (trasmissione e
ricezione)
 chiusura della connessione
Alcune primitive

socket crea un end-point di comunicazione


bind associa un indirizzo locale a un socket
listen si rende disponibile ad accettare connessioni
accept si blocca nell’attesa di una connessione
connect tenta di stabilire una connessione
send invia dati sulla connessione
recv riceve dati dalla connessione
close chiude la connessione
Schema di comunicazione TCP
Client Server

•socket
•socket •bind (indirizzo)
•connect (indirizzo) •listen
•accept (connessione)
•send/receive •send/receive
•close •close
Dichiarazione al sistema operativo
int socket(int domain, int type, int protocol);
 è la prima istruzione eseguita sia dal client che dal server
 definisce un socket: crea le strutture e riserva le risorse necessarie
per la gestione di connessioni
 restituisce un intero che va interpretato come un “descrittore di
file”: un identificatore delle strutture appena create
 il client utilizzerà il socket direttamente, specificando il descrittore
di file in tutte le funzioni che chiamerà
 il server userà il suo socket indirettamente, come se fosse un
modello o un prototipo per creare altri socket che saranno quelli
effettivamente usati
 quando la creazione fallisce, viene restituito il valore -1
Parametri della funzione socket
int domain il valore usuale è AF_INET, cioè “InterNET Address
Family” (comunicazioni tra processi in Internet), si
incontra anche AF_UNIX (comunicazioni tra processi
sullo stesso host unix)

int type (tipo, modalità di comunicazione) SOCK_STREAM


(connesso), SOCK_DGRAM (datagram),
SOCK_RAW (direttamente su livello 3, solo per root)

int protocol solitamente si pone a zero per prendere il protocollo di


default indotto dalla coppia domain e type (per
esempio: AF_INET + SOCK_STREAM
determineranno una connessione TCP, AF_INET +
SOCK_DGRAM determineranno una trasmissione
UDP)
Apertura (lato client)
int connect(int sock_fd,
struct sockaddr *serv_addr,
int addrlen);
 si usa solamente con SOCK_STREAM (protocolli
connessi)
 il server deve essere già in attesa di una connessione
 la struttura struct sockaddr è un input: contiene
l’indirizzo IP e la porta TCP del server
 la funzione ritorna -1 in caso di errore (timeout,
errore nei parametri, connessione già aperta, ...)
Specificare un host
 Un host può essere idenficiato:
– attraverso il suo indirizzo IP
(es. 130.180.5.1)
– attraverso il suo nome
(es. www.linux.it)
 L’associazione nome/indirizzo è gestita dal
servizio DNS (o scritta nel file /etc/hosts)
– in una LAN con pochi host può non esserci
La struttura sockaddr_in
 La struttura dati che specifica un host

struct sockaddr_in {
short sin_family; famiglia Internet
short sin_port; porta TCP
struct in_addr sin_addr; indirizzo
char sin_zero[8];
}

struct in_addr {
unsigned long int s_addr; indirizzo intero a 32 bit
}
Uso di sockaddr_in
 Per convenzione, bisogna sempre settare
TUTTA la struttura indirizzo tutta a zero
prima di riempire i vari campi, usando la
funzione memset().

struct sockaddr_in server;


memset ( &server, 0, sizeof(server) );
Trasmissione dati
int send(int sock_fd, const void *buf,
int len, unsigned int flags);
 invia il contenuto della variabile buf (buffer) al socket
specificato
 si usa esclusivamente con SOCK_STREAM (cioè con
protocolli connessi)
 la funzione ritorna il numero di byte inviati oppure -1 in caso
di errore
 il parametro flags è solitamente impostato a zero
Ricezione dati
int recv(int sock_fd, void *buf,
int len, unsigned int flags);
 solo per socket connessi (SOCK_STREAM)
 legge un messaggio di lunghezza massima len dal socket
specificato
 se non c’è alcun messaggio, il programma rimane sospeso (la
chiamata è bloccante)
 la funzione ritorna il numero di byte letti oppure -1 in caso di
errore
 il parametro flags può essere impostato a zero o al valore
MSG_PEEK per esaminare un messaggio senza effettivamente
estrarlo dalla coda di messaggi arrivati
Funzioni write e read
int write(int sock_fd, const void *buf, int len);

int read(int sock_fd, void *buf, int len);


 è possibile utilizzarle al posto di send e recv
 hanno gli stessi parametri tranne flags
 write e read funzionano anche con descrittori di
file veri e propri
 i sistemi non unix (Windows, OS2) possono però
esserne privi
Chiusura
int close(int sock_fd);
 viene eseguita sia dal client che dal server per dichiarare al
sistema operativo che non si ha più interesse a conservare aperta
la connessione (necessariamente di tipo SOCK_STREAM)
 più processi dello stesso host possono condividere lo stesso
socket: solo quando tutti avranno eseguito una close il sistema
operativo provvederà a chiudere la connessione
 la chiusura è simmetrica: la connessione sarà effettivamente
chiusa quando sarà stata chiusa sia sul server che sul client
Altre funzioni utili
 htons() (host to network short) converte uno short
integer (2 byte) dalla rappresentazione della
piattaforma a quella della rete
 ntohs() esegue la conversione inversa
 htoni() per interi, da piattaforma a rete
 ntohi() esegue la conversione inversa
 htonl() converte long integer (4 byte) dalla
piattaforma alla rete
 ntohl() esegue la conversione inversa
 gethostbyname() per interrogare il sistema
sull’indirizzo ip di un host di cui si dispone del nome
Rappresentazione dei numeri
Le piattaforme si dividono in due classi (Big-Endian e Little
Endian) in base a come assumono che siano accodati i byte
delle rappresentazioni multi-byte.
1025 = 00000000 00000000 00000100 00000001

Address Big-Endian Little-Endian


00 00000000 00000001
01 00000000 00000100
02 00000100 00000000
03 00000001 00000000
Internet, Intel 80x86,
HP, IBM, DEC VAX,
Motorola 68000 Pentium
PowerPC(Bi-Endian)
Schema di un client TCP
inizializzazione client
crea il socket

connessione col server


connettiti col server

gestione messaggi
prendi l'input da tastiera

numero "quit"
invia al server numero
chiudi il socket
o "quit"?

ricevi e stampa risultato fine


Inizializzazione del client
#include <stdio.h>
[…altri include…]
main(int argc, char** argv)
{
int sock; /* descrittore del socket */
struct sockaddr_in server;
struct hostent *hp;
char input[256];
if(argc!=3) {
printf("uso: %s <host> <numero-della-porta>\n", argv[0]);
exit(1);
}
sock = socket(AF_INET, SOCK_STREAM, 0);
if( sock < 0 ) {
printf("client: errore %s nella creazione del socket\n",
strerror(errno));
exit(1);
}
Connessione col server
hp = gethostbyname(argv[1]);
if( hp == NULL ){
printf("client: l'host %s non e' raggiungibile.\n", argv[1]);
exit(1);
}

server.sin_family = AF_INET;
bcopy(hp->h_addr, &server.sin_addr, hp->h_length);
server.sin_port = htons(atoi(argv[2]));

if( connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0 )


{
printf("client: errore %s durante la connect\n",
strerror(errno));
exit(1);
}
printf("client: connesso a %s, porta %d\n", argv[1],
ntohs(server.sin_port));
Gestione messaggi
printf("client: num. o ‘quit’? ");
scanf("%s",&input);
while( strcmp(input,"quit") != 0 )
{
char result[256];
if( write(sock, (char *)&input, strlen(input) ) <0) {
printf("errore %s durante la write\n", strerror(errno));
exit(1);
}
if( read(sock,(char *)&result, sizeof(result)) < 0 ) {
printf("errore %s durante la read\n", strerror(errno));
exit(1);
}
printf("client: ricevo dal server %s\n", result);
printf("client: num. o \"quit\"? ");
scanf("%s",&input);
}
close(sock);
printf("client: ho chiuso il socket\n");
} /* fine della funzione main */
gethostbyname
 Fornisce l’indirizzo IP di un host noto il suo nome
(es. www.linux.it)
 Definita in <netdb.h>
struct hostent *gethostbyname(const char *name);

struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */

E’ possibile recuperare l’indirizzo IP copiando i byte corrispondenti:


bcopy(hp->h_addr, &server.sin_addr, hp->h_length);
Problematiche lato server
 il server deve specificare una porta che identifica il
servizio sull’host
 più client possono richiedere il servizio in rapida
successione. Due soluzioni:

– chi arriva dopo si “accoda”


gestito in automatico dal sistema operativo, il processo
specifica solamente la lunghezza (backlog) della coda

– gestione contemporanea di più client


possibile tramite lo sfruttamento del multitasking del
sistema (fork)
Richiesta di una porta al SO
int bind(int sock_fd,
struct sockaddr * my_addr,
int addrlen);
 assegna una porta al socket appena creato
 è eseguita sempre dal server (opzionalmente dal client)
 l’indirizzo della porta può essere specificato (>1024) nella struttura
struct sockaddr, oppure si può accettare la prima porta
disponibile (passando il valore zero)
 la struttura struct sockaddr funge sia da input che da output
(può essere consultata per sapere il numero della porta assegnata)
 ritorna -1 in caso di fallimento (porta richiesta non disponibile,
parametri non validi, socket già assegnata ad un indirizzo,...)
Specifica il backlog

int listen(int sock_fd, int backlog);


 funzione eseguita solo dal server per richiedere al sistema
operativo la gestione di una coda di un determinato numero
(generalmente 5, o anche meno) richieste contemporanee di
connessione da parte dei client sul socket specificato
 se il server sta gestendo una richiesta e ne arriva un’altra, questa
viene messa nella coda
 se la coda è piena, la connessione viene rifiutata
 ritorna -1 in caso di insuccesso (memoria non sufficiente,
parametri non validi, il socket è già connesso...)
 la listen si applica solamente alle connessioni di tipo
SOCK_STREAM (che sono connesse)
Il server accetta le connessioni
int accept( int sock_fd,
struct sockaddr * addr,
int addrlen);
 funzione eseguita solo dal server quando è pronto a ricevere una
connessione
 il programma viene bloccato su questa istruzione fino a quando
un client non abbia fatto una richiesta di connessione sulla stessa
porta e con lo stesso tipo di socket (stesso input alla funzione
socket)
 la struttura struct sockaddr è un output: descriverà l’indirizzo-
porta del client connesso
 la funzione ritorna una NUOVA SOCKET creata sul prototipo di
quella passata come primo parametro
 la accept si applica solamente alle connessioni di tipo
SOCK_STREAM
Schema di comunicazione TCP
Client Server

•socket
•socket •bind (indirizzo)
•connect (indirizzo) •listen
•accept (connessione)
•send/receive •send/receive
•close •close
Successione di primitive (con fork)
socket
bind il server ha
riservato un
 backlog di 3
server listen 3 connessioni
accept per il socket
server

socket
connect un client si è
connesso
utilizzando una
 delle tre possibili
client server connessioni
Successione di primitive (con fork)
fork


client server

il server esegue una


fork, per delegare
al processo figlio il
servizio del client server
Successione corretta di primitive
close dopo una fork
close

 
server
il processo padre deve chiudere server
il socket appena aperto. Se non
il processo figlio deve
lo fa, il sistema operativo non
chiudere il socket che funge
chiuderà mai la connessione,
da modello, liberandosi di
anche se il processo figlio
tutte le strutture dati (backlog,
chiude il suo socket
ecc.) che non gli servono
Schema a blocchi del server
inizializzazione server

crea il socket, assegnagli una porta e una coda

attendi la connessione del client

leggi messaggio dal client

numero quit

esegui la somma numero o chiudi il socket


quit?

manda il risultato al client


gestione connessioni e messaggi
Server con fork inizializzazione server
crea il socket, assegnagli una porta e una coda

attendi la connessione del client


chiudi il socket padre
fork
gestione delle connessioni figlio

processo server figlio

manda il ris. al client leggi mess. dal client

numero
numero o
esegui la somma
quit.?
quit
chiudi il socket ed esci
Inizializzazione del server
#include <sys/types.h>
[…altri include…]

#define MAX_CODA 5 /* massimo backlog */

main(int argc, char** argv) /* prende in input la porta */


{
int sock; /* socket in attesa */
int sockmsg; /* socket servent per il processo figlio */
struct sockaddr_in server;

if ( argc != 2 ) {
printf("uso: %s <numero-della-porta>\n", argv[0]);
exit(EXIT_FAILURE);
}

sock = socket(AF_INET,SOCK_STREAM,0); /* socket prototipo */


if( sock <0 ) {
printf("server: errore %s nella creazione del socket\n",
strerror(errno));
exit(EXIT_FAILURE);
}
Creazione della coda
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
struttura per
server.sin_port = htons(atoi(argv[1])); il bind
if( bind(sock, (struct sockaddr *)&server, sizeof(server)) )
{
printf("server: bind fallita\n");
exit(EXIT_FAILURE);
}

printf("server: rispondo sulla porta %d\n",


ntohs(server.sin_port));

if( listen(sock, MAX_CODA) <0 ) { dimensiono


printf("server: errore %s nella listen\n",
strerror(errno));
la coda di
exit(EXIT_FAILURE); backlog
}
Gestione delle connessioni
while(1) /* ciclo infinito */
{
int totale=0;
char input[256];
sockmsg = accept(sock, 0, 0);
if( sockmsg <0 ) {
printf("errore %s nella accept\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("server: accetto una nuova connessione\n”);
if ( fork() == 0 ) {
qui ci va il codice che presta il servizio (segue)
}
else
close(sockmsg); /* il padre chiude sockmsg */
} /* fine del ciclo infinito */
close(sock); /* inutile: questo server non
finisce mai il suo lavoro! */
printf("server: ho chiuso il socket\n");
} /* fine della funzione main */
Gestione del client
{ /* questo e’ il codice del processo figlio */
int len;
printf("server %d: iniziato \n", getpid() );
close(sock); /* il figlio chiude il socket prototipo */
while( len = read(sockmsg, input, sizeof(input) ) ) {
int numero;
char tot[256];
input[len]='\0'; /* termina la stringa*/
numero = atoi(input); /* converti in intero */
printf("server: arrivato il numero: %d\n", numero);
totale=totale+numero; /* calcolo totale*/
sprintf(tot, "%d", totale); /* prepara la stringa */
write(sockmsg, tot, sizeof(tot)); /* invia la stringa */
}
close(sockmsg); /* prima di uscire chiudi il socket */
printf("server: socket chiuso\n”);
exit(EXIT_SUCCESS); /* questo processo ha finito */
}
Un programma chat server
main()
{
int create_socket,new_socket,addrlen;
int bufsize = 1024;
char *buffer = malloc(bufsize);
struct sockaddr_in address;

if ((create_socket = socket(AF_INET,SOCK_STREAM,0)) > 0)


printf("The socket was created\n");
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // accept any incoming message
address.sin_port = htons(15000);
if (bind(create_socket,(struct sockaddr *)&address,sizeof(address)) == 0)
printf("Binding Socket\n");
listen(create_socket,3);
addrlen = sizeof(struct sockaddr_in);
new_socket = accept(create_socket,(struct sockaddr *)&address,&addrlen);
if (new_socket > 0) {
printf("The client is connected...\n");
}
do{
printf("Message to send: ");
gets(buffer);
send(new_socket,buffer,bufsize,0);
recv(new_socket,buffer,bufsize,0);
printf("Message received: %s\n",buffer);
}while(strcmp(buffer,"/q")); //user ‘q’ to quit
close(new_socket);
close(create_socket);
}
Un programma chat client
main(int argc,char *argv[])
{
int create_socket;
int bufsize = 1024;
char *buffer = malloc(bufsize);
struct sockaddr_in address;

if ((create_socket = socket(AF_INET,SOCK_STREAM,0)) > 0)


printf("The Socket was created\n");
address.sin_family = AF_INET;
address.sin_port = htons(15000);
address.sin_addr.s_addr = inet_addr("127.0.0.1"); // local host address

if (connect(create_socket,(struct sockaddr *)&address,sizeof(address)) == 0)


printf("The connection was accepted with the server\n");
do{
recv(create_socket,buffer,bufsize,0);
printf("Message received: %s\n",buffer);
if (strcmp(buffer,"/q")){
printf("Message to send: ");
gets(buffer);
send(cria_socket,buffer,bufsize,0);
}
}while (strcmp(buffer,"/q"));
close(create_socket);
}
inet_addr
 Traduce l’indirizzo IP dalla notazione
puntata “aaa.bbb.ccc.ddd” nell’intero
decimale corrispondente (alternativa all’uso
di gethostbyname quando non esiste un
DNS).
unsigned long int inet_addr(const char *cp);
Fasi di una comunicazione UDP

La situazione è molto più semplice perché non


occorre né instaurare la connessione né
chiuderla
 dichiarazione al sistema operativo che si intende instaurare
nel seguito una connessione (specifica delle caratteristiche)
 scambio di dati unidirezionale
Trasmissione di messaggi UDP
int sendto( int sock_fd, const void *msg,
int len, unsigned int flags,
const struct sockaddr *to,
int to_len);
 invia un pacchetto contenente il messaggio nel parametro msg
all’indirizzo specificato
 si usa solamente con SOCK_DGRAM (protocolli non connessi)
 la funzione ritorna -1 in caso di errore e setta la variabile errno al
valore di EMSGSIZE qualora l’errore sia dovuto alla taglia
eccessiva del messaggio (che non entra in un solo pacchetto UDP)
 il parametro flags sarà sempre posto a zero per i nostri scopi
Ricezione di messaggi UDP
int recvfrom(int sock_fd, void *msg,
int len, unsigned int flags,
struct sockaddr *from, int from_len);
 legge un messaggio della lunghezza massima specificata
 la struttura struct sockaddr è un output, che contiene l’indirizzo
del mittente
 se non c’è alcun messaggio, il programma rimane sospeso (la
chiamata è bloccante)
 si usa solamente con SOCK_DGRAM (protocolli non connessi)
 la funzione ritorna il numero di byte letti oppure -1 in caso di
errore
 il parametro flags è generalmente impostato a zero
Successione delle primitive (UDP)

server client
socket socket
bind

recvfrom sendto
Esempio client UDP
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>

#define SERVER_PORT_ID 6090 /* la porta del server */

#define SERV_HOST_ADDR “127.0.0.1"

main()
{
int sockid, retcode;
struct sockaddr_in server_addr;
char msg[12]; /* conterra’ il messaggio da inviare */

/* creo il socket */
printf(”client: creo il socket\n");
if ( (sockid = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
printf(”client: socket() fallita: %d\n",errno);
exit(0);
}
Esempio client UDP (segue)
printf(”client: creating addr structure for server\n");
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERV_HOST_ADDR);
server_addr.sin_port = htons(SERVER_PORT_ID);
printf(”client: initializing message and sending\n");
sprintf(msg, ”hello world");
retcode = sendto(sockid,msg,12,0,(struct sockaddr *)
&server_addr,sizeof(server_addr));
if (retcode <= -1)
{ printf("client: sendto failed: %d\n",errno); exit(0); }
close(sockid); /* close the socket */
}
Esempio di server UDP
#define MY_PORT_ID 6090
main()
{
int sockid, nread, addrlen;
struct sockaddr_in my_addr, client_addr;
char msg[50];
printf("server: creating socket\n");
if ( (sockid = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{ printf(”server: socket error: %d\n",errno); exit(0); }
printf(”server: binding my local socket\n");
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = htons(INADDR_ANY);
my_addr.sin_port = htons(MY_PORT_ID);
if ( (bind(sockid, (struct sockaddr *) &my_addr,
sizeof(my_addr)) < 0) )
{ printf(”server: bind fail: %d\n",errno); exit(0); }
printf(”server: starting blocking message read\n");
nread = recvfrom(sockid,msg,11,0,
(struct sockaddr *) &client_addr, &addrlen);
printf(”server: return code from read is %d\n",nread);
if (nread >0) printf(”server: message is: %.11s\n",msg);
close(sockid);
}
Domande
Per quale motivo nell’istruzione recvfrom compare un
parametro che specifica un indirizzo di rete?
Perché recvfrom si usa nelle trasmissioni UDP, che sono non
connesse, e dunque non c’e’ altro modo per sapere in numero IP e
la porta del client (e decidere se è il caso di rispondere)

Per quale motivo, a differenza dell’istruzione sendto,


nell’istruzione send non compare un parametro che specifica il
destinatario dei dati?
Perché send è usato dal client nelle trasmissioni connesse, e
dunque il destinatario è sottinteso ed è l’altro capo della
connessione instaurata
Domande
Come fa il server UDP a rispondere al client?

Non può. Può solo promuoversi client di una nuova


trasmissione UDP indirizzata ad una porta del client,
assumendo che il client dopo aver inviato il suo pacchetto si
sia a sua volta promosso server su quella stessa porta (che
può coincidere con la porta usata per inviare il primo
pacchetto).

Potrebbero piacerti anche