...proprio l'immagine che uno si aspetta in un blog di programmazione... |
E ora bando alle ciance, vai col codice!
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <errno.h> #include <pthread.h> #define BACKLOG 10 // per listen() #define MYBUFSIZE 1024 // prototipi locali void *connHandler(void *conn_sock); int main(int argc, char *argv[]) { // test argomenti if (argc != 2) { // errore args printf("%s: numero argomenti errato\n", argv[0]); printf("uso: %s port [i.e.: %s 9999]\n", argv[0], argv[0]); return EXIT_FAILURE; } // crea un socket int my_socket; if ((my_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // errore socket() printf("%s: could not create socket (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // prepara la struttura sockaddr_in per questo server struct sockaddr_in server; // (local) server socket info server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = htons(atoi(argv[1])); // bind informazioni del server al socket if (bind(my_socket, (struct sockaddr *)&server, sizeof(server)) == -1) { // errore bind() printf("%s: bind failed (%s)", argv[0], strerror(errno)); return EXIT_FAILURE; } // start ascolto con una coda di max BACKLOG connessioni if (listen(my_socket, BACKLOG) == -1) { // errore listen() printf("%s: listen failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // accetta connessioni da un client entrante printf("%s: attesa connessioni entranti...\n", argv[0]); pthread_t thread_id; socklen_t socksize = sizeof(struct sockaddr_in); struct sockaddr_in client; // (remote) client socket info int client_sock; while ((client_sock = accept(my_socket, (struct sockaddr *)&client, &socksize)) != -1) { printf("%s: connessione accettata\n", argv[0]); if (pthread_create(&thread_id, NULL, &connHandler, (void*)&client_sock) == -1) { // errore pthread_create() printf("%s: pthread_create failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } } // errore accept() printf("%s: accept failed (%s)\n", argv[0], strerror(errno)); return EXIT_FAILURE; } // thread function per gestione connessioni void *connHandler(void *conn_sock) { // estrae il client socket dall'argomento int client_sock = *(int*)conn_sock; // loop di ricezione messaggi dal client int read_size; char client_msg[MYBUFSIZE]; while ((read_size = recv(client_sock, client_msg, MYBUFSIZE, 0)) > 0 ) { // send messaggio di ritorno al client printf("%s: ricevuto messaggio dal sock %d: %s\n", __func__, client_sock, client_msg); char server_msg[MYBUFSIZE]; sprintf(server_msg, "mi hai scritto: %s", client_msg); send(client_sock, server_msg, strlen(server_msg), 0); // clear buffer memset(client_msg, 0, MYBUFSIZE); } // loop terminato: test motivo if (read_size == -1) { // errore recv() printf("%s: recv failed\n", __func__); } else { // read_size == 0: il client si è disconnesso printf("%s: client disconnected\n", __func__); } return NULL; }
Ok, non stiamo a raccontare di nuovo come funziona un Socket Server (già fatto nel vecchio post, rileggere attentamente, please), ma concentriamoci sulle differenze tra il codice single-thread e quello multithread: sicuramente avrete notato che sono praticamente identici fino alla fase di listen(), e anche dopo le differenze sono minime: la fase di accept() adesso è in un loop, e per ogni connessione accettata (di un Client remoto) viene creato un nuovo thread. E cosa esegue il thread? Esegue la funzione connHandler() che contiene, guarda caso, il loop di recv() che nel vecchio codice era eseguito subito dopo la fase di accept(). Anche il successivo test del motivo di uscita (prematura) dal loop è contenuto in connHandler(), e mostra il corretto segnale di errore (recv() error o client disconnected, in base al codice ritornato dalla recv()).
Cosa aggiungere? Semplice e super-funzionale: un Socket Server multithread con quattro righe di codice! Ovviamente la sintassi di creazione del thread e l'esecuzione della start_routine dello stesso sono identiche a quelle descritte qui. Per testare il nostro Socket Server è necessario compilare anche un Socket Client (ovviamente quello descritto nel mio vecchio post Il Client oscuro - Il ritorno), ed eseguire, ad esempio, una istanza del Socket Server e due istanze del Socket Client (in tre terminali diversi della stessa macchina, oppure su tre macchine diverse). Eseguendo sulla mia macchina (Linux, ovviamente) su tre terminali il risultato è il seguente:
Nel terminale 1:
aldo@ao-linux-nb:~/blogtest$ ./sockserver-mt 9999 ./sockserver-mt: attesa connessioni entranti... ./sockserver-mt: connessione accettata ./sockserver-mt: connessione accettata connHandler: ricevuto messaggio dal sock 4: pippo connHandler: ricevuto messaggio dal sock 5: pluto connHandler: client disconnected connHandler: client disconnected
Nel terminale 2:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999 Scrivi un messaggio per il Server remoto: pippo ./sockclient: Server reply: mi hai scritto: pippo Scrivi un messaggio per il Server remoto: ^C aldo@ao-linux-nb:~/blogtest$
Nel terminale 3:
aldo@ao-linux-nb:~/blogtest$ ./sockclient 127.0.0.1 9999 Scrivi un messaggio per il Server remoto: pluto ./sockclient: Server reply: mi hai scritto: pluto Scrivi un messaggio per il Server remoto: ^C aldo@ao-linux-nb:~/blogtest$
notare che quando uno dei Client esce (con un CTRL-C, ad esempio) il Server se ne accorge e visualizza, come previsto, client disconnected... perfetto.
Ok, con i thread abbiamo finito. Adesso cercherò di pensare a qualche nuovo interessante argomento per il prossimo post. Come sempre vi invito a non trattenere il respiro nell'attesa...
Ciao e al prossimo post!
Ottimo post, davvero utile. Grazie!
RispondiEliminaGrazie a te per i complimenti!
Elimina