E' un mondo di client-server, piccola. Quasi tutto nelle reti ha a che fare con processi client che parlano con processi server e viceversa. Prendete il comando telnet, ad esempio. Quando vi collegate ad un host remoto sulla porta 23 con telnet (il client), un programma su quell'host (detto telnetd, il server) viene richiamato in vita. Esso gestisce la connessione in arrivo, vi da un prompt di login, etc.
Figure 2. Interazione Client-Server .
[Diagramma dell'Interazione Client-Server]Lo scambio di informazionitra client e server è riassunto nella Figura 2.
Notate che la coppia client-server può comunicare con SOCK_STREAM, SOCK_DGRAM, o qualsiasi altra cosa (fintanto che continuano a parlare entrambi nello stesso modo.) Alcuni buoni esempi di coppie client-server sono telnet/telnetd, ftp/ftpd, o bootp/bootpd. Ogni volta che usate ftp, c'è un programma remoto, ftpd, che vi offre il servizio.
Spesso, ci sarà solo un server su una certa macchina, e quel server gestirà client multipli usando fork(). La routine di base è: server attende una connessione, la accetta con accept(), e crea un processo figlio con fork() per gestirla. Questo è quello che fa il nostro server d'esempio nella prossima sezione.
Tutto ciò che fa questo server è inviare la stringa "Ciao a tutti!\n" su una connessione di tipo stream. Tutto ciò che dovete fare per provare questo server è eseguirlo in una finestra, e collegarvi ad esso tramite telnet da un'altra con:
$ telnet hostnameremoto 3490 |
dove hostnameremoto è il nome della macchina sulla quale sta girando il server.
Il codice del server: (Nota: una backslash su di una linea indica che quella linea continua a quella seguente.)
/* ** server.c -- un esempio di server con socket stream */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <signal.h> #define MYPORT 3490 // la porta che useranno gli utenti per collegarsi #define BACKLOG 10 // quante connessioni verrano mantenute in coda void sigchld_handler(int s) { while(wait(NULL) > 0); } int main(void) { int sockfd, new_fd; // ascolta su sock_fd, le nuove connessioni su new_fd struct sockaddr_in my_addr; // informazioni sul mio indirizzo struct sockaddr_in their_addr; // informazioni sull'indirizzo di chi si collega int sin_size; struct sigaction sa; int yes=1; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) { perror("setsockopt"); exit(1); } my_addr.sin_family = AF_INET; // host byte order my_addr.sin_port = htons(MYPORT); // short, network byte order my_addr.sin_addr.s_addr = INADDR_ANY; // automaticamente riempita con il mio IP memset(&(my_addr.sin_zero), '\0', 8); // azzzera il resto della struttura if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } if (listen(sockfd, BACKLOG) == -1) { perror("listen"); exit(1); } sa.sa_handler = sigchld_handler; // raccoglie tutti i processi morti sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(1); } while(1) { // principale loop di accept() sin_size = sizeof(struct sockaddr_in); if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) { perror("accept"); continue; } printf("server: connessione stabilita da %s\n", inet_ntoa(their_addr.sin_addr)); if (!fork()) { // questo è il processo figlio close(sockfd); // il figlio non ha bisogno del socket in ascolto if (send(new_fd, "Ciao a tutti!\n", 14, 0) == -1) perror("send"); close(new_fd); exit(0); } close(new_fd); // il genitore non ha bisogno di questo } return 0; } |
In caso foste curiosi, Io ho messo il codice in una sola grande main() perchè (Mi pare) sintatticamente chiaro. Sentitevi liberi di dividerlo in funzioni più piccole se vi fastare meglio.
(Inoltre, tuttaquella cosa con sigaction() potrebbe essere nuova per voi-- è tutto-- è tutto ok. Quel codice è responsabile di elminare i processi zombie che appaiono appena i processi figli creati con fork() terminano. Se create troppi zombie e non li raccogliete, il vostro amministratore di sistema potrebbe turbarsi.)
Potete ottenere i dati da questo server usando il codice del client nella prossima sessione.
Ragazzi, questo è anche più semplice del server. Tutti cio che fa questo client è collegarsi all'host che specificate da riga di comando, sulla porta 3490. E prende la stringa che il server gli manda.
/* ** client.c -- un semplice esempio di client che usa gli stream socket */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #define PORT 3490 // la porta che userà il client per collegarsi #define MAXDATASIZE 100 // massimo numero di byte che potremo leggere in una volta int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct hostent *he; struct sockaddr_in their_addr; // le information sull'indirizo di chi si collega if (argc != 2) { fprintf(stderr,"uso: client hostname\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // prende le informazioni sull'host perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // host byte order their_addr.sin_port = htons(PORT); // short, network byte order their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), '\0', 8); // azzera il resto della struttura if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("Ricevuto: %s",buf); close(sockfd); return 0; } |
Notate che se non avete lanciato il server prima di lanciare il client, connect() restituirà "Connection refused". Molto utile.
Non ho veramente molto da dire qui, dunque presenterò soltanto un paio di programmi: talker.c e listener.c.
listener sta in attesa su una macchina aspettando un pacchetto sulla porta 4950. talker invia un pacchetto a quella porta, sulla macchina specificata, che contiene qualsiasi cosa l'utente abbia scritto sulla riga di comando.
Qui c'è il codice di listener.c:
/* ** listener.c -- un esempio di "server" con i socket datagrammi */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MYPORT 4950 // la porta che verrà usata dagli utenti per collegarsi #define MAXBUFLEN 100 int main(void) { int sockfd; struct sockaddr_in my_addr; // le informazioni sul mio indirizzo struct sockaddr_in their_addr; // le informazioni sull'indirizzo del client int addr_len, numbytes; char buf[MAXBUFLEN]; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } my_addr.sin_family = AF_INET; // host byte order my_addr.sin_port = htons(MYPORT); // short, network byte order my_addr.sin_addr.s_addr = INADDR_ANY; // automaticamente riempito con il mio IP memset(&(my_addr.sin_zero), '\0', 8); // azzera il resto della struttura if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } addr_len = sizeof(struct sockaddr); if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN-1, 0, (struct sockaddr *)&their_addr, &addr_len)) == -1) { perror("recvfrom"); exit(1); } printf("pacchetto ricevuto da %s\n",inet_ntoa(their_addr.sin_addr)); printf("il pacchetto è lungo %d byte\n",numbytes); buf[numbytes] = '\0'; printf("il pacchetto contiene \"%s\"\n",buf); close(sockfd); return 0; } |
Notate che nella nostra chiamata a socket() alla fine stiamo usando SOCK_DGRAM. Also, Inoltre notate che non c'è bisogno di listen() o accept(). Questo è uno dei vantaggi del'usare socket datagramma non connessi!
Subito dopo arriva il sorgente per talker.c:
/* ** talker.c -- un esempio di "client" datagramma */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #define MYPORT 4950 // la porta alla quale si collegherà int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; // le informazioni sul'indirizzo del connettore struct hostent *he; int numbytes; if (argc != 3) { fprintf(stderr,"uso: talker hostname messaggio\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // prende le informazioni sull'host perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // host byte order their_addr.sin_port = htons(MYPORT); // short, network byte order their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), '\0', 8); // azzera il resto della struct if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) { perror("sendto"); exit(1); } printf("inviati %d byte a %s\n", numbytes, inet_ntoa(their_addr.sin_addr)); close(sockfd); return 0; } |
E questo è tutto! Eseguite listener su una qualche macchina, poi eseguite talker su un'altra. Guardateli comunicare! Massimo Divertimento per tutta la famiglia nucleare!
Eccetto che per un piccolo dettaglio che ho già menzionato in passato: datagram socket connessi. Avrei bisogno di parlarvene ora, perchè siamo nella sezione datagrammi del documento. Diciamo che talker chiama connect() e specifica l'indirizzo di listener. Da quel punto in poi, talker potrà soltanto ricevere ed inviare dall'indirizzo specificato da connect(). Per questa ragione, non dovrete usare sendto() e recvfrom(); potrete usare semplicemente send() e recv().